Mutual friendship in Rails

19 April 2015

If you go to StackOverflow, you'll find hundreds of implementations for friendships. Creating a Twitter subscription model with a clear distinction between following and followers is easy. Mutual friendship, on the other hand, requires more code.

In this case, friends are added only after the user's approval:

  1. Foo sends a friend request to Bar.
  2. Bar accepts the friend request from Foo.
  3. Foo and Bar are now friends.

We'll have two join models: FriendRequest and Friendship. Each will have its controller with RESTful routes.

Friend requests

First, we'll create FriendRequest resource.

$ rails generate resource FriendRequest user:references friend:references
$ rake db:migrate

Then, we'll establish the has_many :through association between users. You can read more about associations here.

# app/models/user.rb:
class User < ActiveRecord::Base
  has_many :friend_requests, dependent: :destroy
  has_many :pending_friends, through: :friend_requests, source: :friend
end

We're using source: :friend because we've changed the association name (it should've been friends, but it is reserved for "original" friends).

Because the FriendRequest model has a self-referential association, we have to specify the class name:

class FriendRequest < ActiveRecord::Base
  belongs_to :user
  belongs_to :friend, class_name: 'User'
end

The corresponding controller will have four actions:

  1. index: view incoming and outgoing friend requests
  2. create: send a friend request to another user
  3. update: to accept friend requests
  4. destroy: to decline friend requests

Sending friend requests

To send a request we'll create a new FriendRequest record with our current user as a friend:

# app/controllers/friend_requests_controller.rb
class FriendRequestsController < ApplicationController
  before_action :set_friend_request, except: [:index, :create]

  def create
    friend = User.find(params[:friend_id])
    @friend_request = current_user.friend_requests.new(friend: friend)

    if @friend_request.save
      render :show, status: :created, location: @friend_request
    else
      render json: @friend_request.errors, status: :unprocessable_entity
    end
  end
  ...
  private

  def set_friend_request
    @friend_request = FriendRequest.find(params[:id])
  end
end

Listing requests

Here, we're simply grabbing all incoming and outgoing requests.

def index
  @incoming = FriendRequest.where(friend: current_user)
  @outgoing = current_user.friend_requests
end
...

Canceling requests

Cancelling a request is equivalent to destroying a friend request record.

def destroy
  @friend_request.destroy
  head :no_content
end

Friendships

Now let's move on to another join model: Friendship. We'll be using this association to get and destroy friends.

$ rails g model Friendship user:references friend:references
$ rails g controller Friends index destroy
$ rake db:migrate

First, we must set up an association:

# app/models/friendship.rb
class User < ActiveRecord::Base
  ...
  has_many :friendships, dependent: :destroy
  has_many :friends, through: :friendships
end

Friendship model looks like this:

class Friendship < ActiveRecord::Base
  after_create :create_inverse_relationship
  after_destroy :destroy_inverse_relationship

  belongs_to :user
  belongs_to :friend, class_name: 'User'

  private

  def create_inverse_relationship
    friend.friendships.create(friend: user)
  end

  def destroy_inverse_relationship
    friendship = friend.friendships.find_by(friend: user)
    friendship.destroy if friendship
  end
end

After that, we'll be able to query user records for friends:

User.first.friends # => [Users]

Accepting requests

Next, we'll set up some ancillary methods in FriendRequest model:

# app/models/friend_request.rb
class FriendRequest < ActiveRecord::Base
  ...
  # This method will build the actual association and destroy the request
  def accept
    user.friends << friend
    destroy
  end
end

Thanks to has_many :through association, we're now able to add friends using append (<<) method.

Now it takes just one line of controller code to accept a friend request:

# app/controllers/friend_requests
def update
  @friend_request.accept
  head :no_content
end

Listing friends

The code for Friends controller is straightforward:

# app/controllers/friends_controller.rb
class FriendsController < ApplicationController
  before_action :set_friend, only: :destroy

  def index
    @friends = current_user.friends
  end
  ...
  private

  def set_friend
    @friend = current_user.friends.find(params[:id])
  end
end

Removing friends

Unfriending users is a bit complicated since we must remove friendships from both sides of the association. We'll write a different method that will take this responsibility.

# app/models/user.rb
class User < ActiveRecord::Base
  ...
  def remove_friend(friend)
    current_user.friends.destroy(friend)
  end
end

Now we're able to use this method in our controller:

# app/controllers/friends_controller.rb
def destroy
  current_user.remove_friend(@friend)
  head :no_content
end

Final steps

You might want to add some validations to the models (uniqueness and presence):

validates :user, presence: true
validates :friend, presence: true, uniqueness: { scope: :user }

It's also a good idea to restrict self associations, so the user won't be able to befriend himself. This validation should go into both classes (Friendship and FriendRequest).

validate :not_self

private

def not_self
  errors.add(:friend, "can't be equal to user") if user == friend
end

And users shouldn't be able to send friend requests if it already exists or if they're already friends:

# app/models/friend_request
class FriendRequest < ActiveRecord::Base
  ...
  validate :not_friends
  validate :not_pending
  ...
  private

  def not_friends
    errors.add(:friend, 'is already added') if user.friends.include?(friend)
  end

  def not_pending
    errors.add(:friend, 'already requested friendship') if friend.pending_friends.include?(user)
  end
end