matrix_sdk 1.5.0 → 2.1.2

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: c3aa6522f1b8b49ed1ea60dc27c67fefb9479b5aa27d87c4597edf97e76d4eb6
4
- data.tar.gz: b2a6d3b5ff58c037092515ccf42e0a2683f1ef024a8b6731c824faade7649a0a
3
+ metadata.gz: 5af1e7af0f473a5c44df1ede83655cdbdb120e215e42f125b6af576740b88aa6
4
+ data.tar.gz: cca80fe97ca22f0ddb0c5a2b39b24b70345baa25fb20bea305fa019f46ca7d3a
5
5
  SHA512:
6
- metadata.gz: b55690dbf6fe96eeb24a6831d390e122962b1c68d5e0031deb32641eaeb7f38568d47ec728d0adbd4eb9aea5d1f2064ff926667588b9b77893314b999366245b
7
- data.tar.gz: 73eb136cd9285ffc1415b79c25ac05807eb0b17f52865df329ebff1a83962a5005802a3f914b7ebfe8f6a2371097967955dcdcd6de01d4148c5fd9d3c96dafbd
6
+ metadata.gz: 0e4c694d8489dae1b2602d01f4aab08b20119c6697e83b41a2a463adb7ebcaf7b1afd439db04a6e54a0979c5a93be865c8afa3d2d03b4ca34b98f82afae87a5f
7
+ data.tar.gz: 8f52c12d1eb8af55ac5bc011852c9d5f648e9de1cc3241ea1ed76530036b02cff373cb69b8a4c721956add3df1359377d79b2fa2562e8dab8efe5ce877596e2d
@@ -1,3 +1,47 @@
1
+ ## 2.1.2 - 2020-09-10
2
+
3
+ - Adds method for reading complete member lists for rooms, improves the CS spec adherence
4
+ - Adds test for state events
5
+ - Fixes state event handler for rooms not actually passing events
6
+ - Fixes Api#new_for_domain using a faulty URI in certain cases
7
+
8
+ ## 2.1.1 - 2020-08-21
9
+
10
+ - Fixes crash if state event content is null (#11)
11
+ - Fixes an uninitialized URI constant exception when requiring only the main library file
12
+ - Fixes the Api#get_pushrules method missing an ending slash in the request URI
13
+ - Fixes discovery code for client/server connections based on domain
14
+
15
+ ## 2.1.0 - 2020-05-22
16
+
17
+ - Adds unique query IDs as well as duration in API debug output, to make it easier to track long requests
18
+ - Finishes up MSC support, get sync over SSE working flawlessly
19
+ - Exposes the #listen_forever method in the client abstraction
20
+ - Fixes room access methods
21
+
22
+ ## 2.0.1 - 2020-03-13
23
+
24
+ - Adds code for handling non-final MSC's in protocols
25
+ - Currently implementing clients parts of MSC2018 for Sync over Server Sent Events
26
+
27
+ ## 2.0.0 - 2020-02-14
28
+
29
+ **NB**, this release includes backwards-incompatible changes;
30
+ - Changes room state lookup to separate specific state lookups from full state retrieval.
31
+ This will require changes in client code where `#get_room_state` is called to retrieve
32
+ all state, as it now requires a state key. For retrieving full room state,
33
+ `#get_room_state_all` is now the method to use.
34
+ - Changes some advanced parameters to named parameters, ensure your code is updated if it makes use of them
35
+ - Fixes SSL verification to actually verify certs (#9)
36
+
37
+ - Adds multiple CS API endpoints
38
+ - Adds `:room_id` key to all room events
39
+ - Adds `:self` as a valid option to the client abstraction's `#get_user` method
40
+ - Separates homeserver part stringification for MXIDs
41
+ - Exposes some previously private client abstraction methods (`#ensure_room`, `#next_batch`) for easier bot usage
42
+ - Changes room abstraction member lookups to use `#get_room_joined_members`, reducing transferred data amounts
43
+ - Fixes debug print of methods that return arrays (e.g. CS `/room/{id}/state`)
44
+
1
45
  ## 1.5.0 - 2019-10-25
2
46
 
3
47
  - Adds error event to the client abstraction, for handling errors in the background listener
@@ -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!
@@ -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, :open_timeout, :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
 
@@ -51,10 +47,6 @@ module MatrixSdk
51
47
  @homeserver.path.gsub!(/\/?_matrix\/?/, '') if @homeserver.path =~ /_matrix\/?$/
52
48
  raise ArgumentError, 'Please use the base URL for your HS (without /_matrix/)' if @homeserver.path.include? '/_matrix/'
53
49
 
54
- @protocols = params.fetch(:protocols, %i[CS])
55
- @protocols = [@protocols] unless @protocols.is_a? Array
56
- @protocols << :CS if @protocols.include?(:AS) && !@protocols.include?(:CS)
57
-
58
50
  @proxy_uri = params.fetch(:proxy_uri, nil)
59
51
  @connection_address = params.fetch(:address, nil)
60
52
  @connection_port = params.fetch(:port, nil)
@@ -71,6 +63,10 @@ module MatrixSdk
71
63
  @global_headers.merge!(params.fetch(:global_headers)) if params.key? :global_headers
72
64
  @http = nil
73
65
 
66
+ ([params.fetch(:protocols, [:CS])].flatten - protocols).each do |proto|
67
+ self.class.include MatrixSdk::Protocols.const_get(proto)
68
+ end
69
+
74
70
  login(user: @homeserver.user, password: @homeserver.password) if @homeserver.user && @homeserver.password && !@access_token && !params[:skip_login] && protocol?(:CS)
75
71
  @homeserver.userinfo = '' unless params[:skip_login]
76
72
  end
@@ -96,24 +92,37 @@ module MatrixSdk
96
92
  uri = URI("http#{ssl ? 's' : ''}://#{domain}")
97
93
  well_known = nil
98
94
  target_uri = nil
95
+ logger = ::Logging.logger[self]
96
+ logger.debug "Resolving #{domain}"
99
97
 
100
98
  if !port.nil? && !port.empty?
99
+ # If the domain is fully qualified according to Matrix (FQDN and port) then skip discovery
101
100
  target_uri = URI("https://#{domain}:#{port}")
102
101
  elsif target == :server
103
102
  # Attempt SRV record discovery
104
103
  target_uri = begin
105
104
  require 'resolv'
106
105
  resolver = Resolv::DNS.new
107
- resolver.getresource("_matrix._tcp.#{domain}")
108
- 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}"
109
112
  nil
110
113
  end
111
114
 
112
115
  if target_uri.nil?
116
+ # Attempt .well-known discovery for server-to-server
113
117
  well_known = begin
114
- data = Net::HTTP.get("https://#{domain}/.well-known/matrix/server")
118
+ wk_uri = URI("https://#{domain}/.well-known/matrix/server")
119
+ logger.debug "Trying #{wk_uri}..."
120
+ data = Net::HTTP.start(wk_uri.host, wk_uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5, write_timeout: 5) do |http|
121
+ http.get(wk_uri.path).body
122
+ end
115
123
  JSON.parse(data)
116
- rescue StandardError
124
+ rescue StandardError => e
125
+ logger.debug "Well-known failed with #{e.class}: #{e.message}"
117
126
  nil
118
127
  end
119
128
 
@@ -124,9 +133,14 @@ module MatrixSdk
124
133
  elsif %i[client identity].include? target
125
134
  # Attempt .well-known discovery
126
135
  well_known = begin
127
- data = Net::HTTP.get("https://#{domain}/.well-known/matrix/client")
128
- JSON.parse(data)
129
- rescue StandardError
136
+ wk_uri = URI("https://#{domain}/.well-known/matrix/client")
137
+ logger.debug "Trying #{wk_uri}..."
138
+ data = Net::HTTP.start(wk_uri.host, wk_uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5, write_timeout: 5) do |http|
139
+ http.get(wk_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}"
130
144
  nil
131
145
  end
132
146
 
@@ -140,6 +154,7 @@ module MatrixSdk
140
154
  end
141
155
  end
142
156
  end
157
+ logger.debug "Using #{target_uri.inspect}"
143
158
 
144
159
  # Fall back to direct domain connection
145
160
  target_uri ||= URI("https://#{domain}:8448")
@@ -153,6 +168,29 @@ module MatrixSdk
153
168
  ))
