matrix_sdk 1.4.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Preliminary support for unmerged MSCs (Matrix Spec Changes)
4
+ module MatrixSdk::Protocols::MSC
5
+ def refresh_mscs
6
+ @msc = {}
7
+ end
8
+
9
+ # Check if there's support for MSC2108 - Sync over Server Sent Events
10
+ def msc2108?
11
+ @msc ||= {}
12
+ @msc[2108] ||= \
13
+ begin
14
+ request(:get, :client_r0, '/sync/sse', skip_auth: true, headers: { accept: 'text/event-stream' })
15
+ rescue MatrixSdk::MatrixNotAuthorizedError # Returns 401 if implemented
16
+ true
17
+ rescue MatrixSdk::MatrixRequestError
18
+ false
19
+ end
20
+ rescue StandardError => e
21
+ logger.debug "Failed to check MSC2108 status;\n#{e.inspect}"
22
+ false
23
+ end
24
+
25
+ # Sync over Server Sent Events - MSC2108
26
+ #
27
+ # @example Syncing over SSE
28
+ # @since = 'some token'
29
+ # api.msc2108_sync_sse(since: @since) do |data, event:, id:|
30
+ # if event == 'sync'
31
+ # handle(data) # data is the same as a normal sync response
32
+ # @since = id
33
+ # end
34
+ # end
35
+ #
36
+ # @see Protocols::CS#sync
37
+ # @see https://github.com/matrix-org/matrix-doc/pull/2108/
38
+ # rubocop:disable Metrics/MethodLength
39
+ def msc2108_sync_sse(since: nil, **params, &on_data)
40
+ raise ArgumentError, 'Must be given a block accepting two args - data and { event:, id: }' \
41
+ unless on_data.is_a?(Proc) && on_data.arity == 2
42
+ raise 'Needs to be logged in' unless access_token # TODO: Better error
43
+
44
+ query = params.select do |k, _v|
45
+ %i[filter full_state set_presence].include? k
46
+ end
47
+ query[:user_id] = params.delete(:user_id) if protocol?(:AS) && params.key?(:user_id)
48
+
49
+ req = Net::HTTP::Get.new(homeserver.dup.tap do |u|
50
+ u.path = api_to_path(:client_r0) + '/sync/sse'
51
+ u.query = URI.encode_www_form(query)
52
+ end)
53
+ req['accept'] = 'text/event-stream'
54
+ req['accept-encoding'] = 'identity' # Disable compression on the SSE stream
55
+ req['authorization'] = "Bearer #{access_token}"
56
+ req['last-event-id'] = since if since
57
+
58
+ cancellation_token = { run: true }
59
+
60
+ # rubocop:disable Metrics/BlockLength
61
+ thread = Thread.new(cancellation_token) do |ctx|
62
+ print_http(req)
63
+ http.request req do |response|
64
+ break unless ctx[:run]
65
+
66
+ print_http(response, body: false)
67
+ raise MatrixRequestError.new_by_code(JSON.parse(response.body, symbolize_names: true), response.code) unless response.is_a? Net::HTTPSuccess
68
+
69
+ # Override buffer size for BufferedIO
70
+ socket = response.instance_variable_get :@socket
71
+ if socket.is_a? Net::BufferedIO
72
+ socket.instance_eval do
73
+ def rbuf_fill
74
+ bufsize_override = 1024
75
+ loop do
76
+ case rv = @io.read_nonblock(bufsize_override, exception: false)
77
+ when String
78
+ @rbuf << rv
79
+ rv.clear
80
+ return
81
+ when :wait_readable
82
+ @io.to_io.wait_readable(@read_timeout) || raise(Net::ReadTimeout)
83
+ when :wait_writable
84
+ @io.to_io.wait_writable(@read_timeout) || raise(Net::ReadTimeout)
85
+ when nil
86
+ raise EOFError, 'end of file reached'
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ stream_id = ('A'..'Z').to_a.sample(4).join
94
+
95
+ logger.debug "MSC2108 : #{stream_id} : Starting SSE stream."
96
+
97
+ buffer = ''
98
+ response.read_body do |chunk|
99
+ buffer += chunk
100
+
101
+ while (index = buffer.index(/\r\n\r\n|\n\n/))
102
+ stream = buffer.slice!(0..index)
103
+
104
+ data = ''
105
+ event = nil
106
+ id = nil
107
+
108
+ stream.split(/\r?\n/).each do |part|
109
+ /^data:(.+)$/.match(part) do |m_data|
110
+ data += "\n" unless data.empty?
111
+ data += m_data[1].strip
112
+ end
113
+ /^event:(.+)$/.match(part) do |m_event|
114
+ event = m_event[1].strip
115
+ end
116
+ /^id:(.+)$/.match(part) do |m_id|
117
+ id = m_id[1].strip
118
+ end
119
+ /^:(.+)$/.match(part) do |m_comment|
120
+ logger.debug "MSC2108 : #{stream_id} : Received comment '#{m_comment[1].strip}'"
121
+ end
122
+ end
123
+
124
+ if %w[sync sync_error].include? event
125
+ data = JSON.parse(data, symbolize_names: true)
126
+ yield((MatrixSdk::Response.new self, data), event: event, id: id)
127
+ elsif event
128
+ logger.info "MSC2108 : #{stream_id} : Received unknown event '#{event}'; #{data}"
129
+ end
130
+ end
131
+
132
+ unless ctx[:run]
133
+ socket.close
134
+ break
135
+ end
136
+ end
137
+ break unless ctx[:run]
138
+ end
139
+ end
140
+ # rubocop:enable Metrics/BlockLength
141
+
142
+ thread.run
143
+
144
+ [thread, cancellation_token]
145
+ end
146
+ # rubocop:enable Metrics/MethodLength
147
+ end
@@ -22,6 +22,17 @@ module MatrixSdk
22
22
  # @return [Api] The API connection that returned the response
