Actioncable connected users list
Ya kinda have to roll your own user-tracking.
The important bits are as follows:
# connection
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :uid
def connect
self.uid = get_connecting_uid
logger.add_tags 'ActionCable', uid
end
protected
# the connection URL to this actioncable/channel must be
# domain.tld?uid=the_uid
def get_connecting_uid
# this is just how I get the user id, you can keep using cookies,
# but because this key gets used for redis, I'd
# identify_by the user_id instead of the current_user object
request.params[:uid]
end
end
end
# Your Channel
def subscribed
stop_all_streams
stream_from broadcasting_name
ConnectedList.add(uid)
end
def unsubscribed
ConnectedList.remove(uid)
end
And then in Models:
class ConnectedList
REDIS_KEY = 'connected_nodes'
def self.redis
@redis ||= ::Redis.new(url: ActionCableConfig[:url])
end
# I think this is the method you want.
def self.all
redis.smembers(REDIS_KEY)
end
def self.clear_all
redis.del(REDIS_KEY)
end
def self.add(uid)
redis.sadd(REDIS_KEY, uid)
end
def self.include?(uid)
redis.sismember(REDIS_KEY, uid)
end
def self.remove(uid)
redis.srem(REDIS_KEY, uid)
end
end
from: https://github.com/NullVoxPopuli/mesh-relay/
ActionCable - how to display number of connected users?
Seems that one way is to use
ActionCable.server.connections.length
(See caveats in the comments)
Get list of all registered connections
I finally found a solution.
def connect
self.uuid = SecureRandom.uuid
transmit({'title': 'players_online', 'message': ActionCable.server.connections.size + 1})
ActionCable.server.connections.each do |connection|
connection.transmit({'title': 'players_online', 'message': ActionCable.server.connections.size + 1})
end
end
def disconnect
transmit({'title': 'players_online', 'message': ActionCable.server.connections.size})
ActionCable.server.connections.each do |connection|
connection.transmit({'title': 'players_online', 'message': ActionCable.server.connections.size})
end
end
ActionCable.server.connections
gets a list of all server connection, except of the one which is currently connecting to the socket. That's why you need to transmit him a message directly with ActionCable.server.connections.size + 1
ActionCable display correct number of connected users (problems: multiple tabs, disconnects)
I finally at least got it to work for counting all users to the server (not by room) by doing this:
CoffeeScript of my channel:
App.online_status = App.cable.subscriptions.create "OnlineStatusChannel",
connected: ->
# Called when the subscription is ready for use on the server
#update counter whenever a connection is established
App.online_status.update_students_counter()
disconnected: ->
# Called when the subscription has been terminated by the server
App.cable.subscriptions.remove(this)
@perform 'unsubscribed'
received: (data) ->
# Called when there's incoming data on the websocket for this channel
val = data.counter-1 #-1 since the user who calls this method is also counted, but we only want to count other users
#update "students_counter"-element in view:
$('#students_counter').text(val)
update_students_counter: ->
@perform 'update_students_counter'
Ruby backend of my channel:
class OnlineStatusChannel < ApplicationCable::Channel
def subscribed
#stream_from "specific_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
#update counter whenever a connection closes
ActionCable.server.broadcast(specific_channel, counter: count_unique_connections )
end
def update_students_counter
ActionCable.server.broadcast(specific_channel, counter: count_unique_connections )
end
private:
#Counts all users connected to the ActionCable server
def count_unique_connections
connected_users = []
ActionCable.server.connections.each do |connection|
connected_users.push(connection.current_user.id)
end
return connected_users.uniq.length
end
end
And now it works! When a user connects, the counter increments, when a user closes his window or logs off it decrements. And when a user is logged on with more than 1 tab or window they are only counted once. :)
How can use ActionCable broadcast_to with specific group of users?
I found the solution
I send slug_id with the uid, access_token and client for subscribing to the channel and I use it in connection.rb like this
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
uid = request.query_parameters[:uid]
token = request.query_parameters[:token]
client_id = request.query_parameters[:client]
slug_id = request.query_parameters[:slug_id]
user = User.where(slug_id: slug_id).find_by_uid(uid) <-------- get all users with the same slug_id :)
if user && user.valid_token?(token, client_id)
user
else
reject_unauthorized_connection
end
end
end
end
Rails: Action Cable: How to authorize user to connect specific channel based on role?
$(document).ready ->
App.privateAdminMesssagesChannel = App.cable.subscriptions.create({
channel: 'PrivateAdminMessagesChannel'
},
connected: ->
disconnected: ->
// call this function to send a message from a Non-Admin to All Admins
sendMessageToAdmins: (message) ->
@perform 'send_messsage_to_admins', message: message
// call this function to send a messsage from an Admin to (a Non-admin + all Admins)
sendMessageToUserAndAdmins: (message, toUserId) ->
@perform 'send_messsage_to_user_and_admins', message: message, to_user_id: toUserId
received: (data) ->
console.log(data.from_user_id)
console.log(data.to_user_id)
console.log(data.message)
if data.to_user_id
// this means the message was sent from an Admin to (a Non-admin + all admins)
else
// this means the message was sent from a Non-admin to All Admins
// do some logic here i.e. if current user is an admin, open up one Chatbox
// on the page for each unique `from_user_id`, and put data.message
// in that box accordingly
)
private_admin_messages_channel.rb
class PrivateAdminMessagesChannel < ActionCable::Channel::Base
def subscribed
stream_from :private_admin_messages_channel, coder: ActiveSupport::JSON do |data|
from_user = User.find(data.fetch('from_user_id'))
to_user = User.find(data['to_user_id']) if data['to_user_id']
message = data.fetch('message')
# authorize if "message" is sent to you (a non-admin), and also
# authorize if "message" is sent to you (an admin)
if (to_user && to_user == current_user) || (!to_user && current_user.is_admin?)
# now, finally send the Hash data below and transmit it to the client to be received in the JS-side "received(data)" callback
transmit(
from_user_id: from_user.id,
to_user_id: to_user&.id,
message: message
)
end
end
end
def send_message_to_admins(data)
ActionCable.server.broadcast 'private_admin_messages_channel',
from_user_id: current_user.id,
message: data.fetch('message')
end
def send_message_to_user_and_admins(data)
from_user = current_user
reject unless from_user.is_admin?
ActionCable.server.broadcast 'private_admin_messages_channel',
from_user_id: from_user.id,
to_user_id: data.fetch('to_user_id'),
message: data.fetch('message')
end
end
Above is the easiest way I could think of. Not the most efficient one because there's an extra level of authorization happening per stream (see inside stream_from
block) unlike if we have different broadcast-names of which the authorization would happen only once on the "connecting" itself, and not each "streaming"... which can be done via something like:
- Admin User1 opens page then JS-subscribes to
UserConnectedChannel
- Non-admin User2 opens page then JS-subscribes to
PrivateAdminMessagesChannel
passing in data:user_id: CURRENT_USER_ID
- From 2. above, as User2 has just subscribed; then on the backend, inside
def subscribed
upon connection, youActionCable.server.broadcast :user_connected, { user_id: current_user.id }
- Admin User1 being subscribed to
UserConnectedChannel
then receives withdata { user_id: THAT_USER2_id }
- From 4 above, inside the JS
received(data)
callback, you then now JS-subscribe toPrivateAdminMessagesChannel
passing in data: THAT_USER2_id`. - Now User1 and User2 are both subscribed to
PrivateAdminMessagesChannel user_id: THAT_USER2_id
which means that they can privately talk with each other (other admins should also have had received:user_connected
's JS data:{ user_id: THAT_USER2_ID }
, and so they should also be subscribed as well, because it makes sense that AdminUser1, NonAdminUser2, and AdminUser3 can talk in the same chat channel... from what I was getting with your requirements) - TODO: From 1 to 6 above, do something similar also with the "disconnection" process
Trivias:
- Those you define with
identified_by
in yourApplicationCable::Connection
can be acceessed in your channel files. In particular, in this case,current_user
can be called. - Regarding, rejecting subscriptions, see docs here
With ActionCable, is there a way to count how many subscribers from inside a channel?
I defined a helper method:
app/channels/application_cable/channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
def connections_info
connections_array = []
connection.server.connections.each do |conn|
conn_hash = {}
conn_hash[:current_user] = conn.current_user
conn_hash[:subscriptions_identifiers] = conn.subscriptions.identifiers.map {|k| JSON.parse k}
connections_array << conn_hash
end
connections_array
end
end
end
Now you can call connections_info
anywhere inside your derived channel. The method returns an informational array of data about all the available server socket connections, their respective current_user
s and all their current subscriptions.
Here is an example of my data connections_info
returns:
[1] pry(#<ChatChannel>)> connections_info
=> [{:current_user=>"D8pg2frw5db9PyHzE6Aj8LRf",
:subscriptions_identifiers=>
[{"channel"=>"ChatChannel",
"secret_chat_token"=>"f5a6722dfe04fc883b59922bc99aef4b5ac266af"},
{"channel"=>"AppearanceChannel"}]},
{:current_user=>
#<User id: 2, email: "client1@example.com", created_at: "2017-03-27 13:22:14", updated_at: "2017-04-28 11:13:37", provider: "email", uid: "client1@example.com", first_name: "John", active: nil, last_name: nil, middle_name: nil, email_public: nil, phone: nil, experience: nil, qualification: nil, price: nil, university: nil, faculty: nil, dob_issue: nil, work: nil, staff: nil, dob: nil, balance: nil, online: true>,
:subscriptions_identifiers=>
[{"channel"=>"ChatChannel",
"secret_chat_token"=>"f5a6722dfe04fc883b59922bc99aef4b5ac266af"}]}]
You can then parse this structure the way you want and extract the desired data. You can distinguish your own connection in this list by the same current_user
(the current_user
method is available inside class Channel < ActionCable::Channel::Base
).
If a user connects twice (or more times), then corresponding array elements just double.
Related Topics
How to Include Ё in [А-Я] Regexp Char Interval
Building a Simple Search Form in Rails
How to Define a Method in Ruby Using Splat and an Optional Hash at the Same Time
How to Make Rake Db:Migrate Generate Schema.Rb When Using :SQL Schema Format
Ruby: Automatically Set Instance Variable as Method Argument
Validate That String Contains Only Allowed Characters in Ruby
Parsing Large Xml with Nokogiri
How to Create an Operator for Deep Copy/Cloning of Objects in Ruby
Does Anyone Have Parsing Rules for the Notepad++ Function List Plugin for Ruby and Rake
How to Simplify or Clean Up This Anagram Method
How to Get a Reference to a Method
Minitest, Test::Unit, and Rails
Fresh Install of Rails and Getting Openssl Errors: "Already Initialized Constant Openssl"
Installing MySQL-2.9.0 Gem on Windows Fails Due to Lack of Libmysql
How to Call Methods Defined in Applicationcontroller in Models