async-matrix 2.1.0-x86_64-linux → 3.0.1-x86_64-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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -58
  3. data/lib/async/matrix/3.3/async_matrix_e2ee.so +0 -0
  4. data/lib/async/matrix/3.4/async_matrix_e2ee.so +0 -0
  5. data/lib/async/matrix/4.0/async_matrix_e2ee.so +0 -0
  6. data/lib/async/matrix/api/chain.rb +120 -94
  7. data/lib/async/matrix/api/concat.rb +28 -22
  8. data/lib/async/matrix/api/path_tree.rb +71 -44
  9. data/lib/async/matrix/api.rb +6 -1
  10. data/lib/async/matrix/app_service_client.rb +213 -0
  11. data/lib/async/matrix/auth_error.rb +20 -0
  12. data/lib/async/matrix/bad_json_error.rb +20 -0
  13. data/lib/async/matrix/client/encryption.rb +413 -0
  14. data/lib/async/matrix/client/rooms.rb +478 -0
  15. data/lib/async/matrix/client/sync.rb +508 -0
  16. data/lib/async/matrix/client.rb +217 -113
  17. data/lib/async/matrix/config/vivify.rb +107 -0
  18. data/lib/async/matrix/config.rb +184 -0
  19. data/lib/async/matrix/device_store.rb +1621 -0
  20. data/lib/async/matrix/double_puppet_client.rb +1 -1
  21. data/lib/async/matrix/e2ee/pickle_key.rb +182 -0
  22. data/lib/async/matrix/error.rb +9 -36
  23. data/lib/async/matrix/{application_service/error_response.rb → error_response.rb} +10 -10
  24. data/lib/async/matrix/homeserver_error.rb +20 -0
  25. data/lib/async/matrix/invalid_endpoint_error.rb +20 -0
  26. data/lib/async/matrix/media_client.rb +46 -35
  27. data/lib/async/matrix/not_found_error.rb +20 -0
  28. data/lib/async/matrix/notifier.rb +7 -3
  29. data/lib/async/matrix/response_too_large_error.rb +20 -0
  30. data/lib/async/matrix/version.rb +1 -1
  31. data/lib/async/matrix.rb +16 -7
  32. data/lib/protocol/matrix/canonical_json.rb +200 -0
  33. data/lib/protocol/matrix/content.rb +106 -0
  34. data/lib/protocol/matrix/encrypted_message.rb +1018 -0
  35. data/lib/protocol/matrix/error.rb +49 -0
  36. data/lib/protocol/matrix/event.rb +208 -0
  37. data/lib/protocol/matrix/key_backup.rb +347 -0
  38. data/lib/protocol/matrix/keys.rb +381 -0
  39. data/lib/protocol/matrix/message_batch.rb +482 -0
  40. data/lib/protocol/matrix/schema/registry.rb +381 -0
  41. data/lib/protocol/matrix/schema/validation_error.rb +234 -0
  42. data/lib/protocol/matrix/schema.rb +170 -0
  43. data/lib/protocol/matrix/secret_storage.rb +535 -0
  44. data/lib/protocol/matrix/signing.rb +278 -0
  45. data/lib/protocol/matrix.rb +25 -0
  46. metadata +43 -136
  47. data/lib/async/discord/api/path_tree.rb +0 -127
  48. data/lib/async/discord/api.rb +0 -151
  49. data/lib/async/discord/client.rb +0 -283
  50. data/lib/async/discord/error.rb +0 -84
  51. data/lib/async/discord/gateway.rb +0 -359
  52. data/lib/async/discord.rb +0 -15
  53. data/lib/async/matrix/application_service/bot.rb +0 -232
  54. data/lib/async/matrix/application_service/config/schema/analytics.json +0 -21
  55. data/lib/async/matrix/application_service/config/schema/appservice.json +0 -82
  56. data/lib/async/matrix/application_service/config/schema/backfill.json +0 -91
  57. data/lib/async/matrix/application_service/config/schema/bridge.json +0 -209
  58. data/lib/async/matrix/application_service/config/schema/config.json +0 -61
  59. data/lib/async/matrix/application_service/config/schema/database.json +0 -38
  60. data/lib/async/matrix/application_service/config/schema/direct_media.json +0 -35
  61. data/lib/async/matrix/application_service/config/schema/double_puppet.json +0 -24
  62. data/lib/async/matrix/application_service/config/schema/encryption.json +0 -164
  63. data/lib/async/matrix/application_service/config/schema/homeserver.json +0 -58
  64. data/lib/async/matrix/application_service/config/schema/logging.json +0 -50
  65. data/lib/async/matrix/application_service/config/schema/management_room_texts.json +0 -25
  66. data/lib/async/matrix/application_service/config/schema/matrix.json +0 -45
  67. data/lib/async/matrix/application_service/config/schema/permissions.json +0 -54
  68. data/lib/async/matrix/application_service/config/schema/provisioning.json +0 -23
  69. data/lib/async/matrix/application_service/config/schema/public_media.json +0 -39
  70. data/lib/async/matrix/application_service/config/schema/relay.json +0 -43
  71. data/lib/async/matrix/application_service/config/vivify.rb +0 -109
  72. data/lib/async/matrix/application_service/config.rb +0 -225
  73. data/lib/async/matrix/application_service/dispatcher.rb +0 -185
  74. data/lib/async/matrix/application_service/event.rb +0 -285
  75. data/lib/async/matrix/application_service/server.rb +0 -430
  76. data/lib/async/matrix/application_service/transaction.rb +0 -66
  77. data/lib/async/matrix/application_service/transaction_handler.rb +0 -185
  78. data/lib/async/matrix/application_service/transaction_store.rb +0 -80
  79. data/lib/async/matrix/bridge/discord/db/connection.rb +0 -141
  80. data/lib/async/matrix/bridge/discord/db/file.rb +0 -118
  81. data/lib/async/matrix/bridge/discord/db/guild.rb +0 -120
  82. data/lib/async/matrix/bridge/discord/db/message.rb +0 -160
  83. data/lib/async/matrix/bridge/discord/db/migrations/001_create_users.rb +0 -14
  84. data/lib/async/matrix/bridge/discord/db/migrations/002_create_guilds.rb +0 -14
  85. data/lib/async/matrix/bridge/discord/db/migrations/003_create_portals.rb +0 -23
  86. data/lib/async/matrix/bridge/discord/db/migrations/004_create_puppets.rb +0 -19
  87. data/lib/async/matrix/bridge/discord/db/migrations/005_create_messages.rb +0 -20
  88. data/lib/async/matrix/bridge/discord/db/migrations/006_create_reactions.rb +0 -19
  89. data/lib/async/matrix/bridge/discord/db/migrations/007_create_files.rb +0 -18
  90. data/lib/async/matrix/bridge/discord/db/portal.rb +0 -150
  91. data/lib/async/matrix/bridge/discord/db/puppet.rb +0 -128
  92. data/lib/async/matrix/bridge/discord/db/reaction.rb +0 -165
  93. data/lib/async/matrix/bridge/discord/db/schema.rb +0 -18
  94. data/lib/async/matrix/bridge/discord/db/user.rb +0 -112
  95. data/lib/async/matrix/bridge/discord/db.rb +0 -138
  96. data/lib/async/matrix/schema/registry.rb +0 -354
  97. data/lib/async/matrix/schema/validation_error.rb +0 -225
  98. 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::ApplicationService::Config.new({
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
@@ -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
- class Error < StandardError
9
- attr_reader :errcode, :status
10
-
11
- def initialize(errcode, message, status: nil)
12
- @errcode = errcode
13
- @status = status
14
- super(message)
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
- module ApplicationService
9
- class ErrorResponse
10
- attr_reader :errcode, :error
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
- def initialize(data)
13
- @errcode = data["errcode"]
14
- @error = data["error"]
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::ApplicationService::ErrorResponse" do
22
+ describe "Async::Matrix::ErrorResponse" do
23
23
  it "parses errcode and error" do
24
- resp = Async::Matrix::ApplicationService::ErrorResponse.new({
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::ApplicationService::ErrorResponse.new({})
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(method, url, headers, body)
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 = ApplicationService::ErrorResponse.new(
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("GET", url, @auth_headers, nil)
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 = ApplicationService::ErrorResponse.new(
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
- def internet
117
- @internet ||= Async::HTTP::Internet.new
118
- end
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
- def read_limited(response, limit)
123
- body = response.body
124
- return nil unless body
125
-
126
- if body.respond_to?(:length) && body.length && body.length > limit
127
- body.close
128
- raise ResponseTooLargeError.new(
129
- "M_TOO_LARGE",
130
- "Response Content-Length #{body.length} bytes exceeds limit of #{limit} bytes"
131
- )
132
- end
133
-
134
- buffer = String.new(encoding: Encoding::BINARY)
135
- body.each do |chunk|
136
- buffer << chunk
137
- if buffer.bytesize > limit
138
- body.close
139
- raise ResponseTooLargeError.new(
140
- "M_TOO_LARGE",
141
- "Response body exceeds limit of #{limit} bytes"
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::ApplicationService::Config.new({
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
- return result if result
40
+ if result
41
+ break result
42
+ end
41
43
 
42
44
  remaining = deadline - Time.now
43
- return result if remaining <= 0
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
- return yield
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
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module Matrix
8
- VERSION = "2.1.0"
8
+ VERSION = "3.0.1"
9
9
  end
10
10
  end
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
-