23
23
  module Response
24
24
  def self.new(api, data)
25
+ if data.is_a? Array
26
+ raise ArgumentError, 'Input data was not an array of hashes' unless data.all? { |v| v.is_a? Hash }
27
+
28
+ data.each do |value|
29
+ Response.new api, value
30
+ end
31
+ return data
32
+ end
33
+
34
+ raise ArgumentError, 'Input data was not a hash' unless data.is_a? Hash
35
+
25
36
  data.extend(Extensions)
26
37
  data.instance_variable_set(:@api, api)
27
38
 
@@ -58,10 +58,34 @@ module MatrixSdk
58
58
 
59
59
  alias room_id id
60
60
 
61
+ # Create a new room instance
62
+ #
63
+ # @note This method isn't supposed to be used directly, rather rooms should
64
+ # be retrieved from the Client abstraction.
65
+ #
66
+ # @param client [Client] The underlying connection
67
+ # @param room_id [MXID] The room ID
68
+ # @param data [Hash] Additional data to assign to the room
69
+ # @option data [String] :name The current name of the room
70
+ # @option data [String] :topic The current topic of the room
71
+ # @option data [String,MXID] :canonical_alias The canonical alias of the room
72
+ # @option data [Array(String,MXID)] :aliases All non-canonical aliases of the room
73
+ # @option data [:invite,:public] :join_rule The join rule for the room
74
+ # @option data [:can_join,:forbidden] :guest_access The guest access setting for the room
75
+ # @option data [Boolean] :world_readable If the room is readable by the entire world
76
+ # @option data [Array(User)] :members The list of joined members
77
+ # @option data [Array(Object)] :events The list of current events in the room
78
+ # @option data [Boolean] :members_loaded If the list of members is already loaded
79
+ # @option data [Integer] :event_history_limit (10) The limit of events to store for the room
80
+ # @option data [String,URI] :avatar_url The avatar URL for the room
81
+ # @option data [String] :prev_batch The previous batch token for backfill
61
82
  def initialize(client, room_id, data = {})
