Class: MatrixSdk::Client

Inherits:
Object show all
Extended by:
Forwardable, Extensions
Includes:
Logging
Defined in:
lib/matrix_sdk/client.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Extensions

events, ignore_inspect

Methods included from Logging

#logger, #logger=

Constructor Details

#initialize(hs_url, client_cache: :all, **params) ⇒ Client

Returns a new instance of Client.

Options Hash (**params):

  • :user_id (String, MXID)

    The user ID of the logged-in user

  • :sync_filter_limit (Integer) — default: 20

    Limit of timeline entries in syncs

Raises:

  • (ArgumentError)

See Also:

  • for additional usable params


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/matrix_sdk/client.rb', line 59

def initialize(hs_url, client_cache: :all, **params)
  event_initialize

  params[:user_id] ||= params[:mxid] if params[:mxid]

  if hs_url.is_a? Api
    @api = hs_url
    params.each do |k, v|
      api.instance_variable_set("@#{k}", v) if api.instance_variable_defined? "@#{k}"
    end
  else
    @api = Api.new hs_url, params
  end

  @cache = client_cache
  @identity_server = nil

  @sync_token = nil
  @sync_thread = nil
  @sync_filter = { room: { timeline: { limit: params.fetch(:sync_filter_limit, 20) }, state: { lazy_load_members: true } } }

  @next_batch = nil

  @bad_sync_timeout_limit = 60 * 60

  params.each do |k, v|
    instance_variable_set("@#{k}", v) if instance_variable_defined? "@#{k}"
  end

  @rooms = {}
  @room_handlers = {}
  @users = {}
  @should_listen = false

  raise ArgumentError, 'Cache value must be one of of [:all, :some, :none]' unless %i[all some none].include? @cache

  return unless params[:user_id]

  @mxid = params[:user_id]
end

Instance Attribute Details

#apiApi (readonly)

The underlying API connection



23
24
25
# File 'lib/matrix_sdk/client.rb', line 23

def api
  @api
end

#cache:all, ...

The cache level



23
# File 'lib/matrix_sdk/client.rb', line 23

attr_reader :api, :next_batch

#next_batchObject (readonly)

Returns the value of attribute next_batch.



23
# File 'lib/matrix_sdk/client.rb', line 23

attr_reader :api, :next_batch

#sync_filterHash, String

The global sync filter



23
# File 'lib/matrix_sdk/client.rb', line 23

attr_reader :api, :next_batch

Class Method Details

.new_for_domain(domain, **params) ⇒ Client

Note:

This method will not verify that the created client has a valid connection, it will only perform the necessary lookups to build a connection URL.

Create a new client instance from only a Matrix HS domain

This will use the well-known delegation lookup to find the correct client URL



45
46
47
48
49
50
51
# File 'lib/matrix_sdk/client.rb', line 45

def self.new_for_domain(domain, **params)
  api = MatrixSdk::Api.new_for_domain(domain, keep_wellknown: true)
  return new(api, params) unless api.well_known&.key?('m.identity_server')

  identity_server = MatrixSdk::Api.new(api.well_known['m.identity_server']['base_url'], protocols: %i[IS])
  new(api, params.merge(identity_server: identity_server))
end

Instance Method Details

#create_room(room_alias = nil, **params) ⇒ Room

Creates a new room

Examples:

Creating a room with an alias

client.create_room('myroom')
#<MatrixSdk::Room ... >

See Also:



341
342
343
344
# File 'lib/matrix_sdk/client.rb', line 341

def create_room(room_alias = nil, **params)
  data = api.create_room(params.merge(room_alias: room_alias))
  ensure_room(data.room_id)
end

#ensure_room(room_id) ⇒ Room

Ensures that a room exists in the cache

Raises:

  • (ArgumentError)


517
518
519
520
521
522
523
524
525
526
527
# File 'lib/matrix_sdk/client.rb', line 517

def ensure_room(room_id)
  room_id = MXID.new room_id.to_s unless room_id.is_a? MXID
  raise ArgumentError, 'Must be a room ID' unless room_id.room_id?

  room_id = room_id.to_s
  @rooms.fetch(room_id) do
    room = Room.new(self, room_id)
    @rooms[room_id] = room unless cache == :none
    room
  end
end

#find_room(room_id_or_alias, only_canonical: false) ⇒ Room?

Find a room in the locally cached list of rooms that the current user is part of

Raises:

  • (ArgumentError)


364
365
366
367
368
369
370
371
372
373
# File 'lib/matrix_sdk/client.rb', line 364