154
169
  end
155
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
156
194
  def protocol?(protocol)
157
195
  protocols.include? protocol
158
196
  end
@@ -201,6 +239,29 @@ module MatrixSdk
201
239
  @proxy_uri = proxy_uri
202
240
  end
203
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
204
265
  def request(method, api, path, **options)
205
266
  url = homeserver.dup.tap do |u|
206
267
  u.path = api_to_path(api) + path
@@ -218,7 +279,7 @@ module MatrixSdk
218
279
  request.content_length = (request.body || request.body_stream).size
219
280
  end
220
281
 
221
- request['authorization'] = "Bearer #{access_token}" if access_token
282
+ request['authorization'] = "Bearer #{access_token}" if access_token && !options.fetch(:skip_auth, false)
222
283
  if options.key? :headers
223
284
  options[:headers].each do |h, v|
224
285
  request[h.to_s.downcase] = v
@@ -229,14 +290,20 @@ module MatrixSdk
229
290
  loop do
230
291
  raise MatrixConnectionError, "Server still too busy to handle request after #{failures} attempts, try again later" if failures >= 10
231
292
 
232
- print_http(request)
293
+ req_id = ('A'..'Z').to_a.sample(4).join
294
+
295
+ print_http(request, id: req_id)
233
296
  begin
