matrix_sdk 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ require 'matrix_sdk'
2
+
1
3
  require 'json'
2
4
  require 'net/http'
3
5
  require 'openssl'
@@ -5,17 +7,48 @@ require 'uri'
5
7
 
6
8
  module MatrixSdk
7
9
  class Api
8
- attr_accessor :access_token, :device_id, :validate_certificate
9
- attr_reader :homeserver
10
+ attr_accessor :access_token, :device_id
11
+ attr_reader :homeserver, :validate_certificate
12
+
13
+ ignore_inspect :access_token
10
14
 
11
15
  def initialize(homeserver, params = {})
12
16
  @homeserver = homeserver
13
- @homeserver = URI(@homeserver) unless @homeserver.is_a? URI
14
- @homeserver.path.sub!('/_matrix/', '/') if @homeserver.path.start_with? '/_matrix/'
17
+ @homeserver = URI.parse(@homeserver.to_s) unless @homeserver.is_a? URI
18
+ if @homeserver.path.end_with? '_matrix/'
19
+ @homeserver.path = begin
20
+ split = @homeserver.path.rpartition '_matrix/'
21
+ (split[(split.find_index '_matrix/')] = '/') rescue nil
22
+ split.join
23
+ end
24
+ end
25
+ raise 'Please use the base URL for your HS (without /_matrix/)' if @homeserver.path.include? '/_matrix/'
15
26
 
16
27
  @access_token = params.fetch(:access_token, nil)
17
28
  @device_id = params.fetch(:device_id, nil)
18
29
  @validate_certificate = params.fetch(:validate_certificate, false)
30
+ @transaction_id = params.fetch(:transaction_id, 0)
31
+ @backoff_time = params.fetch(:backoff_time, 5000)
32
+
33
+ login(user: @homeserver.user, password: @homeserver.password) if @homeserver.user && @homeserver.password && !@access_token && !params[:skip_login]
34
+ @homeserver.userinfo = '' unless params[:skip_login]
35
+ end
36
+
37
+ def logger
38
+ @logger ||= Logging.logger[self.class.name]
39
+ end
40
+
41
+ def validate_certificate=(validate)
42
+ # The HTTP connection needs to be reopened if this changes
43
+ @http.finish if @http && validate != @validate_certificate
44
+ @validate_certificate = validate
45
+ end
46
+
47
+ def homeserver=(hs_info)
48
+ # TODO: DNS query for SRV information about HS?
49
+ return unless hs_info.is_a? URI
50
+ @http.finish if @http
51
+ @homeserver = hs_info
19
52
  end
20
53
 
21
54
  def api_versions
@@ -23,33 +56,38 @@ module MatrixSdk
23
56
  end
24
57
 
25
58
  def sync(params = {})
26
- options = {
27
- timeout: 30.0,
28
- }.merge(params).select { |k, _v|
59
+ query = {
60
+ timeout: 30.0
61
+ }.merge(params).select do |k, _v|
29
62
  %i[since timeout filter full_state set_presence].include? k
30
- }
63
+ end
31
64
 
32
- options[:timeout] = ((options[:timeout] || 30) * 1000).to_i
33
- options[:timeout] = options.delete(:timeout_ms).to_i if options.key? :timeout_ms
65
+ query[:timeout] = ((query[:timeout] || 30) * 1000).to_i
66
+ query[:timeout] = params.delete(:timeout_ms).to_i if params.key? :timeout_ms
34
67
 
35
- request(:get, :client_r0, '/sync', query: options)
68
+ request(:get, :client_r0, '/sync', query: query)
36
69
  end
37
70
 
38
71
  def register(params = {})
39
- raise NotImplementedError, 'Registering is not implemented yet'
72
+ kind = params.delete(:kind) { 'user' }
73
+
74
+ request(:post, :client_r0, '/register', body: params, query: { kind: kind })
40
75
  end
41
76
 
42
77
  def login(params = {})
43
78
  options = {}
44
79
  options[:store_token] = params.delete(:store_token) { true }
80
+ options[:store_device_id] = params.delete(:store_device_id) { true }
45
81
 
46
82
  data = {
47
- type: params.delete(:login_type) { 'm.login.password' }
83
+ type: params.delete(:login_type) { 'm.login.password' },
84
+ initial_device_display_name: params.delete(:initial_device_display_name) { user_agent }
48
85
  }.merge params
