activematrix 0.0.0

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,1044 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'matrix_sdk'
4
+ require 'matrix_sdk/util/events'
5
+ require 'matrix_sdk/util/tinycache'
6
+
7
+ module MatrixSdk
8
+ # A class for tracking the information about a room on Matrix
9
+ class Room
10
+ extend MatrixSdk::Extensions
11
+ extend MatrixSdk::Util::Tinycache
12
+ include MatrixSdk::Logging
13
+
14
+ # @!attribute [rw] event_history_limit
15
+ # @return [Fixnum] the limit of events to keep in the event log
16
+ attr_accessor :event_history_limit
17
+ # @!attribute [r] id
18
+ # @return [String] the internal ID of the room
19
+ # @!attribute [r] client
20
+ # @return [Client] the client for the room
21
+ # @!attribute [r] events
22
+ # @return [Array(Object)] the last +event_history_limit+ events to arrive in the room
23
+ # @see https://matrix.org/docs/spec/client_server/r0.3.0.html#get-matrix-client-r0-sync
24
+ # The timeline events are what will end up in here
25
+ attr_reader :id, :client, :events
26
+
27
+ # @!method inspect
28
+ # An inspect method that skips a handful of instance variables to avoid
29
+ # flooding the terminal with debug data.
30
+ # @return [String] a regular inspect string without the data for some variables
31
+ ignore_inspect :client, :events, :prev_batch, :logger, :tinycache_adapter
32
+
33
+ # Requires heavy lookups, so they're cached for an hour
34
+ cached :joined_members, cache_level: :all, expires_in: 60 * 60
35
+
36
+ # Only cache unfiltered requests for aliases and members
37
+ cached :aliases, unless: proc { |args| args.any? }, cache_level: :all, expires_in: 60 * 60
38
+ cached :all_members, unless: proc { |args| args.any? }, cache_level: :all, expires_in: 60 * 60
39
+
40
+ alias room_id id
41
+ alias members joined_members
42
+
43
+ # Create a new room instance
44
+ #
45
+ # @note This method isn't supposed to be used directly, rather rooms should
46
+ # be retrieved from the Client abstraction.
47
+ #
48
+ # @param client [Client] The underlying connection
49
+ # @param room_id [MXID] The room ID
50
+ # @param data [Hash] Additional data to assign to the room
51
+ # @option data [String] :name The current name of the room
52
+ # @option data [String] :topic The current topic of the room
53
+ # @option data [String,MXID] :canonical_alias The canonical alias of the room
54
+ # @option data [Array(String,MXID)] :aliases All non-canonical aliases of the room
55
+ # @option data [:invite,:public,:knock] :join_rule The join rule for the room
56
+ # @option data [:can_join,:forbidden] :guest_access The guest access setting for the room
57
+ # @option data [Boolean] :world_readable If the room is readable by the entire world
58
+ # @option data [Array(User)] :members The list of joined members
59
+ # @option data [Array(Object)] :events The list of current events in the room
60
+ # @option data [Boolean] :members_loaded If the list of members is already loaded
61
+ # @option data [Integer] :event_history_limit (10) The limit of events to store for the room
62
+ # @option data [String,URI] :avatar_url The avatar URL for the room
63
+ # @option data [String] :prev_batch The previous batch token for backfill
64
+ def initialize(client, room_id, data = {})
65
+ if client.is_a? Room
66
+ copy = client
67
+ client = copy.client
68
+ room_id = copy.id
69
+ # data = copy.attributes
70
+ end
71
+
72
+ raise ArgumentError, 'Must be given a Client instance' unless client.is_a? Client
73
+
74
+ @client = client
75
+ room_id = MXID.new room_id unless room_id.is_a?(MXID)
76
+ raise ArgumentError, 'room_id must be a valid Room ID' unless room_id.room_id?
77
+
78
+ @events = []
79
+ @event_history_limit = 10
80
+ @room_type = nil
81
+
82
+ @prev_batch = nil
83
+
84
+ %i[name topic canonical_alias avatar_url].each do |type|
85
+ room_state.tinycache_adapter.write("m.room.#{type}", { type => data.delete(type) }) if data.key? type
86
+ end
87
+ room_state.tinycache_adapter.write('m.room.join_rules', { join_rule: data.delete(:join_rule) }) if data.key? :join_rule
88
+ room_state.tinycache_adapter.write('m.room.history_visibility', { history_visibility: data.delete(:world_readable) ? :world_readable : nil }) if data.key? :world_readable
89
+
90
+ data.each do |k, v|
91
+ next if %i[client].include? k
92
+
93
+ if respond_to?("#{k}_cached?".to_sym) && send("#{k}_cached?".to_sym)
94
+ tinycache_adapter.write(k, v)
95
+ elsif instance_variable_defined? "@#{k}"
96
+ instance_variable_set("@#{k}", v)
97
+ end
98
+ end
99
+
100
+ @id = room_id.to_s
101
+
102
+ logger.debug "Created room #{room_id}"
103
+ end
104
+
105
+ #
106
+ # Casting operators
107
+ #
108
+
109
+ def to_space
110
+ return nil unless space?
111
+
112
+ Rooms::Space.new self, nil
113
+ end
114
+
115
+ def to_s
116
+ prefix = canonical_alias || id
117
+ return "#{prefix} | #{name}" unless name.nil?
118
+
119
+ prefix
120
+ end
121
+
122
+ #
123
+ # Event handlers
124
+ #
125
+
126
+ # @!attribute [r] on_event
127
+ # @return [EventHandlerArray] The list of event handlers for all events
128
+ def on_event
129
+ ensure_room_handlers[:event]
130
+ end
131
+
132
+ # @!attribute [r] on_account_data
133
+ # @return [EventHandlerArray] The list of event handlers for account data changes
134
+ def on_account_data
135
+ ensure_room_handlers[:account_data]
136
+ end
137
+
138
+ # @!attribute [r] on_state_event
139
+ # @return [EventHandlerArray] The list of event handlers for only state events
140
+ def on_state_event
141
+ ensure_room_handlers[:state_event]
142
+ end
143
+
144
+ # @!attribute [r] on_ephemeral_event
145
+ # @return [EventHandlerArray] The list of event handlers for only ephemeral events
146
+ def on_ephemeral_event
147
+ ensure_room_handlers[:ephemeral_event]
148
+ end
149
+
150
+ #
151
+ # State readers
152
+ #
153
+
154
+ # Gets a human-readable name for the room
155
+ #
156
+ # This will return #name or #canonical_alias if they've been set,
157
+ # otherwise it will query the API for members and generate a string from
158
+ # a subset of their names.
159
+ #
160
+ # @return [String] a human-readable name for the room
161
+ # @note This method will populate the #members list if it has to fall back
162
+ # to the member name generation.
163
+ def display_name
164
+ return name if name
165
+ return canonical_alias if canonical_alias
166
+
167
+ members = joined_members
168
+ .reject { |m| m.user_id == client.mxid }
169
+ .map(&:display_name)
170
+
171
+ return members.first if members.one?
172
+ return "#{members.first} and #{members.last}" if members.count == 2
173
+ return "#{members.first} and #{members.count - 1} others" if members.count > 2
174
+
175
+ 'Empty Room'
176
+ end
177
+
178
+ # @return [String, nil] the canonical alias of the room
179
+ def canonical_alias
180
+ get_state('m.room.canonical_alias')[:alias]
181
+ rescue MatrixSdk::MatrixNotFoundError
182
+ nil
183
+ end
184
+
185
+ # Populates and returns the #members array
186
+ #
187
+ # @return [Array(User)] The list of members in the room
188
+ def joined_members
189
+ client.api.get_room_joined_members(id)[:joined].map do |mxid, data|
190
+ User.new(client, mxid.to_s,
191
+ display_name: data.fetch(:display_name, nil),
192
+ avatar_url: data.fetch(:avatar_url, nil))
193
+ end
194
+ end
195
+
196
+ # Get all members (member events) in the room
197
+ #
198
+ # @note This will also count members who've knocked, been invited, have left, or have been banned.
199
+ #
200
+ # @param params [Hash] Additional query parameters to pass to the room member listing - e.g. for filtering purposes.
201
+ #
202
+ # @return [Array(User)] The complete list of members in the room, regardless of membership state
203
+ def all_members(**params)
204
+ client.api.get_room_members(id, **params)[:chunk].map { |ch| client.get_user(ch[:state_key]) }
205
+ end
206
+
207
+ # Gets the current name of the room, querying the API if necessary
208
+ #
209
+ # @note Will cache the current name for 15 minutes
210
+ #
211
+ # @return [String,nil] The room name - if any
212
+ def name
213
+ get_state('m.room.name')[:name]
214
+ rescue MatrixNotFoundError
215
+ # No room name has been specified
216
+ nil
217
+ end
218
+
219
+ # Checks if the room is a direct message / 1:1 room
220
+ #
221
+ # @param members_only [Boolean] Should directness only care about member count?
222
+ # @return [Boolean]
223
+ def dm?(members_only: false)
224
+ return true if !members_only && client.direct_rooms.any? { |_uid, rooms| rooms.include? id.to_s }
225
+
226
+ joined_members.count <= 2
227
+ end
228
+
229
+ # Mark a room as a direct (1:1) message Room
230
+ def dm=(direct)
231
+ rooms = client.direct_rooms
232
+ dirty = false
233
+ list_for_room = (rooms[id.to_s] ||= [])
234
+ if direct && !list_for_room.include?(id.to_s)
235
+ list_for_room << id.to_s
236
+ dirty = true
237
+ elsif !direct && list_for_room.include?(id.to_s)
238
+ list_for_room.delete id.to_s
239
+ rooms.delete id.to_s if list_for_room.empty?
240
+ dirty = true
241
+ end
242
+ client.account_data['m.direct'] = rooms if dirty
243
+ end
244
+
245
+ # Gets the avatar url of the room - if any
246
+ #
247
+ # @return [String,nil] The avatar URL - if any
248
+ def avatar_url
249
+ get_state('m.room.avatar_url')[:url]
250
+ rescue MatrixNotFoundError
251
+ # No avatar has been set
252
+ nil
253
+ end
254
+
255
+ # Gets the room topic - if any
256
+ #
257
+ # @return [String,nil] The topic of the room
258
+ def topic
259
+ get_state('m.room.topic')[:topic]
260
+ rescue MatrixNotFoundError
261
+ # No room name has been specified
262
+ nil
263
+ end
264
+
265
+ # Gets the guest access rights for the room
266
+ #
267
+ # @return [:can_join,:forbidden] The current guest access right
268
+ def guest_access
269
+ get_state('m.room.guest_access')[:guest_access]&.to_sym
270
+ end
271
+
272
+ # Gets the join rule for the room
273
+ #
274
+ # @return [:public,:knock,:invite,:private] The current join rule
275
+ def join_rule
276
+ get_state('m.room.join_rules')[:join_rule]&.to_sym
277
+ end
278
+
279
+ # Checks if +guest_access+ is set to +:can_join+
280
+ def guest_access?
281
+ guest_access == :can_join
282
+ end
283
+
284
+ # Checks if +join_rule+ is set to +:invite+
285
+ def invite_only?
286
+ join_rule == :invite
287
+ end
288
+
289
+ # Checks if +join_rule+ is set to +:knock+
290
+ def knock_only?
291
+ join_rule == :knock
292
+ end
293
+
294
+ def room_state
295
+ return MatrixSdk::Util::StateEventCache.new self if client.cache == :none
296
+
297
+ @room_state ||= MatrixSdk::Util::StateEventCache.new self
298
+ end
299
+
300
+ # Gets a state object in the room
301
+ def get_state(type, state_key: nil)
302
+ room_state[type, state_key]
303
+ end
304
+
305
+ # Sets a state object in the room
306
+ def set_state(type, data, state_key: nil)
307
+ room_state[type, state_key] = data
308
+ end
309
+
310
+ # Gets the history visibility of the room
311
+ #
312
+ # @return [:invited,:joined,:shared,:world_readable] The current history visibility for the room
313
+ def history_visibility
314
+ get_state('m.room.history_visibility')[:history_visibility]&.to_sym
315
+ end
316
+
317
+ # Checks if the room history is world readable
318
+ #
319
+ # @return [Boolean] If the history is world readable
320
+ def world_readable?
321
+ history_visibility == :world_readable
322
+ end
323
+ alias world_readable world_readable?
324
+
325
+ # Gets the room aliases
326
+ #
327
+ # @param canonical_only [Boolean] Should the list of aliases only contain the canonical ones
328
+ # @return [Array[String]] The assigned room aliases
329
+ def aliases(canonical_only: true)
330
+ canonical = get_state('m.room.canonical_alias') rescue {}
331
+ aliases = ([canonical[:alias]].compact + (canonical[:alt_aliases] || [])).uniq.sort
332
+ return aliases if canonical_only
333
+
334
+ (aliases + client.api.get_room_aliases(id).aliases).uniq.sort
335
+ end
336
+
337
+ #
338
+ # Message handling
339
+ #
340
+
341
+ # Sends a plain-text message to the room
342
+ #
343
+ # @param text [String] the message to send
344
+ def send_text(text)
345
+ client.api.send_message(id, text)
346
+ end
347
+
348
+ # Sends a custom HTML message to the room
349
+ #
350
+ # @param html [String] the HTML message to send
351
+ # @param body [String,nil] a plain-text representation of the object
352
+ # (Will default to the HTML with all tags stripped away)
353
+ # @param msgtype [String] ('m.text') The message type for the message
354
+ # @param format [String] ('org.matrix.custom.html') The message format
355
+ # @see https://matrix.org/docs/spec/client_server/r0.3.0.html#m-room-message-msgtypes
356
+ # Possible message types as defined by the spec
357
+ def send_html(html, body = nil, msgtype: nil, format: nil)
358
+ content = {
359
+ body: body || html.gsub(/<\/?[^>]*>/, ''),
360
+ msgtype: msgtype || 'm.text',
361
+ format: format || 'org.matrix.custom.html',
362
+ formatted_body: html
363
+ }
364
+
365
+ client.api.send_message_event(id, 'm.room.message', content)
366
+ end
367
+
368
+ # Sends an emote (/me) message to the room
369
+ #
370
+ # @param text [String] the emote to send
371
+ def send_emote(text)
372
+ client.api.send_emote(id, text)
373
+ end
374
+
375
+ # Sends a link to a generic file to the room
376
+ #
377
+ # @param url [String,URI] the URL to the file
378
+ # @param name [String] the name of the file
379
+ # @param file_info [Hash] extra information about the file
380
+ # @option file_info [String] :mimetype the MIME type of the file
381
+ # @option file_info [Integer] :size the size of the file in bytes
382
+ # @option file_info [String,URI] :thumbnail_url the URL to a thumbnail of the file
383
+ # @option file_info [Hash] :thumbnail_info ThumbnailInfo about the thumbnail file
384
+ # @note The URLs should all be of the 'mxc://' schema
385
+ def send_file(url, name, file_info = {})
386
+ client.api.send_content(id, url, name, 'm.file', extra_information: file_info)
387
+ end
388
+
389
+ # Sends a notice (bot) message to the room
390
+ #
391
+ # @param text [String] the notice to send
392
+ def send_notice(text)
393
+ client.api.send_notice(id, text)
394
+ end
395
+
396
+ # Sends a link to an image to the room
397
+ #
398
+ # @param url [String,URI] the URL to the image
399
+ # @param name [String] the name of the image
400
+ # @param image_info [Hash] extra information about the image
401
+ # @option image_info [Integer] :h the height of the image in pixels
402
+ # @option image_info [Integer] :w the width of the image in pixels
403
+ # @option image_info [String] :mimetype the MIME type of the image
404
+ # @option image_info [Integer] :size the size of the image in bytes
405
+ # @option image_info [String,URI] :thumbnail_url the URL to a thumbnail of the image
406
+ # @option image_info [Hash] :thumbnail_info ThumbnailInfo about the thumbnail image
407
+ # @note The URLs should all be of the 'mxc://' schema
408
+ def send_image(url, name, image_info = {})
409
+ client.api.send_content(id, url, name, 'm.image', extra_information: image_info)
410
+ end
411
+
412
+ # Sends a location object to the room
413
+ #
414
+ # @param geo_uri [String,URI] the geo-URL (e.g. geo:<coords>) of the location
415
+ # @param name [String] the name of the location
416
+ # @param thumbnail_url [String,URI] the URL to a thumbnail image of the location
417
+ # @param thumbnail_info [Hash] a ThumbnailInfo for the location thumbnail
418
+ # @note The thumbnail URL should be of the 'mxc://' schema
419
+ def send_location(geo_uri, name, thumbnail_url = nil, thumbnail_info = {})
420
+ client.api.send_location(id, geo_uri, name, thumbnail_url: thumbnail_url, thumbnail_info: thumbnail_info)
421
+ end
422
+
423
+ # Sends a link to a video to the room
424
+ #
425
+ # @param url [String,URI] the URL to the video
426
+ # @param name [String] the name of the video
427
+ # @param video_info [Hash] extra information about the video
428
+ # @option video_info [Integer] :duration the duration of the video in milliseconds
429
+ # @option video_info [Integer] :h the height of the video in pixels
430
+ # @option video_info [Integer] :w the width of the video in pixels
431
+ # @option video_info [String] :mimetype the MIME type of the video
432
+ # @option video_info [Integer] :size the size of the video in bytes
433
+ # @option video_info [String,URI] :thumbnail_url the URL to a thumbnail of the video
434
+ # @option video_info [Hash] :thumbnail_info ThumbnailInfo about the thumbnail of the video
435
+ # @note The URLs should all be of the 'mxc://' schema
436
+ def send_video(url, name, video_info = {})
437
+ client.api.send_content(id, url, name, 'm.video', extra_information: video_info)
438
+ end
439
+
440
+ # Sends a link to an audio clip to the room
441
+ #
442
+ # @param url [String,URI] the URL to the audio clip
443
+ # @param name [String] the name of the audio clip
444
+ # @param audio_info [Hash] extra information about the audio clip
445
+ # @option audio_info [Integer] :duration the duration of the audio clip in milliseconds
446
+ # @option audio_info [String] :mimetype the MIME type of the audio clip
447
+ # @option audio_info [Integer] :size the size of the audio clip in bytes
448
+ # @note The URLs should all be of the 'mxc://' schema
449
+ def send_audio(url, name, audio_info = {})
450
+ client.api.send_content(id, url, name, 'm.audio', extra_information: audio_info)
451
+ end
452
+
453
+ # Sends a customized message to the Room
454
+ #
455
+ # @param body [String] The clear-text body of the message
456
+ # @param content [Hash] The custom content of the message
457
+ # @param msgtype [String] The type of the message, should be one of the known types (m.text, m.notice, m.emote, etc)
458
+ def send_custom_message(body, content = {}, msgtype: nil)
459
+ content.merge!(
460
+ body: body,
461
+ msgtype: msgtype || 'm.text'
462
+ )
463
+
464
+ client.api.send_message_event(id, 'm.room.message', content)
465
+ end
466
+
467
+ # Sends a custom timeline event to the Room
468
+ #
469
+ # @param type [String,Symbol] The type of the Event.
470
+ # For custom events, this should be written in reverse DNS format (e.g. com.example.event)
471
+ # @param content [Hash] The contents of the message, this will be the
472
+ # :content key of the resulting event object
473
+ # @see Protocols::CS#send_message_event
474
+ def send_event(type, content = {})
475
+ client.api.send_message_event(id, type, content)
476
+ end
477
+
478
+ # Redacts a message from the room
479
+ #
480
+ # @param event_id [String] the ID of the event to redact
481
+ # @param reason [String,nil] the reason for the redaction
482
+ def redact_message(event_id, reason = nil)
483
+ client.api.redact_event(id, event_id, reason: reason)
484
+ true
485
+ end
486
+
487
+ # Reports a message in the room
488
+ #
489
+ # @param event_id [MXID,String] The ID of the event to redact
490
+ # @param reason [String] The reason for the report
491
+ # @param score [Integer] The severity of the report in the range of -100 - 0
492
+ def report_message(event_id, reason:, score: -100)
493
+ client.api.report_event(id, event_id, reason: reason, score: score)
494
+ true
495
+ end
496
+
497
+ # Backfills messages into the room history
498
+ #
499
+ # @param reverse [Boolean] whether to fill messages in reverse or not
500
+ # @param limit [Integer] the maximum number of messages to backfill
501
+ # @note This will trigger the `on_event` events as messages are added
502
+ def backfill_messages(*args, reverse: false, limit: 10)
503
+ # To be backwards-compatible
504
+ if args.length == 2
505
+ reverse = args.first
506
+ limit = args.last
507
+ end
508
+
509
+ data = client.api.get_room_messages(id, @prev_batch, direction: :b, limit: limit)
510
+
511
+ events = data[:chunk]
512
+ events.reverse! unless reverse
513
+ events.each do |ev|
514
+ put_event(ev)
515
+ end
516
+ true
517
+ end
518
+
519
+ #
520
+ # User Management
521
+ #
522
+
523
+ # Invites a user into the room
524
+ #
525
+ # @param user_id [String,User] the MXID of the user
526
+ # @return [Boolean] wether the action succeeded
527
+ def invite_user(user_id)
528
+ user_id = user_id.id if user_id.is_a? MatrixSdk::User
529
+ client.api.invite_user(id, user_id)
530
+ true
531
+ end
532
+
533
+ # Kicks a user from the room
534
+ #
535
+ # @param user_id [String,User] the MXID of the user
536
+ # @param reason [String] the reason for the kick
537
+ # @return [Boolean] wether the action succeeded
538
+ def kick_user(user_id, reason = '')
539
+ user_id = user_id.id if user_id.is_a? MatrixSdk::User
540
+ client.api.kick_user(id, user_id, reason: reason)
541
+ true
542
+ end
543
+
544
+ # Bans a user from the room
545
+ #
546
+ # @param user_id [String,User] the MXID of the user
547
+ # @param reason [String] the reason for the ban
548
+ # @return [Boolean] wether the action succeeded
549
+ def ban_user(user_id, reason = '')
550
+ user_id = user_id.id if user_id.is_a? MatrixSdk::User
551
+ client.api.ban_user(id, user_id, reason: reason)
552
+ true
553
+ end
554
+
555
+ # Unbans a user from the room
556
+ #
557
+ # @param user_id [String,User] the MXID of the user
558
+ # @return [Boolean] wether the action succeeded
559
+ def unban_user(user_id)
560
+ user_id = user_id.id if user_id.is_a? MatrixSdk::User
561
+ client.api.unban_user(id, user_id)
562
+ true
563
+ end
564
+
565
+ # Requests to be removed from the room
566
+ #
567
+ # @return [Boolean] wether the request succeeded
568
+ def leave
569
+ client.api.leave_room(id)
570
+ client.instance_variable_get(:@rooms).delete id
571
+ true
572
+ end
573
+
574
+ def account_data
575
+ return MatrixSdk::Util::AccountDataCache.new client, room: self if client.cache == :none
576
+
577
+ @account_data ||= MatrixSdk::Util::AccountDataCache.new client, room: self
578
+ end
579
+
580
+ # Retrieves a custom entry from the room-specific account data
581
+ #
582
+ # @param type [String] the data type to retrieve
583
+ # @return [Hash] the data that was stored under the given type
584
+ def get_account_data(type)
585
+ account_data[type]
586
+ end
587
+
588
+ # Stores a custom entry into the room-specific account data
589
+ #
590
+ # @param type [String] the data type to store
591
+ # @param account_data [Hash] the data to store
592
+ def set_account_data(type, account_data)
593
+ self.account_data[type] = account_data
594
+ true
595
+ end
596
+
597
+ # Changes the room-specific user profile
598
+ #
599
+ # @param display_name [String] the new display name to use in the room
600
+ # @param avatar_url [String,URI] the new avatar URL to use in the room
601
+ # @note the avatar URL should be a mxc:// URI
602
+ def set_user_profile(display_name: nil, avatar_url: nil, reason: nil)
603
+ return nil unless display_name || avatar_url
604
+
605
+ data = client.api.get_membership(id, client.mxid)
606
+ raise "Can't set profile if you haven't joined the room" unless data[:membership] == 'join'
607
+
608
+ data[:displayname] = display_name unless display_name.nil?
609
+ data[:avatar_url] = avatar_url unless avatar_url.nil?
610
+
611
+ client.api.set_membership(id, client.mxid, 'join', reason || 'Updating room profile information', data)
612
+ true
613
+ end
614
+
615
+ # Gets the room creation information
616
+ #
617
+ # @return [Response] The content of the m.room.create event
618
+ def creation_info
619
+ room_state['m.room.create']
620
+ end
621
+
622
+ # Retrieves the type of the room
623
+ #
624
+ # @return ['m.space',String,nil] The type of the room
625
+ def room_type
626
+ # Can't change, so a permanent cache is ok
627
+ @room_type ||= creation_info[:type]
628
+ end
629
+
630
+ # Retrieves the room version
631
+ #
632
+ # @return [String] The version of the room
633
+ def room_version
634
+ # Can't change, so a permanent cache is ok
635
+ @room_version ||= creation_info[:room_version]
636
+ end
637
+
638
+ # Checks if the room is a Matrix Space
639
+ #
640
+ # @return [Boolean,nil] True if the room is a space
641
+ def space?
642
+ room_type == 'm.space'
643
+ rescue MatrixSdk::MatrixForbiddenError, MatrixSdk::MatrixNotFoundError
644
+ nil
645
+ end
646
+
647
+ # Returns a list of the room tags
648
+ #
649
+ # @return [Response] A list of the tags and their data, with add and remove methods implemented
650
+ # @example Managing tags
651
+ # room.tags
652
+ # # => { :room_tag => { data: false } }
653
+ # room.tags.add('some_tag', data: true)
654
+ # # => { :some_tag => { data: true }, :room_tag => { data: false} }
655
+ # room.tags.remove('room_tag')
656
+ # # => { :some_tag => { data: true} }
657
+ def tags
658
+ client.api.get_user_tags(client.mxid, id)[:tags].tap do |tag_obj|
659
+ tag_obj.instance_variable_set(:@room, self)
660
+ tag_obj.define_singleton_method(:room) do
661
+ @room
662
+ end
663
+ tag_obj.define_singleton_method(:add) do |tag, **data|
664
+ @room.add_tag(tag.to_s.to_sym, **data)
665
+ self[tag.to_s.to_sym] = data
666
+ self
667
+ end
668
+ tag_obj.define_singleton_method(:remove) do |tag|
669
+ @room.remove_tag(tag.to_s.to_sym)
670
+ delete tag.to_s.to_sym
671
+ end
672
+ end
673
+ end
674
+
675
+ # Remove a tag from the room
676
+ #
677
+ # @param [String] tag The tag to remove
678
+ def remove_tag(tag)
679
+ client.api.remove_user_tag(client.mxid, id, tag)
680
+ true
681
+ end
682
+
683
+ # Add a tag to the room
684
+ #
685
+ # @param [String] tag The tag to add
686
+ # @param [Hash] data The data to assign to the tag
687
+ def add_tag(tag, **data)
688
+ client.api.add_user_tag(client.mxid, id, tag, data)
689
+ true
690
+ end
691
+
692
+ #
693
+ # State updates
694
+ #
695
+
696
+ # Refreshes the room state caches for name, topic, and aliases
697
+ def reload!
698
+ reload_name!
699
+ reload_topic!
700
+ reload_aliases!
701
+ true
702
+ end
703
+ alias refresh! reload!
704
+
705
+ # Sets a new name on the room
706
+ #
707
+ # @param name [String] The new name to set
708
+ def name=(name)
709
+ room_state['m.room.name'] = { name: name }
710
+ name
711
+ end
712
+
713
+ # Reloads the name of the room
714
+ #
715
+ # @return [Boolean] if the name was changed or not
716
+ def reload_name!
717
+ room_state.expire('m.room.name')
718
+ end
719
+ alias refresh_name! reload_name!
720
+
721
+ # Sets a new topic on the room
722
+ #
723
+ # @param topic [String] The new topic to set
724
+ def topic=(topic)
725
+ room_state['m.room.topic'] = { topic: topic }
726
+ topic
727
+ end
728
+
729
+ # Reloads the topic of the room
730
+ #
731
+ # @return [Boolean] if the topic was changed or not
732
+ def reload_topic!
733
+ room_state.expire('m.room.topic')
734
+ end
735
+ alias refresh_topic! reload_topic!
736
+
737
+ # Add an alias to the room
738
+ #
739
+ # @return [Boolean] if the addition was successful or not
740
+ def add_alias(room_alias)
741
+ client.api.set_room_alias(id, room_alias)
742
+ tinycache_adapter.read(:aliases) << room_alias if tinycache_adapter.exist?(:aliases)
743
+ true
744
+ end
745
+
746
+ # Reloads the list of aliases by an API query
747
+ #
748
+ # @return [Boolean] if the alias list was updated or not
749
+ # @note The list of aliases is not sorted, ordering changes will result in
750
+ # alias list updates.
751
+ def reload_aliases!
752
+ room_state.expire('m.room.canonical_alias')
753
+ clear_aliases_cache
754
+ end
755
+ alias refresh_aliases! reload_aliases!
756
+
757
+ # Sets if the room should be invite only or not
758
+ #
759
+ # @param invite_only [Boolean] If it should be invite only or not
760
+ def invite_only=(invite_only)
761
+ self.join_rule = invite_only ? :invite : :public
762
+ invite_only
763
+ end
764
+
765
+ # Sets the join rule of the room
766
+ #
767
+ # @param join_rule [:invite,:public] The join rule of the room
768
+ def join_rule=(join_rule)
769
+ room_state['m.room.join_rules'] = { join_rule: join_rule }
770
+ join_rule
771
+ end
772
+
773
+ # Sets if guests are allowed in the room
774
+ #
775
+ # @param allow_guests [Boolean] If guests are allowed to join or not
776
+ def allow_guests=(allow_guests)
777
+ self.guest_access = (allow_guests ? :can_join : :forbidden)
778
+ allow_guests
779
+ end
780
+
781
+ # Sets the guest access status for the room
782
+ #
783
+ # @param guest_access [:can_join,:forbidden] The new guest access status of the room
784
+ def guest_access=(guest_access)
785
+ room_state['m.room.guest_access'] = { guest_access: guest_access }
786
+ guest_access
787
+ end
788
+
789
+ # Sets a new avatar URL for the room
790
+ #
791
+ # @param avatar_url [URI::MXC] The mxc:// URL for the new room avatar
792
+ def avatar_url=(avatar_url)
793
+ avatar_url = URI(avatar_url) unless avatar_url.is_a? URI
794
+ raise ArgumentError, 'Must be a valid MXC URL' unless avatar_url.is_a? URI::MXC
795
+
796
+ room_state['m.room.avatar_url'] = { avatar_url: avatar_url }
797
+ avatar_url
798
+ end
799
+
800
+ # Get the power levels of the room
801
+ #
802
+ # @note The returned power levels are cached for a minute
803
+ # @return [Hash] The current power levels as set for the room
804
+ # @see Protocols::CS#get_power_levels
805
+ def power_levels
806
+ get_state('m.room.power_levels')
807
+ end
808
+
809
+ # Gets the power level of a user in the room
810
+ #
811
+ # @param user [User,MXID,String] The user to check the power level for
812
+ # @param use_default [Boolean] Should the user default level be checked if no user-specific one exists
813
+ # @return [Integer,nil] The current power level for the requested user, nil if there's no user specific level
814
+ # and use_default is false
815
+ def user_powerlevel(user, use_default: true)
816
+ user = user.id if user.is_a? User
817
+ user = MXID.new(user.to_s) unless user.is_a? MXID
818
+ raise ArgumentError, 'Must provide a valid User or MXID' unless user.user?
819
+
820
+ level = power_levels.dig(:users, user.to_s.to_sym)
821
+ level ||= power_levels[:users_default] || 0 if use_default
822
+ level
823
+ end
824
+
825
+ # Checks if a user can send a given event type in the room
826
+ #
827
+ # @param user [User,MXID,String] The user to check
828
+ # @param event [String,Symbol] The event type to check
829
+ # @param state [Boolean] If the given event is a state event or a message event
830
+ # @return [Boolean] If the given user is allowed to send an event of the given type
831
+ def user_can_send?(user, event, state: false)
832
+ user_pl = user_powerlevel(user)
833
+ event_pl = power_levels.dig(:events, event.to_s.to_sym)
834
+ event_pl ||= state ? (power_levels[:state_default] || 50) : (power_levels[:events_default] || 0)
835
+
836
+ user_pl >= event_pl
837
+ end
838
+
839
+ # Check if a user is an admin in the room
840
+ #
841
+ # @param user [User,MXID,String] The user to check for admin privileges
842
+ # @param target_level [Integer] The power level that's to be considered as admin privileges
843
+ # @return [Boolean] If the requested user has a power level highe enough to be an admin
844
+ # @see #user_powerlevel
845
+ def admin?(user, target_level: 100)
846
+ level = user_powerlevel(user, use_default: false)
847
+ return false unless level
848
+
849
+ level >= target_level
850
+ end
851
+
852
+ # Make a user an admin in the room
853
+ #
854
+ # @param user [User,MXID,String] The user to give admin privileges
855
+ # @param level [Integer] The power level to set the user to
856
+ # @see #modify_user_power_levels
857
+ def admin!(user, level: 100)
858
+ return true if admin?(user, target_level: level)
859
+
860
+ user = user.id if user.is_a? User
861
+ user = MXID.new(user.to_s) unless user.is_a? MXID
862
+ raise ArgumentError, 'Must provide a valid user or MXID' unless user.user?
863
+
864
+ modify_user_power_levels({ user.to_s.to_sym => level })
865
+ end
866
+
867
+ # Check if a user is a moderator in the room
868
+ #
869
+ # @param user [User,MXID,String] The user to check for admin privileges
870
+ # @param target_level [Integer] The power level that's to be considered as admin privileges
871
+ # @return [Boolean] If the requested user has a power level highe enough to be an admin
872
+ # @see #user_powerlevel
873
+ def moderator?(user, target_level: 50)
874
+ level = user_powerlevel(user, use_default: false)
875
+ return false unless level
876
+
877
+ level >= target_level
878
+ end
879
+
880
+ # Make a user a moderator in the room
881
+ #
882
+ # @param user [User,MXID,String] The user to give moderator privileges
883
+ # @param level [Integer] The power level to set the user to
884
+ # @see #modify_user_power_levels
885
+ def moderator!(user, level: 50)
886
+ return true if moderator?(user, target_level: level)
887
+
888
+ user = user.id if user.is_a? User
889
+ user = MXID.new(user.to_s) unless user.is_a? MXID
890
+ raise ArgumentError, 'Must provide a valid user or MXID' unless user.user?
891
+
892
+ modify_user_power_levels({ user.to_s.to_sym => level })
893
+ end
894
+
895
+ # Modifies the power levels of the room
896
+ #
897
+ # @param users [Hash] the user-specific power levels to set or remove
898
+ # @param users_default [Hash] the default user power levels to set
899
+ # @return [Boolean] if the change was successful
900
+ def modify_user_power_levels(users = nil, users_default = nil)
901
+ return false if users.nil? && users_default.nil?
902
+
903
+ room_state.tinycache_adapter.expire 'm.room.power_levels'
904
+
905
+ data = power_levels
906
+ data[:users_default] = users_default unless users_default.nil?
907
+
908
+ if users
909
+ data[:users] = {} unless data.key? :users
910
+ users.each do |user, level|
911
+ user = user.id if user.is_a? User
912
+ user = MXID.new(user.to_s) unless user.is_a? MXID
913
+ raise ArgumentError, 'Must provide a valid user or MXID' unless user.user?
914
+
915
+ if level.nil?
916
+ data[:users].delete(user.to_s.to_sym)
917
+ else
918
+ data[:users][user.to_s.to_sym] = level
919
+ end
920
+ end
921
+ end
922
+
923
+ room_state['m.room.power_levels'] = data
924
+ true
925
+ end
926
+
927
+ # Modifies the required power levels for actions in the room
928
+ #
929
+ # @param events [Hash] the event-specific power levels to change
930
+ # @param params [Hash] other power-level params to change
931
+ # @return [Boolean] if the change was successful
932
+ def modify_required_power_levels(events = nil, params = {})
933
+ return false if events.nil? && (params.nil? || params.empty?)
934
+
935
+ room_state.tinycache_adapter.expire 'm.room.power_levels'
936
+
937
+ data = power_levels
938
+ data.merge!(params)
939
+ data.delete_if { |_k, v| v.nil? }
940
+
941
+ if events
942
+ data[:events] = {} unless data.key? :events
943
+ data[:events].merge!(events)
944
+ data[:events].delete_if { |_k, v| v.nil? }
945
+ end
946
+
947
+ room_state['m.room.power_levels'] = data
948
+ true
949
+ end
950
+
951
+ private
952
+
953
+ def ensure_member(member)
954
+ return unless client.cache == :all
955
+
956
+ tinycache_adapter.write(:joined_members, []) unless tinycache_adapter.exist? :joined_members
957
+
958
+ members = tinycache_adapter.read(:joined_members) || []
959
+ members << member unless members.any? { |m| m.id == member.id }
960
+
961
+ tinycache_adapter.write(:joined_members, members)
962
+ end
963
+
964
+ def handle_room_member(event)
965
+ return unless client.cache == :all
966
+
967
+ if event.dig(*%i[content membership]) == 'join'
968
+ ensure_member(client.get_user(event[:state_key]).dup.tap do |u|
969
+ u.instance_variable_set(:@display_name, event.dig(*%i[content displayname]))
970
+ end)
971
+ elsif tinycache_adapter.exist? :joined_members
972
+ members = tinycache_adapter.read(:joined_members)
973
+ members.delete_if { |m| m.id == event[:state_key] }
974
+ end
975
+ end
976
+
977
+ def handle_room_canonical_alias(event)
978
+ room_state.tinycache_adapter.write('m.room.canonical_alias', event[:content], expires_in: room_state.cache_time)
979
+ canonical_alias = event.dig(*%i[content alias])
980
+
981
+ data = tinycache_adapter.read(:aliases) || []
982
+ data << canonical_alias
983
+ data += event.dig(*%i[content alt_aliases]) || []
984
+ tinycache_adapter.write(:aliases, data.uniq.sort)
985
+ end
986
+
987
+ def room_handlers?
988
+ client.instance_variable_get(:@room_handlers).key? id
989
+ end
990
+
991
+ def ensure_room_handlers
992
+ client.instance_variable_get(:@room_handlers)[id] ||= {
993
+ account_data: MatrixSdk::EventHandlerArray.new,
994
+ event: MatrixSdk::EventHandlerArray.new,
995
+ state_event: MatrixSdk::EventHandlerArray.new,
996
+ ephemeral_event: MatrixSdk::EventHandlerArray.new
997
+ }
998
+ end
999
+
1000
+ def put_event(event)
1001
+ ensure_room_handlers[:event].fire(MatrixEvent.new(self, event), event[:type]) if room_handlers?
1002
+
1003
+ @events.push event
1004
+ @events.shift if @events.length > @event_history_limit
1005
+ end
1006
+
1007
+ def put_account_data(event)
1008
+ if client.cache != :none
1009
+ adapter = account_data.tinycache_adapter
1010
+ adapter.write(event[:type], event[:content], expires_in: account_data.cache_time)
1011
+ end
1012
+
1013
+ return unless room_handlers?
1014
+
1015
+ ensure_room_handlers[:account_data].fire(MatrixEvent.new(self, event))
1016
+ end
1017
+
1018
+ def put_ephemeral_event(event)
1019
+ return unless room_handlers?
1020
+
1021
+ ensure_room_handlers[:ephemeral_event].fire(MatrixEvent.new(self, event), event[:type])
1022
+ end
1023
+
1024
+ INTERNAL_HANDLERS = {
1025
+ 'm.room.canonical_alias' => :handle_room_canonical_alias,
1026
+ 'm.room.member' => :handle_room_member
1027
+ }.freeze
1028
+
1029
+ def put_state_event(event)
1030
+ if INTERNAL_HANDLERS.key? event[:type]
1031
+ send(INTERNAL_HANDLERS[event[:type]], event)
1032
+ elsif client.cache != :none
1033
+ adapter = room_state.tinycache_adapter
1034
+ key = event[:type]
1035
+ key += "|#{event[:state_key]}" unless event[:state_key].nil? || event[:state_key].empty?
1036
+ adapter.write(key, event[:content], expires_in: room_state.cache_time)
1037
+ end
1038
+
1039
+ return unless room_handlers?
1040
+
1041
+ ensure_room_handlers[:state_event].fire(MatrixEvent.new(self, event), event[:type])
1042
+ end
1043
+ end
1044
+ end