Building a Chat with Rails, Faye and private_pub

This week, I set out to build a simple chatting application with Rails. This idea was born of a super fun game that I like to play during car rides--Title-ize. The premise is this: one person comes up with a fake movie title ("The Purple Statue", for example), and the other person must provide a quick synopsis or tagline.

Seeing as there are lots of places you can be bored by yourself--working, waiting for the dentist, etc.--I wanted to adapt this game into a web app that you can play remotely with friends (you can play it here, btw).

So, I began simply enough--with a Postgresql database, and models, views and controllers for users, conversations and messages. But then I ran into a problem--how can a user actually receive and view a new message in real time? In other words, when a user hits "send" on their new message, that message gets sent to the controller, created and saved but not rendered in the chat box. How does the server know to send the new messages back to both clients, i.e. the two people participating in the chat?

In a normal request and response cycle, a request is sent from the client (you, when you are googling pictures of cats at work), to the server (the place where the cat pictures are stored). The server sends the requested information (adorable cat picture) back to you, the client. (You are so productive at work, by the way).

With HTTP, the server only sends information to the client upon request. The server is not sentient (there's no such thing--I think) and cannot send information any other way.

Until WebSockets.

What are WebSockets?

WebSockets are a protocol built on top of TCP. They hold the connection to the server open so that the server can send information to the client--not only in response to a request from the client. Web sockets allow fro bi-directional communication between the client and the server.

However, it is very difficult to keep a socket open to a Rails app. Luckily for us, there's Faye.

What is Faye?

Faye is a general server that can handle publishing/subscribing asynchronously. It can connect different clients and let clients know when certain events have occurred.

How does it work?

All Faye clients need a central messaging server to communicate with; the server records which clients are subscribed to which channels and routes messages between clients.

In a Rails app, in order for our users to be notified in real time about new messages they've received--i.e., in order for users of our app to "chat"--we will tell our app to listen to the Faye server and we will tell our Faye server to listen to our Rails app. We can 'subscribe' certain users to certain channels and 'publish' new messages to the appropriate channel. The Faye server is, for all intents and purposes, our open socket, and can be maintained easily without tying up Rails processes and slowing down our app.

Now that we understand why we need to maintain an open connection in order for messages to be sent from the server to the client, let's talk about how we can "subscribe" users to channels and "publish" the appropriate messages for each user.

The Private Pub Gem

Private Pub, a gem developed by Ryan Bates, makes it easy to publish and subscribe to messages through Faye. Private Pub allows you to quickly and easily set up the Faye server and connect it to your app and provides a series of helper methods to help you send ajax requests to the appropriate controller actions when a user "sends" (submits) a new message.

Let's walk through the steps for setting up our chat with Private Pub.

  1. Add gem 'private_pub and gem thin to your Gemfile and bundle install. Faye's documentation discusses set-up with Thin. It should support other web servers, such as Puma, but the documentation on this is sparse. I recommend using Thin.
  2. Run rails g private_pub:install in your command line. This generates the private_pub.yml file in your config directory. This file instructs your Rails app to maintain a connection to another server, run by Faye. Running this command will also generate a secret token, specific to your app, that will preform a handshake with the Faye server and keep your connection secure. Your config/private_pub.yml should look something like this:
    development:
    server: "http://localhost:9292/faye"
    secret_token: "secret"
    test:
    server: "http://localhost:9292/faye"
    secret_token: "secret"
    production:
    server: "https://priv-pub.herokuapp.com/faye"
    secret_token: "XXXXXXXXXXXXXXXXXX"
    signature_expiration: 3600 # one hour`

This file configures a server for each environment. You may notice that the production server is set to a heroku app. More on that later when we discuss deployment.

3 . Add //= require private_pub to your application.js file.

4 . Rackup the connection to the Faye server, using the rackup file that the gem generates. In another terminal window in the root directory of your app, run: rackup private_pub.ru -s thin -E production

Now we're ready to include the Private Pub helper methods in our views.

Using subscribe_ to and publish_to

We already know that our Conversation show page needs a form for submitting new messages, as well as an area to display existing messages. This is, in essence, our "chat". Let's take a look at views/conversations/show.html.erb:

< div class="chatboxcontent">
    <% if @messages.any? %>
        <%= render @messages %>
    <% end %>
< /div>
  
< div class="chatboxinput">
    <%= form_for([@conversation, @message], :remote              
     => true, :html => {id: "conversation_form_# 
`   {@conversation.id}"}) do |f| %>
    <%= f.text_area :body, class:                                   `   "chatboxtextarea", 
       "data-cid" => @conversation.id %>
        
    < div class="chat-form-btns">    
         <%= f.submit "send", class: "btn btn-
         primary btn-sm btn-submit"%>
        <%= f.submit "New Title!", class: "btn btn-             
         success btn-sm btn-new-title"%>
    < /div>

  <% end %>
    
< /div>

Here we have a form_for a new message, built on a conversation (in this app, a conversation has_many messages and a message belongs_to a conversation.)

The above form for a new message posts to the create action of the MessagesController. The form has the :remote => true attribute, so it will send the necessary information for creating a new message on this conversation Ajaxically. But, we can not yet receive/view new messages in real time.

At the bottom of the page, we need to add the subscribe_to method:

%= subscribe_to conversation_path(@conversation) %>

This has the effect of outputting the following script:

< script type="text/javascript">
  PrivatePub.sign({
    channel: "/conversations",
    timestamp: 1302306682972,
    signature: "dc1c71d3e959ebb6f49aa6af088d",
    server: "http://localhost:9292/faye"
  });
< /script>

The channel (in this case, the index action of the Conversations controller) is the route that this particular view is now listening to. We've also specified that it listen to the Faye server that private pub helps us to run.

The signature and timestamp will ensure that the user can only access this particular channel. The signature will automatically expire when it times out (according to the duration set in your config/private_pub.yml.

Okay, let's take a look at the flow of messages through out app so far:

  1. On the conversations show page, we have a form for a new message. When we submit that form (or send a new message in our chat), we are posting to the create action of the Messages controller. Further more, this view is subscribed to the conversation_path(@conversation), so it is listening for any updates from that channel.

  2. The create action of the Messages controller will instantiate and save a new message and then render create.js.erb

  3. create.js.erb will invoke some javascript that will append new messages to the chat box on the conversation show page.

But how will the javascript invoked in the above file send the new messages back to the correct view? In other words, how will it append messages to only the conversation from which the Messages controller received the post request? That's where the publish_to helper method comes in.

We add publish_to conversation_path(@conversation) to views/messages/create.js.erb. This sends a post request to the Faye server, instructing it to send the new message back to the browser.

So, our entire publish-subscribe cycle looks like this:

Now we have a fully functioning chat. Good job us!

Remember that, in order for the publish-subscribe cycle to function, we are running a second serve with Faye in the background. In our development environment, we simply racked up that server in another terminal window and ran it on localhost:9292. To learn about deploying your chat application with Heroku and running your secondary Faye server remotely, check our the next post.

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus