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

#apiObject (readonly)

Returns the value of attribute api.



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_batchString Also known as: sync_token

The batch token for a running sync

Returns:

  • (String)

    The opaque batch token



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

attr_reader :api

#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

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

#account_dataObject

Retrieve an account data helper



170
171
172
173
174
# File 'lib/matrix_sdk/client.rb', line 170

def 
  return MatrixSdk::Util::AccountDataCache.new self if cache == :none

  @account_data ||= MatrixSdk::Util::AccountDataCache.new self
end

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



384
385
386
387
# File 'lib/matrix_sdk/client.rb', line 384

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)


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

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



165
166
167
# File 'lib/matrix_sdk/client.rb', line 165

def direct_rooms
  ['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)


561
562
563
564
565
566
567
568
569
570
571
572
573
574
# File 'lib/matrix_sdk/client.rb', line 561

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
  ret = @rooms.fetch(room_id) do
    room = Room.new(self, room_id)
    @rooms[room_id] = room unless cache == :none
    room
  end
  # Need to figure out a way to handle multiple types
  ret = @rooms[room_id] = ret.to_space if ret.instance_variable_get :@room_type
  ret
end

#find_room(room_id_or_alias, only_canonical: true) ⇒ 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: true)

    Only match alias against the canonical alias

Returns:

  • (Room)

    The found room

  • (nil)

    If no room was found

Raises:

  • (ArgumentError)


407
408
409
410
411
412
413
414
415
416
417
# File 'lib/matrix_sdk/client.rb', line 407

def find_room(room_id_or_alias, only_canonical: true)
  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?

  room = @rooms.values.find { |r| r.aliases.include? room_id_or_alias.to_s }
  return room if only_canonical

  room || @rooms.values.find { |r| r.aliases(canonical_only: false).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



425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/matrix_sdk/client.rb', line 425

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:



395
396
397
398
399
# File 'lib/matrix_sdk/client.rb', line 395

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



576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# File 'lib/matrix_sdk/client.rb', line 576

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))
      return unless @should_listen

      bad_sync_timeout = orig_bad_sync_timeout
      sleep(sync_interval) if sync_interval.positive?
    rescue MatrixRequestError => e
      return unless @should_listen

      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)


519
520
521
# File 'lib/matrix_sdk/client.rb', line 519

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



337
338
339
# File 'lib/matrix_sdk/client.rb', line 337

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)


281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/matrix_sdk/client.rb', line 281

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)


309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/matrix_sdk/client.rb', line 309

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



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

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



107
108
109
110
# File 'lib/matrix_sdk/client.rb', line 107

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:



119
120
121
# File 'lib/matrix_sdk/client.rb', line 119

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



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

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



239
240
241
242
# File 'lib/matrix_sdk/client.rb', line 239

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)


254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/matrix_sdk/client.rb', line 254

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:



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/matrix_sdk/client.rb', line 345

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



222
223
224
225
226
227
228
229
230
231
232
# File 'lib/matrix_sdk/client.rb', line 222

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:



445
446
447
448
449
450
# File 'lib/matrix_sdk/client.rb', line 445

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



194
195
196
197
198
199
200
201
202
# File 'lib/matrix_sdk/client.rb', line 194

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:



129
130
131
132
133
# File 'lib/matrix_sdk/client.rb', line 129

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



207
208
209
210
211
212
213
214
215
# File 'lib/matrix_sdk/client.rb', line 207

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



468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/matrix_sdk/client.rb', line 468

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



502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
# File 'lib/matrix_sdk/client.rb', line 502

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(0.1)
    @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:



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

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:



458
459
460
461
462
463
# File 'lib/matrix_sdk/client.rb', line 458

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