def find_room(room_id_or_alias, only_canonical: false)
  room_id_or_alias = MXID.new(room_id_or_alias.to_s) unless room_id_or_alias.is_a? MXID
  raise ArgumentError, 'Must be a room id or alias' unless room_id_or_alias.room?

  return @rooms.fetch(room_id_or_alias.to_s, nil) if room_id_or_alias.room_id?

  return @rooms.values.find { |r| r.canonical_alias == room_id_or_alias.to_s } if only_canonical

  @rooms.values.find { |r| r.aliases.include? room_id_or_alias.to_s }
end

#get_user(user_id) ⇒ User

Note:

The method doesn’t perform any existence checking, so the returned User object may point to a non-existent user

Get a User instance from a MXID

Raises:

  • (ArgumentError)

    If the input isn’t a valid user ID



381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/matrix_sdk/client.rb', line 381

def get_user(user_id)
  user_id = mxid if user_id == :self

  user_id = MXID.new user_id.to_s unless user_id.is_a? MXID
  raise ArgumentError, 'Must be a User ID' unless user_id.user?

  # To still use regular string storage in the hash itself
  user_id = user_id.to_s

  if cache == :all
    @users[user_id] ||= User.new(self, user_id)
  else
    User.new(self, user_id)
  end
end

#join_room(room_id_or_alias, server_name: []) ⇒ Room

Joins an already created room



352
353
354
355
356
# File 'lib/matrix_sdk/client.rb', line 352

def join_room(room_id_or_alias, server_name: [])
  server_name = [server_name] unless server_name.is_a? Array
  data = api.join_room(room_id_or_alias, server_name: server_name)
  ensure_room(data.fetch(:room_id, room_id_or_alias))
end

#listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 0, **params) ⇒ Object



529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
# File 'lib/matrix_sdk/client.rb', line 529

def listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 0, **params)
  orig_bad_sync_timeout = bad_sync_timeout + 0
  while @should_listen
    begin
      sync(params.merge(timeout: timeout))

      bad_sync_timeout = orig_bad_sync_timeout
      sleep(sync_interval) if sync_interval.positive?
    rescue MatrixRequestError => e
      logger.warn("A #{e.class} occurred during sync")
      if e.httpstatus >= 500
        logger.warn("Serverside error, retrying in #{bad_sync_timeout} seconds...")
        sleep(bad_sync_timeout) if bad_sync_timeout.positive? # rubocop:disable Metrics/BlockNesting
        bad_sync_timeout = [bad_sync_timeout * 2, @bad_sync_timeout_limit].min
      end
    end
  end
rescue StandardError => e
  logger.error "Unhandled #{e.class} raised in background listener"
  logger.error [e.message, *e.backtrace].join($RS)
  fire_error(ErrorEvent.new(e, :listener_thread))
end

#listening?Boolean

Check if there’s a thread listening for events



473
474
475
# File 'lib/matrix_sdk/client.rb', line 473

def listening?
  @sync_thread&.alive? == true
end

#logged_in?Boolean

Note:

This will not check if the session is valid, only if it exists

Check if there’s a currently logged in session



294
295
296
# File 'lib/matrix_sdk/client.rb', line 294

def logged_in?
  !@api.access_token.nil?
end

#login(username, password, sync_timeout: 15, full_state: false, **params) ⇒ Object

Logs in as a user on the connected HS

This will also trigger an initial sync unless no_sync is set

Options Hash (**params):

  • :no_sync (Boolean)

    Skip the initial sync on registering

  • :allow_sync_retry (Boolean)

    Allow sync to retry on failure

Raises:

  • (ArgumentError)


238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/matrix_sdk/client.rb', line 238

def (username, password, sync_timeout: 15, full_state: false, **params)
  username = username.to_s unless username.is_a?(String)
  password = password.to_s unless password.is_a?(String)

  raise ArgumentError, "Username can't be nil or empty" if username.nil? || username.empty?
  raise ArgumentError, "Password can't be nil or empty" if password.nil? || password.empty?

  data = api.(user: username, password: password)
  post_authentication(data)

  return if params[:no_sync]

  sync timeout: sync_timeout,
       full_state: full_state,
       allow_sync_retry: params.fetch(:allow_sync_retry, nil)
end

#login_with_token(username, token, sync_timeout: 15, full_state: false, **params) ⇒ Object

Logs in as a user on the connected HS

This will also trigger an initial sync unless no_sync is set

Options Hash (**params):

  • :no_sync (Boolean)

    Skip the initial sync on registering

  • :allow_sync_retry (Boolean)

    Allow sync to retry on failure

Raises:

  • (ArgumentError)


266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/matrix_sdk/client.rb', line 266

