We have a model ChatRoom which has many messages, the ChatRoom implements add_message and receives the parameters for the message along with a boolean notify which specifies whether we should send an email or not for the created message.
We publish :message_created after the message is created, however in our subscriber we receive an ActiveRecordNotFound error however there is an ID present meaning that the message is persisted in the database.
We have a couple potential fixes however we want to understand the problem and it's cause.
class ChatRoom < ApplicationRecord
has_many :messages, -> { order(:created_at) }
def add_message(args)
content = args.fetch(:content)
creator = args.fetch(:creator)
notify = args.fetch(:notify, false)
message = self.messages.create!(content: content, creator: creator)
publish(:message_created, message_id: message.id, notify: notify)
end
end
class MessageCreated::MailChatSubscriber < ApplicationSubscriber
def message_created(args)
message = Message.find(args.fetch(:message_id))
notify = args.fetch(:notify)
Messages::Organizers::SendChatResponseMail.call(
message: message,
notify: notify
)
end
end
The error is presented below:
Couldn't find Message with 'id'=575565
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!
i want to save this message five time from active jobs not from controller.
is there anyway to that ?
here message.save just returning true and its not saving the message in databas.
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
for i in 0..5
message.save!
ActionCable.server.broadcast 'chat', {message: render_message(message)}
end
end
private
def render_message(message)
MessagesController.render(
partial: 'message',
locals: {
message: message
}
)
end
end
this code is from model.
class Message < ApplicationRecord
belongs_to :user
after_create_commit {
MessageBroadcastJob.perform_later(self)
}
end
You are not calling your job in code anywhere. You are saving that after_create_commit mean after saving message to active record you want to run this job. But you are not doing it.
So you have to save at least one time, to run this job. That mean in your controller or anywhere in code at least save once message, this job will run when you have saved message onces, than it will save message. If you still have issue please check job is running properly? do you have any configuration in redis
I smell from your codes you are using cable, that mean you have to put following in your conversation channel file.
Message.create(message_params)
As soon as above code run than your job will also run. If above all is good than you have also issue in your job, you are saying that same message should be saved, so it won't save 5 message, you have to take parameters in it do following
m = Message.new
m.body = message
m.user_id = message.user_id
# setup other parameters required
m.save
ActionCable.server.broadcast 'chat', {message: render_message(message)}
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.
I want to use Action Cable (Rails 5) to create a chat app.
When the client signs in to the app, I send the message to subscribe for the channel. The channel will stream for connection based on current_user 's room_id.
When the user sends a message it will be sent to all who subscribed for the channel
That 's normal flow of action cable.
Here is my channel:
class RoomChannel < ApplicationCable::Channel
def subscribed
current_user.chatrooms.each do |chatroom|
stream_from "room_#{chatroom.id}"
end
end
end
But I wonder when the client sends the message to the new one who is not in the room. The streaming was not created. How can the other receive the message and keep the conversation move on?
For a user who is sending the message, you can create a callback in your message model that broadcasts to your channel when a message is created. I assume you created correct associations. i.e Rooms has_many messages. Message in turn belongs_to Room and User
Something like this:
class Message < ApplicationRecord
after_create :broadcast_to_room
.......
private
def broadcast_to_room
#room = self.room
RoomChannel.broadcast_to(#room, message: self)
end
.........
end
So whenever a message is created, it is broadcasted to the room the message belongs to.
Your RoomChannel can be like this:
class RoomChannel < ApplicationCable::Channel
def subscribed
room = Room.find(params[:id])
stream_for room
end
end
Now in the from facing page of the room, you should have a function that subscribes to listen for incoming messages. So when a message comes in, your listener picks up the message and displays it.
I wrote a gist here on how you can use actioncable in a client (in react-native). In your client you create a socket that listens for incoming messages.
P.S: I've been working with Ruby for less than 48 hours, so expect me to be dumb!
I have set up validation on a model which all works fine. However I want to modify the message retuned by the uniqueness constraint. The message needs to be returned by a method which does some additional processing.
So for example if i try to create a new user model which the "name" attribute set to "bob", and the bob named account already exists, i want to display a message like "Bob isn't available, click here to view the current bob's account".
So the helper method will lookup the current bob account, get the localised string, and do the usual string placement of the localised string, placing the name, link to account and any other information i want into the message.
Essentially i want something like:
validates_uniqueness_of :text, :message => self.uniquefound()
def uniquefound()
return "some string i can make up myself with #{self.name}'s info in it"
end
No doubt that's completely wrong...
If this isn't possible, i've found i can use users.errors.added? to detect if the name attribute has a unique error added, from there i can probably do some hash manipulation to remove the unique error, and place my own in there either in an "after_validation" callback or in the controller... haven't worked out exactly how to do that, but that's my fallback plan.
So, is there a way of providing a class method callback for the message, AND either passing the current model to that method (if its a static method) or calling it so self inside that method is the model being validated.
Update
Trawling through the rails sourcecode, i have found this private method that is called during adding an error to the error class
private
def normalize_message(attribute, message, options)
case message
when Symbol
generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
when Proc
message.call
else
message
end
end
end
So if i pass a method as the message, i presume that this method is used to call my function/method and get teh return data. However it appears it's not called in the scope of the current object, nor does it pass in the object for which the error pertains...
So, if my digging is on the right path, it appears that calling a method on the current object to the message, or calling a static method passing the object, is not possible.
You could do something like this if you really need this functionality.
Class User < ActiveRecord::Base
include Rails.application.routes.url_helpers
....
validate :unique_name
def unique_name
original_user = User.where("LOWER(name) = ?", name.downcase)
if original_user
message = "#{name} is not available "
message << ActionController::Base.helpers.link_to("Click Here",user_path(original_user))
message << "to view the current #{name}'s account."
errors.add(:name,message)
end
end
end
EDIT
with a Proc Object instead
Class User < ActiveRecord::Base
include Rails.application.routes.url_helpers
....
validates_uniqueness_of :name, message: Proc.new{|error,attributes| User.non_unique(attributes[:value]) }
def self.non_unique(name)
original_user = User.where("LOWER(name) = ? ", name.downcase)
message = "#{name} is not available "
message << ActionController::Base.helpers.link_to("Click Here",Rails.application.routes.url_helpers.user_path(original_user))
message << "to view the current #{name}'s account."
end
end
These will both add the following error message to :name
"Name Bob is not available Click Hereto view the current Bob's account."