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
|
@@ -29,7 +29,7 @@ end
|
|
|
29
29
|
__END__
|
|
30
30
|
describe "Async::Matrix::DoublePuppetClient" do
|
|
31
31
|
def make_config
|
|
32
|
-
Async::Matrix::
|
|
32
|
+
Async::Matrix::Config.new({
|
|
33
33
|
"homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
|
|
34
34
|
"appservice" => { "as_token" => "as_token_value", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
|
|
35
35
|
})
|
|
@@ -0,0 +1,182 @@
|
|
|
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 "openssl"
|
|
7
|
+
|
|
8
|
+
module Async
|
|
9
|
+
module Matrix
|
|
10
|
+
module E2EE
|
|
11
|
+
# The key every stored crypto object in this namespace is encrypted with
|
|
12
|
+
# at rest.
|
|
13
|
+
#
|
|
14
|
+
# A "pickle" is vodozemac's serialised snapshot of an Account or a
|
|
15
|
+
# session -- the only way ratchet state survives a restart:
|
|
16
|
+
#
|
|
17
|
+
# blob = session.pickle(pickle_key.to_s)
|
|
18
|
+
# session = E2EE::Session.from_pickle(blob, pickle_key.to_s)
|
|
19
|
+
#
|
|
20
|
+
# This key is what stops a copy of the database being a plaintext dump of
|
|
21
|
+
# the device's identity keys and every room key it holds.
|
|
22
|
+
#
|
|
23
|
+
# IT MUST NEVER CHANGE. Rotate or lose it and every pickle is
|
|
24
|
+
# undecryptable: the Account goes, every Megolm session goes, and all
|
|
25
|
+
# encrypted history becomes permanently unreadable unless server-side key
|
|
26
|
+
# backup happens to hold copies. There is no recovery path and no warning
|
|
27
|
+
# -- the symptom is simply that nothing decrypts any more.
|
|
28
|
+
#
|
|
29
|
+
# SO THE SECRET IS THE APPLICATION'S, not this library's. #derive exists
|
|
30
|
+
# because the shape requirement below catches everyone, not because a gem
|
|
31
|
+
# should own your key material.
|
|
32
|
+
class PickleKey
|
|
33
|
+
# vodozemac wants exactly 32 bytes AND rejects a binary string: hand it
|
|
34
|
+
# raw KDF output and it fails with "expected utf-8, got ASCII-8BIT".
|
|
35
|
+
LENGTH = 32
|
|
36
|
+
|
|
37
|
+
# 24 bytes of entropy base64s to exactly 32 ASCII characters, which
|
|
38
|
+
# satisfies both constraints at once -- 32 bytes, valid UTF-8, 192 bits.
|
|
39
|
+
# That arithmetic is the whole reason this class exists.
|
|
40
|
+
DERIVED_BYTES = 24
|
|
41
|
+
|
|
42
|
+
DEFAULT_INFO = "matrix device pickle"
|
|
43
|
+
|
|
44
|
+
class Error < StandardError; end
|
|
45
|
+
|
|
46
|
+
# Derive a key from an application secret.
|
|
47
|
+
#
|
|
48
|
+
# Deterministic: the same secret always yields the same key, which is
|
|
49
|
+
# what makes it survive a restart without being written down anywhere.
|
|
50
|
+
# Use a different `info` to derive unrelated keys from one secret.
|
|
51
|
+
def self.derive(secret, info: DEFAULT_INFO, salt: "")
|
|
52
|
+
if secret.nil? || secret.to_s.empty?
|
|
53
|
+
raise Error, "cannot derive a pickle key from an empty secret"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
new(
|
|
57
|
+
[
|
|
58
|
+
OpenSSL::KDF.hkdf(
|
|
59
|
+
secret.to_s,
|
|
60
|
+
salt: salt,
|
|
61
|
+
info: info,
|
|
62
|
+
length: DERIVED_BYTES,
|
|
63
|
+
hash: "SHA256",
|
|
64
|
+
),
|
|
65
|
+
].pack("m0"),
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Wrap a key you already have -- one derived elsewhere, or read from a
|
|
70
|
+
# secret store.
|
|
71
|
+
#
|
|
72
|
+
# @raises [Error] unless it is exactly 32 bytes and valid UTF-8, because
|
|
73
|
+
# vodozemac refuses anything else and the failure it gives is obscure.
|
|
74
|
+
def initialize(value)
|
|
75
|
+
@value = value.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
76
|
+
|
|
77
|
+
unless @value.bytesize == LENGTH
|
|
78
|
+
raise Error, "a pickle key must be exactly #{LENGTH} bytes, got #{@value.bytesize}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
unless @value.valid_encoding?
|
|
82
|
+
raise Error, "a pickle key must be valid UTF-8; vodozemac rejects binary strings"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
@value.freeze
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The key as vodozemac wants it.
|
|
89
|
+
def to_s = @value
|
|
90
|
+
|
|
91
|
+
# Never the key itself: a pickle key in a log or an exception message is
|
|
92
|
+
# as bad as one in a repository.
|
|
93
|
+
def inspect = "#<#{self.class.name} [redacted]>"
|
|
94
|
+
|
|
95
|
+
def ==(other) = other.is_a?(self.class) && to_s == other.to_s
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
__END__
|
|
102
|
+
describe "Async::Matrix::E2EE::PickleKey" do
|
|
103
|
+
K = Async::Matrix::E2EE::PickleKey
|
|
104
|
+
|
|
105
|
+
# The arithmetic this class exists for: 24 bytes of entropy base64s to
|
|
106
|
+
# exactly the 32 ASCII characters vodozemac demands.
|
|
107
|
+
it "derives a key of exactly 32 bytes, valid UTF-8" do
|
|
108
|
+
key = K.derive("an application secret")
|
|
109
|
+
|
|
110
|
+
key.to_s.bytesize.should == 32
|
|
111
|
+
key.to_s.encoding.should == Encoding::UTF_8
|
|
112
|
+
key.to_s.valid_encoding?.should == true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Deterministic, which is what lets it survive a restart without being
|
|
116
|
+
# written down.
|
|
117
|
+
it "derives the same key from the same secret" do
|
|
118
|
+
K.derive("secret").should == K.derive("secret")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "derives different keys from different secrets" do
|
|
122
|
+
K.derive("one").should.not == K.derive("two")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# One secret, several unrelated keys.
|
|
126
|
+
it "separates keys by info string" do
|
|
127
|
+
K.derive("secret", info: "a").should.not == K.derive("secret", info: "b")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "refuses an empty secret" do
|
|
131
|
+
lambda { K.derive("") }.should.raise(Async::Matrix::E2EE::PickleKey::Error)
|
|
132
|
+
lambda { K.derive(nil) }.should.raise(Async::Matrix::E2EE::PickleKey::Error)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# ── Wrapping an existing key ──────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
it "accepts a 32-byte key" do
|
|
138
|
+
K.new("k" * 32).to_s.should == "k" * 32
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The error vodozemac gives for the wrong length is obscure, so this one is
|
|
142
|
+
# not.
|
|
143
|
+
it "refuses a key of the wrong length" do
|
|
144
|
+
lambda { K.new("short") }.should.raise(Async::Matrix::E2EE::PickleKey::Error)
|
|
145
|
+
lambda { K.new("k" * 33) }.should.raise(Async::Matrix::E2EE::PickleKey::Error)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# "expected utf-8, got ASCII-8BIT" is what raw KDF output gets you.
|
|
149
|
+
it "refuses bytes that are not valid UTF-8" do
|
|
150
|
+
lambda { K.new("\xFF\xFE".b + ("k" * 30)) }
|
|
151
|
+
.should.raise(Async::Matrix::E2EE::PickleKey::Error)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "accepts binary-encoded bytes that happen to be valid UTF-8" do
|
|
155
|
+
K.new(("k" * 32).b).to_s.encoding.should == Encoding::UTF_8
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# ── Not leaking ───────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
# A pickle key in a log is as bad as one in a repository.
|
|
161
|
+
it "never shows the key in inspect" do
|
|
162
|
+
key = K.derive("secret")
|
|
163
|
+
|
|
164
|
+
key.inspect.should.not.be.include? key.to_s
|
|
165
|
+
key.inspect.should.be.include? "redacted"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "is frozen, so nothing can mutate it in place" do
|
|
169
|
+
K.derive("secret").to_s.frozen?.should == true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# It belongs beside the objects it encrypts, which is the point of the
|
|
173
|
+
# namespace: every one of these answers #pickle.
|
|
174
|
+
it "sits with the classes whose pickles it protects" do
|
|
175
|
+
[
|
|
176
|
+
Async::Matrix::E2EE::Account,
|
|
177
|
+
Async::Matrix::E2EE::Session,
|
|
178
|
+
Async::Matrix::E2EE::GroupSession,
|
|
179
|
+
Async::Matrix::E2EE::InboundGroupSession,
|
|
180
|
+
].each { |klass| klass.instance_methods.should.be.include? :pickle }
|
|
181
|
+
end
|
|
182
|
+
end
|
data/lib/async/matrix/error.rb
CHANGED
|
@@ -3,24 +3,17 @@
|
|
|
3
3
|
# Released under the Apache License, Version 2.0.
|
|
4
4
|
# Copyright, 2026, by General Intelligence Systems.
|
|
5
5
|
|
|
6
|
+
require_relative "../../protocol/matrix/error"
|
|
7
|
+
|
|
6
8
|
module Async
|
|
7
9
|
module Matrix
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
class NotFoundError < Error; end
|
|
19
|
-
class BadJsonError < Error; end
|
|
20
|
-
class AuthError < Error; end
|
|
21
|
-
class HomeserverError < Error; end
|
|
22
|
-
class InvalidEndpointError < Error; end
|
|
23
|
-
class ResponseTooLargeError < Error; end
|
|
10
|
+
# One error base for the whole gem. See Protocol::Matrix::Error.
|
|
11
|
+
#
|
|
12
|
+
# A CONSTANT, NOT A SUBCLASS: the subclasses below it are declared as
|
|
13
|
+
# `class AuthError < Error`, which resolves this constant at load time, so
|
|
14
|
+
# they inherit the real class and `rescue Async::Matrix::Error` catches
|
|
15
|
+
# everything -- transport failures and format failures together.
|
|
16
|
+
Error = ::Protocol::Matrix::Error
|
|
24
17
|
end
|
|
25
18
|
end
|
|
26
19
|
|
|
@@ -45,23 +38,3 @@ __END__
|
|
|
45
38
|
Async::Matrix::Error.new("M_UNKNOWN", "bad").should.be.kind_of StandardError
|
|
46
39
|
end
|
|
47
40
|
end
|
|
48
|
-
|
|
49
|
-
it "NotFoundError inherits from Error" do
|
|
50
|
-
Async::Matrix::NotFoundError.new("M_NOT_FOUND", "gone").should.be.kind_of Async::Matrix::Error
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
it "BadJsonError inherits from Error" do
|
|
54
|
-
Async::Matrix::BadJsonError.new("M_BAD_JSON", "invalid").should.be.kind_of Async::Matrix::Error
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
it "AuthError inherits from Error" do
|
|
58
|
-
Async::Matrix::AuthError.new("M_FORBIDDEN", "denied").should.be.kind_of Async::Matrix::Error
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
it "HomeserverError inherits from Error" do
|
|
62
|
-
Async::Matrix::HomeserverError.new("M_UNKNOWN", "upstream").should.be.kind_of Async::Matrix::Error
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
it "ResponseTooLargeError inherits from Error" do
|
|
66
|
-
Async::Matrix::ResponseTooLargeError.new("M_TOO_LARGE", "too big").should.be.kind_of Async::Matrix::Error
|
|
67
|
-
end
|
|
@@ -5,23 +5,23 @@
|
|
|
5
5
|
|
|
6
6
|
module Async
|
|
7
7
|
module Matrix
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# A Matrix standard error response body: `{"errcode": ..., "error": ...}`.
|
|
9
|
+
# Both {Client} and {MediaClient} parse failed responses into one of these.
|
|
10
|
+
class ErrorResponse
|
|
11
|
+
attr_reader :errcode, :error
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
end
|
|
13
|
+
def initialize(data)
|
|
14
|
+
@errcode = data["errcode"]
|
|
15
|
+
@error = data["error"]
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
__END__
|
|
22
|
-
describe "Async::Matrix::
|
|
22
|
+
describe "Async::Matrix::ErrorResponse" do
|
|
23
23
|
it "parses errcode and error" do
|
|
24
|
-
resp = Async::Matrix::
|
|
24
|
+
resp = Async::Matrix::ErrorResponse.new({
|
|
25
25
|
"errcode" => "M_FORBIDDEN",
|
|
26
26
|
"error" => "Access denied"
|
|
27
27
|
})
|
|
@@ -30,7 +30,7 @@ __END__
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
it "handles missing fields" do
|
|
33
|
-
resp = Async::Matrix::
|
|
33
|
+
resp = Async::Matrix::ErrorResponse.new({})
|
|
34
34
|
resp.errcode.should.be.nil
|
|
35
35
|
resp.error.should.be.nil
|
|
36
36
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the Apache License, Version 2.0.
|
|
4
|
+
# Copyright, 2026, by General Intelligence Systems.
|
|
5
|
+
|
|
6
|
+
# Async::Matrix::Error is this class's superclass, so it has to exist before
|
|
7
|
+
# this file finishes loading — the glob in async/matrix.rb sorts
|
|
8
|
+
# alphabetically and would otherwise get here first.
|
|
9
|
+
require_relative "error"
|
|
10
|
+
|
|
11
|
+
module Async
|
|
12
|
+
module Matrix
|
|
13
|
+
class HomeserverError < Error; end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
__END__
|
|
18
|
+
it "HomeserverError inherits from Error" do
|
|
19
|
+
Async::Matrix::HomeserverError.new("M_UNKNOWN", "upstream").should.be.kind_of Async::Matrix::Error
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the Apache License, Version 2.0.
|
|
4
|
+
# Copyright, 2026, by General Intelligence Systems.
|
|
5
|
+
|
|
6
|
+
# Async::Matrix::Error is this class's superclass, so it has to exist before
|
|
7
|
+
# this file finishes loading — the glob in async/matrix.rb sorts
|
|
8
|
+
# alphabetically and would otherwise get here first.
|
|
9
|
+
require_relative "error"
|
|
10
|
+
|
|
11
|
+
module Async
|
|
12
|
+
module Matrix
|
|
13
|
+
class InvalidEndpointError < Error; end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
__END__
|
|
18
|
+
it "InvalidEndpointError inherits from Error" do
|
|
19
|
+
Async::Matrix::InvalidEndpointError.new("M_UNRECOGNIZED", "no such route").should.be.kind_of Async::Matrix::Error
|
|
20
|
+
end
|
|
@@ -49,19 +49,24 @@ module Async
|
|
|
49
49
|
|
|
50
50
|
Console.debug(self) { "UPLOAD #{method} #{path} (#{content_type}, #{body.bytesize} bytes)" }
|
|
51
51
|
|
|
52
|
-
response = internet.call(
|
|
52
|
+
response = internet.call(
|
|
53
|
+
method,
|
|
54
|
+
url,
|
|
55
|
+
headers,
|
|
56
|
+
body,
|
|
57
|
+
)
|
|
53
58
|
status = response.status
|
|
54
59
|
|
|
55
60
|
unless (200..299).cover?(status)
|
|
56
61
|
payload = read_limited(response, ERROR_RESPONSE_SIZE_LIMIT)
|
|
57
|
-
parsed =
|
|
58
|
-
begin; JSON.parse(payload); rescue; {} end
|
|
62
|
+
parsed = ErrorResponse.new(
|
|
63
|
+
begin; JSON.parse(payload); rescue; {} end,
|
|
59
64
|
)
|
|
60
65
|
Console.error(self) { "Matrix media upload #{status}: #{parsed.errcode} — #{parsed.error}" }
|
|
61
66
|
raise HomeserverError.new(
|
|
62
67
|
parsed.errcode || "UNKNOWN",
|
|
63
68
|
parsed.error || payload.to_s[0..200],
|
|
64
|
-
status: status
|
|
69
|
+
status: status,
|
|
65
70
|
)
|
|
66
71
|
end
|
|
67
72
|
|
|
@@ -87,19 +92,24 @@ module Async
|
|
|
87
92
|
|
|
88
93
|
Console.debug(self) { "DOWNLOAD #{path}" }
|
|
89
94
|
|
|
90
|
-
response = internet.call(
|
|
95
|
+
response = internet.call(
|
|
96
|
+
"GET",
|
|
97
|
+
url,
|
|
98
|
+
@auth_headers,
|
|
99
|
+
nil,
|
|
100
|
+
)
|
|
91
101
|
status = response.status
|
|
92
102
|
|
|
93
103
|
unless (200..299).cover?(status)
|
|
94
104
|
payload = response.read
|
|
95
|
-
parsed =
|
|
96
|
-
begin; JSON.parse(payload); rescue; {} end
|
|
105
|
+
parsed = ErrorResponse.new(
|
|
106
|
+
begin; JSON.parse(payload); rescue; {} end,
|
|
97
107
|
)
|
|
98
108
|
Console.error(self) { "Matrix media download #{status}: #{parsed.errcode} — #{parsed.error}" }
|
|
99
109
|
raise HomeserverError.new(
|
|
100
110
|
parsed.errcode || "UNKNOWN",
|
|
101
111
|
parsed.error || payload.to_s[0..200],
|
|
102
|
-
status: status
|
|
112
|
+
status: status,
|
|
103
113
|
)
|
|
104
114
|
end
|
|
105
115
|
|
|
@@ -113,37 +123,38 @@ module Async
|
|
|
113
123
|
|
|
114
124
|
private
|
|
115
125
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
def internet
|
|
127
|
+
@internet ||= Async::HTTP::Internet.new
|
|
128
|
+
end
|
|
119
129
|
|
|
120
130
|
# Read response body with a size limit. Raises ResponseTooLargeError
|
|
121
131
|
# if the body exceeds the limit.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
def read_limited(response, limit)
|
|
133
|
+
body = response.body
|
|
134
|
+
if body
|
|
135
|
+
if body.respond_to?(:length) && body.length && body.length > limit
|
|
136
|
+
body.close
|
|
137
|
+
raise ResponseTooLargeError.new(
|
|
138
|
+
"M_TOO_LARGE",
|
|
139
|
+
"Response Content-Length #{body.length} bytes exceeds limit of #{limit} bytes",
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
buffer = String.new(encoding: Encoding::BINARY)
|
|
143
|
+
body.each do |chunk|
|
|
144
|
+
buffer << chunk
|
|
145
|
+
if buffer.bytesize > limit
|
|
146
|
+
body.close
|
|
147
|
+
raise ResponseTooLargeError.new(
|
|
148
|
+
"M_TOO_LARGE",
|
|
149
|
+
"Response body exceeds limit of #{limit} bytes",
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
buffer.empty? ? nil : buffer
|
|
154
|
+
else
|
|
155
|
+
nil
|
|
143
156
|
end
|
|
144
157
|
end
|
|
145
|
-
buffer.empty? ? nil : buffer
|
|
146
|
-
end
|
|
147
158
|
end
|
|
148
159
|
end
|
|
149
160
|
end
|
|
@@ -151,7 +162,7 @@ end
|
|
|
151
162
|
__END__
|
|
152
163
|
describe "Async::Matrix::MediaClient" do
|
|
153
164
|
def make_config
|
|
154
|
-
Async::Matrix::
|
|
165
|
+
Async::Matrix::Config.new({
|
|
155
166
|
"homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
|
|
156
167
|
"appservice" => { "as_token" => "test_token", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
|
|
157
168
|
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the Apache License, Version 2.0.
|
|
4
|
+
# Copyright, 2026, by General Intelligence Systems.
|
|
5
|
+
|
|
6
|
+
# Async::Matrix::Error is this class's superclass, so it has to exist before
|
|
7
|
+
# this file finishes loading — the glob in async/matrix.rb sorts
|
|
8
|
+
# alphabetically and would otherwise get here first.
|
|
9
|
+
require_relative "error"
|
|
10
|
+
|
|
11
|
+
module Async
|
|
12
|
+
module Matrix
|
|
13
|
+
class NotFoundError < Error; end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
__END__
|
|
18
|
+
it "NotFoundError inherits from Error" do
|
|
19
|
+
Async::Matrix::NotFoundError.new("M_NOT_FOUND", "gone").should.be.kind_of Async::Matrix::Error
|
|
20
|
+
end
|
|
@@ -37,17 +37,21 @@ module Async
|
|
|
37
37
|
|
|
38
38
|
loop do
|
|
39
39
|
result = yield
|
|
40
|
-
|
|
40
|
+
if result
|
|
41
|
+
break result
|
|
42
|
+
end
|
|
41
43
|
|
|
42
44
|
remaining = deadline - Time.now
|
|
43
|
-
|
|
45
|
+
if remaining <= 0
|
|
46
|
+
break result
|
|
47
|
+
end
|
|
44
48
|
|
|
45
49
|
begin
|
|
46
50
|
Async::Task.current.with_timeout(remaining) do
|
|
47
51
|
@condition.wait
|
|
48
52
|
end
|
|
49
53
|
rescue Async::TimeoutError
|
|
50
|
-
|
|
54
|
+
break yield
|
|
51
55
|
end
|
|
52
56
|
end
|
|
53
57
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the Apache License, Version 2.0.
|
|
4
|
+
# Copyright, 2026, by General Intelligence Systems.
|
|
5
|
+
|
|
6
|
+
# Async::Matrix::Error is this class's superclass, so it has to exist before
|
|
7
|
+
# this file finishes loading — the glob in async/matrix.rb sorts
|
|
8
|
+
# alphabetically and would otherwise get here first.
|
|
9
|
+
require_relative "error"
|
|
10
|
+
|
|
11
|
+
module Async
|
|
12
|
+
module Matrix
|
|
13
|
+
class ResponseTooLargeError < Error; end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
__END__
|
|
18
|
+
it "ResponseTooLargeError inherits from Error" do
|
|
19
|
+
Async::Matrix::ResponseTooLargeError.new("M_TOO_LARGE", "too big").should.be.kind_of Async::Matrix::Error
|
|
20
|
+
end
|
data/lib/async/matrix/version.rb
CHANGED
data/lib/async/matrix.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
# Released under the Apache License, Version 2.0.
|
|
4
4
|
# Copyright, 2026, by General Intelligence Systems.
|
|
5
5
|
|
|
6
|
-
require "bundler/setup"
|
|
7
6
|
require "async/http"
|
|
8
7
|
|
|
9
8
|
module Async
|
|
@@ -11,12 +10,22 @@ module Async
|
|
|
11
10
|
end
|
|
12
11
|
end
|
|
13
12
|
|
|
13
|
+
# The message FORMAT layer: events, content, schemas, encrypted messages. It
|
|
14
|
+
# does no IO, so it lives beside the other protocol-* libraries rather than
|
|
15
|
+
# under the gem that opens sockets. Required FIRST, because the aliases below
|
|
16
|
+
# and the client both depend on it.
|
|
17
|
+
require "protocol/matrix"
|
|
18
|
+
|
|
19
|
+
module Async
|
|
20
|
+
module Matrix
|
|
21
|
+
# Where these constants used to live. Kept as aliases so code written
|
|
22
|
+
# against 3.0 keeps working; new code should name Protocol::Matrix directly.
|
|
23
|
+
Event = ::Protocol::Matrix::Event
|
|
24
|
+
Content = ::Protocol::Matrix::Content
|
|
25
|
+
Schema = ::Protocol::Matrix::Schema
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
14
29
|
Dir.glob("#{__dir__}/matrix/**/*.rb").sort.each do |path|
|
|
15
|
-
next if path.include?("/migrations/")
|
|
16
|
-
# The Discord bridge is opt-in — it pulls in Sequel and opens a placeholder
|
|
17
|
-
# database at load time. Require "async/matrix/bridge/discord/db" explicitly
|
|
18
|
-
# when you need it, so plain Matrix use stays free of that dependency.
|
|
19
|
-
next if path.include?("/bridge/")
|
|
20
30
|
require path
|
|
21
31
|
end
|
|
22
|
-
|