297
+ dur_start = Time.now
234
298
  response = http.request request
235
- rescue EOFError => e
299
+ dur_end = Time.now
300
+ duration = dur_end - dur_start
301
+ rescue EOFError
236
302
  logger.error 'Socket closed unexpectedly'
237
- raise e
303
+ raise
238
304
  end
239
- print_http(response)
305
+ print_http(response, duration: duration, id: req_id)
306
+
240
307
  data = JSON.parse(response.body, symbolize_names: true) rescue nil
241
308
 
242
309
  if response.is_a? Net::HTTPTooManyRequests
@@ -255,35 +322,41 @@ module MatrixSdk
255
322
  end
256
323
  end
257
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
+
258
334
  private
259
335
 
260
- def print_http(http)
336
+ def print_http(http, body: true, duration: nil, id: nil)
261
337
  return unless logger.debug?
262
338
 
263
339
  if http.is_a? Net::HTTPRequest
264
- dir = '>'
340
+ dir = "#{id ? id + ' : ' : nil}>"
265
341
  logger.debug "#{dir} Sending a #{http.method} request to `#{http.path}`:"
266
342
  else
267
- dir = '<'
268
- 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}"
269
345
  end
270
346
  http.to_hash.map { |k, v| "#{k}: #{k == 'authorization' ? '[ REDACTED ]' : v.join(', ')}" }.each do |h|
271
347
  logger.debug "#{dir} #{h}"
272
348
  end
273
349
  logger.debug dir
274
- clean_body = JSON.parse(http.body) rescue nil if http.body
275
- clean_body.keys.each { |k| clean_body[k] = '[ REDACTED ]' if %w[password access_token].include?(k) }.to_json if clean_body
276
- 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
277
356
  rescue StandardError => e
278
357
  logger.warn "#{e.class} occured while printing request debug; #{e.message}\n#{e.backtrace.join "\n"}"
279
358
  end
280
359
 
281
- def transaction_id
282
- ret = @transaction_id ||= 0
283
- @transaction_id = @transaction_id.succ
284
- ret
285
- end
286
-
287
360
  def api_to_path(api)
288
361
  # TODO: <api>_current / <api>_latest
289
362
  "/_matrix/#{api.to_s.split('_').join('/')}"
@@ -303,7 +376,7 @@ module MatrixSdk
303
376
  @http.open_timeout = open_timeout
304
377
  @http.read_timeout = read_timeout
305
378
  @http.use_ssl = homeserver.scheme == 'https'
306
- @http.verify_mode = validate_certificate ? ::OpenSSL::SSL::VERIFY_NONE : nil
379
+ @http.verify_mode = validate_certificate ? ::OpenSSL::SSL::VERIFY_PEER : ::OpenSSL::SSL::VERIFY_NONE
307
380
  @http.start
308
381
  @http
309
382
  end
@@ -11,7 +11,16 @@ module MatrixSdk
11
11
  include MatrixSdk::Logging
12
12
  extend Forwardable
13
13
 
14
- 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
15
24
  attr_accessor :cache, :sync_filter
16
25
 
17
26
  events :error, :event, :presence_event, :invite_event, :leave_event, :ephemeral_event
@@ -22,9 +31,20 @@ module MatrixSdk
22
31
  :access_token, :access_token=, :device_id, :device_id=, :homeserver, :homeserver=,
23
32
  :validate_certificate, :validate_certificate=
24
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
25
45
  def self.new_for_domain(domain, **params)
26
46
  api = MatrixSdk::Api.new_for_domain(domain, keep_wellknown: true)
27
- 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')
28
48
 
29
49
  identity_server = MatrixSdk::Api.new(api.well_known['m.identity_server']['base_url'], protocols: %i[IS])
30
50
  new(api, params.merge(identity_server: identity_server))
@@ -53,7 +73,7 @@ module MatrixSdk
53
73
  @rooms = {}
54
74
  @users = {}
55
75
  @cache = client_cache
56
- @identity_server = params.fetch(:identity_server, nil)
76
+ @identity_server = nil
57
77
 
58
78
  @sync_token = nil
59
79
  @sync_thread = nil
@@ -75,22 +95,42 @@ module MatrixSdk
75
95
  @mxid = params[:user_id]