83
+ raise ArgumentError, 'Must be given a Client instance' unless client.is_a? Client
84
+
85
+ room_id = MXID.new room_id unless room_id.is_a?(MXID)
86
+ raise ArgumentError, 'room_id must be a valid Room ID' unless room_id.room_id?
87
+
62
88
  event_initialize
63
- @client = client
64
- @id = room_id.to_s
65
89
 
66
90
  @name = nil
67
91
  @topic = nil
@@ -82,6 +106,9 @@ module MatrixSdk
82
106
  instance_variable_set("@#{k}", v) if instance_variable_defined? "@#{k}"
83
107
  end
84
108
 
109
+ @client = client
110
+ @id = room_id.to_s
111
+
85
112
  @name_checked = Time.new(0)
86
113
 
87
114
  logger.debug "Created room #{room_id}"
@@ -116,20 +143,27 @@ module MatrixSdk
116
143
  end
117
144
 
118
145
  # Populates and returns the #members array
146
+ #
147
+ # @return [Array(User)] The list of members in the room
119
148
  def joined_members
120
149
  return members if @members_loaded && !members.empty?
121
150
 
122
- client.api.get_room_members(id)[:chunk].each do |chunk|
123
- next unless chunk [:content][:membership] == 'join'
124
-
125
- ensure_member(User.new(client, chunk[:state_key], display_name: chunk[:content].fetch(:displayname, nil)))
151
+ client.api.get_room_joined_members(id)[:joined].each do |mxid, data|
152
+ ensure_member(User.new(client, mxid.to_s,
153
+ display_name: data.fetch(:display_name, nil),
154
+ avatar_url: data.fetch(:avatar_url, nil)))
126
155
  end
127
156
  @members_loaded = true
128
157
  members
129
158
  end
130
159
 
160
+ # Gets the current name of the room, querying the API if necessary
161
+ #
162
+ # @note Will cache the current name for 15 minutes
163
+ #
164
+ # @return [String,nil] The room name - if any
131
165
  def name
132
- return @name if Time.now - @name_checked < 10
166
+ return @name if Time.now - @name_checked < 900
133
167
 
134
168
  @name_checked = Time.now
135
169
  @name ||= client.api.get_room_name(id)
@@ -139,6 +173,8 @@ module MatrixSdk
139
173
  end
140
174
 
141
175
  # Gets the avatar url of the room - if any
176
+ #
177
+ # @return [String,nil] The avatar URL - if any
142
178
  def avatar_url
143
179
  @avatar_url ||= client.api.get_room_avatar(id).url
144
180
  rescue MatrixNotFoundError
@@ -169,23 +205,26 @@ module MatrixSdk
169
205
  #
170
206
 
171
207
  # Sends a plain-text message to the room
208
+ #
172
209
  # @param text [String] the message to send
173
210
  def send_text(text)
174
211
  client.api.send_message(id, text)
175
212
  end
176
213
 
177
214
  # Sends a custom HTML message to the room
215
+ #
178
216
  # @param html [String] the HTML message to send
179
217
  # @param body [String,nil] a plain-text representation of the object
180
- # (Will default to the HTML with tags stripped away)
181
- # @param msg_type [String] A message type for the message
218
+ # (Will default to the HTML with all tags stripped away)
219
+ # @param msgtype [String] ('m.text') The message type for the message
220
+ # @param format [String] ('org.matrix.custom.html') The message format
182
221
  # @see https://matrix.org/docs/spec/client_server/r0.3.0.html#m-room-message-msgtypes
183
222
  # Possible message types as defined by the spec
184
- def send_html(html, body = nil, msg_type = 'm.text')
223
+ def send_html(html, body = nil, msgtype: nil, format: nil)
185
224
  content = {
186
225
  body: body || html.gsub(/<\/?[^>]*>/, ''),
187
- msgtype: msg_type,
188
- format: 'org.matrix.custom.html',
226
+ msgtype: msgtype || 'm.text',
227
+ format: format || 'org.matrix.custom.html',
189
228
  formatted_body: html
190
229
  }
