matrix_sdk 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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