76
96
  end
77
97
 
98
+ # Gets the currently logged in user's MXID
99
+ #
100
+ # @return [MXID] The MXID of the current user
78
101
  def mxid
79
102
  @mxid ||= begin
80
- api.whoami?[:user_id] if api&.access_token
103
+ MXID.new api.whoami?[:user_id] if api&.access_token
81
104
  end
82
105
  end
83
106
 
84
- def mxid=(id)
85
- id = MXID.new id.to_s unless id.is_a? MXID
86
- raise ArgumentError, 'Must be a User ID' unless id.user?
107
+ alias user_id mxid
87
108
 
88
- @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 }
89
116
  end
90
117
 
91
- alias user_id mxid
92
- 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
93
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
94
134
  def public_rooms
95
135
  rooms = []
96
136
  since = nil
@@ -114,6 +154,12 @@ module MatrixSdk
114
154
  rooms
115
155
  end
116
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
117
163
  def rooms
118
164
  if @rooms.empty? && cache != :none
119
165
  api.get_joined_rooms.joined_rooms.each do |id|
@@ -124,6 +170,11 @@ module MatrixSdk
124
170
  @rooms.values
125
171
  end
126
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
127
178
  def reload_rooms!
128
179
  return true if cache == :none
129
180
 
@@ -137,12 +188,25 @@ module MatrixSdk
137
188
  end
138
189
  alias refresh_rooms! reload_rooms!
139
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
140
194
  def register_as_guest
141
195
  data = api.register(kind: :guest)
142
196
  post_authentication(data)
143
197
  end
144
198
 
145
- 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)
146
210
  username = username.to_s unless username.is_a?(String)
147
211
  password = password.to_s unless password.is_a?(String)
148
212
 
@@ -154,10 +218,21 @@ module MatrixSdk
154
218
 
155
219
  return if params[:no_sync]
156
220
 
157
- sync full_state: full_state,
221
+ sync full_state: true,
158
222
  allow_sync_retry: params.fetch(:allow_sync_retry, nil)
159
223
  end
160
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
161
236
  def login(username, password, sync_timeout: 15, full_state: false, **params)
162
237
  username = username.to_s unless username.is_a?(String)
163
238
  password = password.to_s unless password.is_a?(String)
@@ -175,6 +250,17 @@ module MatrixSdk
175
250
  allow_sync_retry: params.fetch(:allow_sync_retry, nil)
176
251
  end
177
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
178
264
  def login_with_token(username, token, sync_timeout: 15, full_state: false, **params)
179
265
  username = username.to_s unless username.is_a?(String)
180
266
  token = token.to_s unless token.is_a?(String)
@@ -192,30 +278,90 @@ module MatrixSdk
192
278
  allow_sync_retry: params.fetch(:allow_sync_retry, nil)
193
279
  end
194
280
 
281
+ # Logs out of the current session
195
282
  def logout
196
283
  api.logout
197
284
  @api.access_token = nil
198
285
  @mxid = nil
199
286
  end
200
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
201
292
  def logged_in?
202
- !(mxid.nil? || @api.access_token.nil?)
293
+ !@api.access_token.nil?
294
+ end
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
203
328
  end
204
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
205
339
  def create_room(room_alias = nil, **params)
206
340
  data = api.create_room(params.merge(room_alias: room_alias))
207
341
  ensure_room(data.room_id)
208
342
  end
209
343
 
344
+ # Joins an already created room
345
+ #
346
+ # @param room_id_or_alias [String,MXID] A room alias (#room:example.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
210
350
  def join_room(room_id_or_alias, server_name: [])
211
351
  server_name = [server_name] unless server_name.is_a? Array
212
352
  data = api.join_room(room_id_or_alias, server_name: server_name)
213
353
  ensure_room(data.fetch(:room_id, room_id_or_alias))
214
354
  end
215
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
216
362
  def find_room(room_id_or_alias, only_canonical: false)
217
363
  room_id_or_alias = MXID.new(room_id_or_alias.to_s) unless room_id_or_alias.is_a? MXID
218
- 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?
219
365
 
220
366
  return @rooms.fetch(room_id_or_alias.to_s, nil) if room_id_or_alias.room_id?
221
367
 
@@ -224,7 +370,21 @@ module MatrixSdk
224
370
  @rooms.values.find { |r| r.aliases.include? room_id_or_alias.to_s }
225
371
  end
226
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
227
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
+
228
388
  if cache == :all
229
389
  @users[user_id] ||= User.new(self, user_id)
230
390
  else
@@ -232,10 +392,23 @@ module MatrixSdk
232
392
  end
