Send ActionCable to particular user - ruby-on-rails

I have the following code which sends an ActionCable broadcast in my Rails application:
ActionCable.server.broadcast 'notification_channel', notification: 'Test message'
The connection looks as follows:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
def session
cookies.encrypted[Rails.application.config.session_options[:key]]
end
protected
def find_verified_user
User.find_by(id: session['user_id'])
end
end
end
However all users logged into the app will receive it. The identified_by only makes sure that logged in users can connect to the channel but it doesn't restrict which users get the broadcast.
Is there a way to only sending a broadcast to a certain user?
The only way I could think of doing it was:
ActionCable.server.broadcast 'notification_channel', notification: 'Test message' if current_user = User.find_by(id: 1)
Where the 1 is the ID of the user I want to target.

For user-specific notifications I find it useful to have a UserChannel where the subscription is based on the current user:
class UserChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
This way ActionCable creates a separate channel for each user, and you can use commands like this based on the user object:
user = User.find(params[:id])
UserChannel.broadcast_to(user, { notification: 'Test message' })
That way this channel can handle all user-specific broadcasts.

You should use a channel that's specific to that user. For example:
"notifications_channel_#{current_user.id}"
This is also documented in an example from the actioncable repo here: https://github.com/rails/rails/tree/master/actioncable#channel-example-2-receiving-new-web-notifications

Related

How do you stream a broadcast to a specific user in Rails 5 with ActionCable?

I have 2 user types in my application (worker and company). Both user types are created with Devise. I'm currently trying to send a notification to a specific company with ActionCable.
My main problem is that when I send a notification every single company that's signed in receives it. I get that I'm supposed to include the company id in the stream name in some way, but I haven't had any luck so far.
I've included the working code that sends notifications to all companies below:
notifications_channel.rb
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end
Call to the broadcast
ActionCable.server.broadcast 'notifications_channel', { 'My data' }
EDIT
I log the status of the notification with javascript:
notifications.js
App.notifications = App.cable.subscriptions.create("NotificationsChannel", {
connected: function() {
console.log("connected");
};
disconnected: function() {
console.log("disconnected");
};
received: function(data) {
console.log("recieved");
};
});
Broadcast the message from your controller like this:
# Broadcast your message
ActionCable.server.broadcast "notifications_channel:#{target_user.id}
Now Update app/channels/application_cable/connection.rb with the below code
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.name
end
protected
def find_verified_user
verified_user = User.find_by(id: cookies.signed['user.id'])
if verified_user && cookies.signed['user.expires_at'] > Time.now
verified_user
else
reject_unauthorized_connection
end
end
end
end
And subscribe to the stream like this:
def subscribed
stream_from "notifications_channel:#{current_user.id}"
end
Note: This is just an example to show how to target a specific user in Actioncable. You may have to modify the code based on
your requirement.
I also recommend watching this video by GoRails.
I managed to find the solution. Following Abhilashs answer took me most of the way, but I still had trouble authenticating the company. It seems like Warden was not fully configured, so this post made it work: env['warden'] not working with Rails 5

ActionCable broadcast_to not reaching user

