Asynchronous Chat With Rails and ActionCable

By Ilya Bodrov

Rails 5 has introduced many new cool features and ActionCable is probably the most anticipated one. To put it simply, ActionCable is a framework for real-time configuration over web sockets. It provides both client-side (JavaScript) and server-side (Ruby) code and so you can craft sockets-related functionality like any other part of your Rails application. I really like this new addition and recommend giving it a shot.

There are a handful of introductory tutorials on the Internet explaining how to get stated with ActionCable, however students often ask me how to introduce file uploading functionality over web sockets. This topic is not really covered anywhere, so I decided to research it myself.

In this two-parted tutorial we will create a basic chat application powered by Rails 5.1 and ActionCable with the ability to upload files. We will utilize Clearance for authentication as well as Shrine and FileReader API for file uploading.

The source code for this article is available at GitHub. The final application will look like this:

In the first part of the tutorial we are going to create a new application, introduce basic authentication, integrate ActionCable and utilize ActiveJob for the broadcasting. Shall we proceed?

Laying Foundations

Start off by creating a new Rails application:

rails new ActionCableUploader

At the time of writing the newest version of Rails was 5.1.1, so I am going to use it for this demo. Please note that ActionCable is not included in Rails 4 and older.

We will require a basic authentication system. To speed things up, we are not going to write it from scratch but rather use some third-party solution. The most obvious choice that comes to mind is probably Devise but let’s make things a bit more interesting and use another solution called Clearance. This gem is similar to Devise, but is intended to be smaller and simpler. After all, we really do need something simple, as this article is not about authentication solutions. Clearance was created by Thoughtbot, the guys who brought us Paperclip, FactoryGirl and other great solutions.

So, drop a new gem into the Gemfile:

gem 'clearance', '~> 1.16'

and then run:

bundle install
rails generate clearance:install

The latter command is going to equip your application with the Clearance’s code. It is going to perform the following operations:

  • Create a User model and the corresponding migration. If you already have a model with such name, it will be tweaked properly
  • Create an initializer file for Clearance. You are welcome to check it out and modify as needed
  • Insert a Clearance::Controller module into the ApplicationController
  • Make you a coffee (well, actually it won’t)

When you are ready, apply the migration:

rails db:migrate

That’s it, the preparations are done and we can move to the next section!

Adding Chat Page

What I want to do now is create the chat page and restrict access to it. The corresponding controller will be called ChatsController. Add a root route now:

# config/routes.rb
root 'chats#index'

Don’t forget to create the controller itself:

# controllers/chats_controller.rb
class ChatsController < ApplicationController
  before_action :require_login

  def index
  end
end

The before_action :require_login line, as you’ve probably guessed, restricts access to all actions of the current controller. This action does pretty much the same as the authenticate_user! method in Devise.

Now create a views/chats/index.html.erb view that will have only a header for now:

<h1>Demo Chat</h1>

Lastly, populate your application layout with the following contents to display a sign out link and flash messages (if any):

<!-- views/layouts/application.html.erb -->
<% if signed_in? %>
  Signed in as: <%= current_user.email %>
  <%= button_to 'Sign out', sign_out_path, method: :delete %>
<% else %>
  <%= link_to 'Sign in', sign_in_path %>
<% end %>

<div id="flash">
  <% flash.each do |key, value| %>
    <%= tag.div value, class: "flash #{key}" %>
  <% end %>
</div>

Now start the server and navigate to http://localhost:3000. You should see a page similar to this one.

Note that you are automatically redirected to the Sign In page as you are not yet logged in.

Register with some sample credentials—after that you should be able to see the chat page which means everything is working just fine.

Messages

Now let’s create a new model and the corresponding table. I’ll call the model Message which is quite an unsuprising name. It will have a body and a foreign key to establish an association to the users table:

rails g model Message user:belongs_to body:text
rails db:migrate

Make sure that your models have the proper associations set up, as we want each message to have an author (that is, a user):

# models/user.rb
has_many :messages, dependent: :destroy
# models/message.rb
belongs_to :user

Brilliant. Next, of course, we’ll need a form to actually send a message. To render it, I am going to use a new helper method called form_with introduced in Rails 5 which is meant to replace form_for and form_tag (though the latter methods are still supported). The form will be processed by JavaScript, so I’ll use # for the URL. Place the following code into your views/chats/index.html.erb view:

