matrix_sdk 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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])