matrix_sdk 1.4.0 → 2.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44e3f36b32757df6369cafc95f4b6d9b42cff5a8b5152373519556a227aa6be4
4
- data.tar.gz: dfdb434855b6ceea028a2f374f239aeec92504f36d52e8400d500193d0642d80
3
+ metadata.gz: 487904c24bd70f3d0412ba040f84687d6ef7a6bfe570d259f6d5d73fe7194128
4
+ data.tar.gz: 7d6f53ba5ec1e1426c5bd274e0809f0edcb67a8e8a355b1dd6d409ac57ee3e0f
5
5
  SHA512:
6
- metadata.gz: c5a57bafba59ce9616041f64172c4e5c117f7a46455612c8ae8c74693c691037e25dc5037be90c3d18600a3b30cdc2827761bfdcd9baea7ef5e5f6e7cbccc4cc
7
- data.tar.gz: 99792cb46ab43e6e9d0757c5f2c171da05233d5b8d697ef1ad9d60eb2e2bdb707dca57e35b157687b6ea77ecfb955c579f72289d684699077af1f29de4ce0e54
6
+ metadata.gz: cfbaff099abec9777a5593fa1646ff5e4285fc246894f850b000834185d352f024e8bc9340c17554da6e73c056db9f327bd98f27c259d123ceadf78977203ed0
7
+ data.tar.gz: f53ec1ca41f8884a8916609f364db9fd849b8957629c1f46cfaa440910030035d6f4c92de464aaabd3dc3d207ccf47e9bedccce30980ab213a625d5bb4e13473
@@ -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.
@@ -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
- @use_global_logger = !global_logger.nil?
48
+ @global_logger = !global_logger.nil?
46
49
  end
47
50
 
48
51
  def self.global_logger?
49
- @use_global_logger || false
52
+ @global_logger ||= false
50
53
  end
51
54
  end
@@ -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, :read_timeout, :protocols, :well_known, :proxy_uri
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
- resolver.getresource("_matrix._tcp.#{domain}")
106
- rescue StandardError
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
- data = Net::HTTP.get("https://#{domain}/.well-known/matrix/server")
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
- data = Net::HTTP.get("https://#{domain}/.well-known/matrix/client")
126
- JSON.parse(data)
127
- rescue StandardError
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
- print_http(request)
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
- rescue EOFError => e
299
+ dur_end = Time.now
300
+ duration = dur_end - dur_start
301
+ rescue EOFError
227
302
  logger.error 'Socket closed unexpectedly'
228
- raise e
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
- clean_body = JSON.parse(http.body) rescue nil if http.body
266
- clean_body.keys.each { |k| clean_body[k] = '[ REDACTED ]' if %w[password access_token].include?(k) }.to_json if clean_body
267
- logger.debug "#{dir} #{clean_body.length < 200 ? clean_body : clean_body.slice(0..200) + "... [truncated, #{clean_body.length} Bytes]"}" if clean_body
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::VERIFY_NONE : nil
379
+ @http.verify_mode = validate_certificate ? ::OpenSSL::SSL::VERIFY_PEER : ::OpenSSL::SSL::VERIFY_NONE
297
380
  @http.start
298
381
  @http
299
382
  end
@@ -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
- attr_reader :api
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.key? 'm.identity_server'
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 = params.fetch(:identity_server, nil)
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
- def mxid=(id)
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
- @mxid = id
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
- alias user_id mxid
91
- alias user_id= mxid=
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
- def register_with_password(username, password, full_state: true, **params)
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: 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
- !(mxid.nil? || @api.access_token.nil?)
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 %i[room_id room_alias].include? room_id_or_alias.type
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
- thread = Thread.new { listen_forever(params) }
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 = false
256
- @sync_thread.join if @sync_thread.alive?
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
- private
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