Class: MatrixSdk::Api

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

Constant Summary collapse

USER_AGENT =
"Ruby Matrix SDK v#{MatrixSdk::VERSION}"
DEFAULT_HEADERS =
{
  'accept' => 'application/json',
  'user-agent' => USER_AGENT
}.freeze

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(homeserver, **params) ⇒ Api

Note:

Using threadsafe :multithread currently doesn’t support connection re-use

Returns a new instance of Api.

Parameters:

  • homeserver (String, URI)

    The URL to the Matrix homeserver, without the /_matrix/ part

  • params (Hash)

    Additional parameters on creation

Options Hash (**params):

  • :protocols (Symbol[])

    The protocols to include (:AS, :CS, :IS, :SS), defaults to :CS

  • :address (String)

    The connection address to the homeserver, if different to the HS URL

  • :port (Integer)

    The connection port to the homeserver, if different to the HS URL

  • :access_token (String)

    The access token to use for the connection

  • :device_id (String)

    The ID of the logged in decide to use

  • :autoretry (Boolean) — default: true

    Should requests automatically be retried in case of rate limits

  • :validate_certificate (Boolean) — default: false

    Should the connection require valid SSL certificates

  • :transaction_id (Integer) — default: 0

    The starting ID for transactions

  • :backoff_time (Numeric) — default: 5000

    The request backoff time in milliseconds

  • :open_timeout (Numeric) — default: 60

    The timeout in seconds to wait for a TCP session to open

  • :read_timeout (Numeric) — default: 240

    The timeout in seconds for reading responses

  • :global_headers (Hash)

    Additional headers to set for all requests

  • :skip_login (Boolean)

    Should the API skip logging in if the HS URL contains user information

  • :synapse (Boolean) — default: true

    Is the API connecting to a Synapse instance

  • :threadsafe (Boolean, :multithread) — default: :multithread

    Should the connection be threadsafe/mutexed - or safe for simultaneous multi-thread usage. Will default to :multithread - a.k.a. per-thread HTTP connections and requests

Raises:

  • (ArgumentError)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/matrix_sdk/api.rb', line 46

def initialize(homeserver, **params)
  @homeserver = homeserver
  raise ArgumentError, 'Homeserver URL must be String or URI' unless @homeserver.is_a?(String) || @homeserver.is_a?(URI)

  @homeserver = URI.parse("#{'https://' unless @homeserver.start_with? 'http'}#{@homeserver}") unless @homeserver.is_a? URI
  @homeserver.path.gsub!(/\/?_matrix\/?/, '') if @homeserver.path =~ /_matrix\/?$/
  raise ArgumentError, 'Please use the base URL for your HS (without /_matrix/)' if @homeserver.path.include? '/_matrix/'

  @proxy_uri = params.fetch(:proxy_uri, nil)
  @connection_address = params.fetch(:address, nil)
  @connection_port = params.fetch(:port, nil)
  @access_token = params.fetch(:access_token, nil)
  @device_id = params.fetch(:device_id, nil)
  @autoretry = params.fetch(:autoretry, true)
  @validate_certificate = params.fetch(:validate_certificate, false)
  @transaction_id = params.fetch(:transaction_id, 0)
  @backoff_time = params.fetch(:backoff_time, 5000)
  @open_timeout = params.fetch(:open_timeout, nil)
  @read_timeout = params.fetch(:read_timeout, nil)
  @well_known = params.fetch(:well_known, {})
  @global_headers = DEFAULT_HEADERS.dup
  @global_headers.merge!(params.fetch(:global_headers)) if params.key? :global_headers
  @synapse = params.fetch(:synapse, true)
  @http = nil
  @inflight = []

  self.threadsafe = params.fetch(:threadsafe, :multithread)

  ([params.fetch(:protocols, [:CS])].flatten - protocols).each do |proto|
    self.class.include MatrixSdk::Protocols.const_get(proto)
  end

  (user: @homeserver.user, password: @homeserver.password) if @homeserver.user && @homeserver.password && !@access_token && !params[:skip_login] && protocol?(:CS)
  @homeserver.userinfo = '' unless params[:skip_login]
end

Instance Attribute Details

#access_tokenObject

Returns the value of attribute access_token.



21
22
23
# File 'lib/matrix_sdk/api.rb', line 21

def access_token
  @access_token
end

#autoretryObject

Returns the value of attribute autoretry.



21
22
23
# File 'lib/matrix_sdk/api.rb', line 21

def autoretry
  @autoretry
end

#connection_addressObject

Returns the value of attribute connection_address.



21
22
23
# File 'lib/matrix_sdk/api.rb', line 21

def connection_address
  @connection_address
end

#connection_portObject

Returns the value of attribute connection_port.



21
22
23
# File 'lib/matrix_sdk/api.rb', line 21

def connection_port
  @connection_port
end

#device_idObject

Returns the value of attribute device_id.



21
22
23
# File 'lib/matrix_sdk/api.rb', line 21

def device_id
  @device_id
