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


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

  @rooms = {}
  @users = {}
  @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 } } }

  @should_listen = false
  @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

  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



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

def api
  @api
end

#cache:all, ...

The cache level

Returns:

  • (:all, :some, :none)

    The level of caching to do



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

Returns:

  • (Hash, String)

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



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

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:



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

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:



339
340
341
342
# File 'lib/matrix_sdk/client.rb', line 339

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

Parameters:

  • room_id (String, MXID)

    The room ID to ensure

Returns:

  • (Room)

    The room object for the requested room

Raises:

  • (ArgumentError)


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

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)


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

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



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

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:



350
351
352
353
354
# File 'lib/matrix_sdk/client.rb', line 350

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: 30, **params) ⇒ Object



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

def listen_forever(timeout: 30, bad_sync_timeout: 5, sync_interval: 30, **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)


471
472
473
# File 'lib/matrix_sdk/client.rb', line 471

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



292
293
294
# File 'lib/matrix_sdk/client.rb', line 292

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)


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

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)


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

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



282
283
284
285
286
# File 'lib/matrix_sdk/client.rb', line 282

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



101
102
103
104
105
# File 'lib/matrix_sdk/client.rb', line 101

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

#presenceResponse

Gets the current user presence status object

Returns:

See Also:



114
115
116
# File 'lib/matrix_sdk/client.rb', line 114

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



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

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



194
195
196
197
# File 'lib/matrix_sdk/client.rb', line 194

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)


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

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:



300
301
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
# File 'lib/matrix_sdk/client.rb', line 300

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.

Returns:

  • (Boolean)

    If the refresh succeeds



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

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:



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

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



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

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:



124
125
126
127
128
# File 'lib/matrix_sdk/client.rb', line 124

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



422
423
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
# File 'lib/matrix_sdk/client.rb', line 422

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



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

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:



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

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

Parameters:

  • content (String)

    The data to upload

  • content_type (String)

    The MIME type of the data

Returns:

  • (URI::MATRIX)

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

Raises:

See Also:



412
413
414
415
416
417
# File 'lib/matrix_sdk/client.rb', line 412

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