Rails & Amistad: Friendships Controller

I had to build simple friendship functionality into a Rails 3.0 app recently, and discovered this great gem, Amistad, that handles all the friend logic for you. Setting up a friendship system between users is a bit tricky because you need to engineer it in both directions; a friendship can only be instantiated by one user, but the both users need to see each other as friends. Ryan Bates does a really good job of explaining this in one of his RailsCasts (Self-Referential Associations).

Amistad

Amistad is a gem that will supply you with all the logical methods for setting up friendships between users. What it doesn't do, though, is give you an interface or controller for letting users friend each other ... and that's good, because you typically want a lot of control over that anyway.

So, here is a basic version of the friendship controller I wrote for the app using Amistad. The user model here is driven by Devise, but I think it would work on any user model.

First, install the gem according to the documentation.

Next, add the Amistad bindings to your user model.

class User < ActiveRecord::Base

  devise :database_authenticatable, :registerable,
    :recoverable, :rememberable, :trackable, :validatable, :token_authenticatable

  attr_accessible :email, :password, :password_confirmation, :remember_me

  include Amistad::FriendModel

end

Then, create a controller to handle the friendships, and a resourceful route.

$ rails g controller friendships
# config/routes.rb
resources :friends, :controller => 'friendships', :except => [:show, :edit] do
  get "requests", :on => :collection
  get "invites", :on => :collection
end

The Controller

class FriendshipsController < ApplicationController
  
  before_filter :authenticate_user!
  
  def index
    @friends = current_user.friends
  end

  def new
    @users = User.all :conditions => ["id != ?", current_user.id]
  end

  def create
    invitee = User.find_by_id(params[:user_id])
    if current_user.invite invitee
      redirect_to new_friend_path, :notice => "Successfully invited friend!"
    else
      redirect_to new_friend_path, :notice => "Sorry! You can't invite that user!"
    end
  end

  def update
    inviter = User.find_by_id(params[:id])
    if current_user.approve inviter
      redirect_to new_friend_path, :notice => "Successfully confirmed friend!"
    else
      redirect_to new_friend_path, :notice => "Sorry! Could not confirm friend!"
    end
  end
  
  def requests
    @pending_requests = current_user.pending_invited_by
  end
  
  def invites
    @pending_invites = current_user.pending_invited
  end

  def destroy
    user = User.find_by_id(params[:id])
    if current_user.remove_friendship user
      redirect_to friends_path, :notice => "Successfully removed friend!"
    else
      redirect_to friends_path, :notice => "Sorry, couldn't remove friend!"
    end
  end 
  
end

So, this is a semi-restful approach to invited, confirming, and removing friendships between users. You'll notice that I removed the "show" and "edit" actions because they don't fit the functionality we're trying to accomplish. Instead of "showing" a friendship, we would want to just show that user's profile. And there aren't enough configurable qualities to warrant an "edit" interface.

Here's a break down of what each of these actions are doing.

index

This method just returns all of the current user's friends; thanks to Amistad, we can get all friends through one simple method ... regardless of how the friendship was created. Remember, the current user could have invited some friends, and could have been invited by others.

new

In the new action, I simply listed out all users that the current user could invite as friends. Here's how I render out the potential friends in the view. I eventually made this one more robust to totally filter out current friends, and to search by email/name/etc. But here's the simple version.

%ul
  - for user in @friends
    %li
      - if current_user.friend_with? user
        = user.email
        |
        You are already friends!
      - elsif current_user.invited? user
        = user.email
        | 
        Pending request ...
      - elsif user.invited? current_user
        = user.email
        |
        = link_to "Confirm friend?", friend_path(user), :method => "put"
      - else
        = user.email
        |
        = link_to "Add friend?", friends_path(:user_id => user), :method => "post"

create

The create action sends a request to another user to start a friendship through the current_user.invite method.

update

I'm using the update method only to confirm an invitation that another user sent to the current user with the current_user.approve call.

requests

The request action spits out all pending invitations from other users. You have the user confirm the friendship with a PUT request to the update method.

link_to "Confirm friend?", friend_path(user), :method => "put"

invites

This would be pending invitations that the current user sent out to other users. In my view, I let the current user cancel the request by linking to the destroy method.

= link_to "Cancel invite?", friend_path(user), :method => "delete"

destroy

The destroy method can be used to remove a friend (regardless of how the friendship was created), or to cancel a a pending invite with the currentuser.removefriendship method.

Come Work With Me

If you live in NYC, and you're a kick-ass developer, please come work with me. The company I work for, FreeAssociation, is aggressively hiring developers. I need folks who have some skills in:

  • HTML5, CSS3, JavaScript
  • PHP, Ruby, MySQL
  • Symfony, Rails, Codeigniter, WordPress
  • Doctrine ORM
  • Compass / Sass
  • iOS
  • sysadmin chops

We have a really awesome team of designers, developers, and managers. It's a small outfit with 10-15 people at any given time. We're constantly trying new stuff, and have a great view of the Brooklyn Bridge. Please get in touch with us! Say, "Kyle sent me."

Read a more!

Fork me on GitHub