matrix_sdk 2.2.0 → 2.3.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.
@@ -1,54 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'matrix_sdk'
4
+ require 'matrix_sdk/util/events'
5
+ require 'matrix_sdk/util/tinycache'
4
6
 
5
7
  module MatrixSdk
6
8
  # A class for tracking the information about a room on Matrix
7
9
  class Room
8
10
  extend MatrixSdk::Extensions
11
+ extend MatrixSdk::Util::Tinycache
9
12
  include MatrixSdk::Logging
10
13
 
11
- # @!attribute [rw] canonical_alias
12
- # @return [String, nil] the canonical alias of the room
13
14
  # @!attribute [rw] event_history_limit
14
15
  # @return [Fixnum] the limit of events to keep in the event log
15
- attr_accessor :canonical_alias, :event_history_limit
16
+ attr_accessor :event_history_limit
16
17
  # @!attribute [r] id
17
18
  # @return [String] the internal ID of the room
18
19
  # @!attribute [r] client
19
20
  # @return [Client] the client for the room
20
- # @!attribute [rw] name
21
- # @return [String, nil] the user-provided name of the room
22
- # @see reload_name!
23
- # @!attribute [rw] topic
24
- # @return [String, nil] the user-provided topic of the room
25
- # @see reload_topic!
26
- # @!attribute [r] aliases
27
- # @return [Array(String)] a list of user-set aliases for the room
28
- # @see add_alias
29
- # @see reload_alias!
30
- # @!attribute [rw] join_rule
31
- # @return [:invite, :public] the join rule for the room -
32
- # either +:invite+ or +:public+
33
- # @!attribute [rw] guest_access
34
- # @return [:can_join, :forbidden] the guest access for the room -
35
- # either +:can_join+ or +:forbidden+
36
- # @!attribute [r] members
37
- # @return [Array(User)] the members of the room
38
- # @see reload_members!
39
21
  # @!attribute [r] events
40
22
  # @return [Array(Object)] the last +event_history_limit+ events to arrive in the room
41
23
  # @see https://matrix.org/docs/spec/client_server/r0.3.0.html#get-matrix-client-r0-sync
42
24
  # The timeline events are what will end up in here
43
- attr_reader :id, :client, :topic, :aliases, :members, :events
25
+ attr_reader :id, :client, :events
44
26
 
45
27
  # @!method inspect
46
28
  # An inspect method that skips a handful of instance variables to avoid
47
29
  # flooding the terminal with debug data.
48
30
  # @return [String] a regular inspect string without the data for some variables
49
- ignore_inspect :client, :members, :events, :prev_batch, :logger
31
+ ignore_inspect :client, :events, :prev_batch, :logger
32
+
33
+ # Requires heavy lookups, so they're cached for an hour
34
+ cached :joined_members, :aliases, cache_level: :all, expires_in: 60 * 60
35
+ # Only cache unfiltered requests for all members
36
+ cached :all_members, unless: proc { |args| args.any? }, cache_level: :all, expires_in: 3600
37
+
38
+ # Much simpler to look up, and lighter data-wise, so the cache is wider
39
+ cached :canonical_alias, :name, :avatar_url, :topic, :guest_access, :join_rule, :power_levels, cache_level: :some, expires_in: 15 * 60
50
40
 
51
41
  alias room_id id
42
+ alias members joined_members
52
43
 
53
44
  # Create a new room instance
54
45
  #
@@ -74,33 +65,27 @@ module MatrixSdk
74
65
  def initialize(client, room_id, data = {})
75
66
  raise ArgumentError, 'Must be given a Client instance' unless client.is_a? Client
76
67
 
68
+ @client = client
77
69
  room_id = MXID.new room_id unless room_id.is_a?(MXID)
78
70
  raise ArgumentError, 'room_id must be a valid Room ID' unless room_id.room_id?
79
71
 
80
- @name = nil
81
- @topic = nil
82
- @canonical_alias = nil
83
- @aliases = []
84
- @join_rule = nil
85
- @guest_access = nil
86
- @world_readable = nil
87
- @members = []
88
72
  @events = []
