matrix_sdk 1.4.0 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/lib/matrix_sdk.rb +5 -2
- data/lib/matrix_sdk/api.rb +118 -35
- data/lib/matrix_sdk/client.rb +269 -30
- data/lib/matrix_sdk/extensions.rb +17 -1
- data/lib/matrix_sdk/mxid.rb +25 -2
- data/lib/matrix_sdk/protocols/cs.rb +728 -52
- data/lib/matrix_sdk/protocols/msc.rb +147 -0
- data/lib/matrix_sdk/response.rb +11 -0
- data/lib/matrix_sdk/room.rb +112 -15
- data/lib/matrix_sdk/user.rb +75 -8
- data/lib/matrix_sdk/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 487904c24bd70f3d0412ba040f84687d6ef7a6bfe570d259f6d5d73fe7194128
|
4
|
+
data.tar.gz: 7d6f53ba5ec1e1426c5bd274e0809f0edcb67a8e8a355b1dd6d409ac57ee3e0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cfbaff099abec9777a5593fa1646ff5e4285fc246894f850b000834185d352f024e8bc9340c17554da6e73c056db9f327bd98f27c259d123ceadf78977203ed0
|
7
|
+
data.tar.gz: f53ec1ca41f8884a8916609f364db9fd849b8957629c1f46cfaa440910030035d6f4c92de464aaabd3dc3d207ccf47e9bedccce30980ab213a625d5bb4e13473
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,46 @@
|
|
1
|
+
## 2.1.1 - 2020-08-21
|
2
|
+
|
3
|
+
- Fixes crash if state event content is null (#11)
|
4
|
+
- Fixes an uninitialized URI constant exception when requiring only the main library file
|
5
|
+
- Fixes the Api#get_pushrules method missing an ending slash in the request URI
|
6
|
+
- Fixes discovery code for client/server connections based on domain
|
7
|
+
|
8
|
+
## 2.1.0 - 2020-05-22
|
9
|
+
|
10
|
+
- Adds unique query IDs as well as duration in API debug output, to make it easier to track long requests
|
11
|
+
- Finishes up MSC support, get sync over SSE working flawlessly
|
12
|
+
- Exposes the #listen_forever method in the client abstraction
|
13
|
+
- Fixes room access methods
|
14
|
+
|
15
|
+
## 2.0.1 - 2020-03-13
|
16
|
+
|
17
|
+
- Adds code for handling non-final MSC's in protocols
|
18
|
+
- Currently implementing clients parts of MSC2018 for Sync over Server Sent Events
|
19
|
+
|
20
|
+
## 2.0.0 - 2020-02-14
|
21
|
+
|
22
|
+
**NB**, this release includes backwards-incompatible changes;
|
23
|
+
- Changes room state lookup to separate specific state lookups from full state retrieval.
|
24
|
+
This will require changes in client code where `#get_room_state` is called to retrieve
|
25
|
+
all state, as it now requires a state key. For retrieving full room state,
|
26
|
+
`#get_room_state_all` is now the method to use.
|
27
|
+
- Changes some advanced parameters to named parameters, ensure your code is updated if it makes use of them
|
28
|
+
- Fixes SSL verification to actually verify certs (#9)
|
29
|
+
|
30
|
+
- Adds multiple CS API endpoints
|
31
|
+
- Adds `:room_id` key to all room events
|
32
|
+
- Adds `:self` as a valid option to the client abstraction's `#get_user` method
|
33
|
+
- Separates homeserver part stringification for MXIDs
|
34
|
+
- Exposes some previously private client abstraction methods (`#ensure_room`, `#next_batch`) for easier bot usage
|
35
|
+
- Changes room abstraction member lookups to use `#get_room_joined_members`, reducing transferred data amounts
|
36
|
+
- Fixes debug print of methods that return arrays (e.g. CS `/room/{id}/state`)
|
37
|
+
|
38
|
+
## 1.5.0 - 2019-10-25
|
39
|
+
|
40
|
+
- Adds error event to the client abstraction, for handling errors in the background listener
|
41
|
+
- Adds an `open_timeout` setter to the API
|
42
|
+
- Fixes an overly aggressive filter for event handlers
|
43
|
+
|
1
44
|
## 1.4.0 - 2019-09-30
|
2
45
|
|
3
46
|
- Adds the option to change the logger globally or per-object.
|
data/lib/matrix_sdk.rb
CHANGED
@@ -27,6 +27,9 @@ module MatrixSdk
|
|
27
27
|
autoload :CS, 'matrix_sdk/protocols/cs'
|
28
28
|
autoload :IS, 'matrix_sdk/protocols/is'
|
29
29
|
autoload :SS, 'matrix_sdk/protocols/ss'
|
30
|
+
|
31
|
+
# Non-final protocol extensions
|
32
|
+
autoload :MSC, 'matrix_sdk/protocols/msc'
|
30
33
|
end
|
31
34
|
|
32
35
|
def self.debug!
|
@@ -42,10 +45,10 @@ module MatrixSdk
|
|
42
45
|
|
43
46
|
def self.logger=(global_logger)
|
44
47
|
@logger = global_logger
|
45
|
-
@
|
48
|
+
@global_logger = !global_logger.nil?
|
46
49
|
end
|
47
50
|
|
48
51
|
def self.global_logger?
|
49
|
-
@
|
52
|
+
@global_logger ||= false
|
50
53
|
end
|
51
54
|
end
|
data/lib/matrix_sdk/api.rb
CHANGED
@@ -11,10 +11,6 @@ module MatrixSdk
|
|
11
11
|
class Api
|
12
12
|
extend MatrixSdk::Extensions
|
13
13
|
include MatrixSdk::Logging
|
14
|
-
include MatrixSdk::Protocols::AS
|
15
|
-
include MatrixSdk::Protocols::CS
|
16
|
-
include MatrixSdk::Protocols::IS
|
17
|
-
include MatrixSdk::Protocols::SS
|
18
14
|
|
19
15
|
USER_AGENT = "Ruby Matrix SDK v#{MatrixSdk::VERSION}"
|
20
16
|
DEFAULT_HEADERS = {
|
@@ -23,7 +19,7 @@ module MatrixSdk
|
|
23
19
|
}.freeze
|
24
20
|
|
25
21
|
attr_accessor :access_token, :connection_address, :connection_port, :device_id, :autoretry, :global_headers
|
26
|
-
attr_reader :homeserver, :validate_certificate, :
|
22
|
+
attr_reader :homeserver, :validate_certificate, :open_timeout, :read_timeout, :well_known, :proxy_uri
|
27
23
|
|
28
24
|
ignore_inspect :access_token, :logger
|
29
25
|
|
@@ -38,6 +34,7 @@ module MatrixSdk
|
|
38
34
|
# @option params [Boolean] :validate_certificate (false) Should the connection require valid SSL certificates
|
39
35
|
# @option params [Integer] :transaction_id (0) The starting ID for transactions
|
40
36
|
# @option params [Numeric] :backoff_time (5000) The request backoff time in milliseconds
|
37
|
+
# @option params [Numeric] :open_timeout (60) The timeout in seconds to wait for a TCP session to open
|
41
38
|
# @option params [Numeric] :read_timeout (240) The timeout in seconds for reading responses
|
42
39
|
# @option params [Hash] :global_headers Additional headers to set for all requests
|
43
40
|
# @option params [Boolean] :skip_login Should the API skip logging in if the HS URL contains user information
|
@@ -50,10 +47,6 @@ module MatrixSdk
|
|
50
47
|
@homeserver.path.gsub!(/\/?_matrix\/?/, '') if @homeserver.path =~ /_matrix\/?$/
|
51
48
|
raise ArgumentError, 'Please use the base URL for your HS (without /_matrix/)' if @homeserver.path.include? '/_matrix/'
|
52
49
|
|
53
|
-
@protocols = params.fetch(:protocols, %i[CS])
|
54
|
-
@protocols = [@protocols] unless @protocols.is_a? Array
|
55
|
-
@protocols << :CS if @protocols.include?(:AS) && !@protocols.include?(:CS)
|
56
|
-
|
57
50
|
@proxy_uri = params.fetch(:proxy_uri, nil)
|
58
51
|
@connection_address = params.fetch(:address, nil)
|
59
52
|
@connection_port = params.fetch(:port, nil)
|
@@ -63,12 +56,17 @@ module MatrixSdk
|
|
63
56
|
@validate_certificate = params.fetch(:validate_certificate, false)
|
64
57
|
@transaction_id = params.fetch(:transaction_id, 0)
|
65
58
|
@backoff_time = params.fetch(:backoff_time, 5000)
|
59
|
+
@open_timeout = params.fetch(:open_timeout, 60)
|
66
60
|
@read_timeout = params.fetch(:read_timeout, 240)
|
67
61
|
@well_known = params.fetch(:well_known, {})
|
68
62
|
@global_headers = DEFAULT_HEADERS.dup
|
69
63
|
@global_headers.merge!(params.fetch(:global_headers)) if params.key? :global_headers
|
70
64
|
@http = nil
|
71
65
|
|
66
|
+
([params.fetch(:protocols, [:CS])].flatten - protocols).each do |proto|
|
67
|
+
self.class.include MatrixSdk::Protocols.const_get(proto)
|
68
|
+
end
|
69
|
+
|
72
70
|
login(user: @homeserver.user, password: @homeserver.password) if @homeserver.user && @homeserver.password && !@access_token && !params[:skip_login] && protocol?(:CS)
|
73
71
|
@homeserver.userinfo = '' unless params[:skip_login]
|
74
72
|
end
|
@@ -94,24 +92,37 @@ module MatrixSdk
|
|
94
92
|
uri = URI("http#{ssl ? 's' : ''}://#{domain}")
|
95
93
|
well_known = nil
|
96
94
|
target_uri = nil
|
95
|
+
logger = ::Logging.logger[self]
|
96
|
+
logger.debug "Resolving #{domain}"
|
97
97
|
|
98
98
|
if !port.nil? && !port.empty?
|
99
|
+
# If the domain is fully qualified according to Matrix (FQDN and port) then skip discovery
|
99
100
|
target_uri = URI("https://#{domain}:#{port}")
|
100
101
|
elsif target == :server
|
101
102
|
# Attempt SRV record discovery
|
102
103
|
target_uri = begin
|
103
104
|
require 'resolv'
|
104
105
|
resolver = Resolv::DNS.new
|
105
|
-
|
106
|
-
|
106
|
+
srv = "_matrix._tcp.#{domain}"
|
107
|
+
logger.debug "Trying DNS #{srv}..."
|
108
|
+
d = resolver.getresource(srv, Resolv::DNS::Resource::IN::SRV)
|
109
|
+
d
|
110
|
+
rescue StandardError => e
|
111
|
+
logger.debug "DNS lookup failed with #{e.class}: #{e.message}"
|
107
112
|
nil
|
108
113
|
end
|
109
114
|
|
110
115
|
if target_uri.nil?
|
116
|
+
# Attempt .well-known discovery for server-to-server
|
111
117
|
well_known = begin
|
112
|
-
|
118
|
+
uri = URI("https://#{domain}/.well-known/matrix/server")
|
119
|
+
logger.debug "Trying #{uri}..."
|
120
|
+
data = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5, write_timeout: 5) do |http|
|
121
|
+
http.get(uri.path).body
|
122
|
+
end
|
113
123
|
JSON.parse(data)
|
114
|
-
rescue StandardError
|
124
|
+
rescue StandardError => e
|
125
|
+
logger.debug "Well-known failed with #{e.class}: #{e.message}"
|
115
126
|
nil
|
116
127
|
end
|
117
128
|
|
@@ -122,9 +133,14 @@ module MatrixSdk
|
|
122
133
|
elsif %i[client identity].include? target
|
123
134
|
# Attempt .well-known discovery
|
124
135
|
well_known = begin
|
125
|
-
|
126
|
-
|
127
|
-
|
136
|
+
uri = URI("https://#{domain}/.well-known/matrix/client")
|
137
|
+
logger.debug "Trying #{uri}..."
|
138
|
+
data = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5, write_timeout: 5) do |http|
|
139
|
+
http.get(uri.path).body
|
140
|
+
end
|
141
|
+
data = JSON.parse(data)
|
142
|
+
rescue StandardError => e
|
143
|
+
logger.debug "Well-known failed with #{e.class}: #{e.message}"
|
128
144
|
nil
|
129
145
|
end
|
130
146
|
|
@@ -138,6 +154,7 @@ module MatrixSdk
|
|
138
154
|
end
|
139
155
|
end
|
140
156
|
end
|
157
|
+
logger.debug "Using #{target_uri.inspect}"
|
141
158
|
|
142
159
|
# Fall back to direct domain connection
|
143
160
|
target_uri ||= URI("https://#{domain}:8448")
|
@@ -151,10 +168,40 @@ module MatrixSdk
|
|
151
168
|
))
|
152
169
|
end
|
153
170
|
|
171
|
+
# Get a list of enabled protocols on the API client
|
172
|
+
#
|
173
|
+
# @example
|
174
|
+
# MatrixSdk::Api.new_for_domain('matrix.org').protocols
|
175
|
+
# # => [:IS, :CS]
|
176
|
+
#
|
177
|
+
# @return [Symbol[]] An array of enabled APIs
|
178
|
+
def protocols
|
179
|
+
self
|
180
|
+
.class.included_modules
|
181
|
+
.reject { |m| m&.name.nil? }
|
182
|
+
.select { |m| m.name.start_with? 'MatrixSdk::Protocols::' }
|
183
|
+
.map { |m| m.name.split('::').last.to_sym }
|
184
|
+
end
|
185
|
+
|
186
|
+
# Check if a protocol is enabled on the API connection
|
187
|
+
#
|
188
|
+
# @example Checking for identity server API support
|
189
|
+
# api.protocol? :IS
|
190
|
+
# # => false
|
191
|
+
#
|
192
|
+
# @param protocol [Symbol] The protocol to check
|
193
|
+
# @return [Boolean] Is the protocol enabled
|
154
194
|
def protocol?(protocol)
|
155
195
|
protocols.include? protocol
|
156
196
|
end
|
157
197
|
|
198
|
+
# @param seconds [Numeric]
|
199
|
+
# @return [Numeric]
|
200
|
+
def open_timeout=(seconds)
|
201
|
+
@http.finish if @http && @open_timeout != seconds
|
202
|
+
@open_timeout = seconds
|
203
|
+
end
|
204
|
+
|
158
205
|
# @param seconds [Numeric]
|
159
206
|
# @return [Numeric]
|
160
207
|
def read_timeout=(seconds)
|
@@ -192,6 +239,29 @@ module MatrixSdk
|
|
192
239
|
@proxy_uri = proxy_uri
|
193
240
|
end
|
194
241
|
|
242
|
+
# Perform a raw Matrix API request
|
243
|
+
#
|
244
|
+
# @example Simple API query
|
245
|
+
# api.request(:get, :client_r0, '/account/whoami')
|
246
|
+
# # => { :user_id => "@alice:matrix.org" }
|
247
|
+
#
|
248
|
+
# @example Advanced API request
|
249
|
+
# api.request(:post,
|
250
|
+
# :media_r0,
|
251
|
+
# '/upload',
|
252
|
+
# body_stream: open('./file'),
|
253
|
+
# headers: { 'content-type' => 'image/png' })
|
254
|
+
# # => { :content_uri => "mxc://example.com/AQwafuaFswefuhsfAFAgsw" }
|
255
|
+
#
|
256
|
+
# @param method [Symbol] The method to use, can be any of the ones under Net::HTTP
|
257
|
+
# @param api [Symbol] The API symbol to use, :client_r0 is the current CS one
|
258
|
+
# @param path [String] The API path to call, this is the part that comes after the API definition in the spec
|
259
|
+
# @param options [Hash] Additional options to pass along to the request
|
260
|
+
# @option options [Hash] :query Query parameters to set on the URL
|
261
|
+
# @option options [Hash,String] :body The body to attach to the request, will be JSON-encoded if sent as a hash
|
262
|
+
# @option options [IO] :body_stream A body stream to attach to the request
|
263
|
+
# @option options [Hash] :headers Additional headers to set on the request
|
264
|
+
# @option options [Boolean] :skip_auth (false) Skip authentication
|
195
265
|
def request(method, api, path, **options)
|
196
266
|
url = homeserver.dup.tap do |u|
|
197
267
|
u.path = api_to_path(api) + path
|
@@ -209,7 +279,7 @@ module MatrixSdk
|
|
209
279
|
request.content_length = (request.body || request.body_stream).size
|
210
280
|
end
|
211
281
|
|
212
|
-
request['authorization'] = "Bearer #{access_token}" if access_token
|
282
|
+
request['authorization'] = "Bearer #{access_token}" if access_token && !options.fetch(:skip_auth, false)
|
213
283
|
if options.key? :headers
|
214
284
|
options[:headers].each do |h, v|
|
215
285
|
request[h.to_s.downcase] = v
|
@@ -220,14 +290,20 @@ module MatrixSdk
|
|
220
290
|
loop do
|
221
291
|
raise MatrixConnectionError, "Server still too busy to handle request after #{failures} attempts, try again later" if failures >= 10
|
222
292
|
|
223
|
-
|
293
|
+
req_id = ('A'..'Z').to_a.sample(4).join
|
294
|
+
|
295
|
+
print_http(request, id: req_id)
|
224
296
|
begin
|
297
|
+
dur_start = Time.now
|
225
298
|
response = http.request request
|
226
|
-
|
299
|
+
dur_end = Time.now
|
300
|
+
duration = dur_end - dur_start
|
301
|
+
rescue EOFError
|
227
302
|
logger.error 'Socket closed unexpectedly'
|
228
|
-
raise
|
303
|
+
raise
|
229
304
|
end
|
230
|
-
print_http(response)
|
305
|
+
print_http(response, duration: duration, id: req_id)
|
306
|
+
|
231
307
|
data = JSON.parse(response.body, symbolize_names: true) rescue nil
|
232
308
|
|
233
309
|
if response.is_a? Net::HTTPTooManyRequests
|
@@ -246,35 +322,41 @@ module MatrixSdk
|
|
246
322
|
end
|
247
323
|
end
|
248
324
|
|
325
|
+
# Generate a transaction ID
|
326
|
+
#
|
327
|
+
# @return [String] An arbitrary transaction ID
|
328
|
+
def transaction_id
|
329
|
+
ret = @transaction_id ||= 0
|
330
|
+
@transaction_id = @transaction_id.succ
|
331
|
+
ret
|
332
|
+
end
|
333
|
+
|
249
334
|
private
|
250
335
|
|
251
|
-
def print_http(http)
|
336
|
+
def print_http(http, body: true, duration: nil, id: nil)
|
252
337
|
return unless logger.debug?
|
253
338
|
|
254
339
|
if http.is_a? Net::HTTPRequest
|
255
|
-
dir = '>
|
340
|
+
dir = "#{id ? id + ' : ' : nil}>"
|
256
341
|
logger.debug "#{dir} Sending a #{http.method} request to `#{http.path}`:"
|
257
342
|
else
|
258
|
-
dir = '<
|
259
|
-
logger.debug "#{dir} Received a #{http.code} #{http.message} response:"
|
343
|
+
dir = "#{id ? id + ' : ' : nil}<"
|
344
|
+
logger.debug "#{dir} Received a #{http.code} #{http.message} response:#{duration ? " [#{(duration * 1000).to_i}ms]" : nil}"
|
260
345
|
end
|
261
346
|
http.to_hash.map { |k, v| "#{k}: #{k == 'authorization' ? '[ REDACTED ]' : v.join(', ')}" }.each do |h|
|
262
347
|
logger.debug "#{dir} #{h}"
|
263
348
|
end
|
264
349
|
logger.debug dir
|
265
|
-
|
266
|
-
|
267
|
-
|
350
|
+
if body
|
351
|
+
clean_body = JSON.parse(http.body) rescue nil if http.body
|
352
|
+
clean_body.keys.each { |k| clean_body[k] = '[ REDACTED ]' if %w[password access_token].include?(k) }.to_json if clean_body.is_a? Hash
|
353
|
+
clean_body = clean_body.to_s if clean_body
|
354
|
+
logger.debug "#{dir} #{clean_body.length < 200 ? clean_body : clean_body.slice(0..200) + "... [truncated, #{clean_body.length} Bytes]"}" if clean_body
|
355
|
+
end
|
268
356
|
rescue StandardError => e
|
269
357
|
logger.warn "#{e.class} occured while printing request debug; #{e.message}\n#{e.backtrace.join "\n"}"
|
270
358
|
end
|
271
359
|
|
272
|
-
def transaction_id
|
273
|
-
ret = @transaction_id ||= 0
|
274
|
-
@transaction_id = @transaction_id.succ
|
275
|
-
ret
|
276
|
-
end
|
277
|
-
|
278
360
|
def api_to_path(api)
|
279
361
|
# TODO: <api>_current / <api>_latest
|
280
362
|
"/_matrix/#{api.to_s.split('_').join('/')}"
|
@@ -291,9 +373,10 @@ module MatrixSdk
|
|
291
373
|
Net::HTTP.new(host, port)
|
292
374
|
end
|
293
375
|
|
376
|
+
@http.open_timeout = open_timeout
|
294
377
|
@http.read_timeout = read_timeout
|
295
378
|
@http.use_ssl = homeserver.scheme == 'https'
|
296
|
-
@http.verify_mode = validate_certificate ? ::OpenSSL::SSL::
|
379
|
+
@http.verify_mode = validate_certificate ? ::OpenSSL::SSL::VERIFY_PEER : ::OpenSSL::SSL::VERIFY_NONE
|
297
380
|
@http.start
|
298
381
|
@http
|
299
382
|
end
|
data/lib/matrix_sdk/client.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'matrix_sdk'
|
4
4
|
|
5
|
+
require 'English'
|
5
6
|
require 'forwardable'
|
6
7
|
|
7
8
|
module MatrixSdk
|
@@ -10,10 +11,19 @@ module MatrixSdk
|
|
10
11
|
include MatrixSdk::Logging
|
11
12
|
extend Forwardable
|
12
13
|
|
13
|
-
|
14
|
+
# @!attribute api [r] The underlying API connection
|
15
|
+
# @return [Api] The underlying API connection
|
16
|
+
# @!attribute next_batch [r] The batch token for a running sync
|
17
|
+
# @return [String] The opaque batch token
|
18
|
+
# @!attribute cache [rw] The cache level
|
19
|
+
# @return [:all,:some,:none] The level of caching to do
|
20
|
+
# @!attribute sync_filter [rw] The global sync filter
|
21
|
+
# @return [Hash,String] A filter definition, either as defined by the
|
22
|
+
# Matrix spec, or as an identifier returned by a filter creation request
|
23
|
+
attr_reader :api, :next_batch
|
14
24
|
attr_accessor :cache, :sync_filter
|
15
25
|
|
16
|
-
events :event, :presence_event, :invite_event, :leave_event, :ephemeral_event
|
26
|
+
events :error, :event, :presence_event, :invite_event, :leave_event, :ephemeral_event
|
17
27
|
ignore_inspect :api,
|
18
28
|
:on_event, :on_presence_event, :on_invite_event, :on_leave_event, :on_ephemeral_event
|
19
29
|
|
@@ -21,9 +31,20 @@ module MatrixSdk
|
|
21
31
|
:access_token, :access_token=, :device_id, :device_id=, :homeserver, :homeserver=,
|
22
32
|
:validate_certificate, :validate_certificate=
|
23
33
|
|
34
|
+
# Create a new client instance from only a Matrix HS domain
|
35
|
+
#
|
36
|
+
# This will use the well-known delegation lookup to find the correct client URL
|
37
|
+
#
|
38
|
+
# @note This method will not verify that the created client has a valid connection,
|
39
|
+
# it will only perform the necessary lookups to build a connection URL.
|
40
|
+
# @return [Client] The new client instance
|
41
|
+
# @param domain [String] The domain name to look up
|
42
|
+
# @param params [Hash] Additional parameters to pass along to {Api.new_for_domain} as well as {initialize}
|
43
|
+
# @see Api.new_for_domain
|
44
|
+
# @see #initialize
|
24
45
|
def self.new_for_domain(domain, **params)
|
25
46
|
api = MatrixSdk::Api.new_for_domain(domain, keep_wellknown: true)
|
26
|
-
return new(api, params) unless api.well_known
|
47
|
+
return new(api, params) unless api.well_known&.key?('m.identity_server')
|
27
48
|
|
28
49
|
identity_server = MatrixSdk::Api.new(api.well_known['m.identity_server']['base_url'], protocols: %i[IS])
|
29
50
|
new(api, params.merge(identity_server: identity_server))
|
@@ -52,7 +73,7 @@ module MatrixSdk
|
|
52
73
|
@rooms = {}
|
53
74
|
@users = {}
|
54
75
|
@cache = client_cache
|
55
|
-
@identity_server =
|
76
|
+
@identity_server = nil
|
56
77
|
|
57
78
|
@sync_token = nil
|
58
79
|
@sync_thread = nil
|
@@ -74,22 +95,42 @@ module MatrixSdk
|
|
74
95
|
@mxid = params[:user_id]
|
75
96
|
end
|
76
97
|
|
98
|
+
# Gets the currently logged in user's MXID
|
99
|
+
#
|
100
|
+
# @return [MXID] The MXID of the current user
|
77
101
|
def mxid
|
78
102
|
@mxid ||= begin
|
79
|
-
api.whoami?[:user_id] if api&.access_token
|
103
|
+
MXID.new api.whoami?[:user_id] if api&.access_token
|
80
104
|
end
|
81
105
|
end
|
82
106
|
|
83
|
-
|
84
|
-
id = MXID.new id.to_s unless id.is_a? MXID
|
85
|
-
raise ArgumentError, 'Must be a User ID' unless id.user?
|
107
|
+
alias user_id mxid
|
86
108
|
|
87
|
-
|
109
|
+
# Gets the current user presence status object
|
110
|
+
#
|
111
|
+
# @return [Response] The user presence
|
112
|
+
# @see User#presence
|
113
|
+
# @see Protocols::CS#get_presence_status
|
114
|
+
def presence
|
115
|
+
api.get_presence_status(mxid).tap { |h| h.delete :user_id }
|
88
116
|
end
|
89
117
|
|
90
|
-
|
91
|
-
|
118
|
+
# Sets the current user's presence status
|
119
|
+
#
|
120
|
+
# @param status [:online,:offline,:unavailable] The new status to use
|
121
|
+
# @param message [String] A custom status message to set
|
122
|
+
# @see User#presence=
|
123
|
+
# @see Protocols::CS#set_presence_status
|
124
|
+
def set_presence(status, message: nil)
|
125
|
+
raise ArgumentError, 'Presence must be one of :online, :offline, :unavailable' unless %i[online offline unavailable].include?(status)
|
126
|
+
|
127
|
+
api.set_presence_status(mxid, status, message: message)
|
128
|
+
end
|
92
129
|
|
130
|
+
# Gets a list of all the public rooms on the connected HS
|
131
|
+
#
|
132
|
+
# @note This will try to list all public rooms on the HS, and may take a while on larger instances
|
133
|
+
# @return [Array[Room]] The public rooms
|
93
134
|
def public_rooms
|
94
135
|
rooms = []
|
95
136
|
since = nil
|
@@ -113,6 +154,12 @@ module MatrixSdk
|
|
113
154
|
rooms
|
114
155
|
end
|
115
156
|
|
157
|
+
# Gets a list of all relevant rooms, either the ones currently handled by
|
158
|
+
# the client, or the list of currently joined ones if no rooms are handled
|
159
|
+
#
|
160
|
+
# @return [Array[Room]] All the currently handled rooms
|
161
|
+
# @note This will always return the empty array if the cache level is set
|
162
|
+
# to :none
|
116
163
|
def rooms
|
117
164
|
if @rooms.empty? && cache != :none
|
118
165
|
api.get_joined_rooms.joined_rooms.each do |id|
|
@@ -123,6 +170,11 @@ module MatrixSdk
|
|
123
170
|
@rooms.values
|
124
171
|
end
|
125
172
|
|
173
|
+
# Refresh the list of currently handled rooms, replacing it with the user's
|
174
|
+
# currently joined rooms.
|
175
|
+
#
|
176
|
+
# @note This will be a no-op if the cache level is set to :none
|
177
|
+
# @return [Boolean] If the refresh succeeds
|
126
178
|
def reload_rooms!
|
127
179
|
return true if cache == :none
|
128
180
|
|
@@ -136,12 +188,25 @@ module MatrixSdk
|
|
136
188
|
end
|
137
189
|
alias refresh_rooms! reload_rooms!
|
138
190
|
|
191
|
+
# Register - and log in - on the connected HS as a guest
|
192
|
+
#
|
193
|
+
# @note This feature is not commonly supported by many HSes
|
139
194
|
def register_as_guest
|
140
195
|
data = api.register(kind: :guest)
|
141
196
|
post_authentication(data)
|
142
197
|
end
|
143
198
|
|
144
|
-
|
199
|
+
# Register a new user account on the connected HS
|
200
|
+
#
|
201
|
+
# This will also trigger an initial sync unless no_sync is set
|
202
|
+
#
|
203
|
+
# @note This method will currently always use auth type 'm.login.dummy'
|
204
|
+
# @param username [String] The new user's name
|
205
|
+
# @param password [String] The new user's password
|
206
|
+
# @param params [Hash] Additional options
|
207
|
+
# @option params [Boolean] :no_sync Skip the initial sync on registering
|
208
|
+
# @option params [Boolean] :allow_sync_retry Allow sync to retry on failure
|
209
|
+
def register_with_password(username, password, **params)
|
145
210
|
username = username.to_s unless username.is_a?(String)
|
146
211
|
password = password.to_s unless password.is_a?(String)
|
147
212
|
|
@@ -153,10 +218,21 @@ module MatrixSdk
|
|
153
218
|
|
154
219
|
return if params[:no_sync]
|
155
220
|
|
156
|
-
sync full_state:
|
221
|
+
sync full_state: true,
|
157
222
|
allow_sync_retry: params.fetch(:allow_sync_retry, nil)
|
158
223
|
end
|
159
224
|
|
225
|
+
# Logs in as a user on the connected HS
|
226
|
+
#
|
227
|
+
# This will also trigger an initial sync unless no_sync is set
|
228
|
+
#
|
229
|
+
# @param username [String] The username of the user
|
230
|
+
# @param password [String] The password of the user
|
231
|
+
# @param sync_timeout [Numeric] The timeout of the initial sync on login
|
232
|
+
# @param full_state [Boolean] Should the initial sync retrieve full state
|
233
|
+
# @param params [Hash] Additional options
|
234
|
+
# @option params [Boolean] :no_sync Skip the initial sync on registering
|
235
|
+
# @option params [Boolean] :allow_sync_retry Allow sync to retry on failure
|
160
236
|
def login(username, password, sync_timeout: 15, full_state: false, **params)
|
161
237
|
username = username.to_s unless username.is_a?(String)
|
162
238
|
password = password.to_s unless password.is_a?(String)
|
@@ -174,6 +250,17 @@ module MatrixSdk
|
|
174
250
|
allow_sync_retry: params.fetch(:allow_sync_retry, nil)
|
175
251
|
end
|
176
252
|
|
253
|
+
# Logs in as a user on the connected HS
|
254
|
+
#
|
255
|
+
# This will also trigger an initial sync unless no_sync is set
|
256
|
+
#
|
257
|
+
# @param username [String] The username of the user
|
258
|
+
# @param token [String] The token to log in with
|
259
|
+
# @param sync_timeout [Numeric] The timeout of the initial sync on login
|
260
|
+
# @param full_state [Boolean] Should the initial sync retrieve full state
|
261
|
+
# @param params [Hash] Additional options
|
262
|
+
# @option params [Boolean] :no_sync Skip the initial sync on registering
|
263
|
+
# @option params [Boolean] :allow_sync_retry Allow sync to retry on failure
|
177
264
|
def login_with_token(username, token, sync_timeout: 15, full_state: false, **params)
|
178
265
|
username = username.to_s unless username.is_a?(String)
|
179
266
|
token = token.to_s unless token.is_a?(String)
|
@@ -191,30 +278,90 @@ module MatrixSdk
|
|
191
278
|
allow_sync_retry: params.fetch(:allow_sync_retry, nil)
|
192
279
|
end
|
193
280
|
|
281
|
+
# Logs out of the current session
|
194
282
|
def logout
|
195
283
|
api.logout
|
196
284
|
@api.access_token = nil
|
197
285
|
@mxid = nil
|
198
286
|
end
|
199
287
|
|
288
|
+
# Check if there's a currently logged in session
|
289
|
+
#
|
290
|
+
# @note This will not check if the session is valid, only if it exists
|
291
|
+
# @return [Boolean] If there's a current session
|
200
292
|
def logged_in?
|
201
|
-
|
293
|
+
!@api.access_token.nil?
|
202
294
|
end
|
203
295
|
|
296
|
+
# Retrieve a list of all registered third-party IDs for the current user
|
297
|
+
#
|
298
|
+
# @return [Response] A response hash containing the key :threepids
|
299
|
+
# @see Protocols::CS#get_3pids
|
300
|
+
def registered_3pids
|
301
|
+
data = api.get_3pids
|
302
|
+
data.threepids.each do |obj|
|
303
|
+
obj.instance_eval do
|
304
|
+
def added_at
|
305
|
+
Time.at(self[:added_at] / 1000)
|
306
|
+
end
|
307
|
+
|
308
|
+
def validated_at
|
309
|
+
return unless validated?
|
310
|
+
|
311
|
+
Time.at(self[:validated_at] / 1000)
|
312
|
+
end
|
313
|
+
|
314
|
+
def validated?
|
315
|
+
key? :validated_at
|
316
|
+
end
|
317
|
+
|
318
|
+
def to_s
|
319
|
+
"#{self[:medium]}:#{self[:address]}"
|
320
|
+
end
|
321
|
+
|
322
|
+
def inspect
|
323
|
+
"#<MatrixSdk::Response 3pid=#{to_s.inspect} added_at=\"#{added_at}\"#{validated? ? " validated_at=\"#{validated_at}\"" : ''}>"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
data
|
328
|
+
end
|
329
|
+
|
330
|
+
# Creates a new room
|
331
|
+
#
|
332
|
+
# @example Creating a room with an alias
|
333
|
+
# client.create_room('myroom')
|
334
|
+
# #<MatrixSdk::Room ... >
|
335
|
+
#
|
336
|
+
# @param room_alias [String] A default alias to set on the room, should only be the localpart
|
337
|
+
# @return [Room] The resulting room
|
338
|
+
# @see Protocols::CS#create_room
|
204
339
|
def create_room(room_alias = nil, **params)
|
205
340
|
data = api.create_room(params.merge(room_alias: room_alias))
|
206
341
|
ensure_room(data.room_id)
|
207
342
|
end
|
208
343
|
|
344
|
+
# Joins an already created room
|
345
|
+
#
|
346
|
+
# @param room_id_or_alias [String,MXID] A room alias (#room:exmaple.com) or a room ID (!id:example.com)
|
347
|
+
# @param server_name [Array[String]] A list of servers to attempt the join through, required for IDs
|
348
|
+
# @return [Room] The resulting room
|
349
|
+
# @see Protocols::CS#join_room
|
209
350
|
def join_room(room_id_or_alias, server_name: [])
|
210
351
|
server_name = [server_name] unless server_name.is_a? Array
|
211
352
|
data = api.join_room(room_id_or_alias, server_name: server_name)
|
212
353
|
ensure_room(data.fetch(:room_id, room_id_or_alias))
|
213
354
|
end
|
214
355
|
|
356
|
+
# Find a room in the locally cached list of rooms that the current user is part of
|
357
|
+
#
|
358
|
+
# @param room_id_or_alias [String,MXID] A room ID or alias
|
359
|
+
# @param only_canonical [Boolean] Only match alias against the canonical alias
|
360
|
+
# @return [Room] The found room
|
361
|
+
# @return [nil] If no room was found
|
215
362
|
def find_room(room_id_or_alias, only_canonical: false)
|
216
363
|
room_id_or_alias = MXID.new(room_id_or_alias.to_s) unless room_id_or_alias.is_a? MXID
|
217
|
-
raise ArgumentError, 'Must be a room id or alias' unless
|
364
|
+
raise ArgumentError, 'Must be a room id or alias' unless room_id_or_alias.room?
|
218
365
|
|
219
366
|
return @rooms.fetch(room_id_or_alias.to_s, nil) if room_id_or_alias.room_id?
|
220
367
|
|
@@ -223,7 +370,21 @@ module MatrixSdk
|
|
223
370
|
@rooms.values.find { |r| r.aliases.include? room_id_or_alias.to_s }
|
224
371
|
end
|
225
372
|
|
373
|
+
# Get a User instance from a MXID
|
374
|
+
#
|
375
|
+
# @param user_id [String,MXID,:self] The MXID to look up, will also accept :self in order to get the currently logged-in user
|
376
|
+
# @return [User] The User instance for the specified user
|
377
|
+
# @raise [ArgumentError] If the input isn't a valid user ID
|
378
|
+
# @note The method doesn't perform any existence checking, so the returned User object may point to a non-existent user
|
226
379
|
def get_user(user_id)
|
380
|
+
user_id = mxid if user_id == :self
|
381
|
+
|
382
|
+
user_id = MXID.new user_id.to_s unless user_id.is_a? MXID
|
383
|
+
raise ArgumentError, 'Must be a User ID' unless user_id.user?
|
384
|
+
|
385
|
+
# To still use regular string storage in the hash itself
|
386
|
+
user_id = user_id.to_s
|
387
|
+
|
227
388
|
if cache == :all
|
228
389
|
@users[user_id] ||= User.new(self, user_id)
|
229
390
|
else
|
@@ -231,10 +392,23 @@ module MatrixSdk
|
|
231
392
|
end
|
232
393
|
end
|
233
394
|
|
395
|
+
# Remove a room alias
|
396
|
+
#
|
397
|
+
# @param room_alias [String,MXID] The room alias to remove
|
398
|
+
# @see Protocols::CS#remove_room_alias
|
234
399
|
def remove_room_alias(room_alias)
|
400
|
+
room_alias = MXID.new room_alias.to_s unless room_alias.is_a? MXID
|
401
|
+
raise ArgumentError, 'Must be a room alias' unless room_alias.room_alias?
|
402
|
+
|
235
403
|
api.remove_room_alias(room_alias)
|
236
404
|
end
|
237
405
|
|
406
|
+
# Upload a piece of data to the media repo
|
407
|
+
#
|
408
|
+
# @return [URI::MATRIX] A Matrix content (mxc://) URL pointing to the uploaded data
|
409
|
+
# @param content [String] The data to upload
|
410
|
+
# @param content_type [String] The MIME type of the data
|
411
|
+
# @see Protocols::CS#media_upload
|
238
412
|
def upload(content, content_type)
|
239
413
|
data = api.media_upload(content, content_type)
|
240
414
|
return data[:content_uri] if data.key? :content_uri
|
@@ -242,21 +416,72 @@ module MatrixSdk
|
|
242
416
|
raise MatrixUnexpectedResponseError, 'Upload succeeded, but no media URI returned'
|
243
417
|
end
|
244
418
|
|
419
|
+
# Starts a background thread that will listen to new events
|
420
|
+
#
|
421
|
+
# @see sync For What parameters are accepted
|
245
422
|
def start_listener_thread(**params)
|
423
|
+
return if listening?
|
424
|
+
|
246
425
|
@should_listen = true
|
247
|
-
|
426
|
+
if api.protocol?(:MSC) && api.msc2108?
|
427
|
+
params[:filter] = sync_filter unless params.key? :filter
|
428
|
+
params[:filter] = params[:filter].to_json unless params[:filter].nil? || params[:filter].is_a?(String)
|
429
|
+
params[:since] = @next_batch if @next_batch
|
430
|
+
|
431
|
+
errors = 0
|
432
|
+
thread, cancel_token = api.msc2108_sync_sse(params) do |data, event:, id:|
|
433
|
+
@next_batch = id if id
|
434
|
+
if event.to_sym == :sync
|
435
|
+
handle_sync_response(data)
|
436
|
+
errors = 0
|
437
|
+
elsif event.to_sym == :sync_error
|
438
|
+
logger.error "SSE Sync error received; #{data.type}: #{data.message}"
|
439
|
+
errors += 1
|
440
|
+
|
441
|
+
# TODO: Allow configuring
|
442
|
+
raise 'Aborting due to excessive errors' if errors >= 5
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
@should_listen = cancel_token
|
447
|
+
else
|
448
|
+
thread = Thread.new { listen_forever(params) }
|
449
|
+
end
|
248
450
|
@sync_thread = thread
|
249
451
|
thread.run
|
250
452
|
end
|
251
453
|
|
454
|
+
# Stops the running background thread if one is active
|
252
455
|
def stop_listener_thread
|
253
456
|
return unless @sync_thread
|
254
457
|
|
255
|
-
@should_listen
|
256
|
-
|
458
|
+
if @should_listen.is_a? Hash
|
459
|
+
@should_listen[:run] = false
|
460
|
+
else
|
461
|
+
@should_listen = false
|
462
|
+
end
|
463
|
+
if @sync_thread.alive?
|
464
|
+
ret = @sync_thread.join(2)
|
465
|
+
@sync_thread.kill unless ret
|
466
|
+
end
|
257
467
|
@sync_thread = nil
|
258
468
|
end
|
259
469
|
|
470
|
+
# Check if there's a thread listening for events
|
471
|
+
def listening?
|
472
|
+
@sync_thread&.alive? == true
|
473
|
+
end
|
474
|
+
|
475
|
+
# Run a message sync round, triggering events as necessary
|
476
|
+
#
|
477
|
+
# @param skip_store_batch [Boolean] Should this sync skip storing the returned next_batch token,
|
478
|
+
# doing this would mean the next sync re-runs from the same point. Useful with use of filters.
|
479
|
+
# @param params [Hash] Additional options
|
480
|
+
# @option params [String,Hash] :filter (#sync_filter) A filter to use for this sync
|
481
|
+
# @option params [Numeric] :timeout (30) A timeout value in seconds for the sync request
|
482
|
+
# @option params [Numeric] :allow_sync_retry (0) The number of retries allowed for this sync request
|
483
|
+
# @option params [String] :since An override of the "since" token to provide to the sync request
|
484
|
+
# @see Protocols::CS#sync
|
260
485
|
def sync(skip_store_batch: false, **params)
|
261
486
|
extra_params = {
|
262
487
|
filter: sync_filter,
|
@@ -278,11 +503,26 @@ module MatrixSdk
|
|
278
503
|
@next_batch = data[:next_batch] unless skip_store_batch
|
279
504
|
|
280
505
|
handle_sync_response(data)
|
506
|
+
true
|
281
507
|
end
|
282
508
|
|
283
509
|
alias listen_for_events sync
|
284
510
|
|
285
|
-
|
511
|
+
# Ensures that a room exists in the cache
|
512
|
+
#
|
513
|
+
# @param room_id [String,MXID] The room ID to ensure
|
514
|
+
# @return [Room] The room object for the requested room
|
515
|
+
def ensure_room(room_id)
|
516
|
+
room_id = MXID.new room_id.to_s unless room_id.is_a? MXID
|
517
|
+
raise ArgumentError, 'Must be a room ID' unless room_id.room_id?
|
518
|
+
|
519
|
+
room_id = room_id.to_s
|
520
|
+
@rooms.fetch(room_id) do
|
521
|
+
room = Room.new(self, room_id)
|
522
|
+
@rooms[room_id] = room unless cache == :none
|
523
|
+
room
|
524
|
+
end
|
525
|
+
end
|
286
526
|
|
287
527
|
def listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 30, **params)
|
288
528
|
orig_bad_sync_timeout = bad_sync_timeout + 0
|
@@ -301,8 +541,14 @@ module MatrixSdk
|
|
301
541
|
end
|
302
542
|
end
|
303
543
|
end
|
544
|
+
rescue StandardError => e
|
545
|
+
logger.error "Unhandled #{e.class} raised in background listener"
|
546
|
+
logger.error [e.message, *e.backtrace].join($RS)
|
547
|
+
fire_error(ErrorEvent.new(e, :listener_thread))
|
304
548
|
end
|
305
549
|
|
550
|
+
private
|
551
|
+
|
306
552
|
def post_authentication(data)
|
307
553
|
@mxid = data[:user_id]
|
308
554
|
@api.access_token = data[:access_token]
|
@@ -311,15 +557,6 @@ module MatrixSdk
|
|
311
557
|
access_token
|
312
558
|
end
|
313
559
|
|
314
|
-
def ensure_room(room_id)
|
315
|
-
room_id = room_id.to_s unless room_id.is_a? String
|
316
|
-
@rooms.fetch(room_id) do
|
317
|
-
room = Room.new(self, room_id)
|
318
|
-
@rooms[room_id] = room unless cache == :none
|
319
|
-
room
|
320
|
-
end
|
321
|
-
end
|
322
|
-
|
323
560
|
def handle_state(room_id, state_event)
|
324
561
|
return unless state_event.key? :type
|
325
562
|
|
@@ -337,9 +574,9 @@ module MatrixSdk
|
|
337
574
|
when 'm.room.aliases'
|
338
575
|
room.instance_variable_get('@aliases').concat content[:aliases]
|
339
576
|
when 'm.room.join_rules'
|
340
|
-
room.instance_variable_set '@join_rule', content[:join_rule].to_sym
|
577
|
+
room.instance_variable_set '@join_rule', content[:join_rule].nil? ? nil : content[:join_rule].to_sym
|
341
578
|
when 'm.room.guest_access'
|
342
|
-
room.instance_variable_set '@guest_access', content[:guest_access].to_sym
|
579
|
+
room.instance_variable_set '@guest_access', content[:guest_access].nil? ? nil : content[:guest_access].to_sym
|
343
580
|
when 'm.room.member'
|
344
581
|
return unless cache == :all
|
345
582
|
|
@@ -359,10 +596,12 @@ module MatrixSdk
|
|
359
596
|
end
|
360
597
|
|
361
598
|
data[:rooms][:invite].each do |room_id, invite|
|
599
|
+
invite[:room_id] = room_id.to_s
|
362
600
|
fire_invite_event(MatrixEvent.new(self, invite), room_id.to_s)
|
363
601
|
end
|
364
602
|
|
365
603
|
data[:rooms][:leave].each do |room_id, left|
|
604
|
+
left[:room_id] = room_id.to_s
|
366
605
|
fire_leave_event(MatrixEvent.new(self, left), room_id.to_s)
|
367
606
|
end
|
368
607
|
|