matrix_sdk 1.5.0 → 2.1.2

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: 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])