matrix_sdk 1.5.0 → 2.1.2

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,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