def (username, token, sync_timeout: 15, full_state: false, **params)
  username = username.to_s unless username.is_a?(String)
  token = token.to_s unless token.is_a?(String)

  raise ArgumentError, "Username can't be nil or empty" if username.nil? || username.empty?
  raise ArgumentError, "Token can't be nil or empty" if token.nil? || token.empty?

  data = api.(user: username, token: token, type: 'm.login.token')
  post_authentication(data)

  return if params[:no_sync]

  sync timeout: sync_timeout,
       full_state: full_state,
       allow_sync_retry: params.fetch(:allow_sync_retry, nil)
end

#logoutObject

Logs out of the current session



284
285
286
287
288
# File 'lib/matrix_sdk/client.rb', line 284

def logout
  api.logout
  @api.access_token = nil
  @mxid = nil
end

#mxidMXID Also known as: user_id

Gets the currently logged in user’s MXID



103
104
105
106
107
# File 'lib/matrix_sdk/client.rb', line 103

def mxid
  @mxid ||= begin
    MXID.new api.whoami?[:user_id] if api&.access_token
  end
end

#presenceResponse

Gets the current user presence status object



116
117
118
# File 'lib/matrix_sdk/client.rb', line 116

def presence
  api.get_presence_status(mxid).tap { |h| h.delete :user_id }
end

#public_roomsArray[Room]

Note:

This will try to list all public rooms on the HS, and may take a while on larger instances

Gets a list of all the public rooms on the connected HS



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/matrix_sdk/client.rb', line 136

def public_rooms
  rooms = []
  since = nil
  loop do
    data = api.get_public_rooms since: since

    data[:chunk].each do |chunk|
      rooms << Room.new(self, chunk[:room_id],
                        name: chunk[:name], topic: chunk[:topic], aliases: chunk[:aliases],
                        canonical_alias: chunk[:canonical_alias], avatar_url: chunk[:avatar_url],
                        join_rule: :public, world_readable: chunk[:world_readable]).tap do |r|
        r.instance_variable_set :@guest_access, chunk[:guest_can_join] ? :can_join : :forbidden
      end
    end

    break if data[:next_batch].nil?

    since = data.next_batch
  end

  rooms
end

#register_as_guestObject

Note:

This feature is not commonly supported by many HSes

Register - and log in - on the connected HS as a guest



196
197
198
199
# File 'lib/matrix_sdk/client.rb', line 196

def register_as_guest
  data = api.register(kind: :guest)
  post_authentication(data)
end

#register_with_password(username, password, **params) ⇒ Object

Note:

This method will currently always use auth type ‘m.login.dummy’

Register a new user account on the connected HS

This will also trigger an initial sync unless no_sync is set

Options Hash (**params):

  • :no_sync (Boolean)

    Skip the initial sync on registering

  • :allow_sync_retry (Boolean)

    Allow sync to retry on failure

Raises:

  • (ArgumentError)


211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/matrix_sdk/client.rb', line 211

def register_with_password(username, password, **params)
  username = username.to_s unless username.is_a?(String)
  password = password.to_s unless password.is_a?(String)

  raise ArgumentError, "Username can't be nil or empty" if username.nil? || username.empty?
  raise ArgumentError, "Password can't be nil or empty" if password.nil? || username.empty?

  data = api.register(auth: { type: 'm.login.dummy' }, username: username, password: password)
  post_authentication(data)

  return if params[:no_sync]

  sync full_state: true,
       allow_sync_retry: params.fetch(:allow_sync_retry, nil)
end

#registered_3pidsResponse

Retrieve a list of all registered third-party IDs for the current user



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/matrix_sdk/client.rb', line 302

def registered_3pids
  data = api.get_3pids
  data.threepids.each do |obj|
    obj.instance_eval do
      def added_at
        Time.at(self[:added_at] / 1000)
      end

      def validated_at
        return unless validated?

        Time.at(self[:validated_at] / 1000)
      end

      def validated?
        key? :validated_at
      end

      def to_s
        "#{self[:medium]}:#{self[:address]}"
      end

      def inspect
        "#<MatrixSdk::Response 3pid=#{to_s.inspect} added_at=\"#{added_at}\"#{validated? ? " validated_at=\"#{validated_at}\"" : ''}>"
      end
    end
  end
  data
end

#reload_rooms!Boolean Also known as: refresh_rooms!

Note:

This will be a no-op if the cache level is set to :none

Refresh the list of currently handled rooms, replacing it with the user’s currently joined rooms.



180
181
182
183
184
185
186
187
188
189
190
# File 'lib/matrix_sdk/client.rb', line 180

def reload_rooms!
  return true if cache == :none

  @rooms.clear
  api.get_joined_rooms.joined_rooms.each do |id|
    r = ensure_room(id)
    r.reload!
  end

  true