89
- @members_loaded = false
90
73
  @event_history_limit = 10
91
- @avatar_url = nil
92
74
 
93
75
  @prev_batch = nil
94
76
 
95
77
  data.each do |k, v|
96
- instance_variable_set("@#{k}", v) if instance_variable_defined? "@#{k}"
78
+ next if %i[client].include? k
79
+
80
+ if respond_to?("#{k}_cached?".to_sym) && send("#{k}_cached?".to_sym)
81
+ tinycache_adapter.write(k, v)
82
+ elsif instance_variable_defined? "@#{k}"
83
+ instance_variable_set("@#{k}", v)
84
+ end
97
85
  end
98
86
 
99
- @client = client
100
87
  @id = room_id.to_s
101
88
 
102
- @name_checked = Time.new(0)
103
-
104
89
  logger.debug "Created room #{room_id}"
105
90
  end
106
91
 
@@ -154,19 +139,22 @@ module MatrixSdk
154
139
  'Empty Room'
155
140
  end
156
141
 
142
+ # @return [String, nil] the canonical alias of the room
143
+ def canonical_alias
144
+ client.api.get_room_state(id, 'm.room.canonical_alias')[:alias]
145
+ rescue MatrixSdk::MatrixNotFoundError
146
+ nil
147
+ end
148
+
157
149
  # Populates and returns the #members array
158
150
  #
159
151
  # @return [Array(User)] The list of members in the room
160
152
  def joined_members
161
- return members if @members_loaded && !members.empty?
162
-
163
- client.api.get_room_joined_members(id)[:joined].each do |mxid, data|
164
- ensure_member(User.new(client, mxid.to_s,
165
- display_name: data.fetch(:display_name, nil),
166
- avatar_url: data.fetch(:avatar_url, nil)))
153
+ client.api.get_room_joined_members(id)[:joined].map do |mxid, data|
154
+ User.new(client, mxid.to_s,
155
+ display_name: data.fetch(:display_name, nil),
156
+ avatar_url: data.fetch(:avatar_url, nil))
167
157
  end
168
- @members_loaded = true
169
- members
170
158
  end
171
159
 
172
160
  # Get all members (member events) in the room
@@ -186,10 +174,7 @@ module MatrixSdk
186
174
  #
187
175
  # @return [String,nil] The room name - if any
188
176
  def name
189
- return @name if Time.now - @name_checked < 900
190
-
191
- @name_checked = Time.now
192
- @name ||= client.api.get_room_name(id)
177
+ client.api.get_room_name(id)[:name]
193
178
  rescue MatrixNotFoundError
194
179
  # No room name has been specified
195
180
  nil
@@ -199,18 +184,34 @@ module MatrixSdk
199
184
  #
200
185
  # @return [String,nil] The avatar URL - if any
201
186
  def avatar_url
202
- @avatar_url ||= client.api.get_room_avatar(id).url
187
+ client.api.get_room_avatar(id)[:url]
203
188
  rescue MatrixNotFoundError
204
189
  # No avatar has been set
205
190
  nil
206
191
  end
207
192
 
193
+ # Gets the room topic - if any
194
+ #
195
+ # @return [String,nil] The topic of the room
196
+ def topic
197
+ client.api.get_room_topic(id)[:topic]
198
+ rescue MatrixNotFoundError
199
+ # No room name has been specified
200
+ nil
201
+ end
202
+
203
+ # Gets the guest access rights for the room
204
+ #
205
+ # @return [:can_join,:forbidden] The current guest access right
208
206
  def guest_access
209
- @guest_access ||= client.api.get_room_guest_access(id).guest_access.to_sym
207
+ client.api.get_room_guest_access(id)[:guest_access].to_sym
210
208
  end
211
209
 
210
+ # Gets the join rule for the room
211
+ #
212
+ # @return [:public,:knock,:invite,:private] The current join rule
212
213
  def join_rule
