Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing a facebook-style messaging system, where a user can be in either of two different columns of another model

I am trying to set up a messaging system similar to facebook where you have a list of messages sorted by conversations between two users (it is not important for multiple recipients at the moment, but maybe if I'd used a smarter design then it can be easily implemented in the future. I don't think it would be easy to add to what I currently have.) I have something kind of working, but I am unable to implement a few features. I am pretty new to rails / web programming, so any and all tips/hints/solutions is much appreciated.

So I have three relevant models: User, Conversation, and Messages. User has many conversations, Conversation has many messages and belongs to Users, and Message belongs to Conversation. Okay, so this is what the models look like:

User: has relevant fields ID:int, username:string

class User < ActiveRecord::Base

  has_many :conversations, :class_name => "Conversation", :finder_sql =>
            proc { 
            "SELECT * FROM conversations " +
            "WHERE conversations.user1_id = #{id} OR conversations.user2_id = #{id} " +
            "ORDER BY conversations.updated_at DESC" }

Conversation: has relevant fields: ID:int, user1_id:int, user2_id:int, user1_deleted:boolean, user2_deleted:boolean, created_at:datetime, updated_at:datetime

class Conversation < ActiveRecord::Base
  has_many :messages  

  belongs_to :participant_one, :class_name => "User", :foreign_key => :user1_id
  belongs_to :participant_two, :class_name => "User", :foreign_key => :user2_id

  private

    def self.between(user1, user2)
      c = Conversation.arel_table
      Conversation.where(c[:user1_id].eq(user1).and(c[:user2_id].eq(user2)).or(c[:user1_id].eq(user2).and(c[:user2_id].eq(user1))))

Message: has relevant fields: id:int, conversation_id:int, author_id:int, content:text, created_at:datetime, updated_at:datetime

class Message < ActiveRecord::Base
  belongs_to :conversation, :touch => true  

I'm not really sure if I need participant_one and participant_two, but I use

  def conversation_partner(conversation)
    conversation.participant_one == current_user ? conversation.participant_two : conversation.participant_one
  end

in a ConversationHelper so that in views, I can show the other participant.

So this basically works. But one of the complications I have is that I do not really distinguish the users very well in the Conversation, a user can be in either the user1 field or the user2 field. So I need to constantly look for the user in one or the other field, e.g. in the finder_sql of the User has_many declaration. Also, when I create a new message, I first search to see if there's a Conversation parameter, or if there isn't one, see if there's a conversation between the two users, and if not, then create a new conversation. (You can either send a message from the conversation index (like a reply), or the current_user can be viewing the another user and click on the "send this user a message" link. The messagecontroller looks like this, and uses that self.between method in the Conversation model:

class MessagesController < ApplicationController
  before_filter :get_user
  before_filter :find_or_create_conversation, :only => [:new, :create]

  def new
     @message = Message.new
  end

  def create
    @message = @conversation.messages.build(params[:message])
    @message.author_id = current_user.id
    if @message.save
      redirect_to user_conversation_path(current_user, @conversation), :notice => "Message sent!"
    else
      redirect_to @conversation
    end
  end

  private
     def get_user
      @user = User.find(params[:user_id])
    end

    def find_or_create_conversation
      if params[:conversation_id]
        @conversation = Conversation.find(params[:conversation_id]) 
      else
        @conversation = Conversation.between(@user.id, current_user.id).first or @conversation = Conversation.create!(:user1_id => current_user.id, :user2_id => @user.id)
      end
    end

(my routes look like this:)

  resources :users do
    resources :conversations, :only => [:index, :create, :show, :destroy] do
      resources :messages, :only => [:new, :create]
    end
    resources :messages, :only => [:new]
  end

So now, I am having problems trying to set the user1_deleted or user2_deleted flags. (and similarly if/when i implement a read/up-to-date flag). The problem is that because the same User can have many conversations, but he can either be the user1 or the user2, it becomes difficult to find him. I was thinking I can do something like this in the Conversation model:

def self.active(user)
  Conversation.where(which_user?(user) + "_deleted = ?", false)
end

def self.which_user?(user)
  :user1_id == user ? 'user1' : 'user2'
end