end

#remove_room_alias(room_alias) ⇒ Object

Remove a room alias

Raises:

  • (ArgumentError)

See Also:



401
402
403
404
405
406
# File 'lib/matrix_sdk/client.rb', line 401

def remove_room_alias(room_alias)
  room_alias = MXID.new room_alias.to_s unless room_alias.is_a? MXID
  raise ArgumentError, 'Must be a room alias' unless room_alias.room_alias?

  api.remove_room_alias(room_alias)
end

#roomsArray[Room]

Note:

This will always return the empty array if the cache level is set to :none

Gets a list of all relevant rooms, either the ones currently handled by the client, or the list of currently joined ones if no rooms are handled



165
166
167
168
169
170
171
172
173
# File 'lib/matrix_sdk/client.rb', line 165

def rooms
  if @rooms.empty? && cache != :none
    api.get_joined_rooms.joined_rooms.each do |id|
      ensure_room(id)
    end
  end

  @rooms.values
end

#set_presence(status, message: nil) ⇒ Object

Sets the current user’s presence status

Raises:

  • (ArgumentError)

See Also:



126
127
128
129
130
# File 'lib/matrix_sdk/client.rb', line 126

def set_presence(status, message: nil)
  raise ArgumentError, 'Presence must be one of :online, :offline, :unavailable' unless %i[online offline unavailable].include?(status)

  api.set_presence_status(mxid, status, message: message)
end

#start_listener_thread(**params) ⇒ Object

Starts a background thread that will listen to new events



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/matrix_sdk/client.rb', line 424

def start_listener_thread(**params)
  return if listening?

  @should_listen = true
  if api.protocol?(:MSC) && api.msc2108?
    params[:filter] = sync_filter unless params.key? :filter
    params[:filter] = params[:filter].to_json unless params[:filter].nil? || params[:filter].is_a?(String)
    params[:since] = @next_batch if @next_batch

    errors = 0
    thread, cancel_token = api.msc2108_sync_sse(params) do |data, event:, id:|
      @next_batch = id if id
      if event.to_sym == :sync
        handle_sync_response(data)
        errors = 0
      elsif event.to_sym == :sync_error
        logger.error "SSE Sync error received; #{data.type}: #{data.message}"
        errors += 1

        # TODO: Allow configuring
        raise 'Aborting due to excessive errors' if errors >= 5
      end
    end

    @should_listen = cancel_token
  else
    thread = Thread.new { listen_forever(params) }
  end
  @sync_thread = thread
  thread.run
end

#stop_listener_threadObject

Stops the running background thread if one is active



457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/matrix_sdk/client.rb', line 457

def stop_listener_thread
  return unless @sync_thread

  if @should_listen.is_a? Hash
    @should_listen[:run] = false
  else
    @should_listen = false
  end
  if @sync_thread.alive?
    ret = @sync_thread.join(2)
    @sync_thread.kill unless ret
  end
  @sync_thread = nil
end

#sync(skip_store_batch: false, **params) ⇒ Object Also known as: listen_for_events

Run a message sync round, triggering events as necessary

Options Hash (**params):

  • :filter (String, Hash) — default: #sync_filter

    A filter to use for this sync

  • :timeout (Numeric) — default: 30

    A timeout value in seconds for the sync request

  • :allow_sync_retry (Numeric) — default: 0

    The number of retries allowed for this sync request

  • :since (String)

    An override of the “since” token to provide to the sync request

See Also:



487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# File 'lib/matrix_sdk/client.rb', line 487

def sync(skip_store_batch: false, **params)
  extra_params = {
    filter: sync_filter,
    timeout: 30
  }
  extra_params[:since] = @next_batch unless @next_batch.nil?
  extra_params.merge!(params)
  extra_params[:filter] = extra_params[:filter].to_json unless extra_params[:filter].is_a? String

  attempts = 0
  data = loop do
    begin
      break api.sync extra_params
    rescue MatrixSdk::MatrixTimeoutError => e
      raise e if (attempts += 1) >= params.fetch(:allow_sync_retry, 0)
    end
  end

  @next_batch = data[:next_batch] unless skip_store_batch

  handle_sync_response(data)
  true
end

#upload(content, content_type) ⇒ URI::MATRIX

Upload a piece of data to the media repo



414
415
416
417
418
419
# File 'lib/matrix_sdk/client.rb', line 414

def upload(content, content_type)
  data = api.media_upload(content, content_type)
  return data[:content_uri] if data.key? :content_uri

  raise MatrixUnexpectedResponseError, 'Upload succeeded, but no media URI returned'
end