What is TextSomebody?

TextSomebody is a web application that allows a user to initiate a text-message conversation with any SMS-enabled device, and to share the contents of that conversation through a unique url.

How does it work?

TextSomebody is built with Rails 7, and makes use of ActionCable, Rails' websocket API, to simulate the real-time feel of a text-message conversation. It depends on Twilio's API for sending and receiving messages.

Creating a Conversation

When a user submits a valid phone number through the form at the application's root path, three things happen:

  1. A new conversation resource is created in the database
  2. A cookie is set on the user's browser, identifying them as the creator of this conversation
  3. The user is redirected to the url where the new conversation is displayed

At this point, the new conversation is just a blank space. Now it's time to create some messages for it.

Sending Messages

The user's view of the conversation will display the form they'll use to create new messages.

Notice that the new message appears in the conversation without a page reload. If we were relying only on the HTTP request/response cycle, the user would have to refresh the page every time they wanted to check for new messages. But using websockets, our server updates the page every time a new message is added to the conversation, without the user having to do anything at all.

There are a couple of steps we need to take in order to use websockets. First, we use an Active Record Callback in our Message model. This will allow us to specify an action to be taken at a particular point in the lifecycle of an Active Record Object. In this case, whenever a new message is saved and committed to the database, we want to broadcast it over a channel identified by the conversation that message belongs to.

Next, we need to make sure that any client viewing a conversation is subscribed to the channel that carries that conversation's new messages. We do this by adding the turbo_stream_from tag to the view template.

By default, when the client receives the broadcast, it will look to append the new message to an element with an id of "messages". If there isn't one on the page, nothing will happen. So we must make sure the parent element of the rendered messages is appropriately named.

Now the message has been created, joined to its parent conversation, and displayed on the page. But to actually send it as an SMS message, we need to integrate our application with the Twilio API.

Integrating the Twilio API

To use the Twilio API to send SMS messages, two things are required. First, you must register with Twilio and obtain account credentials. Second, you need a Twilio phone number to serve as the "from" number.

With the account credentials and phone number in hand, the most convenient way to use the Twilio API in a Ruby application is to install the twilio-ruby gem and use the Client wrapper class. For this to work, you muContactst make the account credentials accessible through environment variables, specifically TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN. We've set TWILIO_FROM_NUMBER as an environment variable as well.

We want to invoke this code every time a new message is added to the database. Therefore, it makes sense to put it in the create method of the MessagesController. However, each message is sent to one and only one "to:" number, which is owned by the Conversation to which that message belongs.

Considering this, we decided to extract the Twilio code to a method defined on the Conversation model, which is invoked in the Message#create method.

Receiving Messages

When we send a message with TextSomebody, it will appear on the recipient's device as having come from the number designated in our environment variables as TWILIO_FROM_NUMBER. Naturally, when they reply, this is also the number their reply will be sent to. What happens when they reply to that number?

One of the features of Twilio's Programmable SMS API is that it can send a webhook to a specified URL every time a message is received by a Twilio phone number. To take advantage of this, we set up a route in our app specifically for webhooks generated by incoming messages, and a controller to handle requests for that path.

We are interested in two properties of the webhook's payload: From and Body. The From property is the phone number the incoming message was sent from. This is what we use to find the conversation this message should be associated with.

The Bodyproperty is the text of the message itself, which is added to the database in the same way that outgoing messages are. As soon as the transaction is committed, the new message is broadcast over the conversation's channel, so that anyone viewing the conversation will see it updated with the new message in real time.

Sharing a Conversation

Conversations and their messages are publicly accessible, so sharing a conversation is as easy as sharing its url. However, the ability to add new messages to the conversation through the web interface is available only to the conversation's creator.

As stated above, when a user submits a new phone number, a cookie is set in their browser identifying them as the creator of the new conversation. This allows us to conditionally render the form that submits new messages.

If the cookie is not present, or if the encrypted id in the cookie does not match the id of the conversation the user is trying to view, they will not see the new message form.

Further Reading

For more on implementing websockets with Rails, see Action Cable Overview

For an introduction to using Twilio's Programmable SMS API with Ruby, check out their Ruby Quickstart page.