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.

Parameters:

  • hs_url (String, URI, Api)

    The URL to the Matrix homeserver, without the /_matrix/ part, or an existing Api instance

  • client_cache (:all, :some, :none) (defaults to: :all)

    (:all) How much data should be cached in the client

  • params (Hash)

    Additional parameters on creation

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


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
99
100
# File 'lib/matrix_sdk/client.rb', line 60

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
  @mxid = 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

Returns:

  • (Api)

    The underlying API connection



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

def api
  @api
end

#cache:all, ...

The cache level

Returns:

  • (:all, :some, :none)

    The level of caching to do



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

attr_reader :api, :next_batch

#next_batchObject (readonly)

Returns the value of attribute next_batch.



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

attr_reader :api, :next_batch

#sync_filterHash, String

The global sync filter

Returns:

  • (Hash, String)

    A filter definition, either as defined by the Matrix spec, or as an identifier returned by a filter creation request



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

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

Parameters:

  • domain (String)

    The domain name to look up

  • params (Hash)

    Additional parameters to pass along to Api.new_for_domain as well as #initialize

Returns:

  • (Client)

    The new client instance

See Also:



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

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

Parameters:

  • room_alias (String) (defaults to: nil)

    A default alias to set on the room, should only be the localpart

Returns:

  • (Room)

    The resulting room

See Also:



375
376
377
378
# File 'lib/matrix_sdk/client.rb', line 375

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

#direct_room(mxid) ⇒ Room?

Note:

Will return the oldest room if multiple exist

Gets a direct message room for the given user if one exists

Returns:

  • (Room, nil)

    A direct message room if one exists

Raises:

  • (ArgumentError)


171
172
173
174
175
176
177
# File 'lib/matrix_sdk/client.rb', line 171

def direct_room(mxid)
  mxid = MatrixSdk::MXID.new mxid.to_s unless mxid.is_a? MatrixSdk::MXID
  raise ArgumentError, 'Must be a valid user ID' unless mxid.user?

  room_id = direct_rooms[mxid.to_s]&.first
  ensure_room room_id if room_id
end

#direct_roomsHash[String,Array[String]]

Gets a list of all direct chat rooms (1:1 chats / direct message chats) for the currenct user

Returns:

  • (Hash[String,Array[String]])

    A mapping of MXIDs to a list of direct rooms with that user



163
164
165
# File 'lib/matrix_sdk/client.rb', line 163

def direct_rooms
  api.(mxid, 'm.direct').transform_keys(&:to_s)
end

#ensure_room(room_id) ⇒ Room

Ensures that a room exists in the cache

Parameters:

  • room_id (String, MXID)

    The room ID to ensure

Returns:

  • (Room)

    The room object for the requested room

Raises:

  • (ArgumentError)


550
551
552
553
554
555
556
557
558
559
560
# File 'lib/matrix_sdk/client.rb', line 550

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

Parameters:

  • room_id_or_alias (String, MXID)

    A room ID or alias

  • only_canonical (Boolean) (defaults to: false)

    Only match alias against the canonical alias

Returns:

  • (Room)

    The found room

  • (nil)

    If no room was found

Raises:

  • (ArgumentError)


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

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

Parameters:

  • user_id (String, MXID, :self)

    The MXID to look up, will also accept :self in order to get the currently logged-in user

Returns:

  • (User)

    The User instance for the specified user

Raises:

  • (ArgumentError)

    If the input isn't a valid user ID



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/matrix_sdk/client.rb', line 415

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

