matrix_sdk 1.5.0 → 2.1.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.
@@ -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,38 @@ 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
+ # Get all members (member events) in the room
161
+ #
162
+ # @note This will also count members who've knocked, been invited, have left, or have been banned.
163
+ #
164
+ # @param params [Hash] Additional query parameters to pass to the room member listing - e.g. for filtering purposes.
165
+ #
166
+ # @return [Array(User)] The complete list of members in the room, regardless of membership state
167
+ def all_members(**params)
168
+ client.api.get_room_members(id, **params)[:chunk].map { |ch| client.get_user(ch[:state_key]) }
169
+ end
170
+
171
+ # Gets the current name of the room, querying the API if necessary
172
+ #
173
+ # @note Will cache the current name for 15 minutes
174
+ #
175
+ # @return [String,nil] The room name - if any
131
176
  def name
132
- return @name if Time.now - @name_checked < 10
177
+ return @name if Time.now - @name_checked < 900
133
178
 
134
179
  @name_checked = Time.now
135
180
  @name ||= client.api.get_room_name(id)
@@ -139,6 +184,8 @@ module MatrixSdk
139
184
  end
140
185
 
141
186
  # Gets the avatar url of the room - if any
187
+ #
188
+ # @return [String,nil] The avatar URL - if any
142
189
  def avatar_url
143
190
  @avatar_url ||= client.api.get_room_avatar(id).url
144
191
  rescue MatrixNotFoundError
@@ -169,23 +216,26 @@ module MatrixSdk
169
216
  #
170
217
 
171
218
  # Sends a plain-text message to the room
219
+ #
172
220
  # @param text [String] the message to send
173
221
  def send_text(text)
174
222
  client.api.send_message(id, text)
175
223
  end
176
224
 
177
225
  # Sends a custom HTML message to the room
226
+ #
178
227
  # @param html [String] the HTML message to send
179
228
  # @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
229
+ # (Will default to the HTML with all tags stripped away)
230
+ # @param msgtype [String] ('m.text') The message type for the message
231
+ # @param format [String] ('org.matrix.custom.html') The message format
182
232
  # @see https://matrix.org/docs/spec/client_server/r0.3.0.html#m-room-message-msgtypes
183
233
  # Possible message types as defined by the spec
184
- def send_html(html, body = nil, msg_type = 'm.text')
234
+ def send_html(html, body = nil, msgtype: nil, format: nil)
185
235
  content = {
186
236
  body: body || html.gsub(/<\/?[^>]*>/, ''),
187
- msgtype: msg_type,
188
- format: 'org.matrix.custom.html',
237
+ msgtype: msgtype || 'm.text',
238
+ format: format || 'org.matrix.custom.html',
189
239
  formatted_body: html
190
240
  }
191
241
 
@@ -193,12 +243,14 @@ module MatrixSdk
193
243
  end
194
244
 
195
245
  # Sends an emote (/me) message to the room
246
+ #
196
247
  # @param text [String] the emote to send
197
248
  def send_emote(text)
198
249
  client.api.send_emote(id, text)
199
250
  end
200
251
 
201
252
  # Sends a link to a generic file to the room
253
+ #
202
254
  # @param url [String,URI] the URL to the file
203
255
  # @param name [String] the name of the file
204
256
  # @param file_info [Hash] extra information about the file
@@ -212,12 +264,14 @@ module MatrixSdk
212
264
  end
213
265
 
214
266
  # Sends a notice (bot) message to the room
267
+ #
215
268
  # @param text [String] the notice to send
216
269
  def send_notice(text)
217
270
  client.api.send_notice(id, text)
218
271
  end
219
272
 
220
273
  # Sends a link to an image to the room
274
+ #
221
275
  # @param url [String,URI] the URL to the image
222
276
  # @param name [String] the name of the image
223
277
  # @param image_info [Hash] extra information about the image
@@ -233,6 +287,7 @@ module MatrixSdk
233
287
  end
234
288
 
235
289
  # Sends a location object to the room
290
+ #
236
291
  # @param geo_uri [String,URI] the geo-URL (e.g. geo:<coords>) of the location
237
292
  # @param name [String] the name of the location
238
293
  # @param thumbnail_url [String,URI] the URL to a thumbnail image of the location
@@ -243,6 +298,7 @@ module MatrixSdk
243
298
  end
244
299
 
245
300
  # Sends a link to a video to the room
301
+ #
246
302
  # @param url [String,URI] the URL to the video
247
303
  # @param name [String] the name of the video
248
304
  # @param video_info [Hash] extra information about the video
@@ -259,6 +315,7 @@ module MatrixSdk
259
315
  end
260
316
 
261
317
  # Sends a link to an audio clip to the room
318
+ #
262
319
  # @param url [String,URI] the URL to the audio clip
263
320
  # @param name [String] the name of the audio clip
264
321
  # @param audio_info [Hash] extra information about the audio clip
@@ -271,6 +328,7 @@ module MatrixSdk
271
328
  end
272
329
 
273
330
  # Redacts a message from the room