213
- @join_rule ||= client.api.get_room_join_rules(id).join_rule.to_sym
214
+ client.api.get_room_join_rules(id)[:join_rule].to_sym
214
215
  end
215
216
 
216
217
  # Checks if +guest_access+ is set to +:can_join+
@@ -223,6 +224,34 @@ module MatrixSdk
223
224
  join_rule == :invite
224
225
  end
225
226
 
227
+ # Gets the history visibility of the room
228
+ #
229
+ # @return [:invited,:joined,:shared,:world_readable] The current history visibility for the room
230
+ def history_visibility
231
+ client.api.get_room_state(id, 'm.room.history_visibility')[:history_visibility].to_sym
232
+ end
233
+
234
+ # Checks if the room history is world readable
235
+ #
236
+ # @return [Boolean] If the history is world readable
237
+ def world_readable?
238
+ history_visibility == :world_readable
239
+ end
240
+ alias world_readable world_readable?
241
+
242
+ # Gets the room aliases
243
+ #
244
+ # @return [Array[String]] The assigned room aliases
245
+ def aliases
246
+ client.api.get_room_aliases(id).aliases
247
+ rescue MatrixNotFoundError
248
+ data = client.api.get_room_state_all(id)
249
+ data.select { |chunk| chunk[:type] == 'm.room.aliases' && chunk.key?(:content) && chunk[:content].key?(:aliases) }
250
+ .map { |chunk| chunk[:content][:aliases] }
251
+ .flatten
252
+ .compact
253
+ end
254
+
226
255
  #
227
256
  # Message handling
228
257
  #
@@ -363,7 +392,13 @@ module MatrixSdk
363
392
  # @param reverse [Boolean] whether to fill messages in reverse or not
364
393
  # @param limit [Integer] the maximum number of messages to backfill
365
394
  # @note This will trigger the `on_event` events as messages are added
366
- def backfill_messages(reverse = false, limit = 10) # rubocop:disable Style/OptionalBooleanParameter
395
+ def backfill_messages(*args, reverse: false, limit: 10)
396
+ # To be backwards-compatible
397
+ if args.length == 2
398
+ reverse = args.first
399
+ limit = args.last
400
+ end
401
+
367
402
  data = client.api.get_room_messages(id, @prev_batch, direction: :b, limit: limit)
368
403
 
369
404
  events = data[:chunk]
@@ -481,7 +516,7 @@ module MatrixSdk
481
516
  @room
482
517
  end
483
518
  tag_obj.define_singleton_method(:add) do |tag, **data|
484
- @room.add_tag(tag.to_s.to_sym, data)
519
+ @room.add_tag(tag.to_s.to_sym, **data)
485
520
  self[tag.to_s.to_sym] = data
486
521
  self
487
522
  end
@@ -526,22 +561,16 @@ module MatrixSdk
526
561
  #
527
562
  # @param name [String] The new name to set
528
563
  def name=(name)
564
+ tinycache_adapter.write(:name, name)
529
565
  client.api.set_room_name(id, name)
530
- @name = name
566
+ name
531
567
  end
532
568
 
533
569
  # Reloads the name of the room
534
570
  #
535
571
  # @return [Boolean] if the name was changed or not
536
572
  def reload_name!
537
- data = begin
538
- client.api.get_room_name(id)
539
- rescue MatrixNotFoundError
540
- nil
541
- end
542
- changed = data[:name] != @name
543
- @name = data[:name] if changed
544
- changed
573
+ clear_name_cache
545
574
  end
546
575
  alias refresh_name! reload_name!
547
576
 
@@ -549,22 +578,16 @@ module MatrixSdk
549
578
  #
550
579
  # @param topic [String] The new topic to set
551
580
  def topic=(topic)
581
+ tinycache_adapter.write(:topic, topic)
552
582
  client.api.set_room_topic(id, topic)
553
- @topic = topic
583
+ topic
554
584
  end
555
585
 
556
586
  # Reloads the topic of the room
557
587
  #
558
588
  # @return [Boolean] if the topic was changed or not
559
589
  def reload_topic!
