mtproto 0.0.33 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc4ebf89bbc3d69e0b542b04320af0e74acdd828279411bad647e995a5aad06d
4
- data.tar.gz: acb092bf151f543578359c1bcd9d81ff1a2f6bc20e5935276a5c8a7b117892ec
3
+ metadata.gz: 8e9121e732982986db9b71a12288993dfb18888e3fe42e2e2b3bb4db55ca494a
4
+ data.tar.gz: 98b72d7cc330c681c6552a9e3f5b1081e7d614283e685c3f6be70837dae0390b
5
5
  SHA512:
6
- metadata.gz: da18a3d97b307eee7bc93ce71efe179a571c4a6385c94a81b43e6dc8e950aabc002fe31b4312b51a1d91157bb3e102b11ce213409eb4ebafbc37db998dd528ab
7
- data.tar.gz: cdc1d6a7bd55e8ca66841e43e6f40ec8bdfcd950a7c2bad5325dc8dff2f5d4aecb9eb948d76b7787f3c36a255fc5301830145820037c7e9fc5660459747dadd1
6
+ metadata.gz: 9a6fe6fc4c86420ee93a35f44f21fc39778b28fbc7a4c92e2294545dde285de72683db01a282e0517e5b8f4172af211465d17d7141185637d90fccba67b12b7b
7
+ data.tar.gz: 276c70eaffb140cd6b8d5f34582f9009c205d00e46afbefdd5f6050c67e79600c1afd6c5238f38d7cd8121fe0fb611b09a00b5237017bbcb69bc9df2d3e6cbd8
@@ -22,11 +22,13 @@ once.
22
22
 
23
23
  ## How It Works
24
24
 
25
- These are semi-manual tests. Each test case:
25
+ Each test case is a script that drives the whole scenario itself:
26
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
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). 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`.
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 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
136
+ ## Running a scenario
167
137
 
168
138
  ```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'
