bolt githublinkedinPage 1Created with Sketch. spots swoosh target triangle-icon twilio twitterweave

WhatsApp Style Group Messaging With Twilio

TomJune 13, 2017

As app stores become increasingly bloated and competition for hallowed space on the homescreen is hotter than ever, we set out to build a group messaging system leveraging functionality that ships with almost every single device; SMS.

Aside from a cool, low-friction, group messaging tool, there are some other key benefits:

  • Data protection: you don't need to store group member's phone numbers on a device
  • Anonymity: you don't need to share your phone number to communicate with other group members

Core Functionality

When designing the MVP, we established some core functionality that the service should include:

  • Anyone can create a group with a unique phone number
  • Anyone with the group number can join the group
  • The group creator can message all members simultaneously
  • The group creator and all members can message each other directly, without the need for the recipient's phone number

The following technical explanation runs through the steps required to achieve the above feature set. The example is based on a Ruby on Rails application.

Setup on Twilio

1) Purchase a new Number on Twilio; this will be used as the 'base' number and will be responsible for handling group creation

2) Configure the messaging functionality to point at your app


TWILIO Programmable Messaging

3) Create a new TwiML app

https://www.twilio.com/console/phone-numbers/dev-tools/twiml-apps/add


TWILIO TwiML App

N.B. The url should point at the endpoint that will handle the group messaging functionality for any new numbers associated with the TwiML app

4) Get the required variables for interacting with the application via API:

  • Account SID
  • Account Auth Token
  • TwiML Application SID

Once you have completed the setup, let's take a look at the functionality required to process incoming SMS!

Creating a Group

When a text message is sent the 'base' number, Twilio will make an API request to the endpoint you entered in Step 1 above.

Our app receives the request from Twilio, and listens for the #createGroup keyword. If present, it will create a group and assign a new Twilio number to it.

The GroupCreator class is responsible for parsing the message and creating groups:

class GroupCreator
  def create(owner)

    # find available numbers
    number = available_numbers.first.phone_number

    # buy one of the available numbers
    if purchase_number(number)

      # create a group, assigning the new number to it
      Group.create(phone_number: number, owner: owner)
    else
      false
    end
  end

  private

  def available_numbers
    twilio_client.available_phone_numbers.get('GB').local.list(
      sms_enabled: true
    )
  end

  # purchase a new number and assign it to the TwiML application that we
  # created in Step 3 above
  def purchase_number(number)
    twilio_client.incoming_phone_numbers.create(
      phone_number: number,
      sms_application_sid: ENV['TWILIO_SMS_APPLICATION_SID'],
      sms_method: 'POST'
    )
  end

  # use the Account SID and Auth Token to connect to the Twilio API
  def twilio_client
    Twilio::REST::Client.new(
      ENV['TWILIO_ACCOUNT_SID'],
      ENV['TWILIO_ACCOUNT_TOKEN']
    )
  end
end

Build a controller to receive the incoming requests from Twilio:

module Sms
  class ResolversController < BaseController
    def create
      # save the number of the sender for future reference
      owner = Member.where(phone_number: params['From']).first_or_create

      if params['Body'].include?('#createGroup')

        # create a new group leveraging the GroupCreator#create method
        if GroupCreator.create(owner)
          # send message from new group number to group creator with instructions
        else
          # handle error
        end
      else
        # reply with unknown command error
      end

      head :ok
    end
  end
end

Joining a Group

Once the group number has been created, anyone can join by sending the #join keyword to the new number.

Behind the scenes, Twilio receives the SMS on the group number and creates an API call to the endpoint we set up in Step 3.

module Sms
  class GroupsController < BaseController
    def create
      body = params['Body']

      if body.include?('#join')

        # lookup the group associated with the number
        group = Group.where(phone_number: params['To'])

        # find the member by their phone number, or create a record for them
        member = Member.where(phone_number: params['From']).first_or_create

        # assign the member to the group
        group.members << member
      end

      # Note to self: Add error handling for incorrect keyword of missing group

      head :ok
    end
  end