560
- data = begin
561
- client.api.get_room_topic(id)
562
- rescue MatrixNotFoundError
563
- nil
564
- end
565
- changed = data[:topic] != @topic
566
- @topic = data[:topic] if changed
567
- changed
590
+ clear_topic_cache
568
591
  end
569
592
  alias refresh_topic! reload_topic!
570
593
 
@@ -573,7 +596,7 @@ module MatrixSdk
573
596
  # @return [Boolean] if the addition was successful or not
574
597
  def add_alias(room_alias)
575
598
  client.api.set_room_alias(id, room_alias)
576
- @aliases << room_alias
599
+ tinycache_adapter.read(:aliases) << room_alias if tinycache_adapter.exist?(:aliases)
577
600
  true
578
601
  end
579
602
 
@@ -583,21 +606,7 @@ module MatrixSdk
583
606
  # @note The list of aliases is not sorted, ordering changes will result in
584
607
  # alias list updates.
585
608
  def reload_aliases!
586
- begin
587
- new_aliases = client.api.get_room_aliases(id).aliases
588
- rescue MatrixNotFoundError
589
- data = client.api.get_room_state_all(id)
590
- new_aliases = data.select { |chunk| chunk[:type] == 'm.room.aliases' && chunk.key?(:content) && chunk[:content].key?(:aliases) }
591
- .map { |chunk| chunk[:content][:aliases] }
592
- .flatten
593
- .compact
594
- end
595
-
596
- return false if new_aliases.nil?
597
-
598
- changed = new_aliases != aliases
599
- @aliases = new_aliases if changed
600
- changed
609
+ clear_aliases_cache
601
610
  end
602
611
  alias refresh_aliases! reload_aliases!
603
612
 
@@ -606,7 +615,7 @@ module MatrixSdk
606
615
  # @param invite_only [Boolean] If it should be invite only or not
607
616
  def invite_only=(invite_only)
608
617
  self.join_rule = invite_only ? :invite : :public
609
- @join_rule == :invite
618
+ invite_only
610
619
  end
611
620
 
612
621
  # Sets the join rule of the room
@@ -614,7 +623,8 @@ module MatrixSdk
614
623
  # @param join_rule [:invite,:public] The join rule of the room
615
624
  def join_rule=(join_rule)
616
625
  client.api.set_room_join_rules(id, join_rule)
617
- @join_rule = join_rule
626
+ tinycache_adapter.write(:join_rule, join_rule)
627
+ join_rule
618
628
  end
619
629
 
620
630
  # Sets if guests are allowed in the room
@@ -622,7 +632,7 @@ module MatrixSdk
622
632
  # @param allow_guests [Boolean] If guests are allowed to join or not
623
633
  def allow_guests=(allow_guests)
624
634
  self.guest_access = (allow_guests ? :can_join : :forbidden)
625
- @guest_access == :can_join
635
+ allow_guests
626
636
  end
627
637
 
628
638
  # Sets the guest access status for the room
@@ -630,7 +640,8 @@ module MatrixSdk
630
640
  # @param guest_access [:can_join,:forbidden] The new guest access status of the room
631
641
  def guest_access=(guest_access)
632
642
  client.api.set_room_guest_access(id, guest_access)
633
- @guest_access = guest_access
643
+ tinycache_adapter.write(:guest_access, guest_access)
644
+ guest_access
634
645
  end
635
646
 
636
647
  # Sets a new avatar URL for the room
@@ -641,7 +652,89 @@ module MatrixSdk
641
652
  raise ArgumentError, 'Must be a valid MXC URL' unless avatar_url.is_a? URI::MATRIX
642
653
 
643
654
  client.api.set_room_avatar(id, avatar_url)
