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 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