Class: PresenceChannel
- Inherits:
-
Object
- Object
- PresenceChannel
- Defined in:
- lib/presence_channel.rb
Overview
The server-side implementation of PresenceChannels. See also PresenceController and app/assets/javascripts/discourse/app/services/presence.js
Defined Under Namespace
Classes: Config, ConfigNotLoaded, InvalidAccess, InvalidConfig, NotFound, State
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#message_bus_channel_name ⇒ Object
readonly
Returns the value of attribute message_bus_channel_name.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#timeout ⇒ Object
readonly
Returns the value of attribute timeout.
Class Method Summary collapse
-
.auto_leave_all ⇒ Object
Designed to be run periodically.
-
.clear_all! ⇒ Object
Clear all known channels.
-
.redis ⇒ Object
Shortcut to access a redis client for all PresenceChannel activities.
- .redis_eval(key, *args) ⇒ Object
-
.register_prefix(prefix, &block) ⇒ Object
Register a callback to configure channels with a given prefix Prefix must match [a-zA-Z0-9_-]+.
-
.unregister_prefix(prefix) ⇒ Object
For use in a test environment only.
Instance Method Summary collapse
-
#auto_leave ⇒ Object
Automatically expire all users which have not been ‘present’ for more than
DEFAULT_TIMEOUT
. -
#can_enter?(user_id: nil, group_ids: nil) ⇒ Boolean
Is a user allowed to enter this channel? Currently equal to the the can_view? permission.
-
#can_view?(user_id: nil, group_ids: nil) ⇒ Boolean
Is this user allowed to view this channel? Pass ‘nil` for anonymous viewers.
-
#clear ⇒ Object
Clear all members of the channel.
- #count ⇒ Object
-
#initialize(name, raise_not_found: true, use_cache: true) ⇒ PresenceChannel
constructor
A new instance of PresenceChannel.
-
#leave(user_id:, client_id:) ⇒ Object
Immediately mark a user’s client as leaving the channel.
-
#present(user_id:, client_id:) ⇒ Object
Mark a user’s client as present in this channel.
-
#state(count_only: config.count_only) ⇒ Object
Fetch a State instance representing the current state of this.
- #user_ids ⇒ Object
Constructor Details
#initialize(name, raise_not_found: true, use_cache: true) ⇒ PresenceChannel
Returns a new instance of PresenceChannel.
86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/presence_channel.rb', line 86 def initialize(name, raise_not_found: true, use_cache: true) @name = name @message_bus_channel_name = "/presence#{name}" begin @config = fetch_config(use_cache: use_cache) rescue PresenceChannel::NotFound raise if raise_not_found @config = Config.new end @timeout = config.timeout || DEFAULT_TIMEOUT end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
84 85 86 |
# File 'lib/presence_channel.rb', line 84 def config @config end |
#message_bus_channel_name ⇒ Object (readonly)
Returns the value of attribute message_bus_channel_name.
84 85 86 |
# File 'lib/presence_channel.rb', line 84 def @message_bus_channel_name end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
84 85 86 |
# File 'lib/presence_channel.rb', line 84 def name @name end |
#timeout ⇒ Object (readonly)
Returns the value of attribute timeout.
84 85 86 |
# File 'lib/presence_channel.rb', line 84 def timeout @timeout end |
Class Method Details
.auto_leave_all ⇒ Object
Designed to be run periodically. Checks the channel list for channels with expired members, and runs auto_leave for each eligible channel
220 221 222 223 224 |
# File 'lib/presence_channel.rb', line 220 def self.auto_leave_all channels_with_expiring_members = PresenceChannel.redis.zrangebyscore(redis_key_channel_list, "-inf", Time.zone.now.to_i) channels_with_expiring_members.each { |name| new(name, raise_not_found: false).auto_leave } end |
.clear_all! ⇒ Object
Clear all known channels. This is intended for debugging/development only
227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/presence_channel.rb', line 227 def self.clear_all! channels = PresenceChannel.redis.zrangebyscore(redis_key_channel_list, "-inf", "+inf") channels.each { |name| new(name, raise_not_found: false).clear } config_cache_keys = PresenceChannel .redis .scan_each(match: Discourse.redis.namespace_key("_presence_*_config")) .to_a PresenceChannel.redis.del(*config_cache_keys) if config_cache_keys.present? end |
.redis ⇒ Object
Shortcut to access a redis client for all PresenceChannel activities. PresenceChannel must use the same Redis server as MessageBus, so that actions can be applied atomically. For the vast majority of Discourse installations, this is the same Redis server as ‘Discourse.redis`.
243 244 245 246 247 248 249 250 251 |
# File 'lib/presence_channel.rb', line 243 def self.redis if MessageBus.backend == :redis MessageBus.backend_instance.send(:pub_redis) # TODO: avoid a private API? elsif Rails.env.test? Discourse.redis.without_namespace else raise "PresenceChannel is unable to access MessageBus's Redis instance" end end |
.redis_eval(key, *args) ⇒ Object
253 254 255 |
# File 'lib/presence_channel.rb', line 253 def self.redis_eval(key, *args) LUA_SCRIPTS[key].eval(redis, *args) end |
.register_prefix(prefix, &block) ⇒ Object
Register a callback to configure channels with a given prefix Prefix must match [a-zA-Z0-9_-]+
For example, this registration will be used for all channels starting /topic-reply/…:
register_prefix("topic-reply") do |channel_name|
PresenceChannel::Config.new(public: true)
end
At runtime, the block will be passed a full channel name. If the channel should not exist, the block should return ‘nil`. If the channel should exist, the block should return a PresenceChannel::Config object.
Return values may be cached for up to 10 seconds.
Plugins should use the Plugin::Instance#register_presence_channel_prefix API instead
274 275 276 277 278 279 280 281 282 |
# File 'lib/presence_channel.rb', line 274 def self.register_prefix(prefix, &block) unless prefix.match? /[a-zA-Z0-9_-]+/ raise "PresenceChannel prefix #{prefix} must match [a-zA-Z0-9_-]+" end if @@configuration_blocks&.[](prefix) raise "PresenceChannel prefix #{prefix} already registered" end @@configuration_blocks[prefix] = block end |
.unregister_prefix(prefix) ⇒ Object
For use in a test environment only
285 286 287 288 |
# File 'lib/presence_channel.rb', line 285 def self.unregister_prefix(prefix) raise "Only allowed in test environment" if !Rails.env.test? @@configuration_blocks&.delete(prefix) end |
Instance Method Details
#auto_leave ⇒ Object
Automatically expire all users which have not been ‘present’ for more than DEFAULT_TIMEOUT
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/presence_channel.rb', line 193 def auto_leave mutex_value = SecureRandom.hex left_user_ids = retry_on_mutex_error do PresenceChannel.redis_eval(:auto_leave, redis_keys, [name, Time.zone.now.to_i, mutex_value]) end if !left_user_ids.empty? begin (leaving_user_ids: left_user_ids) ensure release_mutex(mutex_value) end end end |
#can_enter?(user_id: nil, group_ids: nil) ⇒ Boolean
Is a user allowed to enter this channel? Currently equal to the the can_view? permission
116 117 118 119 |
# File 'lib/presence_channel.rb', line 116 def can_enter?(user_id: nil, group_ids: nil) return false if user_id.nil? can_view?(user_id: user_id, group_ids: group_ids) end |
#can_view?(user_id: nil, group_ids: nil) ⇒ Boolean
Is this user allowed to view this channel? Pass ‘nil` for anonymous viewers
102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/presence_channel.rb', line 102 def can_view?(user_id: nil, group_ids: nil) return true if config.public return true if user_id && config.allowed_user_ids&.include?(user_id) if user_id && config.allowed_group_ids.present? return true if config.allowed_group_ids.include?(Group::AUTO_GROUPS[:everyone]) group_ids ||= GroupUser.where(user_id: user_id).pluck("group_id") return true if (group_ids & config.allowed_group_ids).present? end false end |
#clear ⇒ Object
Clear all members of the channel. This is intended for debugging/development only
210 211 212 213 214 215 216 |
# File 'lib/presence_channel.rb', line 210 def clear PresenceChannel.redis.del(redis_key_zlist) PresenceChannel.redis.del(redis_key_hash) PresenceChannel.redis.del(redis_key_config) PresenceChannel.redis.del(redis_key_mutex) PresenceChannel.redis.zrem(self.class.redis_key_channel_list, name) end |
#count ⇒ Object
188 189 190 |
# File 'lib/presence_channel.rb', line 188 def count state(count_only: true).count end |
#leave(user_id:, client_id:) ⇒ Object
Immediately mark a user’s client as leaving the channel
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/presence_channel.rb', line 147 def leave(user_id:, client_id:) mutex_value = SecureRandom.hex result = retry_on_mutex_error do PresenceChannel.redis_eval(:leave, redis_keys, [name, user_id, client_id, nil, mutex_value]) end if result == 1 begin (leaving_user_ids: [user_id]) ensure release_mutex(mutex_value) end end end |
#present(user_id:, client_id:) ⇒ Object
Mark a user’s client as present in this channel. The client_id should be unique per browser tab. This method should be called repeatedly (at least once every DEFAULT_TIMEOUT) while the user is present in the channel.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/presence_channel.rb', line 124 def present(user_id:, client_id:) raise PresenceChannel::InvalidAccess if !can_enter?(user_id: user_id) mutex_value = SecureRandom.hex result = retry_on_mutex_error do PresenceChannel.redis_eval( :present, redis_keys, [name, user_id, client_id, (Time.zone.now + timeout).to_i, mutex_value], ) end if result == 1 begin (entering_user_ids: [user_id]) ensure release_mutex(mutex_value) end end end |
#state(count_only: config.count_only) ⇒ Object
Fetch a State instance representing the current state of this
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/presence_channel.rb', line 166 def state(count_only: config.count_only) if count_only last_id, count = retry_on_mutex_error { PresenceChannel.redis_eval(:count, redis_keys) } else last_id, ids = retry_on_mutex_error { PresenceChannel.redis_eval(:user_ids, redis_keys) } end count ||= ids&.count last_id = nil if last_id == -1 if Rails.env.test? && MessageBus.backend == :memory # Doing it this way is not atomic, but we have no other option when # messagebus is not using the redis backend last_id = MessageBus.last_id() end State.new(message_bus_last_id: last_id, user_ids: ids, count: count) end |
#user_ids ⇒ Object
184 185 186 |
# File 'lib/presence_channel.rb', line 184 def user_ids state.user_ids end |