49
86
  data[:device_id] = device_id if device_id
50
87
 
51
88
  request(:post, :client_r0, '/login', body: data).tap do |resp|
52
89
  @access_token = resp[:token] if resp[:token] && options[:store_token]
90
+ @device_id = resp[:device_id] if resp[:device_id] && options[:store_device_id]
53
91
  end
54
92
  end
55
93
 
@@ -58,11 +96,301 @@ module MatrixSdk
58
96
  end
59
97
 
60
98
  def create_room(params = {})
61
- raise NotImplementedError, 'Creating rooms is not implemented yet'
99
+ content = {
100
+ visibility: params.fetch(:visibility, :public)
101
+ }
102
+ content[:room_alias_name] = params[:room_alias] if params[:room_alias]
103
+ content[:invite] = [params[:invite]].flatten if params[:invite]
104
+
105
+ request(:post, :client_r0, '/createRoom', content)
62
106
  end
63
107
 
64
108
  def join_room(id_or_alias)
65
- request(:post, :client_r0, "/join/#{URI.escape id_or_alias}")
109
+ request(:post, :client_r0, "/join/#{CGI.escape id_or_alias}")
110
+ end
111
+
112
+ def send_state_event(room_id, event_type, content, params = {})
113
+ query = {}
114
+ query[:ts] = params[:timestamp].to_i if params.key? :timestamp
115
+
116
+ request(:put, :client_r0, "/rooms/#{room_id}/state/#{event_type}#{"/#{params[:state_key]}" if params.key? :state_key}", body: content, query: query)
117
+ end
118
+
119
+ def send_message_event(room_id, event_type, content, params = {})
120
+ query = {}
121
+ query[:ts] = params[:timestamp].to_i if params.key? :timestamp
122
+
123
+ txn_id = transaction_id
124
+ txn_id = params.fetch(:txn_id, "#{txn_id}#{Time.now.to_i}")
125
+
126
+ request(:put, :client_r0, "/rooms/#{room_id}/send/#{event_type}/#{txn_id}", body: content, query: query)
127
+ end
128
+
129
+ def redact_event(room_id, event_type, params = {})
130
+ query = {}
131
+ query[:ts] = params[:timestamp].to_i if params.key? :timestamp
132
+
133
+ content = {}
134
+ content[:reason] = params[:reason] if params[:reason]
135
+
136
+ txn_id = transaction_id
137
+ txn_id = params.fetch(:txn_id, "#{txn_id}#{Time.now.to_i}")
138
+
139
+ request(:put, :client_r0, "/rooms/#{room_id}/redact/#{event_type}/#{txn_id}", body: content, query: query)
140
+ end
141
+
142
+ def send_content(room_id, url, name, msg_type, params = {})
143
+ content = {
144
+ url: url,
145
+ msgtype: msg_type,
146
+ body: name,
147
+ info: params.delete(:extra_information) { {} }
148
+ }
149
+
150
+ send_message_event(room_id, 'm.room.message', content, params)
151
+ end
152
+
153
+ def send_location(room_id, geo_uri, name, params = {})
154
+ content = {
155
+ geo_uri: geo_uri,
156
+ msgtype: 'm.location',
157
+ body: name
158
+ }
159
+ content[:thumbnail_url] = params.delete(:thumbnail_url) if params.key? :thumbnail_url
160
+ content[:thumbnail_info] = params.delete(:thumbnail_info) if params.key? :thumbnail_info
161
+
162
+ send_message_event(room_id, 'm.room.message', content, params)
163
+ end
164
+
165
+ def send_message(room_id, message, params = {})
166
+ content = {
167
+ msgtype: params.delete(:msg_type) { 'm.text' },
168
+ body: message
169
+ }
170
+ send_message_event(room_id, 'm.room.message', content, params)
171
+ end
172
+
173
+ def send_emote(room_id, emote, params = {})
174
+ content = {
175
+ msgtype: params.delete(:msg_type) { 'm.emote' },
176
+ body: emote
177
+ }
178
+ send_message_event(room_id, 'm.room.message', content, params)
179
+ end
180
+
181
+ def send_notice(room_id, notice, params = {})
182
+ content = {
183
+ msgtype: params.delete(:msg_type) { 'm.notice' },
184
+ body: notice
185
+ }
186
+ send_message_event(room_id, 'm.room.message', content, params)
187
+ end
188
+
189
+ def get_room_messages(room_id, token, direction, params = {})
190
+ query = {
191
+ roomId: room_id,
192
+ from: token,
193
+ dir: direction,
194
+ limit: params.fetch(:limit, 10)
195
+ }
196
+ query[:to] = params[:to] if params.key? :to
197
+
198
+ request(:get, :client_r0, "/rooms/#{room_id}/messages", query: query)
199
+ end
200
+
201
+ def get_room_name(room_id)
202
+ request(:get, :client_r0, "/rooms/#{room_id}/state/m.room.name")
203
+ end
204
+
205
+ def set_room_name(room_id, name, params = {})
206
+ content = {
207
+ name: name
208
+ }
209
+ send_state_event(room_id, 'm.room.name', content, params)
210
+ end
211
+
212
+ def get_room_topic(room_id)
213
+ request(:get, :client_r0, "/rooms/#{room_id}/state/m.room.topic")
214
+ end
215
+
216
+ def set_room_topic(room_id, topic, params = {})
217
+ content = {
218
+ topic: topic
219
+ }
220
+ send_state_event(room_id, 'm.room.topic', content, params)
221
+ end
222
+
223
+ def get_power_levels(room_id)
224
+ request(:get, :client_r0, "/rooms/#{room_id}/state/m.room.power_levels")
225
+ end
226
+
227
+ def set_power_levels(room_id, content)
228
+ content[:events] = {} unless content.key? :events
229
+ send_state_event(room_id, 'm.room.power_levels', content)
230
+ end
231
+
232
+ def leave_room(room_id)
233
+ request(:post, :client_r0, "/rooms/#{room_id}/leave")
234
+ end
235
+
236
+ def forget_room(room_id)
237
+ request(:post, :client_r0, "/rooms/#{room_id}/forget")
238
+ end
239
+
240
+ def invite_user(room_id, user_id)
241
+ content = {
242
+ user_id: user_id
243
+ }
244
+ request(:post, :client_r0, "/rooms/#{room_id}/invite", body: content)
245
+ end
246
+
247
+ def kick_user(room_id, user_id, params = {})
248
+ set_membership(room_id, user_id, 'leave', params)
249
+ end
250
+
251
+ def get_membership(room_id, user_id)
252
+ request(:get, :client_r0, "/rooms/#{room_id}/state/m.room.member/#{user_id}")
253
+ end
254
+
255
+ def set_membership(room_id, user_id, membership, params = {})
256
+ content = {
257
+ membership: membership,
258
+ reason: params.delete(:reason) { '' }
259
+ }
260
+ content[:displayname] = params.delete(:displayname) if params.key? :displayname
261
+ content[:avatar_url] = params.delete(:avatar_url) if params.key? :avatar_url
262
+
263
+ send_state_event(room_id, 'm.room.member', content, params.merge(state_key: user_id))
264
+ end
265
+
266
+ def ban_user(room_id, user_id, params = {})
267
+ content = {
268
+ user_id: user_id,
269
+ reason: params[:reason] || ''
270
+ }
271
+ request(:post, :client_r0, "/rooms/#{room_id}/ban", body: content)
272
+ end
273
+
274
+ def unban_user(room_id, user_id)
275
+ content = {
276
+ user_id: user_id
277
+ }
278
+ request(:post, :client_r0, "/rooms/#{room_id}/unban", body: content)
279
+ end
280
+
281
+ def get_user_tags(user_id, room_id)
282
+ request(:get, :client_r0, "/user/#{user_id}/rooms/#{room_id}/tags")
283
+ end
284
+
285
+ def remove_user_tag(user_id, room_id, tag)
286
+ request(:delete, :client_r0, "/user/#{user_id}/rooms/#{room_id}/tags/#{tag}")
287
+ end
288
+
289
+ def add_user_tag(user_id, room_id, tag, params = {})
290
+ if params[:body]
291
+ content = params[:body]
292
+ else
293
+ content = {}
294
+ content[:order] = params[:order] if params.key? :order
295
+ end
296
+ request(:put, :client_r0, "/user/#{user_id}/rooms/#{room_id}/tags/#{tag}", body: content)
297
+ end
298
+
299
+ def get_account_data(user_id, type)
300
+ request(:get, :client_r0, "/user/#{user_id}/account_data/#{type}")
301
+ end
302
+
303
+ def set_account_data(user_id, type, account_data)
304
+ request(:put, :client_r0, "/user/#{user_id}/account_data/#{type}", body: account_data)
305
+ end
306
+
307
+ def get_room_account_data(user_id, room_id, type)
308
+ request(:get, :client_r0, "/user/#{user_id}/rooms/#{room_id}/account_data/#{type}")
309
+ end
310
+
311
+ def set_room_account_data(user_id, room_id, type, account_data)
312
+ request(:put, :client_r0, "/user/#{user_id}/rooms/#{room_id}/account_data/#{type}", body: account_data)
313
+ end
314
+
315
+ def get_room_state(room_id)
316
+ request(:get, :client_r0, "/rooms/#{room_id}/state")
317
+ end
318
+
319
+ def get_filter(user_id, filter_id)
320
+ request(:get, :client_r0, "/user/#{user_id}/filter/#{filter_id}")
321
+ end
322
+
323
+ def create_filter(user_id, filter_params)
324
+ request(:post, :client_r0, "/user/#{user_id}/filter", body: filter_params)
325
+ end
326
+
327
+ def media_upload(content, content_type)
328
+ request(:post, :media_r0, '/upload', body: content, headers: { 'content-type' => content_type })
329
+ end
330
+
331
+ def get_display_name(user_id)
332
+ request(:get, :client_r0, "/profile/#{user_id}/displayname")
333
+ end
334
+
335
+ def set_display_name(user_id, display_name)
336
+ content = {
337
+ display_name: display_name
338
+ }
339
+ request(:put, :client_r0, "/profile/#{user_id}/displayname", body: content)
340
+ end
341
+
342
+ def get_avatar_url(user_id)
343
+ request(:get, :client_r0, "/profile/#{user_id}/avatar_url")
344
+ end
345
+
346
+ def set_avatar_url(user_id, url)
347
+ content = {
348
+ avatar_url: url
349
+ }
350
+ request(:put, :client_r0, "/profile/#{user_id}/avatar_url", body: content)
351
+ end
352
+
353
+ def get_download_url(mxcurl)
354
+ mxcurl = URI.parse(mxcurl.to_s) unless mxcurl.is_a? URI
355
+ raise 'Not a mxc:// URL' unless mxcurl.is_a? URI::MATRIX
356
+
357
+ homeserver.dup.tap do |u|
358
+ u.path = "/_matrix/media/r0/download/#{mxcurl.full_path}"
359
+ end
360
+ end
361
+
362
+ def get_room_id(room_alias)
363
+ request(:get, :client_r0, "/directory/room/#{room_alias}")
364
+ end
365
+
366
+ def set_room_alias(room_id, room_alias)
367
+ content = {
368
+ room_id: room_id
369
+ }
370
+ request(:put, :client_r0, "/directory/room/#{room_alias}", body: content)
371
+ end
372
+
373
+ def remove_room_alias(room_alias)
374
+ request(:delete, :client_r0, "/directory/room/#{room_alias}")
375
+ end
376
+
377
+ def get_room_members(room_id)
378
+ request(:get, :client_r0, "/rooms/#{room_id}/members")
379
+ end
380
+
381
+ def set_join_rule(room_id, join_rule)
382
+ content = {
383
+ join_rule: join_rule
384
+ }
385
+ send_state_event(room_id, 'm.room.join_rules', content)
386
+ end
387
+
388
+ def set_guest_access(room_id, guest_access)
389
+ # raise ArgumentError, '`guest_access` must be one of [:can_join, :forbidden]' unless %i[can_join forbidden].include? guest_access
390
+ content = {
391
+ guest_access: guest_access
392
+ }
393
+ send_state_event(room_id, 'm.room.guest_access', content)
66
394
  end
67
395
 
68
396
  def whoami?
@@ -72,42 +400,85 @@ module MatrixSdk
72
400
  def request(method, api, path, options = {})
73
401
  url = homeserver.dup.tap do |u|
74
402
  u.path = api_to_path(api) + path
75
- u.query = [u.query, options[:query]].reject(&:nil?).flatten.join('&') if options[:query]
403
+ u.query = [u.query, options[:query].map { |k, v| "#{k}#{"=#{v}" unless v.nil?}" }].flatten.reject(&:nil?).join('&') if options[:query]
404
+ u.query = nil if u.query.nil? || u.query.empty?
76
405
  end
77
406
  request = Net::HTTP.const_get(method.to_s.capitalize.to_sym).new url.request_uri
78
407
  request.body = options[:body] if options.key? :body
79
- request.body = request.body.to_json unless request.body.is_a? String
408
+ request.body = request.body.to_json if options.key?(:body) && !request.body.is_a?(String)
80
409
  request.body_stream = options[:body_stream] if options.key? :body_stream
81
410
 
82
411
  request.content_type = 'application/json' if request.body || request.body_stream
83
412
 
84
413
  request['authorization'] = "Bearer #{access_token}" if access_token
85
- request['user-agent'] = 'Cool string goes here' if false
86
- options[:headers].each do |h, v|
87
- request[h.to_s.downcase] = v
88
- end if options.key? :headers
414
+ request['user-agent'] = user_agent
415
+ if options.key? :headers
416
+ options[:headers].each do |h, v|
417
+ request[h.to_s.downcase] = v
418
+ end
419
+ end
420
+
421
+ failures = 0
422
+ loop do
423
+ raise MatrixConnectionError, "Server still too busy to handle request after #{failures} attempts, try again later" if failures >= 10
89
424
 
90
- response = http.request request
91
- data = JSON.parse response.body, symbolize_names: true
425
+ print_http(request)
426
+ response = http.request request
427
+ print_http(response)
428
+ data = JSON.parse(response.body, symbolize_names: true) rescue nil
92
429
 
93
- return data if response.kind_of? Net::HTTPSuccess
94
- raise MatrixError, data, response.code
430
+ if response.is_a? Net::HTTPTooManyRequests
431
+ failures += 1
432
+ waittime = data[:retry_after_ms] || data[:error][:retry_after_ms] || @backoff_time
433
+ sleep(waittime.to_f / 1000.0)
434
+ next
435
+ end
436
+
437
+ return data if response.is_a? Net::HTTPSuccess
438
+ raise MatrixRequestError.new(data, response.code) if data
439
+ raise MatrixConnectionError, response
440
+ end
95
441
  end
96
442
 
97
443
  private
98
444
 
445
+ def print_http(http)
446
+ if http.is_a? Net::HTTPRequest
447
+ dir = '>'
448
+ logger.debug "#{dir} Sending a #{http.method} request to `#{http.path}`:"
449
+ else
450
+ dir = '<'
451
+ logger.debug "#{dir} Received a #{http.code} #{http.message} response:"
452
+ end
453
+ http.to_hash.map { |k, v| "#{k}: #{k == 'authorization' ? '[redacted]' : v.join(', ')}" }.each do |h|
454
+ logger.debug "#{dir} #{h}"
455
+ end
456
+ logger.debug dir
457
+ logger.debug "#{dir} #{http.body.length < 200 ? http.body : http.body.slice(0..200) + '... [truncated]'}" if http.body
458
+ end
459
+
460
+ def transaction_id
461
+ ret = @transaction_id ||= 0
462
+ @transaction_id = @transaction_id.succ
463
+ ret
464
+ end
465
+
99
466
  def api_to_path(api)
100
467
  # TODO: <api>_current / <api>_latest
101
468
  "/_matrix/#{api.to_s.split('_').join('/')}"
102
469
  end
103
470
 
104
471
  def http
105
- @http ||= (
106
- opts = { }
107
- opts[:use_ssl] = true if homeserver.scheme == 'https'
108
- opts[:verify_mode] = ::OpenSSL::SSL::VERIFY_NONE unless @validate_certificate
109
- Net::HTTP.start homeserver.host, homeserver.port, opts
110
- )
472
+ @http ||= Net::HTTP.new homeserver.host, homeserver.port
473
+ return @http if @http.active?
474
+
475
+ @http.use_ssl = homeserver.scheme == 'https'
476
+ @http.verify_mode = validate_certificate ? ::OpenSSL::SSL::VERIFY_NONE : nil
477
+ @http.start
478
+ end
479
+
480
+ def user_agent
481
+ "Ruby Matrix SDK v#{MatrixSdk::VERSION}"
111
482
  end
112
483
  end
113
484
  end