end

#global_headersObject

Returns the value of attribute global_headers.



21
22
23
# File 'lib/matrix_sdk/api.rb', line 21

def global_headers
  @global_headers
end

#homeserverObject

Returns the value of attribute homeserver.



22
23
24
# File 'lib/matrix_sdk/api.rb', line 22

def homeserver
  @homeserver
end

#open_timeoutObject

Returns the value of attribute open_timeout.



22
23
24
# File 'lib/matrix_sdk/api.rb', line 22

def open_timeout
  @open_timeout
end

#proxy_uriObject

Returns the value of attribute proxy_uri.



22
23
24
# File 'lib/matrix_sdk/api.rb', line 22

def proxy_uri
  @proxy_uri
end

#read_timeoutObject

Returns the value of attribute read_timeout.



22
23
24
# File 'lib/matrix_sdk/api.rb', line 22

def read_timeout
  @read_timeout
end

#threadsafeObject

Returns the value of attribute threadsafe.



22
23
24
# File 'lib/matrix_sdk/api.rb', line 22

def threadsafe
  @threadsafe
end

#validate_certificateObject

Returns the value of attribute validate_certificate.



22
23
24
# File 'lib/matrix_sdk/api.rb', line 22

def validate_certificate
  @validate_certificate
end

#well_knownObject (readonly)

Returns the value of attribute well_known.



22
23
24
# File 'lib/matrix_sdk/api.rb', line 22

def well_known
  @well_known
end

Class Method Details

.new_for_domain(domain, target: :client, keep_wellknown: false, ssl: true, **params) ⇒ API

Create an API connection to a domain entry

This will follow the server discovery spec for client-server and federation

Examples:

Opening a Matrix API connection to a homeserver

hs = MatrixSdk::API.new_for_domain 'example.com'
hs.connection_address
# => 'matrix.example.com'
hs.connection_port
# => 443

Parameters:

  • domain (String)

    The domain to set up the API connection for, can contain a ‘:’ to denote a port

  • target (:client, :identity, :server) (defaults to: :client)

    The target for the domain lookup

  • keep_wellknown (Boolean) (defaults to: false)

    Should the .well-known response be kept for further handling

  • params (Hash)

    Additional options to pass to .new

Returns:

  • (API)

    The API connection



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/matrix_sdk/api.rb', line 98

def self.new_for_domain(domain, target: :client, keep_wellknown: false, ssl: true, **params)
  domain, port = domain.split(':')
  uri = URI("http#{ssl ? 's' : ''}://#{domain}")
  well_known = nil
  target_uri = nil
  logger = ::Logging.logger[self]
  logger.debug "Resolving #{domain}"

  if !port.nil? && !port.empty?
    # If the domain is fully qualified according to Matrix (FQDN and port) then skip discovery
    target_uri = URI("https://#{domain}:#{port}")
  elsif target == :server
    # Attempt SRV record discovery
    target_uri = begin
      require 'resolv'
      resolver = Resolv::DNS.new
      srv = "_matrix._tcp.#{domain}"
      logger.debug "Trying DNS #{srv}..."
      d = resolver.getresource(srv, Resolv::DNS::Resource::IN::SRV)
      d
    rescue StandardError => e
      logger.debug "DNS lookup failed with #{e.class}: #{e.message}"
      nil
    end

    if target_uri.nil?
      # Attempt .well-known discovery for server-to-server
      well_known = begin
        wk_uri = URI("https://#{domain}/.well-known/matrix/server")
        logger.debug "Trying #{wk_uri}..."
        data = Net::HTTP.start(wk_uri.host, wk_uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5, write_timeout: 5) do |http|
          http.get(wk_uri.path).body
        end
        JSON.parse(data)
      rescue StandardError => e
        logger.debug "Well-known failed with #{e.class}: #{e.message}"
        nil
      end

      target_uri = well_known['m.server'] if well_known&.key?('m.server')
    else
      target_uri = URI("https://#{target_uri.target}:#{target_uri.port}")
    end
  elsif %i[client identity].include? target
    # Attempt .well-known discovery
    well_known = begin
      wk_uri = URI("https://#{domain}/.well-known/matrix/client")
      logger.debug "Trying #{wk_uri}..."
      data = Net::HTTP.start(wk_uri.host, wk_uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5, write_timeout: 5) do |http|
        http.get(wk_uri.path).body
      end
      JSON.parse(data)
    rescue StandardError => e
      logger.debug "Well-known failed with #{e.class}: #{e.message}"
      nil
    end

    if well_known
      key = 'm.homeserver'
      key = 'm.identity_server' if target == :identity

      if well_known.key?(key) && well_known[key].key?('base_url')
        uri = URI(well_known[key]['base_url'])
        target_uri = uri
      end
    end
  end
  logger.debug "Using #{target_uri.inspect}"

  # Fall back to direct domain connection
  target_uri ||= URI("https://#{domain}:8448")

  params[:well_known] = well_known if keep_wellknown

  new(
    uri,
    **params.merge(
      address: target_uri.host,
      port: target_uri.port
    )
  )
