async-matrix 2.1.0 → 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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +8 -8
  3. data/README.md +33 -58
  4. data/ext/async_matrix_e2ee/Cargo.toml +7 -1
  5. data/lib/async/matrix/api/chain.rb +120 -94
  6. data/lib/async/matrix/api/concat.rb +28 -22
  7. data/lib/async/matrix/api/path_tree.rb +71 -44
  8. data/lib/async/matrix/api.rb +6 -1
  9. data/lib/async/matrix/auth_error.rb +20 -0
  10. data/lib/async/matrix/bad_json_error.rb +20 -0
  11. data/lib/async/matrix/client.rb +142 -110
  12. data/lib/async/matrix/config/vivify.rb +107 -0
  13. data/lib/async/matrix/config.rb +184 -0
  14. data/lib/async/matrix/content.rb +106 -0
  15. data/lib/async/matrix/double_puppet_client.rb +1 -1
  16. data/lib/async/matrix/error.rb +0 -27
  17. data/lib/async/matrix/{application_service/error_response.rb → error_response.rb} +10 -10
  18. data/lib/async/matrix/event.rb +197 -0
  19. data/lib/async/matrix/homeserver_error.rb +20 -0
  20. data/lib/async/matrix/invalid_endpoint_error.rb +20 -0
  21. data/lib/async/matrix/media_client.rb +46 -35
  22. data/lib/async/matrix/not_found_error.rb +20 -0
  23. data/lib/async/matrix/notifier.rb +7 -3
  24. data/lib/async/matrix/response_too_large_error.rb +20 -0
  25. data/lib/async/matrix/schema/registry.rb +367 -340
  26. data/lib/async/matrix/schema/validation_error.rb +221 -214
  27. data/lib/async/matrix/schema.rb +161 -161
  28. data/lib/async/matrix/version.rb +3 -3
  29. data/lib/async/matrix.rb +0 -7
  30. metadata +24 -132
  31. data/lib/async/discord/api/path_tree.rb +0 -127
  32. data/lib/async/discord/api.rb +0 -151
  33. data/lib/async/discord/client.rb +0 -283
  34. data/lib/async/discord/error.rb +0 -84
  35. data/lib/async/discord/gateway.rb +0 -359
  36. data/lib/async/discord.rb +0 -15
  37. data/lib/async/matrix/application_service/bot.rb +0 -232
  38. data/lib/async/matrix/application_service/config/schema/analytics.json +0 -21
  39. data/lib/async/matrix/application_service/config/schema/appservice.json +0 -82
  40. data/lib/async/matrix/application_service/config/schema/backfill.json +0 -91
  41. data/lib/async/matrix/application_service/config/schema/bridge.json +0 -209
  42. data/lib/async/matrix/application_service/config/schema/config.json +0 -61
  43. data/lib/async/matrix/application_service/config/schema/database.json +0 -38
  44. data/lib/async/matrix/application_service/config/schema/direct_media.json +0 -35
  45. data/lib/async/matrix/application_service/config/schema/double_puppet.json +0 -24
  46. data/lib/async/matrix/application_service/config/schema/encryption.json +0 -164
  47. data/lib/async/matrix/application_service/config/schema/homeserver.json +0 -58
  48. data/lib/async/matrix/application_service/config/schema/logging.json +0 -50
  49. data/lib/async/matrix/application_service/config/schema/management_room_texts.json +0 -25
  50. data/lib/async/matrix/application_service/config/schema/matrix.json +0 -45
  51. data/lib/async/matrix/application_service/config/schema/permissions.json +0 -54
  52. data/lib/async/matrix/application_service/config/schema/provisioning.json +0 -23
  53. data/lib/async/matrix/application_service/config/schema/public_media.json +0 -39
  54. data/lib/async/matrix/application_service/config/schema/relay.json +0 -43
  55. data/lib/async/matrix/application_service/config/vivify.rb +0 -109
  56. data/lib/async/matrix/application_service/config.rb +0 -225
  57. data/lib/async/matrix/application_service/dispatcher.rb +0 -185
  58. data/lib/async/matrix/application_service/event.rb +0 -285
  59. data/lib/async/matrix/application_service/server.rb +0 -430
  60. data/lib/async/matrix/application_service/transaction.rb +0 -66
  61. data/lib/async/matrix/application_service/transaction_handler.rb +0 -185
  62. data/lib/async/matrix/application_service/transaction_store.rb +0 -80
  63. data/lib/async/matrix/bridge/discord/db/connection.rb +0 -141
  64. data/lib/async/matrix/bridge/discord/db/file.rb +0 -118
  65. data/lib/async/matrix/bridge/discord/db/guild.rb +0 -120
  66. data/lib/async/matrix/bridge/discord/db/message.rb +0 -160
  67. data/lib/async/matrix/bridge/discord/db/migrations/001_create_users.rb +0 -14
  68. data/lib/async/matrix/bridge/discord/db/migrations/002_create_guilds.rb +0 -14
  69. data/lib/async/matrix/bridge/discord/db/migrations/003_create_portals.rb +0 -23
  70. data/lib/async/matrix/bridge/discord/db/migrations/004_create_puppets.rb +0 -19
  71. data/lib/async/matrix/bridge/discord/db/migrations/005_create_messages.rb +0 -20
  72. data/lib/async/matrix/bridge/discord/db/migrations/006_create_reactions.rb +0 -19
  73. data/lib/async/matrix/bridge/discord/db/migrations/007_create_files.rb +0 -18
  74. data/lib/async/matrix/bridge/discord/db/portal.rb +0 -150
  75. data/lib/async/matrix/bridge/discord/db/puppet.rb +0 -128
  76. data/lib/async/matrix/bridge/discord/db/reaction.rb +0 -165
  77. data/lib/async/matrix/bridge/discord/db/schema.rb +0 -18
  78. data/lib/async/matrix/bridge/discord/db/user.rb +0 -112
  79. data/lib/async/matrix/bridge/discord/db.rb +0 -138
