Skip to main content

Group Chat with ActionCable: Part 4 Discussion

General • Asked by Chris Oliver

As for message relay jobs matter for this particular subject of this ActionCable example, do you recommend putting down for its current_user's id or name in these kind of scenarios when it comes to logging down for the Message job queue and delivering it to Sidekiq?

Not sure I'm following the question, can you clarify for me? 🤓

Starting at around 12:29, you were talking about the MessageRelayJob for broadcasting the message and chatroom#id into the job. So, what I am trying to say is let's say that the user wants to know if the message sent out into the channel but says it does not not return anything. Looking at the sever log views, would it be technically correct to display the user's username (if provided in the User model) instead of ID and as well as other methods provided for the user when troubleshooting chat history?

Example:


Class MessageRelayJob < ApplicationJob
def perform(message)
ActionCable.server.broadcast "chatrooms:#{message.chatroom.id}", {

message: MessagesController.render(message),
chatroom_id: message.chatroom_id
# Display the User ID or username (if defined and not nil)
user: current_user.username ||= current_user.id

end

end

Even though, it successfully connects to the websocket and is listening to the specific channel.

I gotcha now. So I would keep everything organized on the Message's ID. You can client side send over a message and you could set it up to send back a message ID. That way you now it arrived server side but it's "processing". Then server side you can store that message and tag all your ActionCable broadcast stuff with the Message record's ID. You'll get a database log of messages which is important for loading up the first time, you can load recent history, and you can use that for troubleshooting. The Message record can have a statemachine on it so you know if it was broadcast or not. Slack basically does this when you're having internet issues. It can tell when something got published or not and keeps track of that nicely. You can flag those messages as "ready for broadcasting" or something and then after you broadcast it in the Relay Job, you could then mark it as complete so that if your job did crash or something, you wouldn't resend it.


this series is really cool, thanks Chris

Glad you like it! 🎉


Hi Chris! Thank you for your lessons!=)

<% if chatroom.chatroom_users.find_by_user_id(current_user) %>
<%= form_for [@chatroom, Message.new], remote: true do |f| %>
<%= f.text_area :body, rows: 1, class: 'form-control', autofocus: true %>
<% end %>
<% else %>
<%= link_to "Join", chatroom_chatroom_users_path(@chatroom), method: :post, remote: true %>
<% end %>

Thanks


Hi, Chris! Great tutorial, thank you very much!
I'm trying to follow it to create a Chat Api. Is it possible? I would receive the messages through an Android app, pass it to my Api and then send the new messages to Android again as json. Could you give me any clues about what do I have to change so far? (I am already not implementing all the views).

Hey Diane,

Two different approaches you could use:

1. Keep it simple and go with a Turbolinks-Android hybrid app where your chat is via the webview on the app. This is simpler and easier to setup.
2. You could do a native Android connection directly to the websocket. For this you'd basically use a standard websocket library for Android and then make the small tweaks necessary to setup the consumer just like the Javascript does. You'd basically be porting that JS into Android code. Not entirely sure on all the details or if someone else has done this already, but that's the rough outline for a native approach.


Chris, how can you have you redis working without configuring and running the server in dev env? For me it raises "`rescue in establish_connection': Error connecting to Redis on localhost:6379 (Errno::ECONNREFUSED) (Redis::CannotConnectError)`" which is logical since the redis server is down.

I have my redis server always running on my laptop on the default port so it is always ready to go. Homebrew comes with instructions on how to start it when you login. If you follow those it will set it up the same way I have.


For those getting error about "Request origin not allowed" and "Failed to upgrade to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)"

If you are running rails server with a specific IP address please add "config.action_cable.allowed_request_origins" to your 'development.rb' file and point it to the IP address you are using

Great point Rick! That's probably a common problem for anyone trying this. Thanks for sharing that. 👍

I have to use Vagrant because I am on Windows, as such I had to bind 'rails server' to the IP address Vagrant is using. So I too suspect this is a common issue.

Also, I had to stop and start 'rails server' whenever I manually create controllers, otherwise I get controller errors.


I am surprised no one is getting the error messages I am getting while doing this part.
When I add remote: true i get " ActionController::InvalidAuthenticityToken
in MessagesController#create" error. (to solve it i must add authenticity_token: true).
When I remove the redirect_to @chatroom i get another error:
MessagesController#create is missing a template for this request format and variant.

request.formats: ["text/html"]
request.variant: [] even if I created a messages/create.js.erb file in my views.
I am on a mac using ruby 2.3.0 with rails 5.0.1 via rvm. Could this be the evil source of my problems?