end

Instance Method Details

#protocol?(protocol) ⇒ Boolean

Check if a protocol is enabled on the API connection

Examples:

Checking for identity server API support

api.protocol? :IS
# => false

Parameters:

  • protocol (Symbol)

    The protocol to check

Returns:

  • (Boolean)

    Is the protocol enabled



204
205
206
# File 'lib/matrix_sdk/api.rb', line 204

def protocol?(protocol)
  protocols.include? protocol
end

#protocolsSymbol[]

Get a list of enabled protocols on the API client

Examples:

MatrixSdk::Api.new_for_domain('matrix.org').protocols
# => [:IS, :CS]

Returns:

  • (Symbol[])

    An array of enabled APIs



188
189
190
191
192
193
194
# File 'lib/matrix_sdk/api.rb', line 188

def protocols
  self
    .class.included_modules
    .reject { |m| m&.name.nil? }
    .select { |m| m.name.start_with? 'MatrixSdk::Protocols::' }
    .map { |m| m.name.split('::').last.to_sym }
end

#request(method, api, path, **options) ⇒ Object

Perform a raw Matrix API request

Examples:

Simple API query

api.request(:get, :client_r0, '/account/whoami')
# => { :user_id => "@alice:matrix.org" }

Advanced API request

api.request(:post,
            :media_r0,
            '/upload',
            body_stream: open('./file'),
            headers: { 'content-type' => 'image/png' })
# => { :content_uri => "mxc://example.com/AQwafuaFswefuhsfAFAgsw" }

Parameters:

  • method (Symbol)

    The method to use, can be any of the ones under Net::HTTP

  • api (Symbol)

    The API symbol to use, :client_r0 is the current CS one

  • path (String)

    The API path to call, this is the part that comes after the API definition in the spec

  • options (Hash)

    Additional options to pass along to the request

Options Hash (**options):

  • :query (Hash)

    Query parameters to set on the URL

  • :body (Hash, String)

    The body to attach to the request, will be JSON-encoded if sent as a hash

  • :body_stream (IO)

    A body stream to attach to the request

  • :headers (Hash)

    Additional headers to set on the request

  • :skip_auth (Boolean) — default: false

    Skip authentication



286
287
288
289
290
291
292
293
294
295
296
297
298
299
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
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/matrix_sdk/api.rb', line 286

def request(method, api, path, **options)
  url = homeserver.dup.tap do |u|
    u.path = api_to_path(api) + path
    u.query = [u.query, URI.encode_www_form(options.fetch(:query))].flatten.compact.join('&') if options[:query]
    u.query = nil if u.query.nil? || u.query.empty?
  end

  failures = 0
  loop do
    raise MatrixConnectionError, "Server still too busy to handle request after #{failures} attempts, try again later" if failures >= 10

    req_id = ('A'..'Z').to_a.sample(4).join

    req_obj = construct_request(url: url, method: method, **options)
    print_http(req_obj, id: req_id)
    response = duration = nil

    loc_http = http
    perform_request = proc do
      @inflight << loc_http
      dur_start = Time.now
      response = loc_http.request req_obj
      dur_end = Time.now
      duration = dur_end - dur_start
    rescue EOFError
      logger.error 'Socket closed unexpectedly'
      raise
    ensure
      @inflight.delete loc_http
    end

    if @threadsafe == true
      http_lock.synchronize { perform_request.call }
    else
      perform_request.call
      loc_http.finish if @threadsafe == :multithread
    end
    print_http(response, duration: duration, id: req_id)

    begin
      data = JSON.parse(response.body, symbolize_names: true)
    rescue JSON::JSONError => e
      logger.debug "#{e.class} error when parsing response. #{e}"
      data = nil
    end

    if response.is_a? Net::HTTPTooManyRequests
      raise MatrixRequestError.new_by_code(data, response.code) unless autoretry

      failures += 1
      waittime = data[:retry_after_ms] || data[:error][:retry_after_ms] || @backoff_time
      sleep(waittime.to_f / 1000.0)
      next
    end

    if response.is_a? Net::HTTPSuccess
      unless data
        logger.error "Received non-parsable data in 200 response; #{response.body.inspect}"
        raise MatrixConnectionError, response
      end
      return MatrixSdk::Response.new self, data
    end
    raise MatrixRequestError.new_by_code(data, response.code) if data

    raise MatrixConnectionError.class_by_code(response.code), response
  end
end

#stop_inflightObject



363
364
365
# File 'lib/matrix_sdk/api.rb', line 363

def stop_inflight
  @inflight.each(&:finish)
end

#transaction_idString

Generate a transaction ID

Returns:

  • (String)

    An arbitrary transaction ID



357
358
359
360
361
# File 'lib/matrix_sdk/api.rb', line 357

def transaction_id
  ret = @transaction_id ||= 0
  @transaction_id = @transaction_id.succ
  ret
end