I have a basic setup with rails + devise + actioncable.
I basically want to send notifications directly and privately to currently signed-in user. My code looks like follows:
connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.email
end
private
def find_verified_user
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
end
notifications_channel.rb
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from current_user
end
end
And it all works just fine. Client get's connected and i can see in the logs he is correctly logged in. Also, i can see a following input in the rails console:
[ActionCable] [admin#example.com] Registered connection (Z2lkOi8vcHJpc21vL1VzZXIvMQ)
[ActionCable] [admin#example.com] NotificationsChannel is transmitting the subscription confirmation
[ActionCable] [admin#example.com] NotificationsChannel is streaming from #<User:0x00007f87f4180b68>
However, when trying to send a notification to this user using a code below, it seems the event is not reaching the user (no errors present!):
2.5.1 :010 > NotificationsChannel.broadcast_to(User.first, test: 'foo')
[ActionCable] Broadcasting to notifications:Z2lkOi8vcHJpc21vL1VzZXIvMQ: {:test=>"pass"}
=> nil
And my javascript consumer logs nothing:
let cable = ActionCable.createConsumer(`ws://mydomain.com/cable`)
let actions = {
received(payload) {
console.log(payload) // <== this line logs nothing!
}
}
cable.subscriptions.create('NotificationsChannel', actions)
Have i done anything wrong in here?
It's likely because you are using stream_from instead of stream_for. When referencing an object (model) rather than a string in your channel, you should use stream_for. Try and do this in notifications_channel.rb:
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
Here is a reference to the documentation: https://api.rubyonrails.org/classes/ActionCable/Channel/Streams.html#method-i-stream_for

How to use Action cable in Active Admin

I'm using Activeadmin to make a dashboard page and Devise for User authorize. I have 2 model are Member (regular user) and User(admin user). When I use the Action cable feature to show the user appearance or not then just only Member user received these broadcast. The problem is how the User model can receive these broadcast too ?
Thank you for your time and sorry for my clumsy English
My connection
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_member
def connect
self.current_member = find_verified_member
# self.current_user = find_verified_member
logger.add_tags 'AcctionCable', current_member.email
end
protected
def find_verified_member
# if current_member = Member.find_by(id: cookies.signed[:member_id])
if verified_member = env['warden'].user
verified_member
else
reject_unauthorized_connection
end
end
end
end
If you want to make a chat with actioncable and activeadmin here is an example of a gem that does that. active_admin_chat
Or maybe you can try the piece of code below.
def connect
self.current_member = find_verified_member
end
protected
def find_verified_member
cookies_signed = cookies.signed
admin_user_id = cookies_signed['admin_user.id']
if admin_user_id
AdminUser.find(admin_user_id)
else
User.find(cookies_signed['user.id'])
end
end
Looks like you want to broadcast to a channel for users whenever a member establishes connection with ActionCable. If so, in the def subscribed method for whatever channel member is subscribing to that you wish for the admin users to be notified of, run a broadcast job to a channel accessible to all admin users. Admin members can establish a client to subscribe to this channel in order to receive these broadcasts.

Rails action cable specific consumer

I'm struggling with action cable. In my case I have couple of users (via Devise) who can share tasks with each other.
Now when user#1 share task (via Form) with user#2 all authenticated users receive notifications.
How and where should I identify my user#2 to broadcast only to him?
Here is my code so far:
connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.id
end
protected
def find_verified_user # this checks whether a user is authenticated with devise
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
end
cable.js
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
todo_channel.rb
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_#{current_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def notify
ActionCable.server.broadcast "todo_channel_#{current_user.id}", message: 'some message'(not implemented yet)
end
end
todo.coffee
App.todo = App.cable.subscriptions.create "TodoChannel",
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
console.log(data['message'])
notify: ->
#perform 'notify'
i've faced something similar before until i realized that you can actually call stream_from multiple times in the channel and that user will be subscribed to multiple different "rooms" within the same channel connection. Which means you can basically do this
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_all"
stream_from "todo_channel_#{current_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def notify(data)
# depending on data given from the user, send it to only one specific user or everyone listening to the "todo_channel_all"
if data['message']['other_user_id']
ActionCable.server.broadcast "todo_channel_#{data['message']['other_user_id']}", message: 'some message'
else
ActionCable.server.broadcast "todo_channel_all", message: 'some message'
end
end
end
that code assuming that the user already knows the other user's id and sent it to the channel, you would probably have to wrap that with some security or something, i admit i'm not very well experienced with rails as i'm still learning.
Something else that might be beneficial to you in the future is the fact that you can also broadcast several messages/times in the same channel function. That means you can potentially support sending out your tasks to a single specific user, a list of specific users or everyone. Just iterate on the list/array/whatever of users and broadcast the task/message/notification/whatever to them each on their personal "todo_channel_#{user.id}"
I ended up with a different approach. I'll write it here in case someone will find it helpful.
Notification has an id of a user that has to be notified. So in model I have:
after_commit :broadcast_notification, on: :create
def broadcast_notification
ActionCable.server.broadcast "todo_channel_#{self.user_id}", message: 'some message'
end
As I placed broadcasting into the model my todo_channel.rb looks like this:
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_#{current_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Step#1: Let each user have unique session token. While subscription, each user will send session token in headers and header is accessible in connection class. Find user by using session token.
Step#2: Stream user on user id.
Step#3: While sharing task, take user id too in the request and broadcast on given user id.
This is called "private chat". If you really want to get current_user.id you could do it in 3 ways:
Some AJAX onload call from todo.coffee to server.
Render current_user.id as an attribute in Rails HTML view and then get it via jQuery inside todo.coffee (as in https://www.sitepoint.com/create-a-chat-app-with-rails-5-actioncable-and-devise/ )
Create a plain cookie while a user logging in and then check it inside todo.coffee
But you shouldn't use current_user.id because it's not secure. If you use it then someone might register in the same site and easily listen to other users' private chats by simply providing a random user_id.
Instead use the signed (e.g. Rails-encrypted) cookie as a user's unique broadcast identifier. That way if you register in the same site you would never know some other user's signed cookie - so you can't break into an alien private chat.
app/config/initializers/warden_hooks.rb
See https://rubytutorial.io/actioncable-devise-authentication/
# Be sure to restart your server when you modify this file.
Warden::Manager.after_set_user do |user,auth,opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = user.id
end
Warden::Manager.before_logout do |user, auth, opts|
scope = opts[:scope]
auth.cookies.delete("#{scope}.id")
end
todo_channel.rb
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_#{params['user_signed_cookie']}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def notify(param_message)
ActionCable.server.broadcast "todo_channel_#{param_message['user_signed_cookie']}", message: 'some message'(not implemented yet)
end
end
todo.coffee
user_signed_cookie = document.cookie.replace(/(?:(?:^|.*;\s*)user.id\s*\=\s*([^;]*).*$)|^.*$/, "$1");
user_logged_in = if user_signed_cookie then true else false
if user_logged_in #avoid repetitive HTTP->WebSockets upgrade pointless browser attempts when no user logged in.
App.todo = App.cable.subscriptions.create {
channel:"TodoChannel"
user_signed_cookie: user_signed_cookie
},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
console.log(data['message'])
notify: (name, content, _) -> #name, content - message fields
#perform 'notify', name: name, content: content, user_signed_cookie: user_signed_cookie

How to make state consistency while using Action Cable

Let's assume we use something like current_user which is an instance of a ServiceClass which holds user model, session params and other info.
The thing is that variable is being set during connection with websocket and being reused for all AC calls on different subscriptions.
Then, at some point user decides to update his username, we make a call to current_user.update(new_username) and it works okay.
But other AC subscriptions under that user still use old user model. I suppose as each subscription works under their own thread, thus updating user model under one thread will not update them under other threads. What is the best approach for such case?
class ServiceClass
def initialize(session,...)
#session = session
#user = current_user
end
def update!(username)
#user.username = username
#user.save!
end
...
end
module ApplicationCable
class Channel < ActionCable::Channel::Base
def current_user
#current_user ||= ServiceClass.new(session, user)
end
end
end
What you want to do is to broadcast to current_user and for current_user to be subscribed to his / her stream.
In your controller:
ActionCable.server.broadcast "#{current_user}"
In your channel (eg. app/channels/user_channel.rb)
class UserChannel < ApplicationCable::Channel
def subscribed
stream_from "#{current_user}"
end
end

Resources