end

Sending a Message

Once a member has joined a group, it's time to enable the intra-group messaging functionality. Every message that is sent to the group number is forwarded by way of API call to our server.

To re-iterate, the desired messaging logic is as follows:

  • Group owner can message all members simultaneously
  • Group owner can message any member individually
  • Members can reply to group owner directly
  • Members can message each other individually

In order to keep our controller nice and clean, we offload the message handling to a new class MessageHandler:

class MessageHandler
  BODY_REGEX = /\s.+/
  JOIN_ACTION_WORD = '#join'
  USER_ID_REGEX = /#\d+/

  def initialize(params)
    self.body = params['Body']
    self.from = params['From']
    self.to = params['To']
  end

  def handle_message
    return 'false' if group.nil?

    # subscribe a member to the group if the SMS contains #join
    return subscribe_to_group if body.include?(JOIN_ACTION_WORD)

    # check for group membership
    if lookup_member_by_phone_number.present? || group.owner?

      # send message to the correct recipients
      distribute_message
    else
      # add some error handling
    end
  end

  private

  attr_accessor :body, :from, :to

  def subscribe_to_group
    group.members << Member.create(phone_number: from)
  end

  def distribute_message
    recipients = []

    if send_to_single_member?

      # check if the message is intended for an individual member. If message
      # contains the members ID preceeded by a hash e.g. #12

      recipients << lookup_member_by_id
      sender = lookup_member_by_id
    elsif group_owner?

      # if the message is from the group owner, distribute to everyone

      group.members.each do |member|
        recipients << member
      end
      sender = group.owner
    else

      # if the message is neither from the group owner or destined for an
      # individual, then send it to the group owner

      recipient << group.owner.phone_number
      sender = lookup_member_by_phone_number
    end


    # send out the parsed message to the intended recipient

    msg = "##{sender.id} says: #{parsed_body}"
    sender_number = group.phone_number
    recipients.each do |recipient|
      send_message(recipient, sender_number, msg)
    end
  end

  def send_to_single_member?
    (USER_ID_REGEX.match body).present?
  end

  def lookup_member_by_id
    id = extract_member_id_from_body
    group.members.find(id)
  end

  def lookup_member_by_phone_number
    group.members.find_by(phone_number: from)
  end

  def extract_member_id_from_body
    (USER_ID_REGEX.match body)[0][1..-1]
  end

  def parsed_body
    if send_to_single_user?
      (BODY_REGEX.match body)[0][1..-1]
    else
      body
    end
  end

  def group_owner?
    group.owner.phone_number == from
  end

  def group
    @group ||= Group.find_by(phone_number: to)
  end

  def send_message(recipient, sender, message)
    Twilio::REST::Client.messages.create(
      from: sender,
      to: recipient,
      body: message
    )
  end
end

Finally, update the groups controller to leverage the new MessageHandler class:

module Sms
  class GroupsController < BaseController
    def create

      # hand off the message processing to a separate class
      MessageHandler.new(params).process

      head :ok
    end
  end
end

You now have the key functionality required to build a basic SMS group messaging service!

Extra Credit

There are a number of other features that could be added to improve the service:

  • Handle exceptions; when a group doesn't exist or the recipient is not a member, send them an explanatory response
  • Set a group name
  • Record extra details for a group member; name etc.
  • Allow the member to leave the group
  • Toggle the ability for members to message the entire group

The MessageHandler class in this example is the engine of the group messaging system and could easily be extended to add the above functionality. In the production code, we would refactor this into a collection of smaller classes to make it more easy to interact with and render a more scalable code base.

If you fancy creating your own group messaging app powered by SMS alone, then this should give you a good starting point. Alternatively, you can simply use Our app!

THINK

out loud

We think out loud on our blog. We share our views on coding, remote work and post regular insights into the freshest tech industry news.

Want more? Join our mailing list.

    DVELP

    WORK WITH US.

    Client Engagement

    +44 (0) 20 31 37 63 39

    [email protected]

    So, how can we help you?

    Live chat