191
230
 
@@ -193,12 +232,14 @@ module MatrixSdk
193
232
  end
194
233
 
195
234
  # Sends an emote (/me) message to the room
235
+ #
196
236
  # @param text [String] the emote to send
197
237
  def send_emote(text)
198
238
  client.api.send_emote(id, text)
199
239
  end
200
240
 
201
241
  # Sends a link to a generic file to the room
242
+ #
202
243
  # @param url [String,URI] the URL to the file
203
244
  # @param name [String] the name of the file
204
245
  # @param file_info [Hash] extra information about the file
@@ -212,12 +253,14 @@ module MatrixSdk
212
253
  end
213
254
 
214
255
  # Sends a notice (bot) message to the room
256
+ #
215
257
  # @param text [String] the notice to send
216
258
  def send_notice(text)
217
259
  client.api.send_notice(id, text)
218
260
  end
219
261
 
220
262
  # Sends a link to an image to the room
263
+ #
221
264
  # @param url [String,URI] the URL to the image
222
265
  # @param name [String] the name of the image
223
266
  # @param image_info [Hash] extra information about the image
@@ -233,6 +276,7 @@ module MatrixSdk
233
276
  end
234
277
 
235
278
  # Sends a location object to the room
279
+ #
236
280
  # @param geo_uri [String,URI] the geo-URL (e.g. geo:<coords>) of the location
237
281
  # @param name [String] the name of the location
238
282
  # @param thumbnail_url [String,URI] the URL to a thumbnail image of the location
@@ -243,6 +287,7 @@ module MatrixSdk
243
287
  end
244
288
 
245
289
  # Sends a link to a video to the room
290
+ #
246
291
  # @param url [String,URI] the URL to the video
247
292
  # @param name [String] the name of the video
248
293
  # @param video_info [Hash] extra information about the video
@@ -259,6 +304,7 @@ module MatrixSdk
259
304
  end
260
305
 
261
306
  # Sends a link to an audio clip to the room
307
+ #
262
308
  # @param url [String,URI] the URL to the audio clip
263
309
  # @param name [String] the name of the audio clip
264
310
  # @param audio_info [Hash] extra information about the audio clip
@@ -271,6 +317,7 @@ module MatrixSdk
271
317
  end
272
318
 
273
319
  # Redacts a message from the room
320
+ #
274
321
  # @param event_id [String] the ID of the event to redact
275
322
  # @param reason [String,nil] the reason for the redaction
276
323
  def redact_message(event_id, reason = nil)
@@ -278,7 +325,18 @@ module MatrixSdk
278
325
  true
279
326
  end
280
327
 
328
+ # Reports a message in the room
329
+ #
330
+ # @param event_id [MXID,String] The ID of the event to redact
331
+ # @param reason [String] The reason for the report
332
+ # @param score [Integer] The severity of the report in the range of -100 - 0
333
+ def report_message(event_id, reason:, score: -100)
334
+ client.api.report_event(id, event_id, reason: reason, score: score)
335
+ true
336
+ end
337
+
281
338
  # Backfills messages into the room history
339
+ #
282
340
  # @param reverse [Boolean] whether to fill messages in reverse or not
283
341
  # @param limit [Integer] the maximum number of messages to backfill
284
342
  # @note This will trigger the `on_event` events as messages are added
@@ -298,6 +356,7 @@ module MatrixSdk
298
356
  #
299
357
 
300
358
  # Invites a user into the room
359
+ #
301
360
  # @param user_id [String,User] the MXID of the user
302
361
  # @return [Boolean] wether the action succeeded
303
362
  def invite_user(user_id)
@@ -307,6 +366,7 @@ module MatrixSdk
307
366
  end
308
367
 
309
368
  # Kicks a user from the room
369
+ #
310
370
  # @param user_id [String,User] the MXID of the user
311
371
  # @param reason [String] the reason for the kick
312
372
  # @return [Boolean] wether the action succeeded
@@ -317,6 +377,7 @@ module MatrixSdk
317
377
  end
318
378
 
319
379
  # Bans a user from the room
380
+ #
320
381
  # @param user_id [String,User] the MXID of the user
321
382
  # @param reason [String] the reason for the ban
322
383
  # @return [Boolean] wether the action succeeded
@@ -327,6 +388,7 @@ module MatrixSdk
327
388
  end
328
389
 
329
390
  # Unbans a user from the room
391
+ #
330
392
  # @param user_id [String,User] the MXID of the user
331
393
  # @return [Boolean] wether the action succeeded
332
394
  def unban_user(user_id)
@@ -336,6 +398,7 @@ module MatrixSdk
336
398
  end
337
399
 
338
400
  # Requests to be removed from the room
401
+ #
339
402
  # @return [Boolean] wether the request succeeded
340
403
  def leave
341
404
  client.api.leave_room(id)
@@ -344,6 +407,7 @@ module MatrixSdk
344
407
  end
345
408
 
346
409
  # Retrieves a custom entry from the room-specific account data
410
+ #
347
411
  # @param type [String] the data type to retrieve
348
412
  # @return [Hash] the data that was stored under the given type
349
413
  def get_account_data(type)
@@ -351,6 +415,7 @@ module MatrixSdk
351
415
  end
352
416
 
353
417
  # Stores a custom entry into the room-specific account data
418
+ #
354
419
  # @param type [String] the data type to store
355
420
  # @param account_data [Hash] the data to store
356
421
  def set_account_data(type, account_data)
@@ -359,6 +424,7 @@ module MatrixSdk
359
424
  end
360
425
 
361
426
  # Changes the room-specific user profile
427
+ #
362
428
  # @param display_name [String] the new display name to use in the room
363
429
  # @param avatar_url [String,URI] the new avatar URL to use in the room
364
430
  # @note the avatar URL should be a mxc:// URI
@@ -376,6 +442,7 @@ module MatrixSdk
376
442
  end
377
443
 
378
444
  # Returns a list of the room tags
445
+ #
379
446
  # @return [Response] A list of the tags and their data, with add and remove methods implemented
380
447
  # @example Managing tags
381
448
  # room.tags
@@ -403,6 +470,7 @@ module MatrixSdk
403
470
  end
404
471
 
405
472
  # Remove a tag from the room
473
+ #
406
474
  # @param [String] tag The tag to remove
407
475
  def remove_tag(tag)
408
476
  client.api.remove_user_tag(client.mxid, id, tag)
@@ -410,6 +478,7 @@ module MatrixSdk
410
478
  end
411
479
 
412
480
  # Add a tag to the room
481
+ #
413
482
  # @param [String] tag The tag to add
414
483
  # @param [Hash] data The data to assign to the tag
415
484
  def add_tag(tag, **data)
@@ -421,6 +490,7 @@ module MatrixSdk
421
490
  # State updates
422
491
  #
423
492
 
493
+ # Refreshes the room state caches for name, topic, and aliases
424
494
  def reload!
425
495
  reload_name!
426
496
  reload_topic!
@@ -429,12 +499,16 @@ module MatrixSdk
429
499
  end
430
500
  alias refresh! reload!
431
501
 
502
+ # Sets a new name on the room
503
+ #
504
+ # @param name [String] The new name to set
432
505
  def name=(name)
433
506
  client.api.set_room_name(id, name)
434
507
  @name = name
435
508
  end
436
509
 
437
510
  # Reloads the name of the room
511
+ #
438
512
  # @return [Boolean] if the name was changed or not
439
513
  def reload_name!
440
514
  data = begin
@@ -448,12 +522,16 @@ module MatrixSdk
448
522
  end
449
523
  alias refresh_name! reload_name!
450
524
 
525
+ # Sets a new topic on the room
526
+ #
527
+ # @param topic [String] The new topic to set
451
528
  def topic=(topic)
452
529
  client.api.set_room_topic(id, topic)
453
530
  @topic = topic
454
531
  end
455
532
 
456
533
  # Reloads the topic of the room