You might continue on a bit, I ended up refactoring this to use ActionCable to send messages over instead of an AJAX form because you might as well be using it two ways.

I'm not sure what's up with your authentication token, but it does sound like your server side is looking for an HTML response, not a JS response, even though if it was an AJAX request, it should. Sounds like something's not quite right with that somehow. I'm not entirely sure off the top of my head though.

Since no one has reported similar issues I am inclined to believe something is wrong with my setup somehow. For now I'll just press on. Thanks

The authenticity_token: true thing makes me think the same. I haven't ever had to do that, but maybe something has changed more recently that I don't know about. Always possible!

Getting the same error. 
I encountered the same error message. I reviewed the logs and I can see an HTML response, not a JS response. If you follow all the video content from this video and the next video until 2:35 the error message will disappear. I do not know why it happened, but if you just keep going you can get past it.

Same error for me. Will just press on and hope things work when we addd ActionCable.


Hey Chris, I 've got an error when I click on the button "join" => "The action 'show' could not be found for ChatroomUsersController";
But I did exactly what you did in the last videos... I have the same link_to than yours; An idea what's going on? thanks a lot

Hey Brice, you might check your code because the Join button I believe needs to submit a POST request and your error is looking for the show action which implies it sent a GET request instead. The Join button should create a POST request in order to create the record to set you as a user inside the channel, so it should take you to the ChatroomUsersController's create with that POST. Make sure that link has a "method: :post" option on it?

Hey chris, thanks a lot for your reply. Yes of course, I have the "method: :post"... I copy/past all your code but still doesn't work, very strange.... :(
I send you my roots. Hope it can help, if not that's okay Chris ;)

jquery_ujs is used to trigger that. If you've got method post, then you'll want to see if you have JS errors that are causing it not to run that code to intercept the click and submit a POST instead.

Aside from that, I'm not sure. You might clone the repository and see if that works for you and compare your code with it. Probably something small, but super hard to debug over comments. :)


Hello Chris, just a quick question regarding the best practice with jobs: I learned in the past that passing only the id parameter to the job with sidekiq or else, then find the instance inside the job is the best practice. Since you pass the complete instance, I'm wondering if Rails do it for us automagically with ActiveJob? Thank you

Ideally it's better to pass in only the ID. If the record was deleted before the job runs, you can safely quit if you can't find the record assuming it was deleted.

