I have created one project where I have one customer and another contractor. I implemented ruby on rails actioncable for chat. All it is going good but issue is coming when two different people chat with one person, that person is receiving both messages in socket window. I realised that I have setup conversation-#{user_id} as a channel, so user is listening on this channel now two people send chat to him, they both will come on same channel. How can I avoid this? or can I add another user in channel string, but I found it is very difficult. Any idea where I have to send params to subscribe method.
connection
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_session_user
end
def find_session_user
current_user = User.find_by(id: cookies.signed[:actioncable_user_id])
current_user || reject_unauthorized_connection
end
end
My conversation channel
class ConversationChannel < ApplicationCable::Channel
def subscribed
stream_from "conversations-#{current_user.id}"
end
def unsubscribed
stop_all_streams
end
def speak(data)
message_params = data["message"].each_with_object({}) do |el, hash|
hash[el.values.first] = el.values.last
end
end
ActionCable.server.broadcast(
"conversations-#{current_user.id}",
message: message_params,
)
end
This code is just condense version, but as it will start conversation-user_id as a channel, so definitely when it is connected and other people send message, this user will receive them in same socket. so I have to do like `conversation-user_id-anotehr_user. Right now it is working on web/mobile and all good, but when two user communicate with one user it create issue by displaying two users chat on one socket.
Any idea how can I create such channel.
I have solved this issue by binding chat with 2 persons and I have another requirement of job specific chats, so have bound it with it too. Now my conversation channel is like conversation-(talk_id)-(listern_id)-job_id so it all good now. Following are changes I did
1. I removed channel file from assets/javascript as it is automatically load on my application, but I want to bound it with few parameters so I added it to specific view. My controller has already few parameters so I have changed this javascript to following
<script>
App.conversation = App.cable.subscriptions.create({channel: "ConversationChannel",
job_id: <%= #job.id %>,
contractor: <%= #contractor %>
}, {
connected: function() {},
disconnected: function() {},
received: function(data) {
var conversation = $('#conversations-list').find("[data-conversation-id='" + data['conversation_id'] + "']");
conversation.find('.messages-list').find('ul').append(data['message']);
var messages_list = conversation.find('.messages-list');
var height = messages_list[0].scrollHeight;
messages_list.scrollTop(height);
},
speak: function(message) {
return this.perform('speak', {
message: message
});
}
});
Now when connection establish it sends both parameters and channel channel properly individual. On my conversation.rb I have just minor change
def subscribed
stream_from "conversations-#{current_user.id}-#{params[:contractor]}-#{params[:job_id]}"
end
Now everything working perfectly as per our requirements, each channel is being made with 2 users+job Id so I they can communicate on specific job and with specific users, so there no more other person can listen other conversation.
Just posting may help someone.
Related
Making the title for this question was extremely difficult, but essentially, I have a service object in my Rails app that creates a flow for Resume Processing. I am trying to use ActionCable to connect my frontend with my backend. The current way I do this is by instantiating my Service Object in my controller:
def create_from_resume
...
ResumeParseService.new(#candidate, current_user)
end
My Service then begins by broadcasting to my front end to open the corresponding modal:
Service:
class ResumeParseService
attr_reader :user
attr_reader :employee
attr_reader :candidate
def initialize(candidate, user)
#user = user
#employee = user.employee
#candidate = candidate
#progress = 0
--> broadcast_begin
end
def begin_from_parse_modal
broadcast_progress(10)
parsed_resume = get_a_resume_while_hiding_implementation_details
broadcast_progress(rand(40..60))
...
broadcast_progress(100 - #progress)
...
end
private
def broadcast_begin
ResumeParseChannel.broadcast_and_set_service(self, user, {
event_name: 'transition_screen',
props: {
to: 'parse',
},
})
end
def broadcast_progress(addition)
#progress += addition
ResumeParseChannel.broadcast_to(user, {
event_name: 'progress',
props: {
progress: #progress,
},
})
end
def broadcast_transition_screen(screen_name, body = nil)
ResumeParseChannel.broadcast_to(user, {
event_name: 'transition_screen',
props: {
to: screen_name,
data: body,
},
})
end
end
Rails Channel:
# frozen_string_literal: true
class ResumeParseChannel < ApplicationCable::Channel
def subscribed
stream_for(current_user)
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def self.broadcast_and_set_service(service, *args)
#service = service
broadcast_to *args
end
def screen_transitioned(data)
case data['screen_name']
when 'parse'
pp #service
#service.begin_from_parse_modal
else
# type code here
end
end
private
def current_user
if (current_user = env["warden"].user)
current_user
else
reject_unauthorized_connection
end
end
end
Which my channel then takes care of. Later, my channel will send back a 'progress update' to let my service know the modal opened successfully:
JS Channel:
consumer.subscriptions.create(
{ channel: "ResumeParseChannel" },
{
connected() {
document.addEventListener("resume-parse:screen_transitioned", event =>
--> this.perform("screen_transitioned", event.detail)
);
},
}
);
Now, my problem is that once that message gets sent back to my (ruby) channel, I can't think of a way for it to find my existing instance of my service object and use it. As you can see, I tried to set an instance var on the channel with the service object instance on the first broadcast, but that (and a million other things) did not work. I need to call #begin_from_parse_modal once I get the 'screen_transitioned' with the screen_name of 'parse'. Ideally, I'd like to separate the broadcasting logic and the parsing logic as much as possible.
I understand that the instance of the channel can be thought of as the actual subscription, but I just don't understand what the best practice is of a system where I can send a "do this" message, and then do something once I get a "its been done" message.
Please let me know if I missed anything in terms of explanation and/or code. Feel free to also let me know if I should do something differently next time I ask something! This is my first time asking on stackoverflow, but it's about my billionth time looking for an answer :)
edit: I'm still dumbfounded by this seemingly common scenario. Could it possibly be best practice to just simply have the channel act as the service object? If so, how would we store state on it? The only possible way I can think of this working in any form is to send the full state in each WS message. Or at least the id's to each record thats in state and then lookup each record on each message. This seems unreasonably complex and expensive. I have scoured other questions and even ActionCable tutorials to find anyone using a service object with receiving messages and have found nothing. SOS!
In rails 5, I am using ActionCable for broadcast notification in real-time basis. Broadcast is happening once Notification object is created. Right now I want to revert back the broadcasted notification when Notification object get deleted.
In notification_broadcast_job.rb,
def perform(notification, user_id)
ActionCable.server.broadcast "notifications:#{user_id}", data: NotificationSerializer.new(notification)
end
In notification.rb,
after_commit :broadcast_notification
private
def broadcast_notification
users.each do |user|
NotificationsBroadcastJob.perform_later(self, user.id)
end
end
Now issue is like, when user A likes one post which belongs to user B then B will get notification (without page reload). When user A unlikes it immediately then notification should go off from user B (without page reload). Right now deleted object (notification) will simply shows in a list.
How can I solve this issue? Please help me.
What your problem is that you can't disdinguish whether the object Notification was created or deleted ?
You can use after_create and after_destroy instead of after_commit
The back-end pass data to front-end, then front-end will show or hide data. The key is that font-end need to know whether show or hide. So there should be a protocol between back and front. There is none-elegant example:
In notification.rb,
after_create :broadcast_notification
after_destroy :delete_notification
private
def broadcast_notification
users.each do |user|
NotificationsBroadcastJob.perform_later(self,'created', user.id)
end
end
def delete_notification
users.each do |user|
NotificationsBroadcastJob.perform_later(self,'delete', user.id)
end
end
In notification_broadcast_job.rb,
def perform(notification, message_type , user_id)
ActionCable.server.broadcast "notifications:#{user_id}", data:{ message_type: message_type, info: {id: notification.id, body: notification.body}}
end
The front-end will read the message_type first. Then it know show or hide something.
front-end js code:
if(data.message_type == 'created'){
$('***').append('<li id=' + data.info.id + '>' + data.info.body +'</li>'); //
}else{
$('#'+ data.info.id).hide();
}
Using websocket-rails, I can use the
following code in my controller to tricker a websocket publish event:
WebsocketRails[:channel_name].trigger('event_name', { foo: "bar" }.to_json)
My goal is to create a pub-sub channel for the current user only. Take for example
the event "new chat message sent". I want to push this event only the receiver's channel.
I'm currently making a unique channel name for each user based on their ID.
I put the following code in my controller (having defined a current_user
method elsewhere:
WebsocketRails[:"user#{current_user.id}"].trigger("event_name", { foo: "bar" }.to_json)
And then in my Javascript, I subscribe the current user to their own channel with the following:
<% if #current_user %>
var dispatcher = new WebSocketRails('localhost:3000/websocket');
channel = dispatcher.subscribe('user<%= #current_user.id %>');
channel.bind('event_name', function(data) {
console.log(data)
});
<% end %>
The gist of it is using string interpolation to make a new channel for each user, i.e.
user12 and user123 channels.
The problem is this is not really secure. Any user can access anyone else's
private channel just by pasting some Javascript in. For example, if user #1 wants
to access user #2's news feed, they could just type dispatcher.subscribe('user2').
How would you solve this issue? Is there another pub-sub library which has this feature
built into it?
Looking on WebsocketRails' wiki entry on the subject,
I tried adding the follolwing code to config/initializers/websockets.rb
WebsocketRails::EventMap.describe do
namespace :websocket_rails do
subscribe :subscribe_private, to: ConnectionsController, with_method: :authorize_channels
end
end
And the following to app/controllers/connections_controller.rb
class ConnectionsController < WebsocketRails::BaseController
def authorize_channels
channel_name = WebsocketRails[message[:channel]]
current_user = User.find_by(id: session["current_user_id"])
if current_user && "user#{current_user.id}".eql?(channel_name)
accept_channel current_user
else
deny_channel({ message: "auth failed" })
end
end
end
And then elsewhere, I'm calling WebsocketRails[:"user#{#current_user.id}"].make_private
This doesn't seem to have any effect though.
I am almost completely new to Rails, or I am sure I would know how to answer this question myself. I am just trying to modify the basic chat app created in the basic ActionCable demo: https://medium.com/#dhh/rails-5-action-cable-demo-8bba4ccfc55e#.6lmd6tfi7
Instead of having just one chat room, I want to have multiple chat rooms, so I modified my routes.rb by adding this line:
get '/rooms/show/:topic', to: 'rooms#show'
So now I can visit different chat rooms based on different topics. The rooms controller at /app/controllers/rooms_controller.rb handles these routes with no problem:
class RoomsController < ApplicationController
def show
#topic = params[:topic]
#messages = Message.all
end
end
But this parameter is not being passed to app/channels/room_channel.rb, and I'm just not sure what modifications I need to make. My current attempt:
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel_#{params[:topic]}"
end
only returns "room_channel_"
The problem here was that I failed to understand from where the subscribed method was being called, and thus did not know how to pass parameters to it.
By reading the actioncable documentation: https://github.com/rails/rails/tree/master/actioncable
I found out that the subscribed method is called via client-side javascript, not by the rails controller. In the case of the example chat app, this means I had to change the first line of the file /app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
to
App.room = App.cable.subscriptions.create { channel: "RoomChannel", topic: topic},
Passing a javascript object to this method allowed me to access those parameters in the subscribed method of rooms_controller.rb.
Set the topic_id in a HTML tag, maybe in the body tag in your layout file.
<body data-topic-id="<%= #topic.id %>">
Now read it from JS like this:
document.querySelector('body').dataset.topicId
Your subscription creation line would look like this:
App.room = App.cable.subscriptions.create (channel: 'RoomChannel', topic_id: document.querySelector('body').dataset.topicId)
I'm trying to create a simple chat-like application (planning poker app) with Action Cable. I'm a little bit confused by the terminology, files hierarchy and how the callbacks work.
This is the action that creates user session:
class SessionsController < ApplicationController
def create
cookies.signed[:username] = params[:session][:username]
redirect_to votes_path
end
end
A user can then post a vote that should be broadcasted to everyone:
class VotesController < ApplicationController
def create
ActionCable.server.broadcast 'poker',
vote: params[:vote][:body],
username: cookies.signed[:username]
head :ok
end
end
Up to this point everything is clear for me and works fine. The problem is - how do I display the number of connected users? Is there a callback that fires in JS when a user (consumer?) connects? What I want is when I open 3 tabs in 3 different browsers in incognito mode I would like to display "3". When a new user connects, I would like the number to increment. If any user disconnects, the number should decrement.
My PokerChannel:
class PokerChannel < ApplicationCable::Channel
def subscribed
stream_from 'poker'
end
end
app/assets/javascripts/poker.coffee:
App.poker = App.cable.subscriptions.create 'PokerChannel',
received: (data) ->
$('#votes').append #renderMessage(data)
renderMessage: (data) ->
"<p><b>[#{data.username}]:</b> #{data.vote}</p>"
Seems that one way is to use
ActionCable.server.connections.length
(See caveats in the comments)
For a quick (and probably not ideal) solution you can write a module that tracks subscription counts (using Redis to store data):
#app/lib/subscriber_tracker.rb
module SubscriberTracker
#add a subscriber to a Chat rooms channel
def self.add_sub(room)
count = sub_count(room)
$redis.set(room, count + 1)
end
def self.remove_sub(room)
count = sub_count(room)
if count == 1
$redis.del(room)
else
$redis.set(room, count - 1)
end
end
def self.sub_count(room)
$redis.get(room).to_i
end
end
And update your subscribed and unsubscribed methods in the channel class:
class ChatRoomsChannel < ApplicationCable::Channel
def subscribed
SubscriberTracker.add_sub params['room_id']
end
def unsubscribed
SubscriberTracker.remove_sub params['chat_room_id']
end
end
In a related question on who is connected, there was an answer for those who uses redis:
Redis.new.pubsub("channels", "action_cable/*")
and if you just want number of connections:
Redis.new.pubsub("NUMPAT", "action_cable/*")
This will summarize connections from all your servers.
All the magic covered inside RemoteConnections class and InternalChannel module.
TL;DR all connections subscibed on special channels with a prefix action_cable/* with only purpose of disconnecting sockets from main rails app.
I think i found a answer for you.
Try this:
ActionCable.server.connections.select { |con| con.current_room == room }.length?
I can use it everywhere in my code and check amount of connected users to selected stream :)
in my connection.rb I have something like this:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_room
def connect
self.current_room = find_room
end
private
def find_room
.....
end
end
end
Hope that helps anyone.
With
ActionCable.server.pubsub.send(:listener).instance_variable_get("#subscribers")
you can get map with subscription identifier in the key and array of procedures which will be executed on the broadcast. All procedures accept message as argument and have memoized connection.