Rails action cable specific consumer - ruby-on-rails

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

Related

Broadcast message for identified user with ActionCable from non Rails application

I have action cable channel with identified user
class DesktopNotificationsChannel < ApplicationCable::Channel
def subscribed
reject and return unless current_user.present?
logger.add_tags 'ActionCable DesktopNotificationsChannel', current_user.id
stream_for current_user
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
And i'm connecting this channel from my external application (Desktop application written in C# and I'm using WebSocketSharp library for connections). I can successfully listen all broadcasted messages that sent with,
DesktopNotificationsChannel.broadcast_to(user, data: 'Some Data' )
But i also want to broadcast message from external application. I've tried various ways to broadcast message for specific user but actioncable ignores the message I'm trying to send;
{
"command": "message",
"data": {"status": "current_status"},
"identifier": { "channel": "DesktopNotificationsChannel"}
}
I figured out i have to add user_id or global_id for user that i want to send message in that json but i couldn't succeed.
I also tried change identifier with rails' broadcaster key like desktop_notifications:xxyyyzz but it also didn't work.
I'm missing something but couldn't figured. How can i send broadcast message for specific user from my non rails application.
I managed to fetch messages by adding stream_from and receive method to my channel. I don't know its proper way to do but it but it's working like this.
class DesktopNotificationsChannel < ApplicationCable::Channel
def subscribed
reject and return unless current_user.present?
logger.add_tags 'ActionCable DesktopNotificationsChannel', current_user.id
stream_for current_user
stream_from current_user
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def receive(data)
DesktopNotificationsChannel.broadcast_to(current_user, data:)
end
end

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 channel subscription is not working because channel method isn't executed

I am working on a small book shop in Rails. Users can write reviews for individual books, which are added to a product's page. I want to use ActionCable to add new reviews to the page, so that it is constantly up to date, and display a small alert notification when a review is added for other users that are currently on the same page.
Therefore I want to set up individual channels for each product based on a product's id. When a user opens a product's page, she should subscribe to the corresponding channel.
To achieve this I am trying to call a method called listen, that I added to the ProductChannel class, whenever a new site is loaded by calling App.product.perform('listen', {product_id: 1}) in JS. But the problem is that although perform is called, listen is never executed. What am I doing wrong or misunderstanding? Thanks in advance!
Content of javascript/channels/prouduct.coffee:
App.product = App.cable.subscriptions.create "ProductChannel",
connected: () ->
return
disconnected: ->
# Called when the subscription has been terminated by the server
return
received: (data) ->
# Called when there's incoming data on the websocket for this channel
console.log("there is data incoming so lets show the alert")
$(".alert.alert-info").show()
return
listen_to_comments: ->
#perform "listen", product_id: $("[data-product-id]").data("product-id")
$(document).on 'turbolinks:load', ->
App.product.listen_to_comments()
return
Content of channels/product_channel.rb:
class ProductChannel < ApplicationCable::Channel
def subscribed
end
def unsubscribed
end
def listen(data)
stop_all_streams
stream_for data["product_id"]
end
end
A connection object has to be instantiated :
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags current_user.name
end
def disconnect
# Any cleanup work needed when the cable connection is cut.
end
protected
def find_verified_user
if current_user = User.find_by_identity cookies.signed[:identity_id]
current_user
else
reject_unauthorized_connection
end
end
end
end
Then you need to broadcast_to the #product
class ProductChannel < ApplicationCable::Channel
def subscribed
#product = Product.find(params[:product_id])
end
def unsubscribed
stop_all_streams
end
def listen(data)
stream_for #product
ProductsChannel.broadcast_to(#product)
end
end

Send ActionCable to particular user

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

Keep user "online" with ActionCable

I have code that hugely relies on whether or not a user is online.
Currently I've setup ActionCable like this:
class DriverRequestsChannel < ApplicationCable::Channel
def subscribed
stream_from "requests_#{current_user.id}"
end
def unsubscribed
current_user.unavailable! if current_user.available?
end
end
Now what I'll ideally like to cover is the case where a user just instead of going offline just closes their browser. However the issue with unsubscribed is that it goes on page refresh. So every time they refresh their page they'll trigger the unsubscribed. Thus they'll be put as unavailable even though they think they're available.
Now the key thing is that being available isn't a default so I can just put it back, it's something a user chooses in order to receive requests.
Does anybody have experience with the best way to handle a case like this?
You should not only rely on Websockets, but also put a user online status into the database:
1: Add a migration
class AddOnlineToUsers < ActiveRecord::Migration[5.0]
def change
add_column :users, :online, :boolean, default: false
end
end
2: Add an AppearanceChannel
class AppearanceChannel < ApplicationCable::Channel
def subscribed
stream_from "appearance_channel"
if current_user
ActionCable.server.broadcast "appearance_channel", { user: current_user.id, online: :on }
current_user.online = true
current_user.save!
end
end
def unsubscribed
if current_user
# Any cleanup needed when channel is unsubscribed
ActionCable.server.broadcast "appearance_channel", { user: current_user.id, online: :off }
current_user.online = false
current_user.save!
end
end
end
Now you are guaranteed against any occasional Websockets connection losses. On every HTML-page refresh do 2 things:
Check for database's user online status.
Connect to socket and subscribe to Appearance Channel.
Such combined approach would reliably provide you with users' online status at any time moment.

Resources