644
- @avatar_url = avatar_url
655
+ tinycache_adapter.write(:avatar_url, avatar_url)
656
+ avatar_url
657
+ end
658
+
659
+ # Get the power levels of the room
660
+ #
661
+ # @note The returned power levels are cached for a minute
662
+ # @return [Hash] The current power levels as set for the room
663
+ # @see Protocols::CS#get_power_levels
664
+ def power_levels
665
+ client.api.get_power_levels(id)
666
+ end
667
+
668
+ # Gets the power level of a user in the room
669
+ #
670
+ # @param user [User,MXID,String] The user to check the power level for
671
+ # @param use_default [Boolean] Should the user default level be checked if no user-specific one exists
672
+ # @return [Integer,nil] The current power level for the requested user, nil if there's no user specific level
673
+ # and use_default is false
674
+ def user_powerlevel(user, use_default: true)
675
+ user = user.id if user.is_a? User
676
+ user = MXID.new(user.to_s) unless user.is_a? MXID
677
+ raise ArgumentError, 'Must provide a valid user or MXID' unless user.user?
678
+
679
+ level = power_levels[:users][user.to_s.to_sym]
680
+ level = power_levels[:users_default] || 0 if level.nil? && use_default
681
+ level
682
+ end
683
+
684
+ # Check if a user is an admin in the room
685
+ #
686
+ # @param user [User,MXID,String] The user to check for admin privileges
687
+ # @param target_level [Integer] The power level that's to be considered as admin privileges
688
+ # @return [Boolean] If the requested user has a power level highe enough to be an admin
689
+ # @see #user_powerlevel
690
+ def admin?(user, target_level: 100)
691
+ level = user_powerlevel(user, use_default: false)
692
+ return false unless level
693
+
694
+ level >= target_level
695
+ end
696
+
697
+ # Make a user an admin in the room
698
+ #
699
+ # @param user [User,MXID,String] The user to give admin privileges
700
+ # @param level [Integer] The power level to set the user to
701
+ # @see #modify_user_power_levels
702
+ def admin!(user, level: 100)
703
+ return true if admin?(user, target_level: level)
704
+
705
+ user = user.id if user.is_a? User
706
+ user = MXID.new(user.to_s) unless user.is_a? MXID
707
+ raise ArgumentError, 'Must provide a valid user or MXID' unless user.user?
708
+
709
+ modify_user_power_levels({ user.to_s.to_sym => level })
710
+ end
711
+
712
+ # Check if a user is a moderator in the room
713
+ #
714
+ # @param user [User,MXID,String] The user to check for admin privileges
715
+ # @param target_level [Integer] The power level that's to be considered as admin privileges
716
+ # @return [Boolean] If the requested user has a power level highe enough to be an admin
717
+ # @see #user_powerlevel
718
+ def moderator?(user, target_level: 50)
719
+ level = user_powerlevel(user, use_default: false)
720
+ return false unless level
721
+
722
+ level >= target_level
723
+ end
724
+
725
+ # Make a user a moderator in the room
726
+ #
727
+ # @param user [User,MXID,String] The user to give moderator privileges
728
+ # @param level [Integer] The power level to set the user to
729
+ # @see #modify_user_power_levels
730
+ def moderator!(user, level: 50)
731
+ return true if moderator?(user, target_level: level)
732
+
733
+ user = user.id if user.is_a? User
734
+ user = MXID.new(user.to_s) unless user.is_a? MXID
735
+ raise ArgumentError, 'Must provide a valid user or MXID' unless user.user?
736
+
737
+ modify_user_power_levels({ user.to_s.to_sym => level })
645
738
  end
646
739
 
647
740
  # Modifies the power levels of the room
@@ -652,7 +745,8 @@ module MatrixSdk
652
745
  def modify_user_power_levels(users = nil, users_default = nil)
653
746
  return false if users.nil? && users_default.nil?
654
747
 
655
- data = client.api.get_power_levels(id)
748
+ data = power_levels_without_cache
749
+ tinycache_adapter.write(:power_levels, data)
656
750
  data[:users_default] = users_default unless users_default.nil?
657
751
 
658
752
  if users
@@ -673,7 +767,8 @@ module MatrixSdk
673
767
  def modify_required_power_levels(events = nil, params = {})
674
768
  return false if events.nil? && (params.nil? || params.empty?)
675
769
 
