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.
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.
When a user submits a valid phone number through the form at the application's root path, three things happen:
At this point, the new conversation is just a blank space. Now it's time to create some messages for it.
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.
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.
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 Body
property 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.
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.
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.