331
+ #
274
332
  # @param event_id [String] the ID of the event to redact
275
333
  # @param reason [String,nil] the reason for the redaction
276
334
  def redact_message(event_id, reason = nil)
@@ -278,7 +336,18 @@ module MatrixSdk
278
336
  true
279
337
  end
280
338
 
339
+ # Reports a message in the room
340
+ #
341
+ # @param event_id [MXID,String] The ID of the event to redact
342
+ # @param reason [String] The reason for the report
343
+ # @param score [Integer] The severity of the report in the range of -100 - 0
344
+ def report_message(event_id, reason:, score: -100)
345
+ client.api.report_event(id, event_id, reason: reason, score: score)
346
+ true
347
+ end
348
+
281
349
  # Backfills messages into the room history
350
+ #
282
351
  # @param reverse [Boolean] whether to fill messages in reverse or not
283
352
  # @param limit [Integer] the maximum number of messages to backfill
284
353
  # @note This will trigger the `on_event` events as messages are added
@@ -298,6 +367,7 @@ module MatrixSdk
298
367
  #
299
368
 
300
369
  # Invites a user into the room
370
+ #
301
371
  # @param user_id [String,User] the MXID of the user
302
372
  # @return [Boolean] wether the action succeeded
303
373
  def invite_user(user_id)
@@ -307,6 +377,7 @@ module MatrixSdk
307
377
  end
308
378
 
309
379
  # Kicks a user from the room
380
+ #
310
381
  # @param user_id [String,User] the MXID of the user
311
382
  # @param reason [String] the reason for the kick
312
383
  # @return [Boolean] wether the action succeeded
@@ -317,6 +388,7 @@ module MatrixSdk
317
388
  end
318
389
 
319
390
  # Bans a user from the room
391
+ #
320
392
  # @param user_id [String,User] the MXID of the user
321
393
  # @param reason [String] the reason for the ban
322
394
  # @return [Boolean] wether the action succeeded
@@ -327,6 +399,7 @@ module MatrixSdk
327
399
  end
328
400
 
329
401
  # Unbans a user from the room
402
+ #
330
403
  # @param user_id [String,User] the MXID of the user
331
404
  # @return [Boolean] wether the action succeeded
332
405
  def unban_user(user_id)
@@ -336,6 +409,7 @@ module MatrixSdk
336
409
  end
337
410
 
338
411
  # Requests to be removed from the room
412
+ #
339
413
  # @return [Boolean] wether the request succeeded
340
414
  def leave
341
415
  client.api.leave_room(id)
@@ -344,6 +418,7 @@ module MatrixSdk
344
418
  end
345
419
 
346
420
  # Retrieves a custom entry from the room-specific account data
421
+ #
347
422
  # @param type [String] the data type to retrieve
348
423
  # @return [Hash] the data that was stored under the given type
349
424
  def get_account_data(type)
@@ -351,6 +426,7 @@ module MatrixSdk
351
426
  end
352
427
 
353
428
  # Stores a custom entry into the room-specific account data
429
+ #
354
430
  # @param type [String] the data type to store
355
431
  # @param account_data [Hash] the data to store
356
432
  def set_account_data(type, account_data)
@@ -359,6 +435,7 @@ module MatrixSdk
359
435
  end
360
436
 
361
437
  # Changes the room-specific user profile
438
+ #
362
439
  # @param display_name [String] the new display name to use in the room
363
440
  # @param avatar_url [String,URI] the new avatar URL to use in the room
364
441
  # @note the avatar URL should be a mxc:// URI
@@ -376,6 +453,7 @@ module MatrixSdk
376
453
  end
377
454
 
378
455
  # Returns a list of the room tags
456
+ #
379
457
  # @return [Response] A list of the tags and their data, with add and remove methods implemented
380
458
  # @example Managing tags
381
459
  # room.tags
@@ -403,6 +481,7 @@ module MatrixSdk
403
481
  end
404
482
 
405
483
  # Remove a tag from the room
484
+ #
406
485
  # @param [String] tag The tag to remove
407
486
  def remove_tag(tag)
408
487
  client.api.remove_user_tag(client.mxid, id, tag)
@@ -410,6 +489,7 @@ module MatrixSdk
410
489
  end
411
490
 
412
491
  # Add a tag to the room
492
+ #
413
493
  # @param [String] tag The tag to add
414
494
  # @param [Hash] data The data to assign to the tag
415
495
  def add_tag(tag, **data)
@@ -421,6 +501,7 @@ module MatrixSdk
421
501
  # State updates
422
502
  #
423
503
 
504
+ # Refreshes the room state caches for name, topic, and aliases
424
505
  def reload!
425
506
  reload_name!
426
507
  reload_topic!
@@ -429,12 +510,16 @@ module MatrixSdk
429
510
  end
430
511
  alias refresh! reload!
431
512
 
513
+ # Sets a new name on the room
514
+ #
515
+ # @param name [String] The new name to set
432
516
  def name=(name)
433
517
  client.api.set_room_name(id, name)