<div id="messages">
  <%= render @messages %>
</div>

<%= form_with url: '#', html: {id: 'new-message'} do |f| %>
  <%= f.label :body %>
  <%= f.text_area :body, id: 'message-body' %>
  <br>
  <%= f.submit %>
<% end %>

Note that the form_with does not generate any ids for the tags so I am adding them manually to further select these elements using JS.

I’ve also provided the #messages block to render all the messages. This requires the views/messages/_message.html.erb partial to be present, so add it now:

<div class="message">
  <strong><%= message.user.email %></strong> says:
  <%= message.body %>
  <br>
  <small>at <%= l message.created_at, format: :short %></small>
  <hr>
</div>

Lastly, load the messages inside the index action:

# chats_controller.rb
def index
  @messages = Message.order(created_at: :asc)
end

This will sort them by creation date, ascending. Your chat page will look something like this:

Okay, now it is time for the ActionCable to step into the limelight.

ActionCable: Time for Action!

Our next task is to enable real-time conversation between the client and the server powered by the ActionCable’s magic. Let’s start with adding some configuration:

# config/environments/development.rb
config.action_cable.url = 'ws://localhost:3000/cable'
config.action_cable.allowed_request_origins = [ 'http://localhost:3000', 'http://127.0.0.1:3000' ]

Next, a route:

# routes.rb
# ...
mount ActionCable.server => '/cable'

Lastly, meta tags:

<!-- views/layouts/application.html.erb -->
<!-- ... -->
<%= action_cable_meta_tag %>
<%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<!-- ... -->

Now let’s take care of the client and write some CoffeeScript code. Create a new file app/assets/javascripts/channels/chat.coffee with the following contents:

jQuery(document).on 'turbolinks:load', ->
  $messages = $('#messages')
  $new_message_form = $('#new-message')
  $new_message_body = $new_message_form.find('#message-body')

  if $messages.length > 0
    App.chat = App.cable.subscriptions.create {
      channel: "ChatChannel"
      },
      connected: ->

      disconnected: ->

      received: (data) ->

      send_message: (message) ->

Here we are checking if the #messages block is present on the page and, if yes, set up a new subscription to the ChatChannel. This channel will be used to communicate with the server in real time. Note that there are a bunch of callbacks that you can use: connected, disconnected and received. send_message will be used to actually forward the messages to the server. This new file will be loaded automatically as javascripts/cable.coffee requires the channels folder by default.

One thing to note, however, is that Rails 5.1 apps do not include jQuery as a dependency anymore, so you’ll need to add it yourself:

# Gemfile
gem 'jquery-rails

Run:

bundle install

and include jQuery to the javascripts/application.js file:

//= require jquery3

I am including the latest version of jQuery to support only the modern browsers, but you can also choose versions 1 or 2.

Now we need to listen for the form submit event, prevent the default action and call the send_message method defined for the channel instead:

jQuery(document).on 'turbolinks:load', ->
    # ...
    if $messages.length > 0
        # ...
        $new_message_form.submit (e) ->
          $this = $(this)
          message_body = $new_message_body.val()
          if $.trim(message_body).length > 0
            App.chat.send_message message_body

          e.preventDefault()
          return false

Here I am checking if the body has at least one character and call the send_message method if it is true. Nothing complex going on here.

Next, flesh out the send_message method. What it needs to do is receive the body of the message and forward if to the server where it will be stored to the database. Note that this method will not output anything to the page—it should happen inside the received callback.

# ...
send_message: (message) ->
  @perform 'send_message', message: message

@ means this in CoffeeScript. 'send_message' argument is the name of the method to call on the server-side which we will create in a minute.

Lastly, code the received callback to clear the textarea and render a new message:

# ...
received: (data) ->
  if data['message']
    $new_message_body.val('')
    $messages.append data['message']

That’s it, we have finished coding the client-side! The server-side awaits, so proceed to the next section.

ActionCable: Server-Side

If you have played The Witcher series, you know that the sword of destiny has two edges. So as ActionCable. Therefore, let’s take of the server-side now.

Create a new app/channels/chat_channel.rb file that will process the messages sent from the client-side:

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_channel"
  end

  def unsubscribed
  end

  def send_message(data)
  end
