Class: MatrixSdk::Api

Inherits:
Object show all
Includes:
Logging, Protocols::AS, Protocols::CS, Protocols::IS, Protocols::SS
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 Protocols::SS

#server_version

Methods included from Protocols::IS

#identity_bulk_lookup, #identity_get_pubkey, #identity_lookup, #identity_pubkey_ephemeral_isvalid, #identity_pubkey_isvalid, #identity_status

Methods included from Protocols::CS

#add_user_tag, #allowed_login_methods, #ban_user, #client_api_unstable_features, #client_api_versions, #create_filter, #create_room, #delete_device, #forget_room, #get_account_data, #get_avatar_url, #get_device, #get_devices, #get_display_name, #get_download_url, #get_filter, #get_joined_rooms, #get_membership, #get_profile, #get_public_rooms, #get_room_account_data, #get_room_aliases, #get_room_avatar, #get_room_creation_info, #get_room_encryption_settings, #get_room_guest_access, #get_room_history_visibility, #get_room_id, #get_room_join_rules, #get_room_members, #get_room_messages, #get_room_name, #get_room_pinned_events, #get_room_power_levels, #get_room_server_acl, #get_room_state, #get_room_topic, #get_user_tags, #invite_user, #join_room, #keys_query, #kick_user, #leave_room, #login, #logout, #media_upload, #redact_event, #register, #remove_room_alias, #remove_user_tag, #send_content, #send_emote, #send_location, #send_message, #send_message_event, #send_notice, #send_state_event, #set_account_data, #set_avatar_url, #set_device, #set_display_name, #set_membership, #set_room_account_data, #set_room_alias, #set_room_avatar, #set_room_encryption_settings, #set_room_guest_access, #set_room_history_visibility, #set_room_join_rules, #set_room_name, #set_room_pinned_events, #set_room_power_levels, #set_room_server_acl, #set_room_topic, #sync, #unban_user, #username_available?, #whoami?

Methods included from Protocols::AS

included

Methods included from Logging

#logger

Constructor Details

#initialize(homeserver, **params) ⇒ Api

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

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

  • :well_known (Hash)

    The .well-known object that the server was discovered through, should not be set manually

Raises:

  • (ArgumentError)


44
45
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
# File 'lib/matrix_sdk/api.rb', line 44

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/'

  @protocols = params.fetch(:protocols, %i[CS])
  @protocols = [@protocols] unless @protocols.is_a? Array
  @protocols << :CS if @protocols.include?(:AS) && !@protocols.include?(:CS)

  @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)
  @read_timeout = params.fetch(:read_timeout, 240)
  @well_known = params.fetch(:well_known, {})
  @global_headers = DEFAULT_HEADERS.dup
  @global_headers.merge!(params.fetch(:global_headers)) if params.key? :global_headers
  @http = nil

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



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

def access_token
  @access_token
end

#autoretryObject

Returns the value of attribute autoretry.



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

def autoretry
  @autoretry
end

#connection_addressObject

Returns the value of attribute connection_address.



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

def connection_address
  @connection_address
end

#connection_portObject

Returns the value of attribute connection_port.



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

def connection_port
  @connection_port
end

#device_idObject

Returns the value of attribute device_id.



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

def device_id
  @device_id
end

#global_headersObject

Returns the value of attribute global_headers.



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

def global_headers
  @global_headers
end

#homeserverObject

Returns the value of attribute homeserver.



25
26
27
# File 'lib/matrix_sdk/api.rb', line 25

def homeserver
  @homeserver
end

#protocolsObject (readonly)

Returns the value of attribute protocols.



25
26
27
# File 'lib/matrix_sdk/api.rb', line 25

def protocols
  @protocols
end

#proxy_uriObject

Returns the value of attribute proxy_uri.



25
26
27
# File 'lib/matrix_sdk/api.rb', line 25

def proxy_uri
  @proxy_uri
end

#read_timeoutObject

Returns the value of attribute read_timeout.



25
26
27
# File 'lib/matrix_sdk/api.rb', line 25

def read_timeout
  @read_timeout
end

#validate_certificateObject

Returns the value of attribute validate_certificate.



25
26
27
# File 'lib/matrix_sdk/api.rb', line 25

def validate_certificate
  @validate_certificate
end

#well_knownObject (readonly)

Returns the value of attribute well_known.



25
26
27
# File 'lib/matrix_sdk/api.rb', line 25

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



91
92
93
94
95
96
97
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
# File 'lib/matrix_sdk/api.rb', line 91

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

  if !port.nil? && !port.empty?
    target_uri = URI("https://#{domain}:#{port}")
  elsif target == :server
    # Attempt SRV record discovery
    target_uri = begin
                   require 'resolv'
                   resolver = Resolv::DNS.new
                   resolver.getresource("_matrix._tcp.#{domain}")
                 rescue StandardError
                   nil
                 end

    if target_uri.nil?
      well_known = begin
                     data = Net::HTTP.get("https://#{domain}/.well-known/matrix/server")
                     JSON.parse(data)
                   rescue StandardError
                     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
                   data = Net::HTTP.get("https://#{domain}/.well-known/matrix/client")
                   JSON.parse(data)
                 rescue StandardError
                   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

  # 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

Returns:

  • (Boolean)


153
154
155
# File 'lib/matrix_sdk/api.rb', line 153

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

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



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/matrix_sdk/api.rb', line 194

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
  request = Net::HTTP.const_get(method.to_s.capitalize.to_sym).new url.request_uri
  request.body = options[:body] if options.key? :body
  request.body = request.body.to_json if options.key?(:body) && !request.body.is_a?(String)
  request.body_stream = options[:body_stream] if options.key? :body_stream

  global_headers.each { |h, v| request[h] = v }
  if request.body || request.body_stream
    request.content_type = 'application/json'
    request.content_length = (request.body || request.body_stream).size
  end

  request['authorization'] = "Bearer #{access_token}" if access_token
  if options.key? :headers
    options[:headers].each do |h, v|
      request[h.to_s.downcase] = v
    end
  end

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

    print_http(request)
    begin
      response = http.request request
    rescue EOFError => e
      logger.error 'Socket closed unexpectedly'
      raise e
    end
    print_http(response)
    data = JSON.parse(response.body, symbolize_names: true) rescue nil

    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

    return MatrixSdk::Response.new self, data if response.is_a? Net::HTTPSuccess
    raise MatrixRequestError.new_by_code(data, response.code) if data

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