User Presence with Rails using PrivatePub and Redis

Rails is a really nice framework for developing websites, however if you want real-time communication Rails will not be the best option. Socket.IO and Node.js seem to be a better alternative.

Despite that sometimes you have to add real time communications to your Rails app.In this article we use private pub which is a pub/sub gem along with redis database to persist user and preserve user’s online status in a rails application.


Add these gems to your Gemfile.


gem 'private_pub'
gem 'thin'
gem 'redis

Now use bundler to install new gems.

bundle install

Run the generator to create the private pub configuration files.

rails g private_pub:install

Add the JavaScript file to your application.js file manifest.

//= require private_pub

We will use one global channel ‘/presence’ and one private channel for each user ‘/push/:user_id’, the user should subscribe to them both on login.

‘/presence’ will be a channel for all online users to subscribe, while subscribing to a private channel will publish that this particular user is now logged_in/out to all online users after saving that user online status to redis database.

Now to bind the subscription and unsubscription events on a channel we need to change the privatepub.ru file to look like this

# Run with: rackup private_pub.ru -s thin -E production
require "bundler/setup"
require "yaml"
require "faye"
require "private_pub"
require "redis"

# you must pass any environment variables manually
redis = Redis.new

# delete online records
online_users = redis.keys "user_*_online"
redis.del *online_users unless online_users.empty?
puts 'All users are now offline!'

Faye::WebSocket.load_adapter('thin')

PrivatePub.load_config(File.expand_path("../config/private_pub.yml", __FILE__), ENV['environment'] || 'default')

faye_application = PrivatePub.faye_app(timeout: 30, ping: 20)
# subscribe - online
faye_application.bind(:subscribe) do |client_id, channel|
  puts "Client subscribe: #{client_id}:#{channel}"
  unless channel == "/presence"
    user_id = channel.split('/').last
    redis.set("user_#{user_id}_online", true)
    Thread.new do
      PrivatePub.publish_to "/presence", {:user_id => user_id, :status => "online"}
    end
  end
end

# unsubscribe - offline
faye_application.bind(:unsubscribe) do |client_id, channel|
  puts "Client unsubscribe: #{client_id}:#{channel}"

  unless channel == "/presence"
    user_id = channel.split('/').last
    redis.set("user_#{user_id}_online", false)
    Thread.new do
      PrivatePub.publish_to "/presence", {:user_id => user_id, :status => "offline"}
    end
  end
end

run faye_application

User records are deleted on Faye startup in case of Faye crash.

Now in the user’s web page, we can toggle the online/offline icon accordingly.

<% if (user_signed_in? && !(current_user.online?)) %>
    <%= subscribe_to "/push/#{current_user.id}" %>
    <%= subscribe_to "/presence" %>
    
        PrivatePub.subscribe("/presence", function(data, channel) {
                if($("#users").length > 0){
                    if(data.status == "online"){
                        $("#user-"+data.user_id+"-presence").removeClass("offline").addClass("online");
                    }else{
                      $("#user-"+data.user_id+"-presence").removeClass("online").addClass("offline");
                    }
                }
        });
    
<% end %>

Latly, start up Faye server using the rackup file that was generated.

rackup private_pub.ru -s thin -E production

Now you have an application that keeps the user’s online status in real time.

Authored by Amr Kamel