434
518
  @name = name
435
519
  end
436
520
 
437
521
  # Reloads the name of the room
522
+ #
438
523
  # @return [Boolean] if the name was changed or not
439
524
  def reload_name!
440
525
  data = begin
@@ -448,12 +533,16 @@ module MatrixSdk
448
533
  end
449
534
  alias refresh_name! reload_name!
450
535
 
536
+ # Sets a new topic on the room
537
+ #
538
+ # @param topic [String] The new topic to set
451
539
  def topic=(topic)
452
540
  client.api.set_room_topic(id, topic)
453
541
  @topic = topic
454
542
  end
455
543
 
456
544
  # Reloads the topic of the room
545
+ #
457
546
  # @return [Boolean] if the topic was changed or not
458
547
  def reload_topic!
459
548
  data = begin
@@ -468,6 +557,7 @@ module MatrixSdk
468
557
  alias refresh_topic! reload_topic!
469
558
 
470
559
  # Add an alias to the room
560
+ #
471
561
  # @return [Boolean] if the addition was successful or not
472
562
  def add_alias(room_alias)
473
563
  client.api.set_room_alias(id, room_alias)
@@ -476,6 +566,7 @@ module MatrixSdk
476
566
  end
477
567
 
478
568
  # Reloads the list of aliases by an API query
569
+ #
479
570
  # @return [Boolean] if the alias list was updated or not
480
571
  # @note The list of aliases is not sorted, ordering changes will result in
481
572
  # alias list updates.
@@ -483,7 +574,7 @@ module MatrixSdk
483
574
  begin
484
575
  new_aliases = client.api.get_room_aliases(id).aliases
485
576
  rescue MatrixNotFoundError
486
- data = client.api.get_room_state(id)
577
+ data = client.api.get_room_state_all(id)
487
578
  new_aliases = data.select { |chunk| chunk[:type] == 'm.room.aliases' && chunk.key?(:content) && chunk[:content].key?(:aliases) }
488
579
  .map { |chunk| chunk[:content][:aliases] }
489
580
  .flatten
@@ -498,26 +589,41 @@ module MatrixSdk
498
589
  end
499
590
  alias refresh_aliases! reload_aliases!
500
591
 
592
+ # Sets if the room should be invite only or not
593
+ #
594
+ # @param invite_only [Boolean] If it should be invite only or not
501
595
  def invite_only=(invite_only)
502
596
  self.join_rule = invite_only ? :invite : :public
503
597
  @join_rule == :invite
504
598
  end
505
599
 
600
+ # Sets the join rule of the room
601
+ #
602
+ # @param join_rule [:invite,:public] The join rule of the room
506
603
  def join_rule=(join_rule)
507
- client.api.set_join_rule(id, join_rule)
604
+ client.api.set_room_join_rules(id, join_rule)
508
605
  @join_rule = join_rule
509
606
  end
510
607
 
608
+ # Sets if guests are allowed in the room
609
+ #
610
+ # @param allow_guests [Boolean] If guests are allowed to join or not
511
611
  def allow_guests=(allow_guests)
512
612
  self.guest_access = (allow_guests ? :can_join : :forbidden)
513
613
  @guest_access == :can_join
514
614
  end
515
615
 
616
+ # Sets the guest access status for the room
617
+ #
618
+ # @param guest_access [:can_join,:forbidden] The new guest access status of the room
516
619
  def guest_access=(guest_access)
517
- client.api.set_guest_access(id, guest_access)
620
+ client.api.set_room_guest_access(id, guest_access)
518
621
  @guest_access = guest_access
519
622
  end
520
623
 
624
+ # Sets a new avatar URL for the room
625
+ #
626
+ # @param avatar_url [URI::MATRIX] The mxc:// URL for the new room avatar
521
627
  def avatar_url=(avatar_url)
522
628
  avatar_url = URI(avatar_url) unless avatar_url.is_a? URI
523
629
  raise ArgumentError, 'Must be a valid MXC URL' unless avatar_url.is_a? URI::MATRIX
@@ -527,6 +633,7 @@ module MatrixSdk
527
633
  end
528
634
 
529
635
  # Modifies the power levels of the room
636
+ #
530
637
  # @param users [Hash] the user-specific power levels to set or remove
531
638
  # @param users_default [Hash] the default user power levels to set
532
639
  # @return [Boolean] if the change was successful
@@ -547,6 +654,7 @@ module MatrixSdk
547
654
  end
548
655
 
549
656
  # Modifies the required power levels for actions in the room
657
+ #
550
658
  # @param events [Hash] the event-specific power levels to change
551
659
  # @param params [Hash] other power-level params to change
552
660
  # @return [Boolean] if the change was successful
@@ -583,5 +691,9 @@ module MatrixSdk
583
691
  def put_ephemeral_event(event)
584
692
  fire_ephemeral_event MatrixEvent.new(self, event)
585
693
  end
694
+
695
+ def put_state_event(event)
696
+ fire_state_event MatrixEvent.new(self, event)
697
+ end
586
698
  end
587
699
  end