534
+ #
457
535
  # @return [Boolean] if the topic was changed or not
458
536
  def reload_topic!
459
537
  data = begin
@@ -468,6 +546,7 @@ module MatrixSdk
468
546
  alias refresh_topic! reload_topic!
469
547
 
470
548
  # Add an alias to the room
549
+ #
471
550
  # @return [Boolean] if the addition was successful or not
472
551
  def add_alias(room_alias)
473
552
  client.api.set_room_alias(id, room_alias)
@@ -476,6 +555,7 @@ module MatrixSdk
476
555
  end
477
556
 
478
557
  # Reloads the list of aliases by an API query
558
+ #
479
559
  # @return [Boolean] if the alias list was updated or not
480
560
  # @note The list of aliases is not sorted, ordering changes will result in
481
561
  # alias list updates.
@@ -483,7 +563,7 @@ module MatrixSdk
483
563
  begin
484
564
  new_aliases = client.api.get_room_aliases(id).aliases
485
565
  rescue MatrixNotFoundError
486
- data = client.api.get_room_state(id)
566
+ data = client.api.get_room_state_all(id)
487
567
  new_aliases = data.select { |chunk| chunk[:type] == 'm.room.aliases' && chunk.key?(:content) && chunk[:content].key?(:aliases) }
488
568
  .map { |chunk| chunk[:content][:aliases] }
489
569
  .flatten
@@ -498,26 +578,41 @@ module MatrixSdk
498
578
  end
499
579
  alias refresh_aliases! reload_aliases!
500
580
 
581
+ # Sets if the room should be invite only or not
582
+ #
583
+ # @param invite_only [Boolean] If it should be invite only or not
501
584
  def invite_only=(invite_only)
502
585
  self.join_rule = invite_only ? :invite : :public
503
586
  @join_rule == :invite
504
587
  end
505
588
 
589
+ # Sets the join rule of the room
590
+ #
591
+ # @param join_rule [:invite,:public] The join rule of the room
506
592
  def join_rule=(join_rule)
507
- client.api.set_join_rule(id, join_rule)
593
+ client.api.set_room_join_rules(id, join_rule)
508
594
  @join_rule = join_rule
509
595
  end
510
596
 
597
+ # Sets if guests are allowed in the room
598
+ #
599
+ # @param allow_guests [Boolean] If guests are allowed to join or not
511
600
  def allow_guests=(allow_guests)
512
601
  self.guest_access = (allow_guests ? :can_join : :forbidden)
513
602
  @guest_access == :can_join
514
603
  end
515
604
 
605
+ # Sets the guest access status for the room
606
+ #
607
+ # @param guest_access [:can_join,:forbidden] The new guest access status of the room
516
608
  def guest_access=(guest_access)
517
- client.api.set_guest_access(id, guest_access)
609
+ client.api.set_room_guest_access(id, guest_access)
518
610
  @guest_access = guest_access
519
611
  end
520
612
 
613
+ # Sets a new avatar URL for the room
614
+ #
615
+ # @param avatar_url [URI::MATRIX] The mxc:// URL for the new room avatar
521
616
  def avatar_url=(avatar_url)
522
617
  avatar_url = URI(avatar_url) unless avatar_url.is_a? URI
523
618
  raise ArgumentError, 'Must be a valid MXC URL' unless avatar_url.is_a? URI::MATRIX
@@ -527,6 +622,7 @@ module MatrixSdk
527
622
  end
528
623
 
529
624
  # Modifies the power levels of the room
625
+ #
530
626
  # @param users [Hash] the user-specific power levels to set or remove
531
627
  # @param users_default [Hash] the default user power levels to set
532
628
  # @return [Boolean] if the change was successful
@@ -547,6 +643,7 @@ module MatrixSdk
547
643
  end
548
644
 
549
645
  # Modifies the required power levels for actions in the room
646
+ #
550
647
  # @param events [Hash] the event-specific power levels to change
551
648
  # @param params [Hash] other power-level params to change
552
649
  # @return [Boolean] if the change was successful