But then you can't run it an entire conversation unless you iterate through each of the User's conversation one by one, because sometimes he is user1, and sometimes he is user2. Should I ditch this whole approach and try a new design? If so, does anyone a possible approach that would be more elegant/perform better/actually work and still meet the same needs?

This is a pretty long question, so I appreciate anyone willing to wade through all this with me. Thanks.

like image 260
kindofgreat Avatar asked Nov 20 '11 22:11

kindofgreat


People also ask

Which protocol is used when the user communication with Facebook server?

The mobile application is using the MQTT protocol. The messenger on the webpage on the other hand, along with the login page communicate through a standard HTTPS protocol as the remaining part of the application.

Is there an API for Facebook Messenger?

The Messenger Profile API allows you to set, update, retrieve, and delete properties from the Page Messenger Profile.

What type of communication is a Facebook Messenger message?

Facebook Messenger is a mobile app that enables chat, voice and video communications between the social media site's web-based messaging and smartphones.

How many types of FB messengers are there?

Messages sent with the Messenger Platform are classified as one of three different message types. Each message type has different policies and guidelines for what types of content and under what conditions they can be sent.


1 Answers

kindofgreat,

This question intrigued me a bit, so I spent a couple hours experimenting, and here are my findings. The result is an app where any number of users can participate in a conversation.

I went with a data model that has an intermediate model between User and Conversation called UserConveration; it's a join model and holds data about the state of a user and a conversation together (namely, whether the conversation is read, deleted, etc.)

The implementation is on GitHub, and you can see a diff of the code I wrote (versus the code that was automatically generated, to keep all the cruft out) at https://github.com/BinaryMuse/so_association_expirement/compare/53f2263...master.

Here are my models, stripped down to only the associations:

class User < ActiveRecord::Base
  has_many :user_conversations
  has_many :conversations, :through => :user_conversations
  has_many :messages, :through => :conversations
end

class UserConversation < ActiveRecord::Base
  belongs_to :user
  belongs_to :conversation
  has_many :messages, :through => :conversation

  delegate :subject, :to => :conversation
  delegate :users, :to => :conversation
end

class Conversation < ActiveRecord::Base
  has_many :user_conversations
  has_many :users, :through => :user_conversations
  has_many :messages
end

class Message < ActiveRecord::Base
  belongs_to :user
  belongs_to :conversation
end

And here's what the database looks like:

create_table "conversations", :force => true do |t|
  t.string   "subject"
  t.datetime "created_at"
  t.datetime "updated_at"
end

create_table "messages", :force => true do |t|
  t.integer  "user_id"
  t.integer  "conversation_id"
  t.text     "body"
  t.datetime "created_at"
  t.datetime "updated_at"
end

create_table "user_conversations", :force => true do |t|
  t.integer  "user_id"
  t.integer  "conversation_id"
  t.boolean  "deleted"
  t.boolean  "read"
  t.datetime "created_at"
  t.datetime "updated_at"
end

create_table "users", :force => true do |t|
  t.string   "name"
  t.datetime "created_at"
  t.datetime "updated_at"
end

The basic idea is to present the user with a "conversation," when in reality behind the scenes we're managing UserConversations for all the users involved in a conversation. See especially the method create_user_conversations on the UserConversation model, which is responsible for creating an entry in the join table for every user associated with a conversation.

There are also lots of has_many :through and delegates calls in the models to make it as painless as possible to get the data we want... e.g. instead of @user_conversation.conversation.subject you can use @user_conversation.subject; the same goes for the messages attribute.

I realize it's quite a bit of code, so I encourage you to get the source and play around with it. It all works, other than "deleting" conversations (I didn't bother with this, but marking messages as read/unread does work). Be aware that you have to be "signed in" as a user to perform some operations, e.g. creating a new conversation, etc. You can sign in by clicking on a user from the main page and choosing "Sign In as This User."

One other thing to keep in mind is that the URLs say "conversation" to keep things nice for the user, even though the controller in use is the "UserConversations" controller--check out the routes file.

If you have any longer/in-depth questions, feel free to contact me via GitHub or via the contact details on my StackOverflow profile.

like image 145
Michelle Tilley Avatar answered Nov 09 '22 07:11

Michelle Tilley