139
+ cd ~/devel/alev-pro/mtproto-ruby
140
+ bundle exec spec/level1/<scenario>
172
141
  ```
173
142
 
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
- ```
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
- ### 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
- ```
148
+ ### STDOUT buffering
235
149
 
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.
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.
@@ -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 a prebuilt object (pass
12
- # an object to carry upload progress). `top_msg_id` targets a forum topic /
13
- # thread. Returns the raw Bool reply, normally ignored.
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) unless action.is_a?(TL::SendMessageAction)
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
@@ -17,6 +17,7 @@ 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'
21
22
  require_relative 'api/resolve_username'
22
23
  require_relative 'api/export_bot_token'
@@ -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
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ # The pieces every media-carrying call writes the same way: who it is addressed to,
6
+ # the file just uploaded, and the strings around it.
7
+ #
8
+ # Held together here because an album is sent in two calls — one to turn each upload
9
+ # into a photo Telegram holds, one to send them as a group — and both write the same
10
+ # peer and the same file.
11
+ module MediaParts
12
+ include Binary
13
+
14
+ INPUT_MEDIA_PHOTO = 0xe3af4434
15
+ INPUT_PHOTO = 0x3bb3b94a
16
+
17
+ def serialize_peer(peer)
18
+ case peer[:type]
19
+ when :self
20
+ u32_b(Constructors::INPUT_PEER_SELF)
21
+ when :user
22
+ u32_b(Constructors::INPUT_PEER_USER) + u64_b(peer[:id]) + u64_b(peer[:access_hash] || 0)
23
+ when :chat
24
+ u32_b(Constructors::INPUT_PEER_CHAT) + u64_b(peer[:id])
25
+ when :channel
26
+ u32_b(Constructors::INPUT_PEER_CHANNEL) + u64_b(peer[:id]) + u64_b(peer[:access_hash] || 0)
27
+ else
28
+ raise "Unknown peer type: #{peer[:type]}"
29
+ end
30
+ end
31
+
32
+ def serialize_uploaded_photo(file)
33
+ u32_b(Constructors::INPUT_MEDIA_UPLOADED_PHOTO) + u32_b(0) + serialize_file(file)
34
+ end
35
+
36
+ # A photo Telegram already holds, named by what it answered when the upload was
37
+ # handed to it.
38
+ def serialize_held_photo(photo)
39
+ u32_b(INPUT_MEDIA_PHOTO) + u32_b(0) +
40
+ u32_b(INPUT_PHOTO) + u64_b(photo[:id]) + u64_b(photo[:access_hash]) +
41
+ serialize_raw(photo[:file_reference])
42
+ end
43
+
44
+ def serialize_file(file)
45
+ return u32_b(Constructors::INPUT_FILE_BIG) + u64_b(file[:id]) + u32_b(file[:parts]) +
46
+ serialize_text(file[:name].to_s) if file[:big]
47
+
48
+ u32_b(Constructors::INPUT_FILE) + u64_b(file[:id]) + u32_b(file[:parts]) +
49
+ serialize_text(file[:name].to_s) + serialize_text(file[:md5_checksum].to_s)
50
+ end
51
+
52
+ def serialize_text(str)
53
+ serialize_byte_array(str.encode('UTF-8').bytes)
54
+ end
55
+
56
+ def serialize_raw(raw)
57
+ serialize_byte_array(raw.to_s.b.bytes)
58
+ end
59
+
60
+ def serialize_byte_array(bytes)
61
+ length = bytes.length
62
+ head = length <= 253 ? [length] : [254] + u32_b(length)[0, 3]
63
+ padded = head + bytes
64
+ padded + [0] * ((4 - (padded.length % 4)) % 4)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class ReplyKeyboardForceReply
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x86b40b08
9
+
10
+ def initialize(single_use: false, selective: false, placeholder: nil)
11
+ @single_use = single_use
12
+ @selective = selective
13
+ @placeholder = placeholder
14
+ end
15
+
16
+ def serialize
17
+ flags = 0
18
+ flags |= (1 << 1) if @single_use
19
+ flags |= (1 << 2) if @selective
20
+ flags |= (1 << 3) if @placeholder
21
+
22
+ result = u32_b(CONSTRUCTOR)
23
+ result += u32_b(flags)
24
+ result += serialize_tl_string(@placeholder) if @placeholder
25
+ result
26
+ end
27
+
28
+ private
29
+
30
+ def serialize_tl_string(str)
31
+ bytes = str.encode('UTF-8').bytes
32
+ length = bytes.length
33
+ if length <= 253
34
+ [length] + bytes + padding(length + 1)
35
+ else
36
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
37
+ end
38
+ end
39
+
40
+ def padding(current_length)
41
+ pad_length = (4 - (current_length % 4)) % 4
42
+ [0] * pad_length
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../message_entity'
4
+
5
+ module MTProto
6
+ module TL
7
+ # A SendMessageAction carrying a live draft: the partially generated answer the
8
+ # recipient's client shows at the bottom of the chat while it is still being
9
+ # written. Unlike the other actions it is not a bare constructor id, so it does
10
+ # not fit TL::SendMessageAction's id-plus-progress table.
11
+ #
12
+ # `random_id` identifies the draft: the same value updates the draft already on
13
+ # screen, a different one puts a second draft beside it.
14
+ class SendMessageTextDraftAction
15
+ include Binary
16
+
17
+ CONSTRUCTOR = 0x376d975c
18
+ TEXT_WITH_ENTITIES = 0x751f3146
19
+
20
+ def initialize(random_id:, text:, entities: [])
21
+ @random_id = random_id
22
+ @text = text
23
+ @entities = entities
24
+ end
25
+
26
+ # textWithEntities#751f3146 text:string entities:Vector<MessageEntity> — the
27
+ # entity vector is not optional, so an empty list still goes on the wire.
28
+ def serialize
29
+ u32_b(CONSTRUCTOR) + u64_b(@random_id) +
30
+ u32_b(TEXT_WITH_ENTITIES) + serialize_tl_string(@text) +
31
+ MessageEntities.serialize(@entities)
32
+ end
33
+
34
+ private
35
+
36
+ def serialize_tl_string(str)
37
+ bytes = str.encode('UTF-8').bytes
38
+ length = bytes.length
39
+ if length <= 253
40
+ [length] + bytes + padding(length + 1)
41
+ else
42
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
43
+ end
44
+ end
45
+
46
+ def padding(current_length)
47
+ pad_length = (4 - (current_length % 4)) % 4
48
+ [0] * pad_length
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ require_relative 'media_parts'
6
+
7
+ module MTProto
8
+ module TL
9
+ # messages.sendMultiMedia — several photos as ONE message, the way a person sends an
10
+ # album.
11
+ #
12
+ # The words ride on the first of them and nowhere else: an album carries a single
13
+ # caption, and repeating it under each picture is how it looks when somebody sends
14
+ # them one at a time.
15
+ class SendMultiMedia
16
+ include MediaParts
17
+
18
+ CONSTRUCTOR = 0x1bf89d74
19
+ INPUT_SINGLE_MEDIA = 0x1cc6e91f
20
+ INPUT_REPLY_TO_MESSAGE = 0x3bd4b7c2
21
+
22
+ def initialize(peer:, photos:, message: '', reply_to: nil, silent: false)
23
+ @peer = peer
24
+ @photos = photos
25
+ @message = message
26
+ @reply_to = reply_to
27
+ @silent = silent
28
+ end
29
+
30
+ def serialize
31
+ flags = 0
32
+ flags |= (1 << 0) if @reply_to
33
+ flags |= (1 << 5) if @silent
34
+
35
+ result = u32_b(CONSTRUCTOR) + u32_b(flags) + serialize_peer(@peer)
36
+ result += u32_b(INPUT_REPLY_TO_MESSAGE) + u32_b(0) + u32_b(@reply_to) if @reply_to
37
+ result += u32_b(Constructors::VECTOR) + u32_b(@photos.length)
38
+ @photos.each_with_index { |photo, nth| result += serialize_single(photo, nth) }
39
+ result
40
+ end
41
+
42
+ private
43
+
44
+ def serialize_single(photo, nth)
45
+ u32_b(INPUT_SINGLE_MEDIA) + u32_b(0) +
46
+ serialize_held_photo(photo) +
47
+ u64_b(SecureRandom.random_number(2**63)) +
48
+ serialize_text(nth.zero? ? @message.to_s : '')
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'media_parts'
4
+
5
+ module MTProto
6
+ module TL
7
+ # messages.uploadMedia — hand Telegram an upload and get back the photo it now holds.
8
+ #
9
+ # The step before an album: a group is sent by naming photos Telegram already has, so
10
+ # each file is offered once on its own before the group is sent at all.
11
+ class UploadMedia
12
+ include MediaParts
13
+
14
+ CONSTRUCTOR = 0x14967978
15
+
16
+ def initialize(peer:, file:)
17
+ @peer = peer
18
+ @file = file
19
+ end
20
+
21
+ def serialize
22
+ u32_b(CONSTRUCTOR) + u32_b(0) + serialize_peer(@peer) + serialize_uploaded_photo(@file)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -7,7 +7,7 @@ module MTProto
7
7
  module Peers
8
8
  module_function
9
9
 
10
- Chat = Struct.new(:id, :title, :type, :access_hash, :username, keyword_init: true)
10
+ Chat = Struct.new(:id, :title, :type, :access_hash, :username, :min, keyword_init: true)
11
11
 
12
12
  def chats_at(data, offset)
13
13
  offset += 4 # vector constructor
@@ -119,7 +119,8 @@ module MTProto
119
119
  offset += 8 if flags2.anybits?(1 << 18) # linked_monoforum_id (long)
120
120
 
121
121
  type = flags.anybits?(1 << 8) ? :supergroup : :channel
122
- [Chat.new(id: id, title: title, type: type, access_hash: access_hash, username: username), offset]
122
+ [Chat.new(id: id, title: title, type: type, access_hash: access_hash, username: username,
123
+ min: flags.anybits?(1 << 12)), offset]
123
124
  end
124
125
 
125
126
  # chatForbidden#6592a1a7 id:long title:string
@@ -192,8 +193,8 @@ module MTProto
192
193
  offset += 4
193
194
  id = data[offset, 8].unpack1('Q<')
194
195
  offset += 8
195
- access_hash = nil
196
- if flags.anybits?(1 << 0)
196
+ carries_hash = flags.anybits?(1 << 0)
197
+ if carries_hash
197
198
  access_hash = data[offset, 8].unpack1('Q<')
198
199
  offset += 8
199
200
  end
@@ -223,8 +224,10 @@ module MTProto
223
224
  offset += 8 if flags2.anybits?(1 << 14) # bot_verification_icon
224
225
  offset += 8 if flags2.anybits?(1 << 15) # send_paid_messages_stars
225
226
 
226
- [{ id: id, first_name: first_name, last_name: last_name, username: username,
227
- is_bot: flags.anybits?(1 << 14), access_hash: access_hash }, offset]
227
+ user = { id: id, first_name: first_name, last_name: last_name, username: username,
228
+ is_bot: flags.anybits?(1 << 14), min: flags.anybits?(1 << 20) }
229
+ user[:access_hash] = access_hash if carries_hash
230
+ [user, offset]
228
231
  end
229
232
  private_class_method :parse_chat, :parse_basic_chat, :parse_channel,
230
233
  :parse_chat_forbidden, :parse_channel_forbidden, :parse_user
@@ -18,6 +18,9 @@ module MTProto
18
18
  MEDIA_DOCUMENT = 0x52d8ccd9
19
19
  DOCUMENT = 0x8fd4c4d8
20
20
  PHOTO = 0xfb197a65
21
+ PHOTO_SIZE = 0x75c78e60
22
+ PHOTO_CACHED_SIZE = 0x021e1ad6
23
+ PHOTO_SIZE_PROGRESSIVE = 0xfa3efb95
21
24
  DOCUMENT_ATTRIBUTE_AUDIO = 0x9852f9c6
22
25
  DOCUMENT_ATTRIBUTE_FILENAME = 0x15590068
23
26
 
@@ -158,13 +161,37 @@ module MTProto
158
161
  id = data[offset, 8].unpack1('q<')
159
162
  access_hash = data[offset + 8, 8].unpack1('q<')
160
163
  offset += 16
161
- file_reference, = read_tl_bytes(data, offset)
164
+ file_reference, offset = read_tl_bytes(data, offset)
162
165
 
163
- { id: id, access_hash: access_hash, file_reference: file_reference }
166
+ { id: id, access_hash: access_hash, file_reference: file_reference,
167
+ sizes: photo_sizes(data, offset + 4) } # +4: date, which precedes the sizes
164
168
  rescue StandardError
165
169
  nil
166
170
  end
167
171
 
172
+ # Every size Telegram holds this photo in, by the name upload.getFile asks for it.
173
+ #
174
+ # A photo, unlike a document, is not fetched whole: the location names which size is
175
+ # wanted, and naming none is answered as if the reference had gone stale — which is
176
+ # what makes a missing size look like an expired one.
177
+ #
178
+ # Only the sizes with a width can be fetched: the other kinds carry their own bytes
179
+ # inline (a blur, an outline) and have nothing to ask for.
180
+ SIZES_WITH_WIDTH = [PHOTO_SIZE, PHOTO_CACHED_SIZE, PHOTO_SIZE_PROGRESSIVE].freeze
181
+
182
+ def photo_sizes(data, offset)
183
+ found = []
184
+ schema.skip_vector(data, offset) do |bytes, at|
185
+ constructor = bytes[at, 4].unpack1('L<')
186
+ name, after = read_tl_string(bytes, at + 4)
187
+ found << { type: name, width: bytes[after, 4].unpack1('l<') } if SIZES_WITH_WIDTH.include?(constructor)
188
+ schema.skip(bytes, at)
189
+ end
190
+ found
191
+ rescue StandardError
192
+ []
193
+ end
194
+
168
195
  # Vector<DocumentAttribute>: pull voice/duration from documentAttributeAudio
169
196
  # and the name from documentAttributeFilename; advance over the rest via
170
197
  # schema so an unknown attribute never throws off the read.
@@ -51,14 +51,14 @@ module MTProto
51
51
 
52
52
  def dispatch(batch, &)
53
53
  remember_channels(batch.chats)
54
- batch.events.each { |event| ingest(event, &) }
54
+ batch.events.each { |event| ingest(event, batch, &) }
55
55
  batch.channel_too_long.each { |channel_id| recover_channel(channel_id, &) }
56
56
  catch_up(&) if batch.too_long
57
57
  end
58
58
 
59
- def ingest(event, &)
59
+ def ingest(event, batch, &)
60
60
  detect_gap(event, &)
61
- emit(event, &)
61
+ emit(event, batch, &)
62
62
  end
63
63
 
64
64
  # A live update whose pts jumps past our stored position means updates were
@@ -80,11 +80,11 @@ module MTProto
80
80
  # host's transaction (atomic with the host's own handling); the in-memory @pts is
81
81
  # advanced only AFTER the transaction commits, so a rollback can never leave the
82
82
  # cache ahead of the store and silently drop later account updates.
83
- def emit(event, &block)
83
+ def emit(event, batch, &block)
84
84
  return if outdated?(event)
85
85
 
86
86
  @store.transaction do
87
- block.call(event)
87
+ block.call(event, batch)
88
88
  checkpoint(event)
89
89
  end
90
90
  cache_account_position(event)
@@ -142,7 +142,7 @@ module MTProto
142
142
 
143
143
  def apply_difference(diff, &)
144
144
  remember_channels(diff.chats)
145
- diff.events.each { |event| emit(event, &) }
145
+ diff.events.each { |event| emit(event, diff, &) }
146
146
  diff.channel_too_long.each { |channel_id| recover_channel(channel_id, &) }
147
147
  @pts = diff.pts if diff.pts
148
148
  @qts = diff.qts if diff.qts
@@ -164,7 +164,7 @@ module MTProto
164
164
  return catch_up(&) if diff.type == :too_long
165
165
 
166
166
  remember_channels(diff.chats)
167
- diff.events.each { |event| emit(event, &) }
167
+ diff.events.each { |event| emit(event, diff, &) }
168
168
  if diff.pts&.positive?
169
169
  from = diff.pts
170
170
  @store.set_channel_pts(channel_id, diff.pts)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MTProto
4
- VERSION = '0.0.33'
4
+ VERSION = '0.0.34'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mtproto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.33
4
+ version: 0.0.34
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artem Levenkov
@@ -85,6 +85,7 @@ files:
85
85
  - lib/mtproto/client/api/send_message.rb
86
86
  - lib/mtproto/client/api/set_typing.rb
87
87
  - lib/mtproto/client/api/sign_in.rb
88
+ - lib/mtproto/client/api/stream_draft.rb
88
89
  - lib/mtproto/client/api/unpin_all_messages.rb
89
90
  - lib/mtproto/client/rpc.rb
90
91
  - lib/mtproto/client/rpc/response.rb
@@ -100,6 +101,7 @@ files:
100
101
  - lib/mtproto/crypto/srp.rb
101
102
  - lib/mtproto/dc.rb
102
103
  - lib/mtproto/delegate_methods.rb
104
+ - lib/mtproto/draft_stream.rb
103
105
  - lib/mtproto/encrypted_message.rb
104
106
  - lib/mtproto/errors.rb
105
107
  - lib/mtproto/file_downloader.rb
@@ -170,6 +172,7 @@ files:
170
172
  - lib/mtproto/tl/objects/invoke_with_layer.rb
171
173
  - lib/mtproto/tl/objects/keyboard_button_callback.rb
172
174
  - lib/mtproto/tl/objects/login_token.rb
175
+ - lib/mtproto/tl/objects/media_parts.rb
173
176
  - lib/mtproto/tl/objects/message.rb
174
177
  - lib/mtproto/tl/objects/message_service.rb
175
178
  - lib/mtproto/tl/objects/messages.rb
@@ -181,6 +184,7 @@ files:
181
184
  - lib/mtproto/tl/objects/pq_inner_data.rb
182
185
  - lib/mtproto/tl/objects/raw_response.rb
183
186
  - lib/mtproto/tl/objects/reply_inline_markup.rb
187
+ - lib/mtproto/tl/objects/reply_keyboard_force_reply.rb
184
188
  - lib/mtproto/tl/objects/reply_keyboard_markup.rb
185
189
  - lib/mtproto/tl/objects/req_dh_params.rb
186
190
  - lib/mtproto/tl/objects/req_pq_multi.rb
@@ -198,6 +202,8 @@ files:
198
202
  - lib/mtproto/tl/objects/send_media.rb
199
203
  - lib/mtproto/tl/objects/send_message.rb
200
204
  - lib/mtproto/tl/objects/send_message_action.rb
205
+ - lib/mtproto/tl/objects/send_message_text_draft_action.rb
206
+ - lib/mtproto/tl/objects/send_multi_media.rb
201
207
  - lib/mtproto/tl/objects/send_reaction.rb
202
208
  - lib/mtproto/tl/objects/sent_code.rb
203
209
  - lib/mtproto/tl/objects/server_dh_inner_data.rb
@@ -220,6 +226,7 @@ files:
220
226
  - lib/mtproto/tl/objects/updates.rb
221
227
  - lib/mtproto/tl/objects/updates_difference.rb
222
228
  - lib/mtproto/tl/objects/updates_state.rb
229
+ - lib/mtproto/tl/objects/upload_media.rb
223
230
  - lib/mtproto/tl/objects/upload_profile_photo.rb
224
231
  - lib/mtproto/tl/objects/users.rb
225
232
  - lib/mtproto/tl/peers.rb