async-matrix 2.0.1-arm-linux → 3.0.0-arm-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/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 +29 -123
- 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
|
@@ -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::
|
|
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
|
})
|
data/lib/async/matrix/e2ee.rb
CHANGED
|
@@ -56,6 +56,43 @@ __END__
|
|
|
56
56
|
inbound.session_id.should == group.session_id
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
it "imports an exported session key" do
|
|
60
|
+
# The path room keys take out of server-side key backup and out of
|
|
61
|
+
# m.forwarded_room_key. Without .import these are unusable, and a client
|
|
62
|
+
# can never read history it did not get a live m.room_key for.
|
|
63
|
+
group = Async::Matrix::E2EE::GroupSession.new
|
|
64
|
+
inbound = Async::Matrix::E2EE::InboundGroupSession.new(group.session_key)
|
|
65
|
+
message = group.encrypt("restored from backup")
|
|
66
|
+
|
|
67
|
+
exported = inbound.export_at_first_known_index
|
|
68
|
+
restored = Async::Matrix::E2EE::InboundGroupSession.import(exported)
|
|
69
|
+
|
|
70
|
+
restored.session_id.should == group.session_id
|
|
71
|
+
restored.decrypt(message).first.should == "restored from backup"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "rejects an exported key passed to .new, which requires a signature" do
|
|
75
|
+
# .new takes a version-2 SessionKey and verifies its signature; an exported
|
|
76
|
+
# key is version 1 and unsigned, so it must go through .import instead.
|
|
77
|
+
group = Async::Matrix::E2EE::GroupSession.new
|
|
78
|
+
inbound = Async::Matrix::E2EE::InboundGroupSession.new(group.session_key)
|
|
79
|
+
exported = inbound.export_at_first_known_index
|
|
80
|
+
|
|
81
|
+
lambda {
|
|
82
|
+
Async::Matrix::E2EE::InboundGroupSession.new(exported)
|
|
83
|
+
}.should.raise(RuntimeError)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "exports at a given message index" do
|
|
87
|
+
group = Async::Matrix::E2EE::GroupSession.new
|
|
88
|
+
inbound = Async::Matrix::E2EE::InboundGroupSession.new(group.session_key)
|
|
89
|
+
3.times { |i| group.encrypt("msg #{i}") }
|
|
90
|
+
|
|
91
|
+
Async::Matrix::E2EE::InboundGroupSession
|
|
92
|
+
.import(inbound.export_at(0))
|
|
93
|
+
.first_known_index.should == 0
|
|
94
|
+
end
|
|
95
|
+
|
|
59
96
|
it "round-trips an olm 1:1 message" do
|
|
60
97
|
alice = Async::Matrix::E2EE::Account.new
|
|
61
98
|
bob = Async::Matrix::E2EE::Account.new
|
data/lib/async/matrix/error.rb
CHANGED
|
@@ -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
|
-
|
|
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,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
|
|
@@ -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
|