676
- data = client.api.get_power_levels(id)
770
+ data = power_levels_without_cache
771
+ tinycache_adapter.write(:power_levels, data)
677
772
  data.merge!(params)
678
773
  data.delete_if { |_k, v| v.nil? }
679
774
 
@@ -690,9 +785,62 @@ module MatrixSdk
690
785
  private
691
786
 
692
787
  def ensure_member(member)
788
+ tinycache_adapter.write(:joined_members, []) unless tinycache_adapter.exist? :joined_members
789
+
790
+ members = tinycache_adapter.read(:joined_members) || []
693
791
  members << member unless members.any? { |m| m.id == member.id }
694
792
  end
695
793
 
794
+ def handle_power_levels(event)
795
+ tinycache_adapter.write(:power_levels, event[:content])
796
+ end
797
+
798
+ def handle_room_name(event)
799
+ tinycache_adapter.write(:name, event[:content][:name])
800
+ end
801
+
802
+ def handle_room_topic(event)
803
+ tinycache_adapter.write(:topic, event[:content][:topic])
804
+ end
805
+
806
+ def handle_room_guest_access(event)
807
+ tinycache_adapter.write(:guest_access, event[:content][:guest_access].to_sym)
808
+ end
809
+
810
+ def handle_room_join_rules(event)
811
+ tinycache_adapter.write(:join_rule, event[:content][:join_rule].to_sym)
812
+ end
813
+
814
+ def handle_room_member(event)
815
+ return unless client.cache == :all
816
+
817
+ if event[:content][:membership] == 'join'
818
+ ensure_member(client.get_user(event[:state_key]).dup.tap do |u|
819
+ u.instance_variable_set :@display_name, event[:content][:displayname]
820
+ end)
821
+ elsif tinycache_adapter.exist? :joined_members
822
+ members = tinycache_adapter.read(:joined_members)
823
+ members.delete_if { |m| m.id == event[:state_key] }
824
+ end
825
+ end
826
+
827
+ def handle_room_canonical_alias(event)
828
+ canonical_alias = tinycache_adapter.write :canonical_alias, event[:content][:alias]
829
+
830
+ data = tinycache_adapter.read(:aliases) || []
831
+ data << canonical_alias
832
+ tinycache_adapter.write(:aliases, data)
833
+ end
834
+
835
+ def handle_room_aliases(event)
836
+ tinycache_adapter.write(:aliases, []) unless tinycache_adapter.exist? :aliases
837
+
838
+ aliases = tinycache_adapter.read(:aliases) || []
839
+ aliases.concat event[:content][:aliases]
840
+
841
+ tinycache_adapter.write(:aliases, aliases)
842
+ end
843
+
696
844
  def room_handlers?
697
845
  client.instance_variable_get(:@room_handlers).key? id
698
846
  end
@@ -705,6 +853,16 @@ module MatrixSdk
705
853
  }
706
854
  end
707
855
 
856
+ INTERNAL_HANDLERS = {
857
+ 'm.room.aliases' => :handle_room_aliases,
858
+ 'm.room.canonical_alias' => :handle_room_canonical_alias,
859
+ 'm.room.guest_access' => :handle_room_guest_access,
860
+ 'm.room.join_rules' => :handle_room_join_rules,
861
+ 'm.room.member' => :handle_room_member,
862
+ 'm.room.name' => :handle_room_name,
863
+ 'm.room.power_levels' => :handle_power_levels,
864
+ 'm.room.topic' => :handle_room_topic
865
+ }.freeze
708
866
  def put_event(event)
709
867
  ensure_room_handlers[:event].fire(MatrixEvent.new(self, event), event[:type]) if room_handlers?
710
868
 
@@ -719,6 +877,8 @@ module MatrixSdk
719
877
  end
720
878
 
721
879
  def put_state_event(event)
880
+ send(INTERNAL_HANDLERS[event[:type]], event) if INTERNAL_HANDLERS.key? event[:type]
881
+
722
882
  return unless room_handlers?
723
883
 
724
884
  ensure_room_handlers[:state_event].fire(MatrixEvent.new(self, event), event[:type])