mtproto 0.0.32 → 0.0.34
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/docs/test_architecture_level1.md +23 -107
- data/lib/mtproto/auth_key_generator.rb +3 -1
- data/lib/mtproto/client/api/resolve_username.rb +18 -0
- data/lib/mtproto/client/api/send_message.rb +5 -2
- data/lib/mtproto/client/api/set_typing.rb +4 -4
- data/lib/mtproto/client/api/stream_draft.rb +13 -0
- data/lib/mtproto/client/api.rb +2 -0
- data/lib/mtproto/crypto/dh_key_exchange.rb +2 -1
- data/lib/mtproto/draft_stream.rb +52 -0
- data/lib/mtproto/tl/constructors.rb +1 -0
- data/lib/mtproto/tl/objects/authorization.rb +4 -3
- data/lib/mtproto/tl/objects/channel_difference.rb +13 -32
- data/lib/mtproto/tl/objects/contacts.rb +7 -89
- data/lib/mtproto/tl/objects/dialogs.rb +14 -352
- data/lib/mtproto/tl/objects/edit_message.rb +1 -1
- data/lib/mtproto/tl/objects/media_parts.rb +68 -0
- data/lib/mtproto/tl/objects/message.rb +14 -0
- data/lib/mtproto/tl/objects/message_service.rb +40 -0
- data/lib/mtproto/tl/objects/messages.rb +7 -32
- data/lib/mtproto/tl/objects/reply_keyboard_force_reply.rb +46 -0
- data/lib/mtproto/tl/objects/resolved_peer.rb +29 -0
- data/lib/mtproto/tl/objects/send_message.rb +1 -1
- data/lib/mtproto/tl/objects/send_message_text_draft_action.rb +52 -0
- data/lib/mtproto/tl/objects/send_multi_media.rb +52 -0
- data/lib/mtproto/tl/objects/update.rb +3 -35
- data/lib/mtproto/tl/objects/updates.rb +4 -6
- data/lib/mtproto/tl/objects/updates_difference.rb +9 -29
- data/lib/mtproto/tl/objects/upload_media.rb +26 -0
- data/lib/mtproto/tl/peers.rb +236 -0
- data/lib/mtproto/tl/reader.rb +29 -2
- data/lib/mtproto/updates/event_builder.rb +20 -32
- data/lib/mtproto/updates/parser.rb +9 -8
- data/lib/mtproto/updates/state_machine.rb +7 -7
- data/lib/mtproto/version.rb +1 -1
- data/lib/mtproto.rb +1 -0
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e9121e732982986db9b71a12288993dfb18888e3fe42e2e2b3bb4db55ca494a
|
|
4
|
+
data.tar.gz: 98b72d7cc330c681c6552a9e3f5b1081e7d614283e685c3f6be70837dae0390b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a6fe6fc4c86420ee93a35f44f21fc39778b28fbc7a4c92e2294545dde285de72683db01a282e0517e5b8f4172af211465d17d7141185637d90fccba67b12b7b
|
|
7
|
+
data.tar.gz: 276c70eaffb140cd6b8d5f34582f9009c205d00e46afbefdd5f6050c67e79600c1afd6c5238f38d7cd8121fe0fb611b09a00b5237017bbcb69bc9df2d3e6cbd8
|
|
@@ -22,11 +22,13 @@ once.
|
|
|
22
22
|
|
|
23
23
|
## How It Works
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Each test case is a script that drives the whole scenario itself:
|
|
26
26
|
|
|
27
|
-
1. Connects to Telegram with
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
1. Connects to Telegram with pre-authenticated sessions — one per side the
|
|
28
|
+
scenario needs (a user, a bot, a second user)
|
|
29
|
+
2. Performs the user-facing actions from those sessions, so no operator has to
|
|
30
|
+
do anything by hand
|
|
31
|
+
3. Waits out the traffic the actions produce
|
|
30
32
|
4. Saves the raw decrypted message bodies as `.bin` files and a `captured.json`
|
|
31
33
|
listing the TL constructors received
|
|
32
34
|
|
|
@@ -112,11 +114,11 @@ No metadata file — the folder + filename are the catalogue.
|
|
|
112
114
|
|
|
113
115
|
## File Structure
|
|
114
116
|
|
|
115
|
-
Each scenario is an executable script (not an RSpec spec).
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
Each scenario is an executable script (not an RSpec spec). It talks to live
|
|
118
|
+
Telegram, needs credentials, and narrates what it observes as it goes —
|
|
119
|
+
RSpec's declarative `describe`/`it` shape fits none of that, so these scripts
|
|
120
|
+
live under `spec/level1/` alongside the rspec tree, are invoked directly, and
|
|
121
|
+
never run as part of `rake spec`.
|
|
120
122
|
|
|
121
123
|
```
|
|
122
124
|
spec/
|
|
@@ -131,107 +133,21 @@ spec/
|
|
|
131
133
|
constructors/ # Deduplicated constructor variant library
|
|
132
134
|
```
|
|
133
135
|
|
|
134
|
-
## Running
|
|
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
|
|
136
|
+
## Running a scenario
|
|
167
137
|
|
|
168
138
|
```sh
|
|
169
|
-
~/devel/alev-pro/
|
|
170
|
-
|
|
171
|
-
&& bundle exec spec/level1/receive_private_text_message'
|
|
139
|
+
cd ~/devel/alev-pro/mtproto-ruby
|
|
140
|
+
bundle exec spec/level1/<scenario>
|
|
172
141
|
```
|
|
173
142
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
```
|
|
143
|
+
The script runs to completion on its own and exits; nothing has to be typed at
|
|
144
|
+
it. It reads its credentials from the environment (`.env` is loaded): the test
|
|
145
|
+
DC address, key and number, the API id and hash, and one or more
|
|
146
|
+
pre-authenticated sessions. Each script names the variables it needs.
|
|
195
147
|
|
|
196
|
-
###
|
|
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
|
-
```
|
|
148
|
+
### STDOUT buffering
|
|
235
149
|
|
|
236
|
-
|
|
237
|
-
|
|
150
|
+
Ruby's `STDOUT` defaults to full buffering when stdout is not a tty, so a script
|
|
151
|
+
whose output is piped or captured would hold everything in-process until it
|
|
152
|
+
exits. Each Level 1 script therefore sets `STDOUT.sync = true` near the top,
|
|
153
|
+
before any output.
|
|
@@ -193,7 +193,9 @@ module MTProto
|
|
|
193
193
|
Digest::SHA1.digest(new_nonce + "\x03".b + auth_key_aux_hash)[-16..]
|
|
194
194
|
end
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
unless dh_gen_response.new_nonce_hash == expected_new_nonce_hash
|
|
197
|
+
raise "new_nonce_hash mismatch! (dh_gen #{dh_gen_response.status}, auth_key #{auth_key.bytesize} bytes)"
|
|
198
|
+
end
|
|
197
199
|
|
|
198
200
|
case dh_gen_response.status
|
|
199
201
|
when :retry
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../tl/objects/resolve_username'
|
|
4
|
+
require_relative '../../tl/objects/resolved_peer'
|
|
5
|
+
|
|
6
|
+
module MTProto
|
|
7
|
+
class Client
|
|
8
|
+
class API
|
|
9
|
+
def resolve_username(username:)
|
|
10
|
+
rpc_call(TL::ResolveUsername.new(username: username), TL::ResolvedPeer).body
|
|
11
|
+
rescue RpcError => e
|
|
12
|
+
raise unless e.error_message == 'USERNAME_NOT_OCCUPIED'
|
|
13
|
+
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -16,14 +16,17 @@ module MTProto
|
|
|
16
16
|
# it (`message`/`entities` are ignored). Note: server-gated (premium).
|
|
17
17
|
# A `reply_to` (with optional `quote_text`/`quote_offset`) replies to a message,
|
|
18
18
|
# optionally pinning the quoted fragment.
|
|
19
|
+
# `silent:` delivers without the recipient's notification sound — for a message that
|
|
20
|
+
# is not news to them: a transcript of what they just said, a repaint of something
|
|
21
|
+
# already on screen.
|
|
19
22
|
def send_message(peer:, message: '', random_id: nil, reply_to: nil, entities: nil,
|
|
20
23
|
parse_mode: nil, rich_markdown: nil, quote_text: nil, quote_offset: nil, no_webpage: false,
|
|
21
|
-
reply_markup: nil)
|
|
24
|
+
reply_markup: nil, silent: false)
|
|
22
25
|
message, entities = Markdown.parse(message) if parse_mode && entities.nil? && rich_markdown.nil?
|
|
23
26
|
rpc_call(
|
|
24
27
|
TL::SendMessage.new(peer: peer, message: message, random_id: random_id, reply_to: reply_to,
|
|
25
28
|
entities: entities, rich_markdown: rich_markdown, quote_text: quote_text, quote_offset: quote_offset,
|
|
26
|
-
no_webpage: no_webpage, reply_markup: reply_markup),
|
|
29
|
+
no_webpage: no_webpage, reply_markup: reply_markup, silent: silent),
|
|
27
30
|
TL::UpdateShortSentMessage
|
|
28
31
|
).body
|
|
29
32
|
end
|
|
@@ -8,11 +8,11 @@ module MTProto
|
|
|
8
8
|
class Client
|
|
9
9
|
class API
|
|
10
10
|
# messages.setTyping — show a chat action in `peer`. `action` is a
|
|
11
|
-
# SendMessageAction type symbol (default :typing) or
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# SendMessageAction type symbol (default :typing), or any object that
|
|
12
|
+
# serializes itself. `top_msg_id` targets a forum topic / thread. Returns
|
|
13
|
+
# the raw Bool reply, normally ignored.
|
|
14
14
|
def set_typing(peer:, action: :typing, top_msg_id: nil)
|
|
15
|
-
action = TL::SendMessageAction.new(action)
|
|
15
|
+
action = TL::SendMessageAction.new(action) if action.is_a?(Symbol)
|
|
16
16
|
rpc_call(
|
|
17
17
|
TL::SetTyping.new(peer: peer, action: action, top_msg_id: top_msg_id),
|
|
18
18
|
TL::RawResponse
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../draft_stream'
|
|
4
|
+
|
|
5
|
+
module MTProto
|
|
6
|
+
class Client
|
|
7
|
+
class API
|
|
8
|
+
def stream_draft(peer:, random_id: nil, top_msg_id: nil)
|
|
9
|
+
DraftStream.new(api: self, peer: peer, random_id: random_id, top_msg_id: top_msg_id)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/mtproto/client/api.rb
CHANGED
|
@@ -17,7 +17,9 @@ require_relative 'api/get_channel_difference'
|
|
|
17
17
|
require_relative 'api/get_bot_callback_answer'
|
|
18
18
|
require_relative 'api/send_message'
|
|
19
19
|
require_relative 'api/set_typing'
|
|
20
|
+
require_relative 'api/stream_draft'
|
|
20
21
|
require_relative 'api/get_contacts'
|
|
22
|
+
require_relative 'api/resolve_username'
|
|
21
23
|
require_relative 'api/export_bot_token'
|
|
22
24
|
require_relative 'api/pin_message'
|
|
23
25
|
require_relative 'api/unpin_all_messages'
|
|
@@ -29,7 +29,8 @@ module MTProto
|
|
|
29
29
|
dh_prime = OpenSSL::BN.new(dh_prime_bytes, 2)
|
|
30
30
|
|
|
31
31
|
auth_key_bn = g_a.mod_exp(b, dh_prime)
|
|
32
|
-
|
|
32
|
+
# BN#to_s(2) drops leading zero bytes, and roughly one shared secret in 256 has one.
|
|
33
|
+
auth_key_bn.to_s(2).rjust(dh_prime_bytes.bytesize, "\x00".b)
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def generate_random_2048_bit_number
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require_relative 'tl/objects/send_message_text_draft_action'
|
|
5
|
+
|
|
6
|
+
module MTProto
|
|
7
|
+
# Telegram caps the underlying call at 20 per 5s and 40 per 30s per peer, and
|
|
8
|
+
# breaching that returns FLOOD_WAIT and blocks what follows. A frame with no room
|
|
9
|
+
# left is therefore dropped rather than queued — the next frame carries newer text
|
|
10
|
+
# anyway.
|
|
11
|
+
class DraftStream
|
|
12
|
+
WINDOWS = [[5.0, 20], [30.0, 40]].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :random_id
|
|
15
|
+
|
|
16
|
+
def initialize(api:, peer:, random_id: nil, top_msg_id: nil, clock: nil)
|
|
17
|
+
@api = api
|
|
18
|
+
@peer = peer
|
|
19
|
+
@random_id = random_id || SecureRandom.random_number(2**63)
|
|
20
|
+
@top_msg_id = top_msg_id
|
|
21
|
+
@clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
22
|
+
@sent_at = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def update(text, entities: [])
|
|
26
|
+
now = @clock.call
|
|
27
|
+
return false unless room?(now)
|
|
28
|
+
|
|
29
|
+
@api.set_typing(
|
|
30
|
+
peer: @peer, top_msg_id: @top_msg_id,
|
|
31
|
+
action: TL::SendMessageTextDraftAction.new(random_id: @random_id, text: text, entities: entities)
|
|
32
|
+
)
|
|
33
|
+
@sent_at << now
|
|
34
|
+
true
|
|
35
|
+
rescue RpcError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Send the finished answer as an ordinary message, which also clears the draft.
|
|
40
|
+
def finish(text, **)
|
|
41
|
+
@api.send_message(peer: @peer, message: text, **)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def room?(now)
|
|
47
|
+
longest = WINDOWS.map(&:first).max
|
|
48
|
+
@sent_at.reject! { |at| now - at >= longest }
|
|
49
|
+
WINDOWS.all? { |span, cap| @sent_at.count { |at| now - at < span } < cap }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -27,13 +27,14 @@ module MTProto
|
|
|
27
27
|
flags = data[offset, 4].unpack1('L<')
|
|
28
28
|
offset += 4
|
|
29
29
|
|
|
30
|
-
offset += 4 if flags.anybits?(1 << 0) # tmp_sessions
|
|
31
30
|
offset += 4 if flags.anybits?(1 << 1) # otherwise_relogin_days
|
|
31
|
+
offset += 4 if flags.anybits?(1 << 0) # tmp_sessions
|
|
32
32
|
|
|
33
33
|
offset = skip_tl_bytes(data, offset) if flags.anybits?(1 << 2) # future_auth_token
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
offset += 4
|
|
35
|
+
user_constructor = data[offset, 4].unpack1('L<')
|
|
36
|
+
offset += 4
|
|
37
|
+
raise UnexpectedConstructorError, user_constructor unless user_constructor == Constructors::USER
|
|
37
38
|
|
|
38
39
|
user_flags = data[offset, 4].unpack1('L<')
|
|
39
40
|
offset += 4
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../
|
|
3
|
+
require_relative '../reader'
|
|
4
|
+
require_relative '../peers'
|
|
4
5
|
require_relative 'message'
|
|
5
|
-
require_relative 'dialogs'
|
|
6
6
|
|
|
7
7
|
module MTProto
|
|
8
8
|
module TL
|
|
@@ -18,8 +18,6 @@ module MTProto
|
|
|
18
18
|
class ChannelDifference
|
|
19
19
|
attr_reader :type, :final, :pts, :timeout, :new_messages, :chats, :users
|
|
20
20
|
|
|
21
|
-
SCHEMA_PATH = File.expand_path('../../../../data/tl-schema.json', __dir__)
|
|
22
|
-
|
|
23
21
|
# Channel messages ride in new_messages, but message-bearing updates can also
|
|
24
22
|
# appear in other_updates — mine those too, like UpdatesDifference.
|
|
25
23
|
UPDATE_NEW_MESSAGE = 0x1f2b0afd
|
|
@@ -35,10 +33,6 @@ module MTProto
|
|
|
35
33
|
@users = users
|
|
36
34
|
end
|
|
37
35
|
|
|
38
|
-
def self.schema
|
|
39
|
-
@schema ||= Schema.new(SCHEMA_PATH)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
36
|
def self.deserialize(data)
|
|
43
37
|
constructor = data[0, 4].unpack1('L<')
|
|
44
38
|
|
|
@@ -77,10 +71,11 @@ module MTProto
|
|
|
77
71
|
offset += 4
|
|
78
72
|
end
|
|
79
73
|
|
|
80
|
-
|
|
74
|
+
raw_messages, offset = Message.vector_at(data, offset)
|
|
75
|
+
messages = raw_messages.map(&:to_h)
|
|
81
76
|
other_messages, offset = parse_other_updates(data, offset)
|
|
82
|
-
chats, offset =
|
|
83
|
-
users, =
|
|
77
|
+
chats, offset = Peers.chats_at(data, offset)
|
|
78
|
+
users, = Peers.users_at(data, offset)
|
|
84
79
|
|
|
85
80
|
new(
|
|
86
81
|
type: :difference,
|
|
@@ -107,10 +102,11 @@ module MTProto
|
|
|
107
102
|
offset += 4
|
|
108
103
|
end
|
|
109
104
|
|
|
110
|
-
offset = schema.skip(data, offset) # dialog:Dialog
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
offset = Reader.schema.skip(data, offset) # dialog:Dialog
|
|
106
|
+
raw_messages, offset = Message.vector_at(data, offset)
|
|
107
|
+
messages = raw_messages.map(&:to_h)
|
|
108
|
+
chats, offset = Peers.chats_at(data, offset)
|
|
109
|
+
users, = Peers.users_at(data, offset)
|
|
114
110
|
|
|
115
111
|
new(
|
|
116
112
|
type: :too_long,
|
|
@@ -122,24 +118,9 @@ module MTProto
|
|
|
122
118
|
)
|
|
123
119
|
end
|
|
124
120
|
|
|
125
|
-
def parse_messages_vector(data, offset)
|
|
126
|
-
offset += 4 # vector constructor
|
|
127
|
-
count = data[offset, 4].unpack1('L<')
|
|
128
|
-
offset += 4
|
|
129
|
-
|
|
130
|
-
messages = []
|
|
131
|
-
count.times do
|
|
132
|
-
constructor = data[offset, 4].unpack1('L<')
|
|
133
|
-
msg = Message.deserialize(data, offset, constructor)
|
|
134
|
-
messages << msg.to_h if msg
|
|
135
|
-
offset = schema.skip(data, offset)
|
|
136
|
-
end
|
|
137
|
-
[messages, offset]
|
|
138
|
-
end
|
|
139
|
-
|
|
140
121
|
# other_updates:Vector<Update> — mine message-bearing updates
|
|
141
122
|
# (updateNewMessage/updateNewChannelMessage: the ctor, then an inner Message,
|
|
142
|
-
# then pts/pts_count), and advance the rest via schema.
|
|
123
|
+
# then pts/pts_count), and advance the rest via Reader.schema.
|
|
143
124
|
def parse_other_updates(data, offset)
|
|
144
125
|
offset += 4 # vector constructor
|
|
145
126
|
count = data[offset, 4].unpack1('L<')
|
|
@@ -153,7 +134,7 @@ module MTProto
|
|
|
153
134
|
msg = Message.deserialize(data, inner_off, data[inner_off, 4].unpack1('L<'))
|
|
154
135
|
messages << msg.to_h if msg
|
|
155
136
|
end
|
|
156
|
-
offset = schema.skip(data, offset)
|
|
137
|
+
offset = Reader.schema.skip(data, offset)
|
|
157
138
|
end
|
|
158
139
|
[messages, offset]
|
|
159
140
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../
|
|
3
|
+
require_relative '../reader'
|
|
4
|
+
require_relative '../peers'
|
|
4
5
|
|
|
5
6
|
module MTProto
|
|
6
7
|
module TL
|
|
@@ -8,8 +9,6 @@ module MTProto
|
|
|
8
9
|
Contact = Struct.new(:user_id, :mutual, keyword_init: true)
|
|
9
10
|
User = Struct.new(:id, :first_name, :last_name, :access_hash, keyword_init: true)
|
|
10
11
|
|
|
11
|
-
SCHEMA_PATH = File.expand_path('../../../../data/tl-schema.json', __dir__)
|
|
12
|
-
|
|
13
12
|
attr_reader :contacts, :saved_count, :users, :not_modified
|
|
14
13
|
|
|
15
14
|
def initialize(contacts: [], saved_count: 0, users: [], not_modified: false)
|
|
@@ -23,10 +22,6 @@ module MTProto
|
|
|
23
22
|
@not_modified
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
def self.schema
|
|
27
|
-
@schema ||= Schema.new(SCHEMA_PATH)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
25
|
def self.deserialize(data)
|
|
31
26
|
constructor = data[0, 4].unpack1('L<')
|
|
32
27
|
return new(not_modified: true) if constructor == Constructors::CONTACTS_CONTACTS_NOT_MODIFIED
|
|
@@ -41,9 +36,6 @@ module MTProto
|
|
|
41
36
|
new(contacts: contacts, saved_count: saved_count, users: users)
|
|
42
37
|
end
|
|
43
38
|
|
|
44
|
-
USER_EMPTY = 0xd3bc4b7a
|
|
45
|
-
private_constant :USER_EMPTY
|
|
46
|
-
|
|
47
39
|
class << self
|
|
48
40
|
private
|
|
49
41
|
|
|
@@ -68,87 +60,13 @@ module MTProto
|
|
|
68
60
|
[contacts, offset]
|
|
69
61
|
end
|
|
70
62
|
|
|
71
|
-
# User parsing mirrors TL::Dialogs.parse_user (user#31774388). Kept private here
|
|
72
|
-
# so this class can be used without instantiating Dialogs.
|
|
73
63
|
def parse_users_vector(data, offset)
|
|
74
|
-
offset
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
users = []
|
|
79
|
-
count.times do
|
|
80
|
-
user, offset = parse_user(data, offset)
|
|
81
|
-
users << user if user
|
|
82
|
-
end
|
|
83
|
-
[users, offset]
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def parse_user(data, offset)
|
|
87
|
-
constructor = data[offset, 4].unpack1('L<')
|
|
88
|
-
offset += 4
|
|
89
|
-
|
|
90
|
-
if constructor == USER_EMPTY
|
|
91
|
-
offset += 8
|
|
92
|
-
return [nil, offset]
|
|
64
|
+
records, offset = Peers.users_at(data, offset)
|
|
65
|
+
mapped = records.map do |user|
|
|
66
|
+
User.new(id: user[:id], first_name: user[:first_name],
|
|
67
|
+
last_name: user[:last_name], access_hash: user[:access_hash])
|
|
93
68
|
end
|
|
94
|
-
|
|
95
|
-
raise "Unknown User constructor: 0x#{constructor.to_s(16)}" unless constructor == Constructors::USER
|
|
96
|
-
|
|
97
|
-
flags = data[offset, 4].unpack1('L<')
|
|
98
|
-
offset += 4
|
|
99
|
-
flags2 = data[offset, 4].unpack1('L<')
|
|
100
|
-
offset += 4
|
|
101
|
-
id = data[offset, 8].unpack1('Q<')
|
|
102
|
-
offset += 8
|
|
103
|
-
access_hash = nil
|
|
104
|
-
if flags.anybits?(1 << 0)
|
|
105
|
-
access_hash = data[offset, 8].unpack1('Q<')
|
|
106
|
-
offset += 8
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
first_name = nil
|
|
110
|
-
first_name, offset = read_tl_string(data, offset) if flags.anybits?(1 << 1)
|
|
111
|
-
last_name = nil
|
|
112
|
-
last_name, offset = read_tl_string(data, offset) if flags.anybits?(1 << 2)
|
|
113
|
-
|
|
114
|
-
offset = skip_tl_string(data, offset) if flags.anybits?(1 << 3) # username
|
|
115
|
-
offset = skip_tl_string(data, offset) if flags.anybits?(1 << 4) # phone
|
|
116
|
-
offset = schema.skip(data, offset) if flags.anybits?(1 << 5) # photo
|
|
117
|
-
offset = schema.skip(data, offset) if flags.anybits?(1 << 6) # status
|
|
118
|
-
offset += 4 if flags.anybits?(1 << 14) # bot_info_version
|
|
119
|
-
offset = schema.skip_vector(data, offset) { |d, o| schema.skip(d, o) } if flags.anybits?(1 << 18)
|
|
120
|
-
offset = skip_tl_string(data, offset) if flags.anybits?(1 << 19) # bot_inline_placeholder
|
|
121
|
-
offset = skip_tl_string(data, offset) if flags.anybits?(1 << 22) # lang_code
|
|
122
|
-
offset = schema.skip(data, offset) if flags.anybits?(1 << 30) # emoji_status
|
|
123
|
-
offset = schema.skip_vector(data, offset) { |d, o| schema.skip(d, o) } if flags2.anybits?(1 << 0)
|
|
124
|
-
offset += 4 if flags2.anybits?(1 << 5) # stories_max_id
|
|
125
|
-
offset = schema.skip(data, offset) if flags2.anybits?(1 << 8) # color
|
|
126
|
-
offset = schema.skip(data, offset) if flags2.anybits?(1 << 9) # profile_color
|
|
127
|
-
offset += 4 if flags2.anybits?(1 << 12) # bot_active_users
|
|
128
|
-
offset += 8 if flags2.anybits?(1 << 14) # bot_verification_icon
|
|
129
|
-
offset += 8 if flags2.anybits?(1 << 15) # send_paid_messages_stars
|
|
130
|
-
|
|
131
|
-
[User.new(id: id, first_name: first_name, last_name: last_name, access_hash: access_hash), offset]
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def read_tl_string(data, offset)
|
|
135
|
-
first_byte = data.getbyte(offset)
|
|
136
|
-
if first_byte == 254
|
|
137
|
-
len = data.getbyte(offset + 1) |
|
|
138
|
-
(data.getbyte(offset + 2) << 8) |
|
|
139
|
-
(data.getbyte(offset + 3) << 16)
|
|
140
|
-
total = 4 + len
|
|
141
|
-
else
|
|
142
|
-
len = first_byte
|
|
143
|
-
total = 1 + len
|
|
144
|
-
end
|
|
145
|
-
padding = (4 - (total % 4)) % 4
|
|
146
|
-
[data[offset + (total - len), len].force_encoding('UTF-8'), offset + total + padding]
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def skip_tl_string(data, offset)
|
|
150
|
-
_, new_offset = read_tl_string(data, offset)
|
|
151
|
-
new_offset
|
|
69
|
+
[mapped, offset]
|
|
152
70
|
end
|
|
153
71
|
end
|
|
154
72
|
end
|