end

There are two callbacks here that are run automatically: subscribed (that runs as soon as the new client subscribes to the channel using App.cable.subscriptions.create code we’ve written a moment ago) and unsubscribed. send_message is the method that is called by the following line of code in our chat.coffee file:

@perform 'send_message', message: message

Note, by the way, that the files inside the app/channels directory are not auto-reloaded (even in development environment), so you must restart the server after modifying them.

The data local variable contains a hash so we can access the message’s body quite easily to save it to the database:

# ...
def send_message(data)
  Message.create(body: data['message'])
end

There is a problem, however: we don’t have access to the Clearance’s current_user method from inside the channel’s code, therefore it is not possible to enforce authentication and associate the created message to a user.

To fix this problem, the current_user should be defined manually. We are going to employ the methods similar to the ones provided in the Clearance’s session.rb file:

# app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_current_user
      reject_unauthorized_connection unless self.current_user
    end

    private

    def find_current_user
      if remember_token.present?
        @current_user ||= user_from_remember_token(remember_token)
      end

      @current_user
    end

    def cookies
      @cookies ||= ActionDispatch::Request.new(@env).cookie_jar
    end

    def remember_token
      cookies[Clearance.configuration.cookie_name]
    end

    def user_from_remember_token(token)
      Clearance.configuration.user_model.find_by(remember_token: token)
    end
  end
end

The following code simply tries to find a currently logged in user by a remember token stored in the cookie (the cookie’s name is taken from the Clearance configuration). The user is then assiged to the self.current_user. If, however, the user cannot be found, we reject connection effectively disallowing to communicate using the channel. The connect method is called automatically each time someone tries to subscribe to a channel, so there nothing else we need to do here.

Now return to the ChatChannel and tweak the send_message method a bit:

# ...
def send_message(data)
  current_user.messages.create(body: data['message'])
end

At this point our ActionCable setup is finished. Later you can add other channels using the same principle. There is, however, one last thing to do (yeah, there is always “one last thing”, isn’t it?). After the message is stored in the database, it should be broadcasted to all users who are subscribed to the channel. The client code will then run the received callback and render the new message. So, let’s do it now!

Callback and (Very) ActiveJob

We are going to employ a model callback to broadcast a newly created message. However, I’d like to perform this task in background, therefore using the ActiveJob for this task seems like a good idea as well.

Here is the code for the Message model:

# models/message.rb
class Message < ApplicationRecord
  belongs_to :user

  validates :body, presence: true

  after_create_commit :broadcast_message

  private

  def broadcast_message
    MessageBroadcastJob.perform_later(self)
  end
end

First of all, I’ve added a very basic validation rule to ensure the body is present. Next, there is a new after_create_commit callback that runs only after the commit was performed. Inside the corresponding method we are queueing the broadcasting job while passing self as an argument. self in this case points to the created message. Using the background job here is convenient because later you may extend it and, for example, send notification emails to the users saying that there is a new message waiting for them.

The background job itself is quite simple:

# app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast 'chat_channel', message: render_message(message)
  end

  private

  def render_message(message)
    MessagesController.render partial: 'messages/message', locals: {message: message}
  end
end

We queue the job with a default priority. Inside the perform action we broadcast the message rendered by the MessagesController. Note that the same partial created earlier is utilized here.

The MessagesController does not exist, so create it now:

# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
end

And that’s it! Our messages are now saved and broadcasted properly, so you can boot the server, navigate to the main page of the site and try to chat with yourself. Note that after you load the page, a request to the /cable will be performed (you may observe it using Firebug or a similar tool):

To make process a bit more interesting you may open two separate browser windows and note that messages appear in both of them nearly instantly:

Inside the terminal you should see an output like this:

Conclusion

This ends the first part of the tutorial. We have crafted the real-time chatting application that can now be extended quite easily. Throughout the article you have learned how to:

  • Integrate Clearance gem
  • Code the client-side for ActionCable
  • Code the server-side for ActionCable
  • Enforce server-side authentication
  • Use ActiveJob to broadcast messages

In the second part we will finalize this application and allow the users to upload files via ActionCable with the help of the Shrine gem and FileReader Web API.

So, stay tuned and see you soon!

Source:: scotch.io