Now I believe that if you pass in an ActiveRecord job to ActiveJob, it will transform it to a GlobalID (https://github.com/rails/gl... / http://edgeguides.rubyonrai... which is a representation of the ID and class name so that you don't have to specifically pass the ID.

If you use Sidekiq, etc directly without ActiveJob it is going to try using that object which is why they recommend using the ID specifically.

ActiveJob + Global ID should look up the record once the job starts (you should see this in the logs) which will make running those jobs safe.


Hey Chris, i've been following the videos for the group chat and so far they're awesome (as are all the videos of yours that i've watched).
I've run into a bit of a stale point though, in your video around 15:31 we're logging the message data to the console, but seems it's not working for me, nothing is printing to the chrome console.

my rails server output is showing:


[ActionCable] [User 2] ChatroomsChannel is streaming from chatrooms:2
Started POST "/chatrooms/1/messages" for ::1 at 2016-10-16 14:45:53 +0100
Processing by MessagesController#create as JS
Parameters: {"utf8"=>"✓", "message"=>{"body"=>"show me"}, "chatroom_id"=>"1"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ? [["id", 2], ["LIMIT", 1]]
Chatroom Load (0.1ms) SELECT "chatrooms".* FROM "chatrooms" WHERE "chatrooms"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.0ms) begin transaction
SQL (0.4ms) INSERT INTO "messages" ("chatroom_id", "user_id", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["chatroom_id", 1], ["user_id", 2], ["body", "show me"], ["created_at", 2016-10-16 13:45:53 UTC], ["updated_at", 2016-10-16 13:45:53 UTC]]
(7.4ms) commit transaction
[ActiveJob] Enqueued MessageRelayJob (Job ID: a83d7365-8e56-4ea6-a03f-db172f5b1c62) to Async(default) with arguments: #<globalid:0x007ff34bd10358 @uri="#&lt;URI::GID" gid:="" slack-clone-rails5="" message="" 16="">>
Message Load (0.3ms) SELECT "messages".* FROM "messages" WHERE "messages"."id" = ? LIMIT ? [["id", 16], ["LIMIT", 1]]
Rendering messages/create.js.erb
[ActiveJob] [MessageRelayJob] [a83d7365-8e56-4ea6-a03f-db172f5b1c62] Performing MessageRelayJob from Async(default) with arguments: #<globalid:0x007ff34c393810 @uri="#&lt;URI::GID" gid:="" slack-clone-rails5="" message="" 16="">>
Rendered messages/create.js.erb (0.6ms)
Completed 200 OK in 113ms (Views: 26.6ms | ActiveRecord: 8.0ms)

[ActiveJob] [MessageRelayJob] [a83d7365-8e56-4ea6-a03f-db172f5b1c62] Chatroom Load (0.1ms) SELECT "chatrooms".* FROM "chatrooms" WHERE "chatrooms"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
[ActiveJob] [MessageRelayJob] [a83d7365-8e56-4ea6-a03f-db172f5b1c62] Performed MessageRelayJob from Async(default) in 31.33ms


************
Which seems all good, just wondered if you had any input why this wouldn't be showing the console.log data

chatrooms.coffee:


App.chatrooms = App.cable.subscriptions.create "ChatroomsChannel",
connected: ->

disconnected: ->

received: (data) ->
console.log data

message_relay_job:


class MessageRelayJob < ApplicationJob
queue_as :default

def perform(message)
ActionCable.server.broadcast "chatrooms:#{message.chatroom.id}", {
message: MessageController.render(message),
chatroom_id: message.chatroom.id
}
end
end

chatrooms_channel:


class ChatroomsChannel < ApplicationCable::Channel
def subscribed
current_user.chatrooms.each do |chatroom|
stream_from "chatrooms:#{chatroom.id}"
end
end

def unsubscribed
stop_all_streams
end
end

not sure what else you would need to check? if you need my repo it's here
https://github.com/ggomersa...

Hope you can help :)

Hard to say off the top of my head. You might just need to put in some more debugging lines to make sure that everything is connecting correctly. Make sure that 1) the JS connects to the websocket by watching the Chrome network logs 2) Make sure that your server side background job is executing by printing out in your logs 3) Make sure your user is connected to the channel so their websocket streams from it correctly.

Probably something small isn't connected right and it should be an easy fix once you figure out what it is. Also here's the link to the final app code for this that might be of help: https://github.com/gorails-...

found it! :)

my message_relay_job.rb file was showing:


message: MessageController.render(message)

instead of:


message: MessagesController.render(message)

It's working a treat now :)

Ah ha! That would do it! Good find. 👍


Hi Chris,

At 5:59, in the connection.rb file you add:

protected

def find_verified_user
if current_user = env['warden'].user
current_user
else
reject_unauthorized_connection
end
end

If I'm not using devise, is "env['warden'].user" just the devise way of authenticate_user ? Is this just to make sure the current_user is the logged_in user? Want to make sure I'm implementing correctly.

Thanks!

Exactly, you can just replace that with however you find the current user normally. 👍

Ok cool. If current user already checks that, do you have any suggestions for how to rewrite that? Would it just be:


def connect
self.current_user = current_user
logger.add_tags "ActionCable", "User #{current_user.id}"
end

Don't worry about this. Worked on it today. Figured it out!

I was going to say, I don't think you can do that because ActionCable doesn't go through Rails controllers. You'd want to set self.current_user equal to the logic that finds the user from the session.

What was your solution? I'm sure other people would love to know it as well!

I created a new ruby symbol "e.g. :actioncable_user_id" specifically for this action cable use.

In 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", "User #{current_user.id}"
end

protected

def find_verified_user
current_user = User.find_by(id: cookies.signed[:actioncable_user_id])

current_user || reject_unauthorized_connection
end

end
end

and in my sessions_helper.rb (used for logging in / logging out)


module SessionsHelper

# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
#below is unique code added for ActionCable find_verified_user
cookies.signed[:actioncable_user_id] = user.id
end
end


Just offering a solution here for some error messages I kept getting. The server error I got was:


Started GET "/cable" for ::1 at 2017-02-09 16:35:06 -0800

ActionController::RoutingError (No route matches [GET] "/cable"):

I added to my routes.rb :


Rails.application.routes.draw do
mount ActionCable.server, at: '/cable'
...
...
end

Not sure why in this repo example action cable works without it, but in my app once I added this everything worked. I'm using Rails '5.0.1' for reference.

Hmm weird, they changed that to be the default URL for Rails in 5 I believe so it shouldn't be necessary. The project I'm using in 5.0.1 doesn't have it and it finds ActionCable just fine. Not sure what's up there.

