In my Ruby on Rails project, I have a user model that is set up by devise. Each user belongs to an account with an account_id.
In my application_controller.rb I have
def set_account
#account = current_user.account
end
This is working fine as in many places of my project I have before_action: set_account and it does assign #account correctly.
When a user signs in, I want the user to subscribe to message_notifications_channel_#{account_id} where account_id is the id of the account that the user belongs to.
This is how I set up connection.rb:
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
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
When I entered byebug here, User.find_by(id: cookies.encrypted[:user_id]) returned nil and cookies.encrypted[:user_id] is nil too.
This is the setup for message_notifications_channel.rb:
class MessageNotificationsChannel < ApplicationCable::Channel
def subscribed
current_user.appear
# stream_from "some_channel"
stream_from "message_notifications_channel_#{params[:account_id]}"
end
def unsubscribed
current_user.disappear
# Any cleanup needed when channel is unsubscribed
end
end
For message_notifications.coffee, this is the code:
App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: current_user.account_id},
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) ->
# Called when there's incoming data on the websocket for this channel
if data['direction'] == 'incoming'
ding = new Audio('/assets/ding.wav');
ding.play();
$('#conversation-messages').append String(data['message']);
if data['direction'] == 'outgoing'
if $('#message_'+data['id']).length == 0
iphone_sms_sent_sound = new Audio('/assets/iphone_send_sms.mp3');
iphone_sms_sent_sound.play();
$('#conversation-messages').append String(data['message']);
else
$('#message_'+data['id']).replaceWith(data['message']);
I'm using the following to broadcast message in the after_create callback of Message.rb:
ActionCable.server.broadcast "message_notifications_channel_#{self.account_id}", {id: self.id, direction: self.direction, message: ApplicationController.render(partial:'inbox/message', locals: {message: self})}
This would not work and I got "An unauthorized connection attempt was rejected". I tried using App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: #account.id}
This would not work either.
Then I did App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: 3} and commented out current_user.appear and current_user.disappear in message_notifications_channel.rb and everything inside
module ApplicationCable
end
in connection.rb. Then packets will get broadcast and received and things will show up.
How do I get App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: }, to use the id of #account.id or current_user.account_id while keeping the methods for user verification in connection.rb?
Thanks!
Related
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
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
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
I have a setup where User A rejects User B's offer. This triggers ActionCable to push a form created using button_to to User B informing him of this, and let's him click the message to delete it. But the form raises ActionController::InvalidAuthenticityToken
params {"_method"=>"delete", "authenticity_token"=>"4lqu8...", "id"=>"31"}
I do have another form which I replace a create form with an update form using ActionCable and it works fine. So, my attempt was to change the flow to when User A rejects the offer it triggers User B to request the message. And for that I moved the logic from a service object to
class AuctionOwnChannel < ApplicationCable::Channel
def subscribed
#...
end
def display(hash)
ActionCable.server.broadcast "auction_own_channel_#{params[:oPId]}_#{current_user.id}", hash
end
# In the code below `rack_inputs` is passed into `frustrated_replacement` and the subsequent methods to reach `render`
def rejected(data)
info = data["info"]
display BidServices::Process.frustrated_replacement(info["product_id"], info["message_id"], current_user)
end
end
module BidServices
class Process
class << self
def frustrated_replacement(product_id, message_id, user)
message = Message.find message_id
thing = {partial: "messages/rejected", locals: {message: message}}
replacements(thing, user)
end
def replacements(thing, user)
{ processed_bid: render(thing),
cart: render(partial: "bids/cart", locals: {current_user: user} )
}
end
def render(*args)
a_c = ApplicationController
a_c.per_form_csrf_tokens = true # Have tried with and without this
a_c.renderer.render(*args)
end
end
end
end
EDIT: Since writing this I did further research and went through some of the code for ActionView and I think I need to set the session
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user, :rack_inputs
def connect
self.rack_inputs = find_rack_inputs
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.id
end
protected
def find_rack_inputs
# I just tried adding `session` in the hash but it did not help
{input: env["rack.input"], http_host: env['HTTP_HOST'], session: env["rack.session"]}
end
# Attempt to replace whole env
# def find_rack_inputs
# env
# end
end
end
module BidServices
class Process
def render(*args)
rack_inputs = args.pop
controller = MessagesController
renderer = controller.renderer.new(rack_inputs)
renderer.render(*args)
end
# The changes when I was replacing the env
# def render(*args)
# rack_inputs = args.pop # This attempt rack_inputs was `env`
# MessagesController.renderer.new(rack_inputs).render(*args)
# end
end
end
end
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