@@ -0,0 +1,184 @@
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 "forwardable"
7
+ require "yaml"
8
+ require "pathname"
9
+ require_relative "config/vivify"
10
+
11
+ module Async
12
+ module Matrix
13
+ # Dot-notation access to a Matrix service's YAML configuration.
14
+ #
15
+ # {Client} only reads two fields — `homeserver.address` and
16
+ # `appservice.as_token` — so this class validates nothing: it vivifies
17
+ # whatever hash (or YAML file) it is handed and exposes every nested field
18
+ # as a method call.
19
+ #
20
+ # config = Config.load("config/appservice.yml")
21
+ # config.homeserver.address # => "http://synapse:8008"
22
+ # config.appservice.as_token # => "secret..."
23
+ # config.appservice.bot.username # => "bot"
24
+ # config.bot_mxid # => "@bot:localhost"
25
+ #
26
+ # A subclass layers validation on by overriding {.validate!}; that is how
27
+ # async-matrix-bridge's Config applies the mautrix bridgev2 JSON Schema
28
+ # suite without reimplementing any of the loading below.
29
+ class Config
30
+ extend Forwardable
31
+
32
+ # Delegate every top-level config section to the vivified data hash. An
33
+ # explicit list, not a method_missing forward: a typo'd section should
34
+ # raise NoMethodError rather than autovivify into an empty hash.
35
+ def_delegators :@data,
36
+ :network,
37
+ :bridge,
38
+ :database,
39
+ :homeserver,
40
+ :appservice,
41
+ :matrix,
42
+ :analytics,
43
+ :provisioning,
44
+ :public_media,
45
+ :direct_media,
46
+ :backfill,
47
+ :double_puppet,
48
+ :encryption,
49
+ :logging,
50
+ :management_room_texts,
51
+ :env_config_prefix
52
+
53
+ def initialize(data)
54
+ self.class.validate!(data)
55
+ @data = Vivify.deep_vivify(data)
56
+ end
57
+
58
+ # Load a YAML config file from disk.
59
+ def self.load(path)
60
+ unless File.exist?(path)
61
+ raise Async::Matrix::NotFoundError.new(
62
+ "M_NOT_FOUND",
63
+ "Config not found: #{path}",
64
+ )
65
+ end
66
+
67
+ data = YAML.safe_load_file(path, permitted_classes: [Symbol])
68
+ new(data)
69
+ end
70
+
71
+ # Validation hook, called with the raw (string-keyed) hash before it is
72
+ # vivified. A no-op here; subclasses override it to raise on bad input,
73
+ # and may mutate `data` to insert defaults.
74
+ def self.validate!(data)
75
+ nil
76
+ end
77
+
78
+ # Convenience: derive the bot's full Matrix ID from
79
+ # appservice.bot.username + homeserver.domain.
80
+ def bot_mxid
81
+ "@#{appservice.bot.username}:#{homeserver.domain}"
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ __END__
88
+ require "tempfile"
89
+
90
+ describe "Async::Matrix::Config" do
91
+ def minimal_data
92
+ {
93
+ "homeserver" => {
94
+ "address" => "http://localhost:8008",
95
+ "domain" => "localhost",
96
+ },
97
+ "appservice" => {
98
+ "as_token" => "as_secret_token_value",
99
+ "hs_token" => "hs_secret_token_value",
100
+ "bot" => {"username" => "bot"},
101
+ },
102
+ }
103
+ end
104
+
105
+ it "exposes top-level sections" do
106
+ config = Async::Matrix::Config.new(minimal_data)
107
+ config.homeserver.address.should == "http://localhost:8008"
108
+ config.homeserver.domain.should == "localhost"
109
+ config.appservice.as_token.should == "as_secret_token_value"
110
+ config.appservice.hs_token.should == "hs_secret_token_value"
111
+ config.appservice.bot.username.should == "bot"
112
+ end
113
+
114
+ it "derives bot_mxid from appservice.bot.username and homeserver.domain" do
115
+ Async::Matrix::Config.new(minimal_data).bot_mxid.should == "@bot:localhost"
116
+ end
117
+
118
+ it "provides dot-notation access to deeply nested fields" do
119
+ data = minimal_data.merge(
120
+ "encryption" => {
121
+ "allow" => true,
122
+ "rotation" => {"messages" => 200},
123
+ },
124
+ )
125
+ config = Async::Matrix::Config.new(data)
126
+ config.encryption.allow.should == true
127
+ config.encryption.rotation.messages.should == 200
128
+ end
129
+
130
+ it "autovivifies missing optional sections as empty hashes" do
131
+ config = Async::Matrix::Config.new(minimal_data)
132
+ config.analytics.should.be.kind_of Hash
133
+ config.analytics.should.be.empty?
134
+ end
135
+
136
+ it "raises NoMethodError for sections it does not delegate" do
137
+ config = Async::Matrix::Config.new(minimal_data)
138
+ lambda { config.bogus }.should.raise(NoMethodError)
139
+ end
140
+
141
+ it "validates nothing by default" do
142
+ lambda { Async::Matrix::Config.new({}) }.should.not.raise
143
+ end
144
+
145
+ it "loads from a YAML file" do
146
+ file = Tempfile.new(["config", ".yml"])
147
+ file.write(<<~YAML)
148
+ homeserver:
149
+ address: "http://localhost:8008"
150
+ domain: "localhost"
151
+ appservice:
152
+ as_token: "as123_secret"
153
+ hs_token: "hs456_secret"
154
+ bot:
155
+ username: "testbot"
156
+ YAML
157
+ file.close
158
+
159
+ config = Async::Matrix::Config.load(file.path)
160
+ config.homeserver.address.should == "http://localhost:8008"
161
+ config.bot_mxid.should == "@testbot:localhost"
162
+ ensure
163
+ file.unlink
164
+ end
165
+
166
+ it "raises NotFoundError for a missing file" do
167
+ lambda {
168
+ Async::Matrix::Config.load("/nonexistent/path.yml")
169
+ }.should.raise(Async::Matrix::NotFoundError)
170
+ end
171
+
172
+ it "runs the validate! hook a subclass overrides" do
173
+ strict = Class.new(Async::Matrix::Config) do
174
+ def self.validate!(data)
175
+ unless data.key?("homeserver")
176
+ raise Async::Matrix::BadJsonError.new("M_BAD_JSON", "no homeserver")
177
+ end
178
+ end
179
+ end
180
+
181
+ lambda { strict.new({}) }.should.raise(Async::Matrix::BadJsonError)
182
+ strict.new(minimal_data).homeserver.domain.should == "localhost"
183
+ end
184
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ module Async
7
+ module Matrix
8
+ # Wraps the content object of a Matrix event, providing typed access to
9
+ # schema-defined properties via method_missing.
10
+ #
11
+ # Common fields like `msgtype`, `body`, and `membership` have dedicated
12
+ # accessors for convenience. Any other field defined in the event's schema
13
+ # (or present in the raw hash) is accessible as a method call:
14
+ #
15
+ # content.msgtype # => "m.text"
16
+ # content.body # => "hello"
17
+ # content.membership # => "join"
18
+ # content.avatar_url # => "mxc://example.org/abc"
19
+ # content["custom"] # => direct hash access for non-standard fields
20
+ #
21
+ class Content
22
+ attr_reader :msgtype, :body, :membership
23
+
24
+ def initialize(data)
25
+ @data = data
26
+ @msgtype = data["msgtype"]
27
+ @body = data["body"]
28
+ @membership = data["membership"]
29
+ end
30
+
31
+ def to_h = @data
32
+ def to_s = @body.to_s
33
+ def to_str = to_s
34
+
35
+ # Direct hash access for any content field.
36
+ def [](key) = @data[key.to_s]
37
+
38
+ # Dynamic access to any content field present in the raw data.
39
+ def method_missing(name, *args)
40
+ key = name.to_s
41
+
42
+ if @data.key?(key)
43
+ @data[key]
44
+ else
45
+ nil
46
+ end
47
+ end
48
+
49
+ def respond_to_missing?(name, include_private = false)
50
+ @data.key?(name.to_s) || super
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ __END__
57
+ describe "Async::Matrix::Content" do
58
+ it "parses msgtype, body, and membership" do
59
+ content = Async::Matrix::Content.new({
60
+ "msgtype" => "m.text",
61
+ "body" => "hello",
62
+ "membership" => "join"
63
+ })
64
+ content.msgtype.should == "m.text"
65
+ content.body.should == "hello"
66
+ content.membership.should == "join"
67
+ end
68
+
69
+ it "handles missing fields gracefully" do
70
+ content = Async::Matrix::Content.new({})
71
+ content.msgtype.should.be.nil
72
+ content.body.should.be.nil
73
+ content.membership.should.be.nil
74
+ end
75
+
76
+ it "provides hash access via []" do
77
+ content = Async::Matrix::Content.new({"custom_field" => "value"})
78
+ content["custom_field"].should == "value"
79
+ end
80
+
81
+ it "provides dynamic access via method_missing" do
82
+ content = Async::Matrix::Content.new({
83
+ "avatar_url" => "mxc://example.org/abc",
84
+ "displayname" => "Alice"
85
+ })
86
+ content.avatar_url.should == "mxc://example.org/abc"
87
+ content.displayname.should == "Alice"
88
+ end
89
+
90
+ it "returns nil for unknown fields via method_missing" do
91
+ content = Async::Matrix::Content.new({})
92
+ content.nonexistent.should.be.nil
93
+ end
94
+
95
+ it "returns the raw hash via to_h" do
96
+ data = {"msgtype" => "m.text", "body" => "hi"}
97
+ content = Async::Matrix::Content.new(data)
98
+ content.to_h.should == data
99
+ end
100
+
101
+ it "responds to keys present in the data" do
102
+ content = Async::Matrix::Content.new({"avatar_url" => "mxc://x/y"})
103
+ content.respond_to?(:avatar_url).should == true
104
+ content.respond_to?(:nonexistent).should == false
105
+ end
106
+ end
@@ -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
  })