233
393
  end
234
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
235
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
+
236
403
  api.remove_room_alias(room_alias)
237
404
  end
238
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
239
412
  def upload(content, content_type)
240
413
  data = api.media_upload(content, content_type)
241
414
  return data[:content_uri] if data.key? :content_uri
@@ -243,25 +416,72 @@ module MatrixSdk
243
416
  raise MatrixUnexpectedResponseError, 'Upload succeeded, but no media URI returned'
244
417
  end
245
418
 
419
+ # Starts a background thread that will listen to new events
420
+ #
421
+ # @see sync For What parameters are accepted
246
422
  def start_listener_thread(**params)
423
+ return if listening?
424
+
247
425
  @should_listen = true
248
- 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
249
450
  @sync_thread = thread
250
451
  thread.run
251
452
  end
252
453
 
454
+ # Stops the running background thread if one is active
253
455
  def stop_listener_thread
254
456
  return unless @sync_thread
255
457
 
256
- @should_listen = false
257
- @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
258
467
  @sync_thread = nil
259
468
  end
260
469
 
470
+ # Check if there's a thread listening for events
261
471
  def listening?
262
472
  @sync_thread&.alive? == true
263
473
  end
264
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
265
485
  def sync(skip_store_batch: false, **params)
266
486
  extra_params = {
267
487
  filter: sync_filter,
@@ -283,11 +503,26 @@ module MatrixSdk
283
503
  @next_batch = data[:next_batch] unless skip_store_batch
284
504
 
285
505
  handle_sync_response(data)
506
+ true
286
507
  end
287
508
 
288
509
  alias listen_for_events sync
289
510
 
290
- 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
291
526
 
292
527
  def listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 30, **params)
293
528
  orig_bad_sync_timeout = bad_sync_timeout + 0
@@ -312,6 +547,8 @@ module MatrixSdk
312
547
  fire_error(ErrorEvent.new(e, :listener_thread))
313
548
  end
314
549
 
550
+ private
551
+
315
552
  def post_authentication(data)
316
553
  @mxid = data[:user_id]
317
554
  @api.access_token = data[:access_token]
@@ -320,19 +557,11 @@ module MatrixSdk
320
557
  access_token
321
558
  end
322
559
 
323
- def ensure_room(room_id)
324
- room_id = room_id.to_s unless room_id.is_a? String
325
- @rooms.fetch(room_id) do
326
- room = Room.new(self, room_id)
327
- @rooms[room_id] = room unless cache == :none
328
- room
329
- end
330
- end
331
-
332
560
  def handle_state(room_id, state_event)
333
561
  return unless state_event.key? :type
334
562
 
335
563
  room = ensure_room(room_id)
564
+ room.send :put_state_event, state_event
336
565
  content = state_event[:content]
337
566
  case state_event[:type]
338
567
  when 'm.room.name'
@@ -346,9 +575,9 @@ module MatrixSdk
346
575
  when 'm.room.aliases'
347
576
  room.instance_variable_get('@aliases').concat content[:aliases]
348
577
  when 'm.room.join_rules'
349
- room.instance_variable_set '@join_rule', content[:join_rule].to_sym
578
+ room.instance_variable_set '@join_rule', content[:join_rule].nil? ? nil : content[:join_rule].to_sym
350
579
  when 'm.room.guest_access'
351
- room.instance_variable_set '@guest_access', content[:guest_access].to_sym
580
+ room.instance_variable_set '@guest_access', content[:guest_access].nil? ? nil : content[:guest_access].to_sym
352
581
  when 'm.room.member'
353
582
  return unless cache == :all
354
583
 
@@ -368,10 +597,12 @@ module MatrixSdk
368
597
  end
369
598
 
370
599
  data[:rooms][:invite].each do |room_id, invite|
600
+ invite[:room_id] = room_id.to_s
371
601
  fire_invite_event(MatrixEvent.new(self, invite), room_id.to_s)
372
602
  end
373
603
 
374
604
  data[:rooms][:leave].each do |room_id, left|
605
+ left[:room_id] = room_id.to_s
375
606
  fire_leave_event(MatrixEvent.new(self, left), room_id.to_s)
376
607
  end
377
608
 
@@ -387,7 +618,7 @@ module MatrixSdk
387
618
 
388
619
  join[:timeline][:events].each do |event|
389
620
  event[:room_id] = room_id.to_s
390
- handle_state(room_id, event) unless event[:type] == 'm.room.message'
621
+ handle_state(room_id, event) if event.key? :state_key
391
622
  room.send :put_event, event
392
623
 
393
624
  fire_event(MatrixEvent.new(self, event), event[:type])