Parameters:

  • room_id_or_alias (String, MXID)

    A room alias (#room:example.com) or a room ID (!id:example.com)

  • server_name (Array[String]) (defaults to: [])

    A list of servers to attempt the join through, required for IDs

Returns:

  • (Room)

    The resulting room

See Also:



386
387
388
389
390
# File 'lib/matrix_sdk/client.rb', line 386

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



562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'lib/matrix_sdk/client.rb', line 562

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

Returns:

  • (Boolean)


508
509
510
# File 'lib/matrix_sdk/client.rb', line 508

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

Returns:

  • (Boolean)

    If there's a current session



328
329
330
# File 'lib/matrix_sdk/client.rb', line 328

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

Parameters:

  • username (String)

    The username of the user

  • password (String)

    The password of the user

  • sync_timeout (Numeric) (defaults to: 15)

    The timeout of the initial sync on login

  • full_state (Boolean) (defaults to: false)

    Should the initial sync retrieve full state

  • params (Hash)

    Additional options

Options Hash (**params):

  • :no_sync (Boolean)

    Skip the initial sync on registering

  • :allow_sync_retry (Boolean)

    Allow sync to retry on failure

Raises:

  • (ArgumentError)


272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/matrix_sdk/client.rb', line 272

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

Parameters:

  • username (String)

    The username of the user

  • token (String)

    The token to log in with

  • sync_timeout (Numeric) (defaults to: 15)

    The timeout of the initial sync on login

  • full_state (Boolean) (defaults to: false)

    Should the initial sync retrieve full state

  • params (Hash)

    Additional options

Options Hash (**params):

  • :no_sync (Boolean)

    Skip the initial sync on registering

  • :allow_sync_retry (Boolean)

    Allow sync to retry on failure

Raises:

  • (ArgumentError)


300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/matrix_sdk/client.rb', line 300

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



318
319
320
321
322
# File 'lib/matrix_sdk/client.rb', line 318

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

Returns:

  • (MXID)

    The MXID of the current user



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

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

#presenceResponse

Gets the current user presence status object

Returns:

See Also:



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

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

Returns:

  • (Array[Room])

    The public rooms



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

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



230
231
232
233
# File 'lib/matrix_sdk/client.rb', line 230

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

Parameters:

  • username (String)

    The new user's name

  • password (String)

    The new user's password

  • params (Hash)

    Additional options

Options Hash (**params):

  • :no_sync (Boolean)

    Skip the initial sync on registering

  • :allow_sync_retry (Boolean)

    Allow sync to retry on failure

Raises:

  • (ArgumentError)


245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/matrix_sdk/client.rb', line 245

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

Returns:

  • (Response)

    A response hash containing the key :threepids

See Also:



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/matrix_sdk/client.rb', line 336

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!, reload_spaces!

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.

Returns:

  • (Boolean)

    If the refresh succeeds



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

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

Parameters:

  • room_alias (String, MXID)

    The room alias to remove

Raises:

  • (ArgumentError)

See Also:



435
436
437
438
439
440
# File 'lib/matrix_sdk/client.rb', line 435

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

Returns:

  • (Array[Room])

    All the currently handled rooms



185
186
187
188
189
190
191
192
193
# File 'lib/matrix_sdk/client.rb', line 185

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

Parameters:

  • status (:online, :offline, :unavailable)

    The new status to use

  • message (String) (defaults to: nil)

    A custom status message to set

Raises:

  • (ArgumentError)

See Also:



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

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

#spacesArray[Room]

Get a list of all joined Matrix Spaces

Returns:

  • (Array[Room])

    All the currently joined Spaces



198
199
200
201
202
203
204
205
206
# File 'lib/matrix_sdk/client.rb', line 198

def spaces
  rooms = if cache == :none
            api.get_joined_rooms.joined_rooms.map { |id| Room.new(self, id) }
          else
            self.rooms
          end

  rooms.select(&:space?)
end

#start_listener_thread(**params) ⇒ Object

Starts a background thread that will listen to new events



458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'lib/matrix_sdk/client.rb', line 458

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
      case event.to_sym
      when :sync
        handle_sync_response(data)
        errors = 0
      when :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



492
493
494
495
496
497
498
499
500
501
502
503
504
505
# File 'lib/matrix_sdk/client.rb', line 492

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

Parameters:

  • skip_store_batch (Boolean) (defaults to: false)

    Should this sync skip storing the returned next_batch token, doing this would mean the next sync re-runs from the same point. Useful with use of filters.

  • params (Hash)

    Additional options

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:



522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
# File 'lib/matrix_sdk/client.rb', line 522

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
    break api.sync(**extra_params)
  rescue MatrixSdk::MatrixTimeoutError => e
    raise e if (attempts += 1) >= params.fetch(:allow_sync_retry, 0)
  end

  @next_batch = data[:next_batch] unless skip_store_batch

  handle_sync_response(data)
  true
end

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

Upload a piece of data to the media repo

Parameters:

  • content (String)

    The data to upload

  • content_type (String)

    The MIME type of the data

Returns:

  • (URI::MXC)

    A Matrix content (mxc://) URL pointing to the uploaded data

Raises:

See Also:



448
449
450
451
452
453
# File 'lib/matrix_sdk/client.rb', line 448

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

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