@@ -14,13 +14,6 @@ module Async
14
14
  super(message)
15
15
  end
16
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
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,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ module Async
7
+ module Matrix
8
+ # Represents a Matrix event, however it arrived — a /sync response, an
9
+ # application service transaction, or a hash you built yourself.
10
+ #
11
+ # Provides typed accessors for all envelope fields plus schema-driven
12
+ # validation using the official Matrix spec YAML schemas.
13
+ #
14
+ # event = Event.new(raw_hash)
15
+ # event.type # => "m.room.message"
16
+ # event.sender # => "@alice:example.org"
17
+ # event.content.body # => "hello"
18
+ # event.valid? # => true
19
+ # event.valid! # => true (or raises Schema::ValidationError)
20
+ #
21
+ class Event
22
+ attr_reader :type,
23
+ :sender,
24
+ :room_id,
25
+ :state_key,
26
+ :content,
27
+ :event_id,
28
+ :origin_server_ts,
29
+ :unsigned,
30
+ :raw
31
+
32
+ def initialize(data)
33
+ @raw = data
34
+ @type = data["type"]
35
+ @sender = data["sender"]
36
+ @room_id = data["room_id"]
37
+ @state_key = data["state_key"]
38
+ @event_id = data["event_id"]
39
+ @origin_server_ts = data["origin_server_ts"]
40
+ @unsigned = data["unsigned"]
41
+ @content = Content.new(data["content"] || {})
42
+ end
43
+
44
+ # The JSONSchemer::Schema for this event's type, or nil if unknown.
45
+ def schema = Schema[@type]
46
+
47
+ # Validate this event against its schema.
48
+ # Returns true if valid or if no schema exists (lenient).
49
+ def valid? = Schema.valid?(@raw)
50
+
51
+ # Validate this event against its schema.
52
+ # Raises Schema::ValidationError with detailed errors on failure.
53
+ # Returns true if valid or if no schema exists.
54
+ def valid!
55
+ errors = Schema.validate(@raw)
56
+
57
+ unless errors.empty?
58
+ raise Schema::ValidationError.new(
59
+ errors,
60
+ event_type: @type,
61
+ event_id: @event_id,
62
+ )
63
+ end
64
+
65
+ true
66
+ end
67
+
68
+ # Content property names defined by the schema for this event type.
69
+ # @return [Array<String>]
70
+ def content_properties = Schema.content_properties(@type)
71
+
72
+ # Is this a state event? (has a state_key)
73
+ def state_event? = !@state_key.nil?
74
+ end
75
+ end
76
+ end
77
+
78
+ __END__
79
+ describe "Async::Matrix::Event" do
80
+ it "parses all event fields" do
81
+ event = Async::Matrix::Event.new({
82
+ "type" => "m.room.message",
83
+ "sender" => "@alice:example.com",
84
+ "room_id" => "!abc:example.com",
85
+ "state_key" => "",
86
+ "event_id" => "$evt1",
87
+ "origin_server_ts" => 1234567890,
88
+ "unsigned" => {"age" => 1000},
89
+ "content" => {"msgtype" => "m.text", "body" => "hi"}
90
+ })
91
+ event.type.should == "m.room.message"
92
+ event.sender.should == "@alice:example.com"
93
+ event.room_id.should == "!abc:example.com"
94
+ event.state_key.should == ""
95
+ event.event_id.should == "$evt1"
96
+ event.origin_server_ts.should == 1234567890
97
+ event.unsigned.should == {"age" => 1000}
98
+ event.content.should.be.kind_of Async::Matrix::Content
99
+ event.content.body.should == "hi"
100
+ end
101
+
102
+ it "defaults content to empty Content when missing" do
103
+ event = Async::Matrix::Event.new({"type" => "m.room.message"})
104
+ event.content.should.be.kind_of Async::Matrix::Content
105
+ event.content.body.should.be.nil
106
+ end
107
+
108
+ it "exposes the raw hash" do
109
+ data = {"type" => "m.room.message", "content" => {"body" => "hi", "msgtype" => "m.text"}}
110
+ event = Async::Matrix::Event.new(data)
111
+ event.raw.should.equal data
112
+ end
113
+
114
+ it "detects state events" do
115
+ state = Async::Matrix::Event.new({"type" => "m.room.member", "state_key" => "@a:b"})
116
+ state.state_event?.should == true
117
+
118
+ msg = Async::Matrix::Event.new({"type" => "m.room.message"})
119
+ msg.state_event?.should == false
120
+ end
121
+
122
+ it "returns the schema for known event types" do
123
+ event = Async::Matrix::Event.new({"type" => "m.room.message", "content" => {}})
124
+ event.schema.should.not.be.nil
125
+ event.schema.should.be.kind_of JSONSchemer::Schema
126
+ end
127
+
128
+ it "returns nil schema for unknown event types" do
129
+ event = Async::Matrix::Event.new({"type" => "m.custom.event", "content" => {}})
130
+ event.schema.should.be.nil
131
+ end
132
+
133
+ it "validates a correct event" do
134
+ event = Async::Matrix::Event.new({
135
+ "type" => "m.room.message",
136
+ "content" => {"msgtype" => "m.text", "body" => "hello"},
137
+ "event_id" => "$abc123",
138
+ "sender" => "@alice:example.org",
139
+ "origin_server_ts" => 1234567890,
140
+ "room_id" => "!room:example.org"
141
+ })
142
+ event.valid?.should == true
143
+ event.valid!.should == true
144
+ end
145
+
146
+ it "rejects an invalid event" do
147
+ event = Async::Matrix::Event.new({
148
+ "type" => "m.room.message",
149
+ "content" => {"msgtype" => "m.text"},
150
+ "event_id" => "$abc123",
151
+ "sender" => "@alice:example.org",
152
+ "origin_server_ts" => 1234567890,
153
+ "room_id" => "!room:example.org"
154
+ })
155
+ event.valid?.should == false
156
+ end
157
+
158
+ it "raises ValidationError from valid!" do
159
+ event = Async::Matrix::Event.new({
160
+ "type" => "m.room.member",
161
+ "content" => {"membership" => "invalid_state"},
162
+ "state_key" => "@alice:example.org",
163
+ "event_id" => "$abc123",
164
+ "sender" => "@alice:example.org",
165
+ "origin_server_ts" => 1234567890,
166
+ "room_id" => "!room:example.org"
167
+ })
168
+ begin
169
+ event.valid!
170
+ raise "should have raised"
171
+ rescue Async::Matrix::Schema::ValidationError => e
172
+ e.message.should.include "m.room.member"
173
+ e.message.should.include "$abc123"
174
+ e.errors.should.not.be.empty
175
+ end
176
+ end
177
+
178
+ it "is lenient with unknown event types" do
179
+ event = Async::Matrix::Event.new({
180
+ "type" => "com.custom.event",
181
+ "content" => {"anything" => "goes"},
182
+ "event_id" => "$x",
183
+ "sender" => "@a:b"
184
+ })
185
+ event.valid?.should == true
186
+ event.valid!.should == true
187
+ end
188
+
189
+ it "returns content properties for known types" do
190
+ event = Async::Matrix::Event.new({
191
+ "type" => "m.room.member",
192
+ "content" => {"membership" => "join"}
193
+ })
194
+ props = event.content_properties
195
+ props.should.include "membership"
196
+ end
197
+ 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