async-matrix 2.0.1 → 3.0.0
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/Cargo.lock +8 -8
- data/README.md +33 -58
- data/ext/async_matrix_e2ee/Cargo.toml +7 -1
- data/ext/async_matrix_e2ee/src/lib.rs +43 -2
- 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/auth_error.rb +20 -0
- data/lib/async/matrix/bad_json_error.rb +20 -0
- data/lib/async/matrix/client.rb +142 -110
- data/lib/async/matrix/config/vivify.rb +107 -0
- data/lib/async/matrix/config.rb +184 -0
- data/lib/async/matrix/content.rb +106 -0
- data/lib/async/matrix/double_puppet_client.rb +1 -1
- data/lib/async/matrix/e2ee.rb +37 -0
- data/lib/async/matrix/error.rb +0 -27
- data/lib/async/matrix/{application_service/error_response.rb → error_response.rb} +10 -10
- data/lib/async/matrix/event.rb +197 -0
- 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/schema/registry.rb +367 -340
- data/lib/async/matrix/schema/validation_error.rb +221 -214
- data/lib/async/matrix/schema.rb +161 -161
- data/lib/async/matrix/version.rb +3 -3
- data/lib/async/matrix.rb +0 -7
- metadata +28 -122
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65f191895135eb9d4f0580991c9beca1a319ed524c12b29d4f52c2cbff4aebc9
|
|
4
|
+
data.tar.gz: d3331bde0b5cea1d2e69081ab72747e72903f4c6977c9fb331e990ff0e1c31f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: deffa5fa0c1bb2cbdf0ca642400bfa67c0b4bdc2839472a3923f3fc1aacccc060159b0b5d8646ffb8d544b577020a1035c07956fb884132ad2293ff78e27a3f1
|
|
7
|
+
data.tar.gz: 35de48c7bf9adb335031dbf8c9036947bea84f57cf8c0c3a4b22551b3be682c1df584eb9cc883c09c1ec109fd25772d96ad6d978c96385e7e87149301b032ff4
|
data/Cargo.lock
CHANGED
|
@@ -423,9 +423,9 @@ dependencies = [
|
|
|
423
423
|
|
|
424
424
|
[[package]]
|
|
425
425
|
name = "magnus"
|
|
426
|
-
version = "0.
|
|
426
|
+
version = "0.9.2"
|
|
427
427
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
428
|
-
checksum = "
|
|
428
|
+
checksum = "b9068710e00d21be762edca59b9c4c8946dbdb29820d4f76ad559afea07982c9"
|
|
429
429
|
dependencies = [
|
|
430
430
|
"magnus-macros",
|
|
431
431
|
"rb-sys",
|
|
@@ -435,9 +435,9 @@ dependencies = [
|
|
|
435
435
|
|
|
436
436
|
[[package]]
|
|
437
437
|
name = "magnus-macros"
|
|
438
|
-
version = "0.
|
|
438
|
+
version = "0.9.1"
|
|
439
439
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
440
|
-
checksum = "
|
|
440
|
+
checksum = "9dafde2eeb66f090e0f326112323eb8324b12cb32b5d1210e541d3df6b0ce77e"
|
|
441
441
|
dependencies = [
|
|
442
442
|
"proc-macro2",
|
|
443
443
|
"quote",
|
|
@@ -606,18 +606,18 @@ dependencies = [
|
|
|
606
606
|
|
|
607
607
|
[[package]]
|
|
608
608
|
name = "rb-sys"
|
|
609
|
-
version = "0.9.
|
|
609
|
+
version = "0.9.130"
|
|
610
610
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
611
|
-
checksum = "
|
|
611
|
+
checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb"
|
|
612
612
|
dependencies = [
|
|
613
613
|
"rb-sys-build",
|
|
614
614
|
]
|
|
615
615
|
|
|
616
616
|
[[package]]
|
|
617
617
|
name = "rb-sys-build"
|
|
618
|
-
version = "0.9.
|
|
618
|
+
version = "0.9.130"
|
|
619
619
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
620
|
-
checksum = "
|
|
620
|
+
checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694"
|
|
621
621
|
dependencies = [
|
|
622
622
|
"bindgen",
|
|
623
623
|
"lazy_static",
|
data/README.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
[](https://github.com/general-intelligence-systems/async-matrix/blob/main/LICENSE)
|
|
6
6
|
[](https://www.ruby-lang.org)
|
|
7
7
|
|
|
8
|
-
Async-native [Matrix](https://matrix.org)
|
|
8
|
+
Async-native [Matrix](https://matrix.org) protocol primitives for Ruby. Built on the [Socketry](https://github.com/socketry) ecosystem (`async`, `async-http`, Falcon). No threads, no callbacks -- just fibers.
|
|
9
|
+
|
|
10
|
+
This gem is the protocol layer: a Client-Server API client, schema-validated events, media, and end-to-end encryption. The *server* side of a bridge or bot -- receiving transactions from a homeserver and dispatching events to handlers -- is [async-matrix-bridge](https://github.com/general-intelligence-systems/async-matrix-bridge), which builds on this gem.
|
|
9
11
|
|
|
10
12
|
## Usage
|
|
11
13
|
|
|
@@ -20,60 +22,17 @@ gem "async-matrix"
|
|
|
20
22
|
## Quick Start
|
|
21
23
|
|
|
22
24
|
```ruby
|
|
23
|
-
# config.ru
|
|
24
25
|
require "async/matrix"
|
|
25
26
|
|
|
26
|
-
config = Async::Matrix::
|
|
27
|
+
config = Async::Matrix::Config.load("config/appservice.yml")
|
|
27
28
|
client = Async::Matrix::Client.new(config)
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
on "m.room.message", msgtype: "m.text", not_from: :self do |event|
|
|
35
|
-
send_notice event.room_id, "Echo: #{event.content.body}"
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
app = Async::Matrix::ApplicationService::Server.new(
|
|
40
|
-
hs_token: config.appservice.hs_token,
|
|
41
|
-
client: client
|
|
42
|
-
) do
|
|
43
|
-
dispatch bot
|
|
30
|
+
Async do
|
|
31
|
+
client.join_room("!room:example.org")
|
|
32
|
+
client.send_notice("!room:example.org", "Hello from a fiber")
|
|
44
33
|
end
|
|
45
|
-
|
|
46
|
-
run app
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
falcon serve --bind http://0.0.0.0:9292
|
|
51
34
|
```
|
|
52
35
|
|
|
53
|
-
A complete working example with Docker Compose and Synapse lives in [`examples/echo_bot/`](https://github.com/general-intelligence-systems/async-matrix/tree/main/examples/echo_bot).
|
|
54
|
-
|
|
55
|
-
## Handlers
|
|
56
|
-
|
|
57
|
-
Any object that responds to `#event_types` and `#call(event)` is a handler. Use this when you need more control than the Bot DSL provides.
|
|
58
|
-
|
|
59
|
-
```ruby
|
|
60
|
-
class Echo
|
|
61
|
-
def initialize(client) = @client = client
|
|
62
|
-
|
|
63
|
-
def event_types = ["m.room.message"]
|
|
64
|
-
|
|
65
|
-
def call(event)
|
|
66
|
-
return unless event.content&.msgtype == "m.text"
|
|
67
|
-
return unless event.sender != @client.config.bot_mxid
|
|
68
|
-
@client.send_notice(event.room_id, "Echo: #{event.content.body}")
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
app.dispatch(Echo.new(client))
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Dispatch is fault-tolerant -- one handler raising won't take down the rest.
|
|
76
|
-
|
|
77
36
|
## Client
|
|
78
37
|
|
|
79
38
|
```ruby
|
|
@@ -93,40 +52,56 @@ client.api.createRoom.post(name: "Pub")
|
|
|
93
52
|
client.api.rooms("!room:ex.com").messages.get(dir: "b", limit: 10)
|
|
94
53
|
```
|
|
95
54
|
|
|
96
|
-
|
|
55
|
+
Binary routes (upload, download, thumbnail) are detected and dispatched to a dedicated `MediaClient`, so raw bytes never pass through JSON encoding.
|
|
56
|
+
|
|
57
|
+
All methods are fiber-safe with automatic connection pooling. The client retries 502/503/504 with exponential backoff and full jitter, honours `Retry-After` on 429, and caps response bodies while streaming them.
|
|
58
|
+
|
|
59
|
+
## Events
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
event = Async::Matrix::Schema.parse(raw_hash)
|
|
63
|
+
|
|
64
|
+
event.type # => "m.room.message"
|
|
65
|
+
event.sender # => "@alice:example.org"
|
|
66
|
+
event.content.body # => "hello"
|
|
67
|
+
event.valid? # => true
|
|
68
|
+
event.valid! # => true, or raises Schema::ValidationError
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Events validate against the official Matrix event JSON schemas, bundled into the gem.
|
|
97
72
|
|
|
98
73
|
## Configuration
|
|
99
74
|
|
|
100
|
-
|
|
75
|
+
`Async::Matrix::Config` loads a YAML file (or a plain hash) and exposes it through dot notation. `Client` reads `homeserver.address` and `appservice.as_token`; it ignores the rest.
|
|
101
76
|
|
|
102
77
|
```yaml
|
|
78
|
+
# config/appservice.yml
|
|
103
79
|
homeserver:
|
|
104
80
|
address: "http://synapse:8008"
|
|
105
81
|
domain: "localhost"
|
|
106
82
|
|
|
107
83
|
appservice:
|
|
108
84
|
as_token: "your-appservice-token"
|
|
109
|
-
hs_token: "your-homeserver-token"
|
|
110
85
|
bot:
|
|
111
86
|
username: "bot"
|
|
112
87
|
```
|
|
113
88
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
APPSERVICE_CONFIG=/etc/bot/appservice.yml falcon serve
|
|
89
|
+
```ruby
|
|
90
|
+
config = Async::Matrix::Config.load("config/appservice.yml")
|
|
91
|
+
config.homeserver.address # => "http://synapse:8008"
|
|
92
|
+
config.bot_mxid # => "@bot:localhost"
|
|
120
93
|
```
|
|
121
94
|
|
|
95
|
+
It validates nothing by default. A subclass adds validation by overriding `.validate!` -- which is how async-matrix-bridge layers a mautrix bridgev2 JSON Schema suite on top without reimplementing the loading.
|
|
96
|
+
|
|
122
97
|
## Built With
|
|
123
98
|
|
|
124
99
|
- [async](https://github.com/socketry/async) -- fiber-based concurrency framework
|
|
125
100
|
- [async-http](https://github.com/socketry/async-http) -- HTTP client/server with connection pooling
|
|
126
|
-
- [falcon](https://github.com/socketry/falcon) -- async Rack-compatible web server
|
|
127
101
|
- [json_schemer](https://github.com/davishmcclurg/json_schemer) -- JSON Schema validation
|
|
128
102
|
- [scampi](https://github.com/general-intelligence-systems/scampi) -- inline co-located test framework
|
|
129
103
|
- [string_builder](https://github.com/general-intelligence-systems/string_builder) -- method-chain string builder
|
|
104
|
+
- [vodozemac](https://github.com/matrix-org/vodozemac) -- Olm/Megolm, via a native Rust binding
|
|
130
105
|
|
|
131
106
|
## License
|
|
132
107
|
|
|
@@ -11,7 +11,13 @@ crate-type = ["cdylib"]
|
|
|
11
11
|
[dependencies]
|
|
12
12
|
# magnus 0.8 is required for Ruby 4.0 C-API compatibility (0.7 fails to compile
|
|
13
13
|
# against Ruby 4.0: rb_fiber_raise pointer mutability + RTypedData.typed_flag).
|
|
14
|
-
|
|
14
|
+
#
|
|
15
|
+
# 0.9 is required to cross-compile for arm-linux-musl. On 32-bit musl, bindgen
|
|
16
|
+
# renders timespec's padding around tv_nsec as bitfield members, so magnus 0.8's
|
|
17
|
+
# `From<Timespec> for timespec` struct literal fails with E0063 (missing fields
|
|
18
|
+
# `_bitfield_1`, `_bitfield_align_1`). 0.9 builds the struct with mem::zeroed
|
|
19
|
+
# and assigns the two fields instead.
|
|
20
|
+
magnus = "0.9"
|
|
15
21
|
|
|
16
22
|
# vodozemac 0.10.0 (2026-04-13), default features (libolm-compat).
|
|
17
23
|
#
|
|
@@ -20,8 +20,9 @@ use vodozemac::olm::{
|
|
|
20
20
|
SessionConfig as OlmSessionConfig, SessionPickle,
|
|
21
21
|
};
|
|
22
22
|
use vodozemac::megolm::{
|
|
23
|
-
GroupSession as MegolmGroupSession, GroupSessionPickle,
|
|
24
|
-
|
|
23
|
+
ExportedSessionKey, GroupSession as MegolmGroupSession, GroupSessionPickle,
|
|
24
|
+
InboundGroupSession as MegolmInbound, InboundGroupSessionPickle, MegolmMessage,
|
|
25
|
+
SessionConfig as MegolmSessionConfig, SessionKey,
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
// --- helpers ---------------------------------------------------------------
|
|
@@ -236,6 +237,27 @@ impl InboundGroupSession {
|
|
|
236
237
|
))))
|
|
237
238
|
}
|
|
238
239
|
|
|
240
|
+
/// Build from a base64 **exported** session key.
|
|
241
|
+
///
|
|
242
|
+
/// This is the format room keys travel in outside of `m.room_key`: server-side
|
|
243
|
+
/// key backup (`m.megolm_backup.v1.curve25519-aes-sha2`) and
|
|
244
|
+
/// `m.forwarded_room_key` both carry an `ExportedSessionKey`, which is
|
|
245
|
+
/// version 1 and carries no signature — so `new` (which requires a signed
|
|
246
|
+
/// version 2 `SessionKey`) cannot accept them. Without this, a client can
|
|
247
|
+
/// never read history it did not receive a live `m.room_key` for.
|
|
248
|
+
///
|
|
249
|
+
/// The resulting session is NOT signature-verified, because an exported key
|
|
250
|
+
/// has no signature to verify. That is inherent to the format: the sender
|
|
251
|
+
/// identity of such a session is only as trustworthy as whoever handed it
|
|
252
|
+
/// over, which is why the spec treats backup-restored keys as unverified.
|
|
253
|
+
fn import(exported_key: String) -> Result<Self, Error> {
|
|
254
|
+
let key = ExportedSessionKey::from_base64(&exported_key).map_err(rt_err)?;
|
|
255
|
+
Ok(InboundGroupSession(RefCell::new(MegolmInbound::import(
|
|
256
|
+
&key,
|
|
257
|
+
MegolmSessionConfig::version_1(),
|
|
258
|
+
))))
|
|
259
|
+
}
|
|
260
|
+
|
|
239
261
|
fn session_id(&self) -> String {
|
|
240
262
|
self.0.borrow().session_id()
|
|
241
263
|
}
|
|
@@ -244,6 +266,19 @@ impl InboundGroupSession {
|
|
|
244
266
|
self.0.borrow().first_known_index()
|
|
245
267
|
}
|
|
246
268
|
|
|
269
|
+
/// Export this session at `index`, base64, for uploading to key backup or
|
|
270
|
+
/// forwarding to another device. Returns nil when the ratchet has already
|
|
271
|
+
/// moved past `index` and can no longer produce a key for it — megolm
|
|
272
|
+
/// ratchets forward only, so an earlier index is unrecoverable by design.
|
|
273
|
+
fn export_at(&self, index: u32) -> Option<String> {
|
|
274
|
+
self.0.borrow_mut().export_at(index).map(|key| key.to_base64())
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// Export at the earliest index this session can still reach.
|
|
278
|
+
fn export_at_first_known_index(&self) -> String {
|
|
279
|
+
self.0.borrow().export_at_first_known_index().to_base64()
|
|
280
|
+
}
|
|
281
|
+
|
|
247
282
|
/// Decrypts a base64 megolm message; returns `[plaintext, message_index]`.
|
|
248
283
|
fn decrypt(&self, message: String) -> Result<(String, u32), Error> {
|
|
249
284
|
let msg = MegolmMessage::from_base64(&message).map_err(rt_err)?;
|
|
@@ -316,11 +351,17 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
316
351
|
|
|
317
352
|
let inbound = namespace.define_class("InboundGroupSession", ruby.class_object())?;
|
|
318
353
|
inbound.define_singleton_method("new", function!(InboundGroupSession::new, 1))?;
|
|
354
|
+
inbound.define_singleton_method("import", function!(InboundGroupSession::import, 1))?;
|
|
319
355
|
inbound.define_singleton_method("from_pickle", function!(InboundGroupSession::from_pickle, 2))?;
|
|
320
356
|
inbound.define_method("session_id", method!(InboundGroupSession::session_id, 0))?;
|
|
321
357
|
inbound.define_method("first_known_index", method!(InboundGroupSession::first_known_index, 0))?;
|
|
322
358
|
inbound.define_method("decrypt", method!(InboundGroupSession::decrypt, 1))?;
|
|
323
359
|
inbound.define_method("pickle", method!(InboundGroupSession::pickle, 1))?;
|
|
360
|
+
inbound.define_method("export_at", method!(InboundGroupSession::export_at, 1))?;
|
|
361
|
+
inbound.define_method(
|
|
362
|
+
"export_at_first_known_index",
|
|
363
|
+
method!(InboundGroupSession::export_at_first_known_index, 0),
|
|
364
|
+
)?;
|
|
324
365
|
|
|
325
366
|
namespace.define_module_function("verify_signature", function!(verify_signature, 3))?;
|
|
326
367
|
|
|
@@ -108,122 +108,148 @@ module Async
|
|
|
108
108
|
|
|
109
109
|
private
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
111
|
+
def execute(method, body: nil, query: nil, content_type: nil, max_retries: nil)
|
|
112
|
+
segments = _rewrite_version(_build_full_segments)
|
|
113
|
+
result = @path_tree.match(segments, method)
|
|
114
|
+
|
|
115
|
+
if !result[:valid]
|
|
116
|
+
path = "/" + segments.join("/")
|
|
117
|
+
available = result[:methods]
|
|
118
|
+
if available.empty?
|
|
119
|
+
::Kernel.raise ::Async::Matrix::InvalidEndpointError.new(
|
|
120
|
+
"INVALID_ENDPOINT",
|
|
121
|
+
"No Matrix API endpoint matches: #{method} #{path}",
|
|
122
|
+
)
|
|
123
|
+
else
|
|
124
|
+
::Kernel.raise ::Async::Matrix::InvalidEndpointError.new(
|
|
125
|
+
"METHOD_NOT_ALLOWED",
|
|
126
|
+
"#{method} not allowed for #{path}. Valid methods: #{available.map(&:upcase).join(", ")}",
|
|
127
|
+
)
|
|
128
|
+
end
|
|
128
129
|
end
|
|
129
|
-
end
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
path = "/" + segments.map { |s| _encode(s) }.join("/")
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
if query && !query.empty?
|
|
134
|
+
qs = query.map { |k, v| "#{_encode(k.to_s)}=#{_encode(v.to_s)}" }.join("&")
|
|
135
|
+
path = "#{path}?#{qs}"
|
|
136
|
+
end
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
if _binary_route?(segments)
|
|
139
|
+
case method
|
|
140
|
+
when "GET"
|
|
141
|
+
@client.media_client.download(path)
|
|
142
|
+
when "POST"
|
|
143
|
+
@client.media_client.upload(
|
|
144
|
+
"POST",
|
|
145
|
+
path,
|
|
146
|
+
body,
|
|
147
|
+
content_type || "application/octet-stream",
|
|
148
|
+
)
|
|
149
|
+
when "PUT"
|
|
150
|
+
@client.media_client.upload(
|
|
151
|
+
"PUT",
|
|
152
|
+
path,
|
|
153
|
+
body,
|
|
154
|
+
content_type || "application/octet-stream",
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
if max_retries
|
|
159
|
+
retry_opts = {max_retries: max_retries}
|
|
160
|
+
else
|
|
161
|
+
retry_opts = {}
|
|
146
162
|
end
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
case method
|
|
164
|
+
when "GET"
|
|
165
|
+
@client.get(path, **retry_opts)
|
|
166
|
+
when "POST"
|
|
167
|
+
@client.post(path, body || {}, **retry_opts)
|
|
168
|
+
when "PUT"
|
|
169
|
+
@client.put(path, body || {}, **retry_opts)
|
|
170
|
+
when "DELETE"
|
|
171
|
+
@client.request(
|
|
172
|
+
"DELETE",
|
|
173
|
+
path,
|
|
174
|
+
nil,
|
|
175
|
+
**retry_opts,
|
|
176
|
+
)
|
|
177
|
+
when "PATCH"
|
|
178
|
+
@client.request(
|
|
179
|
+
"PATCH",
|
|
180
|
+
path,
|
|
181
|
+
body || {},
|
|
182
|
+
**retry_opts,
|
|
183
|
+
)
|
|
184
|
+
end
|
|
160
185
|
end
|
|
161
186
|
end
|
|
162
|
-
end
|
|
163
187
|
|
|
164
188
|
# Rewrites version segments for endpoints that only exist at
|
|
165
189
|
# a different version than the gateway prefix provides.
|
|
166
190
|
# _matrix/media/v3/create → _matrix/media/v1/create
|
|
167
191
|
# _matrix/client/v3/media/... → _matrix/client/v1/media/...
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return segments
|
|
192
|
+
def _rewrite_version(segments)
|
|
193
|
+
# POST /_matrix/media/v1/create (only exists at v1)
|
|
194
|
+
media_create = segments.length == 4 &&
|
|
195
|
+
segments[0] == "_matrix" && segments[1] == "media" &&
|
|
196
|
+
segments[2] == "v3" && segments[3] == "create"
|
|
197
|
+
|
|
198
|
+
# Authenticated media endpoints live at /_matrix/client/v1/media/...
|
|
199
|
+
client_media = segments.length >= 5 &&
|
|
200
|
+
segments[0] == "_matrix" && segments[1] == "client" &&
|
|
201
|
+
segments[2] == "v3" && segments[3] == "media"
|
|
202
|
+
|
|
203
|
+
if media_create || client_media
|
|
204
|
+
segments.dup.tap { |rewritten| rewritten[2] = "v1" }
|
|
205
|
+
else
|
|
206
|
+
segments
|
|
207
|
+
end
|
|
185
208
|
end
|
|
186
209
|
|
|
187
|
-
segments
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
210
|
+
def _binary_route?(segments)
|
|
211
|
+
BINARY_ROUTES.any? do |pattern|
|
|
212
|
+
parts = pattern.split("/")
|
|
213
|
+
unless parts.length == segments.length + 1
|
|
214
|
+
next false
|
|
215
|
+
end # leading slash adds empty element
|
|
216
|
+
parts.shift # remove empty string from leading /
|
|
217
|
+
parts.length == segments.length &&
|
|
218
|
+
parts.zip(segments).all? { |pat, seg| pat == "*" || pat == seg }
|
|
219
|
+
end
|
|
197
220
|
end
|
|
198
|
-
end
|
|
199
221
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
def _extract_query_params(kwargs)
|
|
223
|
+
query = {}
|
|
224
|
+
kwargs.each_key do |k|
|
|
225
|
+
key_s = k.to_s
|
|
226
|
+
if key_s.start_with?("?")
|
|
227
|
+
query[key_s[1..]] = kwargs.delete(k)
|
|
228
|
+
end
|
|
206
229
|
end
|
|
230
|
+
query.empty? ? nil : query
|
|
207
231
|
end
|
|
208
|
-
query.empty? ? nil : query
|
|
209
|
-
end
|
|
210
232
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
233
|
+
def _build_full_segments
|
|
234
|
+
chain_segments = []
|
|
235
|
+
@buffer.each do |name, args|
|
|
236
|
+
chain_segments << name
|
|
237
|
+
if args.nil? || args.empty?
|
|
238
|
+
next
|
|
239
|
+
end
|
|
240
|
+
args.each do |arg|
|
|
241
|
+
if arg.is_a?(::Hash)
|
|
242
|
+
next
|
|
243
|
+
end
|
|
244
|
+
chain_segments << arg.to_s
|
|
245
|
+
end
|
|
219
246
|
end
|
|
247
|
+
@prefix + chain_segments
|
|
220
248
|
end
|
|
221
|
-
@prefix + chain_segments
|
|
222
|
-
end
|
|
223
249
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
250
|
+
def _encode(value)
|
|
251
|
+
::ERB::Util.url_encode(value.to_s)
|
|
252
|
+
end
|
|
227
253
|
end
|
|
228
254
|
end
|
|
229
255
|
end
|
|
@@ -24,36 +24,42 @@ module Async
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def initialize(buffer)
|
|
27
|
-
|
|
27
|
+
if buffer.respond_to?(:to_a)
|
|
28
|
+
@buffer = buffer.to_a
|
|
29
|
+
else
|
|
30
|
+
@buffer = buffer
|
|
31
|
+
end
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def segments
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
[].tap do |result|
|
|
36
|
+
@buffer.each do |entry|
|
|
37
|
+
case entry
|
|
38
|
+
when Symbol
|
|
39
|
+
# :slash, :dash -- ignore in URL context
|
|
40
|
+
next
|
|
41
|
+
when Array
|
|
42
|
+
name, args = entry
|
|
43
|
+
unless name.is_a?(String)
|
|
44
|
+
next
|
|
45
|
+
end
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
if args.nil? || args.empty?
|
|
48
|
+
# Bare method -> path segment
|
|
49
|
+
result << name
|
|
50
|
+
else
|
|
51
|
+
# Method with args -> method name as segment, then each arg as segment
|
|
52
|
+
result << name
|
|
53
|
+
args.each do |arg|
|
|
54
|
+
if arg.is_a?(Hash)
|
|
55
|
+
next
|
|
56
|
+
end # kwargs are not path segments
|
|
57
|
+
result << arg.to_s
|
|
58
|
+
end
|
|
51
59
|
end
|
|
52
60
|
end
|
|
53
61
|
end
|
|
54
62
|
end
|
|
55
|
-
|
|
56
|
-
result
|
|
57
63
|
end
|
|
58
64
|
end
|
|
59
65
|
end
|