Definitely, I don't know why either. The only thing I could think of is at one time my app was Rails 4, and then also Rails 5 beta. Perhaps that requirement never got fully uninstalled or something. Beats me. Oh well, works now though!


Hey Chris, Can you elaborate on the difference between what you use chatrooms_channel.rb and chatrooms.coffee for?

My interpretation so far: Chatroom_channel.rb is saying "These are the things we want to listen to" and chatrooms.coffee is where we say "Ok, those things we're listening to, if some new things happen, do these actions".


Hello Chris,

My problem is that my channel is broadcasting but on the client side(the coffee script) didn't received anything, I did add an alert if it was connected and it is connected though, any idea why this happening?


great tuto :)
i now want to manage rights so that the author of a comment can edit it. any ideas?


Hey @excid3:disqus awesome series. My messages get saved to the db and in the response I can also see that turbolinks. But the message does not get added in the front end. I have to refresh the page in order to see it on the front end side. Any thoughts?

I am also getting this, were you able to fix this?



Since rails 5.2 the remote: true doesn't really work anymore for the form_for when you are trying to submit a new chat message in JS. I found that no matter how I tried to get it to answer with remote: true as JS it would always post it as html. The answer for rails 5.2+ is to use form_with as form_for is being depreciated. Also by default form_with sends all submits as JS, so we don't use remote: true.

In your show.html.erb, change the form_for line to this:
<%= form_with model: [@chatroom, Message.new], :html => { :id => 'chatroom_form' } do |f| %>

In your create.js.erb change the reset line to this:
$("#chatroom_form")[0].reset()

And if you are still doing the area thing (I didn't like that style personally) you need to change the keypress line to this:
  $("#message_body").on "keypress", (e) ->


I've figure out the problem. a trick.
after use form_with, you must change the code in this video.
my code in chatrooms.coffee:
document.addEventListener 'turbolinks:load', ->
document.getElementById("new_message").addEventListener 'keypress', (e) ->
if e && e.keyCode == 13
e.preventDefault()
url = this.action
data = new FormData(this)
Rails.ajax({
type: "POST"
url: url
data: data
dataType: "json"
})

Do you have a repo on github with a working Rails 5.2 version, tried these updates and it is still trying to redirect.


Hi Chris,

Question: Two errors that I'm hoping you can assist with:

  1. https://gyazo.com/19aaab916790ec3c1a41476cc621398b It says the redis gem is not loaded. But it is. I found some posts on StackOverflow that says the redis gem 4.0.2 doesn't work with ActionCable, so you needed to use 3.3. But I installed 3.3 and still no success. Is there perhaps some type of additional setup? I did make sure to change the cable.yml as follows:
development:
  adapter: redis
  url: redis://localhost:6379/1

test:
  adapter: async

production:
  adapter: redis
  url: redis://localhost:6379/1

Anything obvious that I am missing, or forgetting to do?

  1. This may be relate to the above question. In console, when I type a message, it disappears properly. And posts when I refresh the page. But it's not showing up in the other console as an object, as the video suggests it should. Thoughts?

Thanks very much!
-Monroe

Update: Chris helped me out. Here's the thing that was covered in another video, but not made clear in this video: installing the redis gem alone is not enough: YOU MUST ALSO INSTALL REDIS TO YOUR COMPUTER AND LAUNCH IT. It's a server, just like postgresql. And it needs to be running. So if you have the same problems I was having, it may be because you didn't have redis actually installed, and it wasn't running.

Another thing I learned: in a model, if you have any associations through another, you must first declare the 'another' on the line above the 'through' association, or you'll get an error.

Thanks Chris!

TIP: One more thing: and if the redis server isn't running, and you try to go to a page where the redis server isn't running, the rails server will perhaps crash. Therefore, it seems best to launch the redis server prior to launching the rails server, so you don't get an error if you end up heading to a page that requires the redis server.


TIP: The coffeescript files ARE indent dependent. With ruby (and I also believe generic javascript), whitespace doesn't matter. However, we learned that with these coffeescript/javascript files, if you have even ONE indent wrong, it may mess everything up. Be CERTAIN that the indentation is correct in the coffeescript files. We had the most annoying issue where the messageRelayJob kept firing multiple times, posting blank messages. The solution? One tiny indent needed to move one space to the left.

Be VERY meticulous with your whitespace in your coffeescript files! :D


You sir, are awesome! This is exactly what I've been looking for.


Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 24,647+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.