async-matrix 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ext/async_matrix_e2ee/Cargo.toml +3 -1
- data/ext/async_matrix_e2ee/src/lib.rs +3 -1
- data/lib/async/discord/api/path_tree.rb +2 -5
- data/lib/async/discord/api.rb +1 -6
- data/lib/async/discord/client.rb +1 -4
- data/lib/async/discord/error.rb +1 -5
- data/lib/async/discord/gateway.rb +1 -4
- data/lib/async/discord.rb +0 -1
- data/lib/async/matrix/api/chain.rb +1 -4
- data/lib/async/matrix/api/concat.rb +1 -5
- data/lib/async/matrix/api/path_tree.rb +1 -4
- data/lib/async/matrix/api.rb +1 -5
- data/lib/async/matrix/application_service/bot.rb +4 -7
- data/lib/async/matrix/application_service/config/vivify.rb +1 -5
- data/lib/async/matrix/application_service/config.rb +1 -4
- data/lib/async/matrix/application_service/dispatcher.rb +65 -9
- data/lib/async/matrix/application_service/error_response.rb +1 -5
- data/lib/async/matrix/application_service/event.rb +1 -5
- data/lib/async/matrix/application_service/server.rb +339 -211
- data/lib/async/matrix/application_service/transaction.rb +1 -5
- data/lib/async/matrix/application_service/transaction_handler.rb +185 -0
- data/lib/async/matrix/application_service/transaction_store.rb +1 -5
- data/lib/async/matrix/bridge/discord/db/connection.rb +1 -3
- data/lib/async/matrix/bridge/discord/db/file.rb +2 -4
- data/lib/async/matrix/bridge/discord/db/guild.rb +2 -4
- data/lib/async/matrix/bridge/discord/db/message.rb +2 -4
- data/lib/async/matrix/bridge/discord/db/portal.rb +2 -4
- data/lib/async/matrix/bridge/discord/db/puppet.rb +2 -4
- data/lib/async/matrix/bridge/discord/db/reaction.rb +2 -4
- data/lib/async/matrix/bridge/discord/db/schema.rb +18 -0
- data/lib/async/matrix/bridge/discord/db/user.rb +2 -4
- data/lib/async/matrix/bridge/discord/db.rb +14 -16
- data/lib/async/matrix/client.rb +1 -4
- data/lib/async/matrix/connection.rb +1 -4
- data/lib/async/matrix/double_puppet_client.rb +2 -4
- data/lib/async/matrix/e2ee.rb +1 -2
- data/lib/async/matrix/endpoint.rb +1 -4
- data/lib/async/matrix/error.rb +1 -4
- data/lib/async/matrix/media_client.rb +1 -4
- data/lib/async/matrix/notifier.rb +1 -4
- data/lib/async/matrix/schema/registry.rb +1 -2
- data/lib/async/matrix/schema/validation_error.rb +1 -2
- data/lib/async/matrix/schema.rb +1 -5
- data/lib/async/matrix/server.rb +1 -4
- data/lib/async/matrix/stream.rb +1 -4
- data/lib/async/matrix/version.rb +1 -1
- data/lib/async/matrix.rb +4 -1
- metadata +23 -7
|
@@ -3,300 +3,428 @@
|
|
|
3
3
|
# Released under the Apache License, Version 2.0.
|
|
4
4
|
# Copyright, 2026, by General Intelligence Systems.
|
|
5
5
|
|
|
6
|
-
require "
|
|
6
|
+
require "forwardable"
|
|
7
7
|
require "json"
|
|
8
|
-
require "
|
|
8
|
+
require "grape"
|
|
9
9
|
require "console"
|
|
10
|
-
require "async/matrix"
|
|
11
10
|
|
|
12
11
|
module Async
|
|
13
12
|
module Matrix
|
|
14
13
|
module ApplicationService
|
|
15
|
-
#
|
|
14
|
+
# Matrix Application Service (server side).
|
|
16
15
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
16
|
+
# A +Server+ wraps a Grape::API into which the Matrix wire-protocol routes
|
|
17
|
+
# are mixed (via the {Server::Grape} concern). It forwards the Grape route
|
|
18
|
+
# DSL, so you can declare application-specific endpoints right alongside
|
|
19
|
+
# the Matrix routes, and register event handlers with #dispatch:
|
|
20
|
+
#
|
|
21
|
+
# server = Async::Matrix::ApplicationService::Server.new(
|
|
22
|
+
# hs_token: config.appservice.hs_token,
|
|
23
|
+
# client: Async::Matrix::Client.new(config)
|
|
24
|
+
# ) do
|
|
25
|
+
# dispatch do
|
|
26
|
+
# on "m.room.member" do |event|
|
|
27
|
+
# join_room(event.room_id) if event.content.membership == "invite"
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# dispatch SomeHandler.new # plain handler or Bot
|
|
32
|
+
#
|
|
33
|
+
# post "/_webhook/send" do # your own endpoint, no Matrix auth
|
|
34
|
+
# client.send_text(params[:room_id], params[:body])
|
|
35
|
+
# {ok: true}
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# run server
|
|
40
|
+
#
|
|
41
|
+
# +Server+ is a Rack app (it delegates #call to the wrapped Grape::API), so
|
|
42
|
+
# `run server` works directly, and it can be mounted into a larger Rack app.
|
|
43
|
+
#
|
|
44
|
+
# {Server::Grape} is a plain mix-in, so you can also skip +Server+ entirely
|
|
45
|
+
# and mix the routes straight into your own Grape::API:
|
|
46
|
+
#
|
|
47
|
+
# class MyAppService < Grape::API
|
|
48
|
+
# include Async::Matrix::ApplicationService::Server::Grape
|
|
49
|
+
# post("/_webhook/send") { ... }
|
|
50
|
+
# end
|
|
51
|
+
# MyAppService.configure { |c| c[:hs_token] = ...; c[:dispatcher] = ... }
|
|
22
52
|
class Server
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@hs_token = hs_token
|
|
33
|
-
@dispatcher = dispatcher
|
|
34
|
-
@txn_store = TransactionStore.new
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Register a Bot or a plain handler on the internal dispatcher.
|
|
53
|
+
extend Forwardable
|
|
54
|
+
|
|
55
|
+
# The Matrix Application Service wire protocol, as an includable concern.
|
|
56
|
+
# Mixing it into a Grape::API defines these routes (all under
|
|
57
|
+
# /_matrix/app/v1), reading its collaborators from Grape +configuration+:
|
|
58
|
+
# configuration[:hs_token] — homeserver token (authentication)
|
|
59
|
+
# configuration[:dispatcher] — Dispatcher receiving transactions
|
|
60
|
+
# configuration[:thirdparty] — optional third-party lookup handler
|
|
61
|
+
# configuration[:client] — optional Client (exposed to endpoints)
|
|
38
62
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
63
|
+
# Routes:
|
|
64
|
+
# PUT transactions/{txnId} — receive events (authenticated)
|
|
65
|
+
# POST ping — healthcheck (unauthenticated)
|
|
66
|
+
# GET users/{userId} — user existence query (authenticated)
|
|
67
|
+
# GET rooms/{roomAlias} — room alias query (authenticated)
|
|
68
|
+
# GET thirdparty/protocol/{proto} — protocol metadata (authenticated)
|
|
69
|
+
# GET thirdparty/location — location lookup (authenticated)
|
|
70
|
+
# GET thirdparty/location/{proto} — reverse location lookup (authenticated)
|
|
71
|
+
# GET thirdparty/user — user lookup (authenticated)
|
|
72
|
+
# GET thirdparty/user/{proto} — reverse user lookup (authenticated)
|
|
42
73
|
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
# Third-party queries delegate to configuration[:thirdparty], a duck type:
|
|
75
|
+
# protocol(name) -> Hash | nil (nil ⇒ 404 M_NOT_FOUND)
|
|
76
|
+
# locations(proto, params) -> Array (default [])
|
|
77
|
+
# users(proto, params) -> Array (default [])
|
|
78
|
+
module Grape
|
|
79
|
+
def self.included(base)
|
|
80
|
+
base.class_eval do
|
|
81
|
+
format :json
|
|
82
|
+
content_type :json, "application/json"
|
|
83
|
+
default_format :json
|
|
84
|
+
|
|
85
|
+
helpers do
|
|
86
|
+
def hs_token
|
|
87
|
+
configuration[:hs_token]
|
|
88
|
+
end
|
|
50
89
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
path = request.path_info
|
|
90
|
+
def dispatcher
|
|
91
|
+
configuration[:dispatcher]
|
|
92
|
+
end
|
|
55
93
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return RESP_FORBIDDEN unless authorized?(request)
|
|
60
|
-
handle_transaction(request, Regexp.last_match(1))
|
|
94
|
+
def thirdparty
|
|
95
|
+
configuration[:thirdparty]
|
|
96
|
+
end
|
|
61
97
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
[200, CONTENT_JSON, EMPTY_BODY]
|
|
98
|
+
def client
|
|
99
|
+
configuration[:client]
|
|
100
|
+
end
|
|
66
101
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
102
|
+
# Homeserver token from the Authorization header (Bearer scheme)
|
|
103
|
+
# or the legacy access_token query parameter.
|
|
104
|
+
def request_token
|
|
105
|
+
auth = headers["Authorization"]
|
|
106
|
+
if auth&.start_with?("Bearer ")
|
|
107
|
+
auth.delete_prefix("Bearer ")
|
|
108
|
+
else
|
|
109
|
+
params[:access_token]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
71
112
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
def authenticate!
|
|
114
|
+
token = request_token
|
|
115
|
+
unless token && secure_compare(token, hs_token.to_s)
|
|
116
|
+
error!({errcode: "M_FORBIDDEN"}, 403)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
75
119
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
120
|
+
def secure_compare(a, b)
|
|
121
|
+
return false unless a.bytesize == b.bytesize
|
|
122
|
+
|
|
123
|
+
l = a.unpack("C*")
|
|
124
|
+
r = b.unpack("C*")
|
|
125
|
+
result = 0
|
|
126
|
+
l.each_with_index { |byte, i| result |= byte ^ r[i] }
|
|
127
|
+
result.zero?
|
|
128
|
+
end
|
|
80
129
|
|
|
81
|
-
|
|
130
|
+
# Raw JSON body, parsed here (rather than via Grape's param
|
|
131
|
+
# coercion) so malformed input yields the Matrix M_BAD_JSON.
|
|
132
|
+
def json_body
|
|
133
|
+
raw = request.body.read
|
|
134
|
+
request.body.rewind if request.body.respond_to?(:rewind)
|
|
135
|
+
return {} if raw.nil? || raw.empty?
|
|
136
|
+
|
|
137
|
+
JSON.parse(raw)
|
|
138
|
+
rescue JSON::ParserError => e
|
|
139
|
+
Console.error(self) { "Bad JSON in request: #{e.message}" }
|
|
140
|
+
error!({errcode: "M_BAD_JSON"}, 400)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
82
143
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
false
|
|
144
|
+
# --- Healthcheck (unauthenticated) -----------------------------
|
|
145
|
+
|
|
146
|
+
post "/_matrix/app/v1/ping" do
|
|
147
|
+
status 200
|
|
148
|
+
{}
|
|
89
149
|
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
150
|
|
|
93
|
-
|
|
94
|
-
auth = request.get_header("HTTP_AUTHORIZATION")
|
|
95
|
-
if auth && auth.start_with?("Bearer ")
|
|
96
|
-
auth.delete_prefix("Bearer ")
|
|
97
|
-
else
|
|
98
|
-
request.params["access_token"]
|
|
99
|
-
end
|
|
100
|
-
end
|
|
151
|
+
# --- Authenticated homeserver-facing routes --------------------
|
|
101
152
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
l = a.unpack("C*")
|
|
105
|
-
r = b.unpack("C*")
|
|
106
|
-
result = 0
|
|
107
|
-
l.each_with_index { |byte, i| result |= byte ^ r[i] }
|
|
108
|
-
result.zero?
|
|
109
|
-
else
|
|
110
|
-
false
|
|
111
|
-
end
|
|
112
|
-
end
|
|
153
|
+
namespace "/_matrix/app/v1" do
|
|
154
|
+
before { authenticate! }
|
|
113
155
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
[200, CONTENT_JSON, EMPTY_BODY]
|
|
118
|
-
else
|
|
119
|
-
parse_json(request).then do |body|
|
|
120
|
-
if body
|
|
121
|
-
Console.info(self) {
|
|
156
|
+
put "transactions/:txn_id" do
|
|
157
|
+
body = json_body
|
|
158
|
+
Console.info(self) do
|
|
122
159
|
events = body["events"] || []
|
|
123
|
-
"Transaction #{txn_id}: #{events.size} event(s)
|
|
124
|
-
|
|
160
|
+
"Transaction #{params[:txn_id]}: #{events.size} event(s)"
|
|
161
|
+
end
|
|
162
|
+
dispatcher.receive_transaction(params[:txn_id], body)
|
|
163
|
+
{}
|
|
164
|
+
end
|
|
125
165
|
|
|
126
|
-
|
|
127
|
-
|
|
166
|
+
get "users/:user_id", requirements: {user_id: %r{[^/]+}} do
|
|
167
|
+
{}
|
|
168
|
+
end
|
|
128
169
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
170
|
+
get "rooms/:room_alias", requirements: {room_alias: %r{[^/]+}} do
|
|
171
|
+
error!({errcode: "M_NOT_FOUND"}, 404)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# --- Third-party protocol lookups ---
|
|
175
|
+
|
|
176
|
+
get "thirdparty/protocol/:protocol" do
|
|
177
|
+
result = thirdparty&.protocol(params[:protocol])
|
|
178
|
+
error!({errcode: "M_NOT_FOUND"}, 404) unless result
|
|
179
|
+
|
|
180
|
+
result
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
get "thirdparty/location" do
|
|
184
|
+
thirdparty&.locations(nil, params) || []
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
get "thirdparty/location/:protocol" do
|
|
188
|
+
thirdparty&.locations(params[:protocol], params) || []
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
get "thirdparty/user" do
|
|
192
|
+
thirdparty&.users(nil, params) || []
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
get "thirdparty/user/:protocol" do
|
|
196
|
+
thirdparty&.users(params[:protocol], params) || []
|
|
132
197
|
end
|
|
133
198
|
end
|
|
134
199
|
end
|
|
135
200
|
end
|
|
201
|
+
end
|
|
136
202
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
203
|
+
# Grape route DSL — forwarded to the wrapped API, so app-specific
|
|
204
|
+
# endpoints declared in the Server.new block land on the same Grape::API
|
|
205
|
+
# as the Matrix routes. `mount` is included, so you can compose in other
|
|
206
|
+
# Grape APIs too.
|
|
207
|
+
def_delegators :@api,
|
|
208
|
+
:get, :post, :put, :patch, :delete, :head, :options,
|
|
209
|
+
:namespace, :group, :resource, :resources, :route, :route_param,
|
|
210
|
+
:before, :after, :rescue_from, :helpers, :params, :desc, :use, :mount,
|
|
211
|
+
:version, :prefix, :format, :content_type,
|
|
212
|
+
# Rack + introspection:
|
|
213
|
+
:call, :routes, :recognize_path
|
|
214
|
+
|
|
215
|
+
attr_reader :api, :dispatcher, :client
|
|
216
|
+
|
|
217
|
+
def initialize(hs_token:, dispatcher: Dispatcher.new, client: nil, thirdparty: nil, &block)
|
|
218
|
+
@dispatcher = dispatcher
|
|
219
|
+
@client = client
|
|
220
|
+
|
|
221
|
+
# A fresh Grape::API with the Matrix routes mixed in, configured with
|
|
222
|
+
# our collaborators. No mounting — the routes are defined inline by the
|
|
223
|
+
# concern, so adding app-specific routes never triggers a re-mount.
|
|
224
|
+
@api = ::Class.new(::Grape::API)
|
|
225
|
+
@api.include(Grape)
|
|
226
|
+
@api.configure do |c|
|
|
227
|
+
c[:hs_token] = hs_token
|
|
228
|
+
c[:dispatcher] = dispatcher
|
|
229
|
+
c[:thirdparty] = thirdparty
|
|
230
|
+
c[:client] = client
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
instance_eval(&block) if block
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Register handlers with the dispatcher. Sugar over dispatcher.register:
|
|
237
|
+
#
|
|
238
|
+
# dispatch do # block → Bot built with `client`
|
|
239
|
+
# on "m.room.message" do |e| ... end
|
|
240
|
+
# end
|
|
241
|
+
# dispatch Bot.new(other_client) { } # explicit bot
|
|
242
|
+
# dispatch SomeHandler.new # plain handler
|
|
243
|
+
#
|
|
244
|
+
def dispatch(handler = nil, &block)
|
|
245
|
+
if block
|
|
246
|
+
unless @client
|
|
247
|
+
raise ArgumentError, "dispatch { ... } needs a client; pass `client:` to Server.new"
|
|
142
248
|
end
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
249
|
+
|
|
250
|
+
@dispatcher.register(Bot.new(@client, &block))
|
|
251
|
+
elsif handler
|
|
252
|
+
@dispatcher.register(handler)
|
|
253
|
+
else
|
|
254
|
+
raise ArgumentError, "dispatch requires a handler argument or a block"
|
|
146
255
|
end
|
|
256
|
+
@dispatcher
|
|
257
|
+
end
|
|
147
258
|
end
|
|
148
259
|
end
|
|
149
260
|
end
|
|
150
261
|
end
|
|
151
262
|
|
|
152
|
-
|
|
153
|
-
require "
|
|
263
|
+
__END__
|
|
264
|
+
require "rack/mock"
|
|
154
265
|
|
|
155
266
|
describe "Async::Matrix::ApplicationService::Server" do
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
267
|
+
Server = Async::Matrix::ApplicationService::Server
|
|
268
|
+
Dispatcher = Async::Matrix::ApplicationService::Dispatcher
|
|
269
|
+
|
|
270
|
+
# Positional opts hash (not kwargs) so the helper survives scampi's
|
|
271
|
+
# nested-describe delegation, which forwards via *args.
|
|
272
|
+
def mock_req(app, method, path, opts = {})
|
|
273
|
+
env = {}
|
|
274
|
+
env["HTTP_AUTHORIZATION"] = "Bearer #{opts[:token]}" if opts[:token]
|
|
275
|
+
if opts[:body]
|
|
276
|
+
env["CONTENT_TYPE"] = "application/json"
|
|
277
|
+
env[:input] = opts[:body]
|
|
278
|
+
end
|
|
279
|
+
Rack::MockRequest.new(app).request(method, path, env)
|
|
159
280
|
end
|
|
160
281
|
|
|
161
|
-
def
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"PATH_INFO" => path,
|
|
165
|
-
"QUERY_STRING" => params.map { |k, v| "#{k}=#{v}" }.join("&"),
|
|
166
|
-
"rack.input" => body ? StringIO.new(body) : StringIO.new("")
|
|
167
|
-
}
|
|
168
|
-
headers.each { |k, v| rack_env["HTTP_#{k.upcase.tr("-", "_")}"] = v }
|
|
169
|
-
rack_env
|
|
282
|
+
def build(hs_token: "secret", dispatcher: nil, client: nil, thirdparty: nil, &block)
|
|
283
|
+
Server.new(hs_token: hs_token, dispatcher: dispatcher || Dispatcher.new,
|
|
284
|
+
client: client, thirdparty: thirdparty, &block)
|
|
170
285
|
end
|
|
171
286
|
|
|
172
|
-
# ---
|
|
287
|
+
# --- Matrix wire protocol ---
|
|
173
288
|
|
|
174
|
-
it "responds to POST /ping with 200" do
|
|
175
|
-
|
|
176
|
-
status, _, _ = server.call(env("POST", "/_matrix/app/v1/ping"))
|
|
177
|
-
status.should == 200
|
|
289
|
+
it "responds to POST /ping with 200 (no auth required)" do
|
|
290
|
+
mock_req(build, "POST", "/_matrix/app/v1/ping").status.should == 200
|
|
178
291
|
end
|
|
179
292
|
|
|
180
293
|
it "rejects GET /ping with 405" do
|
|
181
|
-
|
|
182
|
-
status, _, _ = server.call(env("GET", "/_matrix/app/v1/ping"))
|
|
183
|
-
status.should == 405
|
|
294
|
+
mock_req(build, "GET", "/_matrix/app/v1/ping").status.should == 405
|
|
184
295
|
end
|
|
185
296
|
|
|
186
|
-
# --- Auth ---
|
|
187
|
-
|
|
188
297
|
it "rejects transactions without a token" do
|
|
189
|
-
|
|
190
|
-
status, _, _ = server.call(env("PUT", "/_matrix/app/v1/transactions/txn1"))
|
|
191
|
-
status.should == 403
|
|
298
|
+
mock_req(build, "PUT", "/_matrix/app/v1/transactions/txn1", body: "{}").status.should == 403
|
|
192
299
|
end
|
|
193
300
|
|
|
194
301
|
it "rejects transactions with wrong token" do
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
headers: {"authorization" => "Bearer wrong"}))
|
|
198
|
-
status.should == 403
|
|
302
|
+
app = build(hs_token: "correct")
|
|
303
|
+
mock_req(app, "PUT", "/_matrix/app/v1/transactions/txn1", token: "wrong", body: "{}").status.should == 403
|
|
199
304
|
end
|
|
200
305
|
|
|
201
|
-
# --- Transactions ---
|
|
202
|
-
|
|
203
306
|
it "accepts valid transactions with Bearer auth" do
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
headers: {"authorization" => "Bearer secret"},
|
|
207
|
-
body: '{"events":[]}'))
|
|
208
|
-
status.should == 200
|
|
307
|
+
mock_req(build, "PUT", "/_matrix/app/v1/transactions/txn1",
|
|
308
|
+
token: "secret", body: '{"events":[]}').status.should == 200
|
|
209
309
|
end
|
|
210
310
|
|
|
211
|
-
it "
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
status, _, _ = server.call(rack2)
|
|
222
|
-
status.should == 200
|
|
311
|
+
it "dispatches events from a transaction" do
|
|
312
|
+
received = []
|
|
313
|
+
handler = Object.new
|
|
314
|
+
handler.define_singleton_method(:event_types) { ["m.room.message"] }
|
|
315
|
+
handler.define_singleton_method(:call) { |e| received << e }
|
|
316
|
+
|
|
317
|
+
app = build { dispatch handler }
|
|
318
|
+
mock_req(app, "PUT", "/_matrix/app/v1/transactions/txn_disp", token: "secret",
|
|
319
|
+
body: '{"events":[{"type":"m.room.message","content":{"body":"hi"}}]}')
|
|
320
|
+
received.length.should == 1
|
|
223
321
|
end
|
|
224
322
|
|
|
225
|
-
it "
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
323
|
+
it "deduplicates transactions" do
|
|
324
|
+
received = []
|
|
325
|
+
handler = Object.new
|
|
326
|
+
handler.define_singleton_method(:event_types) { ["m.room.message"] }
|
|
327
|
+
handler.define_singleton_method(:call) { |e| received << e }
|
|
328
|
+
|
|
329
|
+
app = build { dispatch handler }
|
|
330
|
+
body = '{"events":[{"type":"m.room.message","content":{"body":"hi"}}]}'
|
|
331
|
+
mock_req(app, "PUT", "/_matrix/app/v1/transactions/dup", token: "secret", body: body)
|
|
332
|
+
resp = mock_req(app, "PUT", "/_matrix/app/v1/transactions/dup", token: "secret", body: body)
|
|
333
|
+
|
|
334
|
+
resp.status.should == 200
|
|
335
|
+
received.length.should == 1
|
|
231
336
|
end
|
|
232
337
|
|
|
233
|
-
|
|
338
|
+
it "rejects bad JSON with 400" do
|
|
339
|
+
mock_req(build, "PUT", "/_matrix/app/v1/transactions/txn1",
|
|
340
|
+
token: "secret", body: "not json").status.should == 400
|
|
341
|
+
end
|
|
234
342
|
|
|
235
343
|
it "responds 200 for user queries" do
|
|
236
|
-
|
|
237
|
-
status, _, _ = server.call(env("GET", "/_matrix/app/v1/users/@bot:localhost",
|
|
238
|
-
headers: {"authorization" => "Bearer secret"}))
|
|
239
|
-
status.should == 200
|
|
344
|
+
mock_req(build, "GET", "/_matrix/app/v1/users/@bot:localhost", token: "secret").status.should == 200
|
|
240
345
|
end
|
|
241
346
|
|
|
242
347
|
it "responds 404 for room queries" do
|
|
243
|
-
|
|
244
|
-
status, _, _ = server.call(env("GET", "/_matrix/app/v1/rooms/#room:localhost",
|
|
245
|
-
headers: {"authorization" => "Bearer secret"}))
|
|
246
|
-
status.should == 404
|
|
348
|
+
mock_req(build, "GET", "/_matrix/app/v1/rooms/%23room:localhost", token: "secret").status.should == 404
|
|
247
349
|
end
|
|
248
350
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
status, _, _ = server.call(env("GET", "/unknown"))
|
|
254
|
-
status.should == 404
|
|
351
|
+
it "returns [] for third-party user lookup with no handler" do
|
|
352
|
+
resp = mock_req(build, "GET", "/_matrix/app/v1/thirdparty/user", token: "secret")
|
|
353
|
+
resp.status.should == 200
|
|
354
|
+
resp.body.should == "[]"
|
|
255
355
|
end
|
|
256
356
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
it "creates a default dispatcher when none is provided" do
|
|
260
|
-
server = Async::Matrix::ApplicationService::Server.new(hs_token: "secret")
|
|
261
|
-
status, _, _ = server.call(env("POST", "/_matrix/app/v1/ping"))
|
|
262
|
-
status.should == 200
|
|
357
|
+
it "404s third-party protocol lookup with no handler" do
|
|
358
|
+
mock_req(build, "GET", "/_matrix/app/v1/thirdparty/protocol/irc", token: "secret").status.should == 404
|
|
263
359
|
end
|
|
264
360
|
|
|
265
|
-
it "
|
|
266
|
-
|
|
361
|
+
it "delegates third-party protocol lookup to the handler" do
|
|
362
|
+
tp = Object.new
|
|
363
|
+
tp.define_singleton_method(:protocol) { |name| {"instances" => [], "name" => name} }
|
|
364
|
+
app = build(thirdparty: tp)
|
|
365
|
+
resp = mock_req(app, "GET", "/_matrix/app/v1/thirdparty/protocol/irc", token: "secret")
|
|
366
|
+
resp.status.should == 200
|
|
367
|
+
JSON.parse(resp.body)["name"].should == "irc"
|
|
368
|
+
end
|
|
267
369
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
handler.define_singleton_method(:call) { |e| received << e }
|
|
370
|
+
it "responds 404 for unknown paths" do
|
|
371
|
+
mock_req(build, "GET", "/unknown").status.should == 404
|
|
372
|
+
end
|
|
272
373
|
|
|
273
|
-
|
|
374
|
+
# --- App-specific endpoints via the forwarded DSL ---
|
|
274
375
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
376
|
+
it "serves app-specific routes declared with the forwarded post DSL" do
|
|
377
|
+
app = build do
|
|
378
|
+
post "/_webhook/send" do
|
|
379
|
+
{ok: true}
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
resp = mock_req(app, "POST", "/_webhook/send")
|
|
383
|
+
resp.status.should == 201
|
|
384
|
+
JSON.parse(resp.body)["ok"].should == true
|
|
385
|
+
end
|
|
278
386
|
|
|
279
|
-
|
|
387
|
+
it "does not require Matrix auth for app-specific routes" do
|
|
388
|
+
app = build { post("/_webhook/send") { {ok: true} } }
|
|
389
|
+
mock_req(app, "POST", "/_webhook/send").status.should == 201
|
|
280
390
|
end
|
|
281
391
|
|
|
282
|
-
it "
|
|
283
|
-
|
|
392
|
+
it "does not duplicate the Matrix routes when app routes are added" do
|
|
393
|
+
app = build { post("/_webhook/send") { {} } }
|
|
394
|
+
ping_routes = app.routes.select { |r| r.path.include?("/ping") }
|
|
395
|
+
ping_routes.length.should == 1
|
|
396
|
+
end
|
|
284
397
|
|
|
285
|
-
|
|
286
|
-
handler = Object.new
|
|
287
|
-
handler.define_singleton_method(:event_types) { ["m.room.message"] }
|
|
288
|
-
handler.define_singleton_method(:call) { |e| received << e }
|
|
398
|
+
# --- dispatch DSL ---
|
|
289
399
|
|
|
290
|
-
|
|
291
|
-
|
|
400
|
+
it "dispatch { on ... } builds a Bot from the block using the client" do
|
|
401
|
+
config = Async::Matrix::ApplicationService::Config.new({
|
|
402
|
+
"homeserver" => {"address" => "http://localhost:8008", "domain" => "localhost"},
|
|
403
|
+
"appservice" => {"as_token" => "a", "hs_token" => "secret", "bot" => {"username" => "bot"}}
|
|
404
|
+
})
|
|
405
|
+
bot_client = Async::Matrix::Client.new(config)
|
|
292
406
|
|
|
293
|
-
|
|
407
|
+
app = build(client: bot_client) do
|
|
408
|
+
dispatch do
|
|
409
|
+
on "m.room.message" do |event|
|
|
410
|
+
# not executed in this test
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
app.dispatcher.handler_count.should == 1
|
|
415
|
+
end
|
|
294
416
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
417
|
+
it "dispatch { } without a client raises ArgumentError" do
|
|
418
|
+
lambda { build { dispatch { on("m.room.message") { |e| } } } }.should.raise(ArgumentError)
|
|
419
|
+
end
|
|
298
420
|
|
|
299
|
-
|
|
421
|
+
it "isolates dispatcher and routes between instances" do
|
|
422
|
+
a = build(hs_token: "token_a") { post("/only_a") { {} } }
|
|
423
|
+
b = build(hs_token: "token_b")
|
|
424
|
+
mock_req(a, "PUT", "/_matrix/app/v1/transactions/x", token: "token_b", body: "{}").status.should == 403
|
|
425
|
+
mock_req(a, "PUT", "/_matrix/app/v1/transactions/x", token: "token_a", body: "{}").status.should == 200
|
|
426
|
+
# /only_a exists on `a` but not on `b`
|
|
427
|
+
mock_req(a, "POST", "/only_a").status.should == 201
|
|
428
|
+
mock_req(b, "POST", "/only_a").status.should == 404
|
|
300
429
|
end
|
|
301
430
|
end
|
|
302
|
-
end
|