async-matrix 2.1.0-aarch64-linux → 3.0.1-aarch64-linux
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/README.md +33 -58
- data/lib/async/matrix/3.3/async_matrix_e2ee.so +0 -0
- data/lib/async/matrix/3.4/async_matrix_e2ee.so +0 -0
- data/lib/async/matrix/4.0/async_matrix_e2ee.so +0 -0
- data/lib/async/matrix/api/chain.rb +120 -94
- data/lib/async/matrix/api/concat.rb +28 -22
- data/lib/async/matrix/api/path_tree.rb +71 -44
- data/lib/async/matrix/api.rb +6 -1
- data/lib/async/matrix/app_service_client.rb +213 -0
- data/lib/async/matrix/auth_error.rb +20 -0
- data/lib/async/matrix/bad_json_error.rb +20 -0
- data/lib/async/matrix/client/encryption.rb +413 -0
- data/lib/async/matrix/client/rooms.rb +478 -0
- data/lib/async/matrix/client/sync.rb +508 -0
- data/lib/async/matrix/client.rb +217 -113
- data/lib/async/matrix/config/vivify.rb +107 -0
- data/lib/async/matrix/config.rb +184 -0
- data/lib/async/matrix/device_store.rb +1621 -0
- data/lib/async/matrix/double_puppet_client.rb +1 -1
- data/lib/async/matrix/e2ee/pickle_key.rb +182 -0
- data/lib/async/matrix/error.rb +9 -36
- data/lib/async/matrix/{application_service/error_response.rb → error_response.rb} +10 -10
- data/lib/async/matrix/homeserver_error.rb +20 -0
- data/lib/async/matrix/invalid_endpoint_error.rb +20 -0
- data/lib/async/matrix/media_client.rb +46 -35
- data/lib/async/matrix/not_found_error.rb +20 -0
- data/lib/async/matrix/notifier.rb +7 -3
- data/lib/async/matrix/response_too_large_error.rb +20 -0
- data/lib/async/matrix/version.rb +1 -1
- data/lib/async/matrix.rb +16 -7
- data/lib/protocol/matrix/canonical_json.rb +200 -0
- data/lib/protocol/matrix/content.rb +106 -0
- data/lib/protocol/matrix/encrypted_message.rb +1018 -0
- data/lib/protocol/matrix/error.rb +49 -0
- data/lib/protocol/matrix/event.rb +208 -0
- data/lib/protocol/matrix/key_backup.rb +347 -0
- data/lib/protocol/matrix/keys.rb +381 -0
- data/lib/protocol/matrix/message_batch.rb +482 -0
- data/lib/protocol/matrix/schema/registry.rb +381 -0
- data/lib/protocol/matrix/schema/validation_error.rb +234 -0
- data/lib/protocol/matrix/schema.rb +170 -0
- data/lib/protocol/matrix/secret_storage.rb +535 -0
- data/lib/protocol/matrix/signing.rb +278 -0
- data/lib/protocol/matrix.rb +25 -0
- metadata +43 -136
- data/lib/async/discord/api/path_tree.rb +0 -127
- data/lib/async/discord/api.rb +0 -151
- data/lib/async/discord/client.rb +0 -283
- data/lib/async/discord/error.rb +0 -84
- data/lib/async/discord/gateway.rb +0 -359
- data/lib/async/discord.rb +0 -15
- data/lib/async/matrix/application_service/bot.rb +0 -232
- data/lib/async/matrix/application_service/config/schema/analytics.json +0 -21
- data/lib/async/matrix/application_service/config/schema/appservice.json +0 -82
- data/lib/async/matrix/application_service/config/schema/backfill.json +0 -91
- data/lib/async/matrix/application_service/config/schema/bridge.json +0 -209
- data/lib/async/matrix/application_service/config/schema/config.json +0 -61
- data/lib/async/matrix/application_service/config/schema/database.json +0 -38
- data/lib/async/matrix/application_service/config/schema/direct_media.json +0 -35
- data/lib/async/matrix/application_service/config/schema/double_puppet.json +0 -24
- data/lib/async/matrix/application_service/config/schema/encryption.json +0 -164
- data/lib/async/matrix/application_service/config/schema/homeserver.json +0 -58
- data/lib/async/matrix/application_service/config/schema/logging.json +0 -50
- data/lib/async/matrix/application_service/config/schema/management_room_texts.json +0 -25
- data/lib/async/matrix/application_service/config/schema/matrix.json +0 -45
- data/lib/async/matrix/application_service/config/schema/permissions.json +0 -54
- data/lib/async/matrix/application_service/config/schema/provisioning.json +0 -23
- data/lib/async/matrix/application_service/config/schema/public_media.json +0 -39
- data/lib/async/matrix/application_service/config/schema/relay.json +0 -43
- data/lib/async/matrix/application_service/config/vivify.rb +0 -109
- data/lib/async/matrix/application_service/config.rb +0 -225
- data/lib/async/matrix/application_service/dispatcher.rb +0 -185
- data/lib/async/matrix/application_service/event.rb +0 -285
- data/lib/async/matrix/application_service/server.rb +0 -430
- data/lib/async/matrix/application_service/transaction.rb +0 -66
- data/lib/async/matrix/application_service/transaction_handler.rb +0 -185
- data/lib/async/matrix/application_service/transaction_store.rb +0 -80
- data/lib/async/matrix/bridge/discord/db/connection.rb +0 -141
- data/lib/async/matrix/bridge/discord/db/file.rb +0 -118
- data/lib/async/matrix/bridge/discord/db/guild.rb +0 -120
- data/lib/async/matrix/bridge/discord/db/message.rb +0 -160
- data/lib/async/matrix/bridge/discord/db/migrations/001_create_users.rb +0 -14
- data/lib/async/matrix/bridge/discord/db/migrations/002_create_guilds.rb +0 -14
- data/lib/async/matrix/bridge/discord/db/migrations/003_create_portals.rb +0 -23
- data/lib/async/matrix/bridge/discord/db/migrations/004_create_puppets.rb +0 -19
- data/lib/async/matrix/bridge/discord/db/migrations/005_create_messages.rb +0 -20
- data/lib/async/matrix/bridge/discord/db/migrations/006_create_reactions.rb +0 -19
- data/lib/async/matrix/bridge/discord/db/migrations/007_create_files.rb +0 -18
- data/lib/async/matrix/bridge/discord/db/portal.rb +0 -150
- data/lib/async/matrix/bridge/discord/db/puppet.rb +0 -128
- data/lib/async/matrix/bridge/discord/db/reaction.rb +0 -165
- data/lib/async/matrix/bridge/discord/db/schema.rb +0 -18
- data/lib/async/matrix/bridge/discord/db/user.rb +0 -112
- data/lib/async/matrix/bridge/discord/db.rb +0 -138
- data/lib/async/matrix/schema/registry.rb +0 -354
- data/lib/async/matrix/schema/validation_error.rb +0 -225
- data/lib/async/matrix/schema.rb +0 -170
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the Apache License, Version 2.0.
|
|
4
|
+
# Copyright, 2026, by General Intelligence Systems.
|
|
5
|
+
|
|
6
|
+
require_relative "error"
|
|
7
|
+
require_relative "event"
|
|
8
|
+
require_relative "encrypted_message"
|
|
9
|
+
|
|
10
|
+
module Protocol
|
|
11
|
+
module Matrix
|
|
12
|
+
# One batch of messages, read exactly once.
|
|
13
|
+
#
|
|
14
|
+
# A homeserver hands over messages in batches, never one at a time: a /sync
|
|
15
|
+
# response or an application service transaction arrives whole, carrying
|
|
16
|
+
# to-device messages and room timelines together. This is that batch, turned
|
|
17
|
+
# into a single-pass stream — #read gives the next message or nil, in the
|
|
18
|
+
# shape Protocol::GRPC::Body::Readable uses for gRPC frames.
|
|
19
|
+
#
|
|
20
|
+
# batch = MessageBatch.from_sync(response)
|
|
21
|
+
#
|
|
22
|
+
# while message = batch.read
|
|
23
|
+
# message.to_device? # plumbing (room keys) or timeline?
|
|
24
|
+
# message.encrypted? # needs a session before it can be read?
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# batch.next_batch # the cursor this batch ends at
|
|
28
|
+
#
|
|
29
|
+
# NOT REWINDABLE, AND THAT IS THE POINT. Reading a message is not free:
|
|
30
|
+
# decrypting one ratchets a session forward, so "read it again" is not a
|
|
31
|
+
# thing the protocol can offer. A drained batch answers nil forever, and
|
|
32
|
+
# #each consumes rather than iterating a collection. If a consumer needs to
|
|
33
|
+
# keep a message, it keeps the message — not the batch.
|
|
34
|
+
#
|
|
35
|
+
# TO-DEVICE MESSAGES COME FIRST, always. A batch routinely carries both an
|
|
36
|
+
# `m.room_key` and the timeline event that key unlocks, and the order they
|
|
37
|
+
# are read in decides whether that event decrypts on the first pass or is
|
|
38
|
+
# stored as a placeholder and repaired later. Making the stream ordered is
|
|
39
|
+
# what takes that decision away from the caller and from any scheduler: a
|
|
40
|
+
# timeline message is unreachable until the to-device messages ahead of it
|
|
41
|
+
# have been read.
|
|
42
|
+
#
|
|
43
|
+
# The two wire formats differ, and only in how they are unpacked:
|
|
44
|
+
#
|
|
45
|
+
# /sync to_device.events, then rooms.{join,invite,leave}
|
|
46
|
+
# AS transaction de.sorunome.msc2409.to_device, then events
|
|
47
|
+
#
|
|
48
|
+
# So they get two constructors and produce one stream, which is why an
|
|
49
|
+
# appservice and a syncing client can share everything downstream of here.
|
|
50
|
+
class MessageBatch
|
|
51
|
+
# Where a message sat in the batch it came from. Carried because a
|
|
52
|
+
# consumer projecting rooms needs to know that an event arrived as invite
|
|
53
|
+
# state rather than timeline, which the event itself does not say.
|
|
54
|
+
SECTIONS = %i[to_device join invite leave transaction].freeze
|
|
55
|
+
|
|
56
|
+
class Error < Protocol::Matrix::Error; end
|
|
57
|
+
|
|
58
|
+
# #read after the batch was drained is fine (nil). This is for a consumer
|
|
59
|
+
# that asks for a second pass over something already consumed.
|
|
60
|
+
class ConsumedError < Error; end
|
|
61
|
+
|
|
62
|
+
# Unpack a /sync response.
|
|
63
|
+
#
|
|
64
|
+
# @parameter response [Hash] the parsed response body, string keys.
|
|
65
|
+
def self.from_sync(response)
|
|
66
|
+
messages = []
|
|
67
|
+
|
|
68
|
+
to_device = response.dig("to_device", "events") || []
|
|
69
|
+
to_device.each { |event| messages << build(event, :to_device, nil) }
|
|
70
|
+
|
|
71
|
+
rooms = response["rooms"] || {}
|
|
72
|
+
|
|
73
|
+
(rooms["join"] || {}).each do |room_id, data|
|
|
74
|
+
timeline = data.dig("timeline", "events") || []
|
|
75
|
+
timeline.each { |event| messages << build(event, :join, room_id) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Invites carry stripped state and no timeline, but it is the only thing
|
|
79
|
+
# that says who invited us, so dropping it loses the invite itself.
|
|
80
|
+
(rooms["invite"] || {}).each do |room_id, data|
|
|
81
|
+
state = data.dig("invite_state", "events") || []
|
|
82
|
+
state.each { |event| messages << build(event, :invite, room_id) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
(rooms["leave"] || {}).each do |room_id, data|
|
|
86
|
+
timeline = data.dig("timeline", "events") || []
|
|
87
|
+
timeline.each { |event| messages << build(event, :leave, room_id) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
new(messages, next_batch: response["next_batch"])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Unpack an application service transaction.
|
|
94
|
+
#
|
|
95
|
+
# To-device messages ride the MSC4203 key, which Synapse still sends under
|
|
96
|
+
# its original MSC2409 name; the stable spelling is accepted first so this
|
|
97
|
+
# keeps working when that changes.
|
|
98
|
+
#
|
|
99
|
+
# @parameter transaction [Hash] the parsed PUT body, string keys.
|
|
100
|
+
# @parameter txn_id [String] the transaction id, which is this batch's
|
|
101
|
+
# cursor — the appservice equivalent of next_batch.
|
|
102
|
+
def self.from_transaction(transaction, txn_id: nil)
|
|
103
|
+
messages = []
|
|
104
|
+
|
|
105
|
+
to_device = transaction["to_device"] ||
|
|
106
|
+
transaction["de.sorunome.msc2409.to_device"] || []
|
|
107
|
+
to_device.each { |event| messages << build(event, :to_device, nil) }
|
|
108
|
+
|
|
109
|
+
# Transaction events carry their own room_id; there is no per-room
|
|
110
|
+
# grouping to unpack.
|
|
111
|
+
(transaction["events"] || []).each do |event|
|
|
112
|
+
messages << build(event, :transaction, event["room_id"])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
new(messages, next_batch: txn_id)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# An encrypted event becomes an EncryptedMessage, anything else an Event.
|
|
119
|
+
# Both answer #type, #content, #encrypted?, #decrypted? and #to_device?,
|
|
120
|
+
# so a consumer reads either without asking which it has.
|
|
121
|
+
def self.build(event, section, room_id)
|
|
122
|
+
# A to-device event has no room_id of its own; a /sync room section
|
|
123
|
+
# knows the room its events belong to, and the events themselves may
|
|
124
|
+
# omit it. Supplying it keeps #to_device? honest either way.
|
|
125
|
+
if room_id && event["room_id"].nil?
|
|
126
|
+
event = event.merge("room_id" => room_id)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if EncryptedMessage.encrypted?(event)
|
|
130
|
+
[EncryptedMessage.new(event), section]
|
|
131
|
+
else
|
|
132
|
+
[Event.new(event), section]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def initialize(messages, next_batch: nil)
|
|
137
|
+
@messages = messages
|
|
138
|
+
@next_batch = next_batch
|
|
139
|
+
@position = 0
|
|
140
|
+
@section = nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The cursor this batch ends at: `next_batch` from /sync, the transaction
|
|
144
|
+
# id from an appservice.
|
|
145
|
+
#
|
|
146
|
+
# COMMIT IT ONLY ONCE THE BATCH IS DRAINED AND ITS EFFECTS ARE DURABLE.
|
|
147
|
+
# Storing it earlier means a crash loses every message still in this
|
|
148
|
+
# batch, with nothing anywhere recording that they were missed.
|
|
149
|
+
attr_reader :next_batch
|
|
150
|
+
|
|
151
|
+
# How many messages the batch held. Fixed at construction — it does not
|
|
152
|
+
# count down as the batch is read.
|
|
153
|
+
def size = @messages.length
|
|
154
|
+
|
|
155
|
+
# Have all of them been read?
|
|
156
|
+
def drained? = @position >= @messages.length
|
|
157
|
+
|
|
158
|
+
# The next message, or nil once drained. To-device messages first.
|
|
159
|
+
def read
|
|
160
|
+
if drained?
|
|
161
|
+
nil
|
|
162
|
+
else
|
|
163
|
+
message, section = @messages[@position]
|
|
164
|
+
@position += 1
|
|
165
|
+
@section = section
|
|
166
|
+
message
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The section the message most recently returned by #read came from, one
|
|
171
|
+
# of SECTIONS. Read it after #read, not before.
|
|
172
|
+
attr_reader :section
|
|
173
|
+
|
|
174
|
+
# Did the last message read arrive as to-device rather than from a room?
|
|
175
|
+
#
|
|
176
|
+
# ON THE BATCH, NOT THE MESSAGE, because the batch is the only thing that
|
|
177
|
+
# knows. Asking the message would mean inferring it from a missing
|
|
178
|
+
# room_id, and that is wrong twice over: a transaction timeline event may
|
|
179
|
+
# omit the room_id the section supplies, and inference would call it
|
|
180
|
+
# to-device. The section is stated by the wire format; nothing has to
|
|
181
|
+
# guess.
|
|
182
|
+
def to_device? = @section == :to_device
|
|
183
|
+
|
|
184
|
+
# Read every remaining message. CONSUMES the batch: this is a drain, not
|
|
185
|
+
# an iteration, and a second call yields nothing because there is nothing
|
|
186
|
+
# left rather than because iteration restarted.
|
|
187
|
+
def each
|
|
188
|
+
unless block_given?
|
|
189
|
+
raise ConsumedError, "MessageBatch#each requires a block; a batch is not enumerable"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
while (message = read)
|
|
193
|
+
yield(message, @section)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
self
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
__END__
|
|
203
|
+
describe "Protocol::Matrix::MessageBatch" do
|
|
204
|
+
def to_device_event
|
|
205
|
+
{
|
|
206
|
+
"type" => "m.room.encrypted",
|
|
207
|
+
"sender" => "@bob:example.org",
|
|
208
|
+
"content" => {
|
|
209
|
+
"algorithm" => "m.olm.v1.curve25519-aes-sha2",
|
|
210
|
+
"sender_key" => "senderkey",
|
|
211
|
+
"ciphertext" => {"ourkey" => {"type" => 0, "body" => "body"}},
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def encrypted_timeline_event(event_id = "$enc1")
|
|
217
|
+
{
|
|
218
|
+
"type" => "m.room.encrypted",
|
|
219
|
+
"event_id" => event_id,
|
|
220
|
+
"sender" => "@alice:example.org",
|
|
221
|
+
"origin_server_ts" => 1,
|
|
222
|
+
"content" => {
|
|
223
|
+
"algorithm" => "m.megolm.v1.aes-sha2",
|
|
224
|
+
"ciphertext" => "AwgAEnAC",
|
|
225
|
+
"session_id" => "session1",
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def plaintext_event(event_id = "$msg1")
|
|
231
|
+
{
|
|
232
|
+
"type" => "m.room.message",
|
|
233
|
+
"event_id" => event_id,
|
|
234
|
+
"sender" => "@alice:example.org",
|
|
235
|
+
"origin_server_ts" => 2,
|
|
236
|
+
"content" => {"msgtype" => "m.text", "body" => "hello"},
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def sync_response(extra = {})
|
|
241
|
+
{
|
|
242
|
+
"next_batch" => "s72_1234",
|
|
243
|
+
"to_device" => {"events" => [to_device_event]},
|
|
244
|
+
"rooms" => {
|
|
245
|
+
"join" => {
|
|
246
|
+
"!room:example.org" => {
|
|
247
|
+
"timeline" => {"events" => [encrypted_timeline_event, plaintext_event]},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
}.merge(extra)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# ── Unpacking /sync ───────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
it "reads every message in the batch, then nil" do
|
|
257
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
|
|
258
|
+
|
|
259
|
+
batch.size.should == 3
|
|
260
|
+
batch.read.should.be.kind_of Protocol::Matrix::EncryptedMessage
|
|
261
|
+
batch.read.should.be.kind_of Protocol::Matrix::EncryptedMessage
|
|
262
|
+
batch.read.should.be.kind_of Protocol::Matrix::Event
|
|
263
|
+
batch.read.should.be.nil
|
|
264
|
+
batch.drained?.should == true
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# THE ORDERING GUARANTEE. A batch carrying both a room key and the event it
|
|
268
|
+
# unlocks must hand over the key first, or the event decrypts as a
|
|
269
|
+
# placeholder on the first pass.
|
|
270
|
+
it "yields to-device messages before any timeline message" do
|
|
271
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
|
|
272
|
+
|
|
273
|
+
batch.read
|
|
274
|
+
batch.to_device?.should == true
|
|
275
|
+
batch.section.should == :to_device
|
|
276
|
+
|
|
277
|
+
batch.read
|
|
278
|
+
batch.to_device?.should == false
|
|
279
|
+
batch.section.should == :join
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it "carries the cursor the batch ends at" do
|
|
283
|
+
Protocol::Matrix::MessageBatch.from_sync(sync_response).next_batch.should == "s72_1234"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
it "builds an EncryptedMessage for an encrypted event and an Event otherwise" do
|
|
287
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
|
|
288
|
+
batch.read
|
|
289
|
+
|
|
290
|
+
encrypted = batch.read
|
|
291
|
+
encrypted.encrypted?.should == true
|
|
292
|
+
encrypted.decrypted?.should == false
|
|
293
|
+
encrypted.session_id.should == "session1"
|
|
294
|
+
|
|
295
|
+
plain = batch.read
|
|
296
|
+
plain.encrypted?.should == false
|
|
297
|
+
plain.decrypted?.should == true
|
|
298
|
+
plain.type.should == "m.room.message"
|
|
299
|
+
plain.content.body.should == "hello"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# A /sync room section knows the room; the events inside it may not repeat it.
|
|
303
|
+
it "attributes a timeline event to its room" do
|
|
304
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
|
|
305
|
+
batch.read
|
|
306
|
+
|
|
307
|
+
batch.read.room_id.should == "!room:example.org"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
it "leaves a to-device message without a room" do
|
|
311
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
|
|
312
|
+
|
|
313
|
+
batch.read.room_id.should.be.nil
|
|
314
|
+
batch.to_device?.should == true
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
it "does not overwrite a room_id the event already carries" do
|
|
318
|
+
event = encrypted_timeline_event.merge("room_id" => "!actual:example.org")
|
|
319
|
+
response = {
|
|
320
|
+
"rooms" => {"join" => {"!section:example.org" => {"timeline" => {"events" => [event]}}}},
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
Protocol::Matrix::MessageBatch.from_sync(response).read.room_id.should == "!actual:example.org"
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it "reads invite state, which is the only record of the invite" do
|
|
327
|
+
response = {
|
|
328
|
+
"rooms" => {
|
|
329
|
+
"invite" => {
|
|
330
|
+
"!invited:example.org" => {
|
|
331
|
+
"invite_state" => {
|
|
332
|
+
"events" => [{
|
|
333
|
+
"type" => "m.room.member",
|
|
334
|
+
"sender" => "@carol:example.org",
|
|
335
|
+
"state_key" => "@us:example.org",
|
|
336
|
+
"content" => {"membership" => "invite"},
|
|
337
|
+
}],
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(response)
|
|
344
|
+
message = batch.read
|
|
345
|
+
|
|
346
|
+
message.type.should == "m.room.member"
|
|
347
|
+
message.room_id.should == "!invited:example.org"
|
|
348
|
+
batch.section.should == :invite
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it "reads the timeline of a room we left" do
|
|
352
|
+
response = {
|
|
353
|
+
"rooms" => {
|
|
354
|
+
"leave" => {"!gone:example.org" => {"timeline" => {"events" => [plaintext_event]}}},
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(response)
|
|
358
|
+
batch.read
|
|
359
|
+
|
|
360
|
+
batch.section.should == :leave
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
it "handles an empty sync with nothing in it" do
|
|
364
|
+
batch = Protocol::Matrix::MessageBatch.from_sync({"next_batch" => "s1"})
|
|
365
|
+
|
|
366
|
+
batch.size.should == 0
|
|
367
|
+
batch.drained?.should == true
|
|
368
|
+
batch.read.should.be.nil
|
|
369
|
+
batch.next_batch.should == "s1"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# ── Unpacking an appservice transaction ───────────────────────────────────
|
|
373
|
+
|
|
374
|
+
it "unpacks a transaction into the same stream" do
|
|
375
|
+
transaction = {
|
|
376
|
+
"de.sorunome.msc2409.to_device" => [to_device_event],
|
|
377
|
+
"events" => [encrypted_timeline_event.merge("room_id" => "!room:example.org")],
|
|
378
|
+
}
|
|
379
|
+
batch = Protocol::Matrix::MessageBatch.from_transaction(transaction, txn_id: "txn42")
|
|
380
|
+
|
|
381
|
+
batch.size.should == 2
|
|
382
|
+
batch.read
|
|
383
|
+
batch.to_device?.should == true
|
|
384
|
+
batch.section.should == :to_device
|
|
385
|
+
|
|
386
|
+
timeline = batch.read
|
|
387
|
+
timeline.room_id.should == "!room:example.org"
|
|
388
|
+
batch.section.should == :transaction
|
|
389
|
+
batch.next_batch.should == "txn42"
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Synapse sends the unstable MSC2409 spelling today; the stable MSC4203 key
|
|
393
|
+
# is preferred so this keeps working when that lands.
|
|
394
|
+
it "prefers the stable to_device key over the unstable one" do
|
|
395
|
+
transaction = {
|
|
396
|
+
"to_device" => [to_device_event],
|
|
397
|
+
"de.sorunome.msc2409.to_device" => [to_device_event, to_device_event],
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
Protocol::Matrix::MessageBatch.from_transaction(transaction).size.should == 1
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
it "handles a transaction with no to-device messages" do
|
|
404
|
+
transaction = {"events" => [plaintext_event]}
|
|
405
|
+
batch = Protocol::Matrix::MessageBatch.from_transaction(transaction)
|
|
406
|
+
|
|
407
|
+
batch.size.should == 1
|
|
408
|
+
batch.read.type.should == "m.room.message"
|
|
409
|
+
batch.to_device?.should == false
|
|
410
|
+
batch.section.should == :transaction
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
it "handles an empty transaction" do
|
|
414
|
+
batch = Protocol::Matrix::MessageBatch.from_transaction({})
|
|
415
|
+
|
|
416
|
+
batch.size.should == 0
|
|
417
|
+
batch.read.should.be.nil
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# ── Read exactly once ─────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
# Decrypting ratchets a session forward, so "read it again" is not something
|
|
423
|
+
# the protocol can offer. A drained batch stays drained.
|
|
424
|
+
it "cannot be rewound" do
|
|
425
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
|
|
426
|
+
3.times { batch.read }
|
|
427
|
+
|
|
428
|
+
batch.read.should.be.nil
|
|
429
|
+
batch.read.should.be.nil
|
|
430
|
+
batch.drained?.should == true
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
it "consumes the batch when drained with each" do
|
|
434
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
|
|
435
|
+
seen = []
|
|
436
|
+
batch.each { |message| seen << message }
|
|
437
|
+
|
|
438
|
+
seen.length.should == 3
|
|
439
|
+
batch.drained?.should == true
|
|
440
|
+
|
|
441
|
+
again = []
|
|
442
|
+
batch.each { |message| again << message }
|
|
443
|
+
again.should == []
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
it "reports the section alongside each message in each" do
|
|
447
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
|
|
448
|
+
sections = []
|
|
449
|
+
batch.each { |_message, section| sections << section }
|
|
450
|
+
|
|
451
|
+
sections.should == [:to_device, :join, :join]
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
it "continues a partly read batch rather than restarting" do
|
|
455
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
|
|
456
|
+
batch.read
|
|
457
|
+
|
|
458
|
+
remaining = []
|
|
459
|
+
batch.each { |message| remaining << message }
|
|
460
|
+
|
|
461
|
+
remaining.length.should == 2
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# A batch is a stream, not a collection: there is no non-consuming read, so
|
|
465
|
+
# #each without a block has nothing sensible to return.
|
|
466
|
+
it "refuses to be treated as an enumerable" do
|
|
467
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
|
|
468
|
+
|
|
469
|
+
lambda { batch.each }.should.raise(Protocol::Matrix::MessageBatch::ConsumedError)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
it "keeps size fixed as it is read" do
|
|
473
|
+
batch = Protocol::Matrix::MessageBatch.from_sync(sync_response)
|
|
474
|
+
batch.read
|
|
475
|
+
|
|
476
|
+
batch.size.should == 3
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
it "has no section before anything has been read" do
|
|
480
|
+
Protocol::Matrix::MessageBatch.from_sync(sync_response).section.should.be.nil
|
|
481
|
+
end
|
|
482
|
+
end
|