Back around 1998, more and more billboards started to display some odd messages, along the lines of “Visit us on the web at www.companyname.com”. This was a major shift in advertising and gave marketers another avenue to explore. It was, of course, the beginning of the Internet’s move from academia to the mainstream, and the proliferation of Web sites seemed to happen almost overnight.
Here in 2010, we’re now back in 1998 territory. This time it’s the mobile Web that has everybody excited. The thought of potential customers being able to access services from anywhere – and not just in front of a PC – has marketers drooling. SMS plays an important role. On billboards ads, one is very likely to see something like “text companyname to 81707 for more information”. This is a shortcode/keyword combination. By texting the specified keyword (companyname in the example above) to the specified shortcode (81707 in the example), a mobile system can perform some type of action, defined by the company offering the keyword/shortcode. This may be as simple as sending a “Thanks for contacting us, we’ll be in touch” message back to the user, or beginning some kind of workflow process. Or, as in our example above, the user may receive a text message back containing a URL to a mobile Web site.
But just how do developers go about setting up keyword/shortcodes? And how do they process incoming messages to those keyword/shortcodes? Let’s take a look at how to do this. We’ll build upon the system we’ve built in previous articles that allows us to send SMS messages and to process delivery receipts. We’ll build on the basic message processing system we built in the delivery receipts article. Remember we’re using SQL Server and Visual Studio (C#) to build our system.
Obtaining a Keyword/Shortcode
In the Sending SMS Messages article, we talked about ways of sending SMS messages, and demonstrated how to do this using an aggregator called iTagg. We also need to use an aggregator to set up keyword/shortcodes. You have two options here:
- Purchase a dedicated shortcode This is the expensive option. Expect to pay several thousand pounds for a dedicated shortcode. ITV, for example, has the dedicated shortcode 63330. This means ITV can set up any keyword they like on this shortcode; no other company has access to it. It is purely for ITV’s exclusive use.
- Purchase a keyword on a shared shortcode This is what most companies use. An aggregator provides a shared shortcode (so called because several companies use keywords on the same shortcode), and the aggregator’s clients purchase keywords to use on it. iTagg own the shared shortcode 60300. So client 1 may purchase the keyword mobile. If client 2 comes along and also wants the keyword mobile, they cannot have it as it has already been assigned. Client 2 would have to select an alternative keyword.
The costs of purchasing a keyword/shortcode vary from aggregator to aggregator. You can expect to pay a monthly or annual fee for the use of the platform and a monthly rental fee for the keyword/shortcode. Some aggregators allow you to pay a fee and have a certain number of keywords – contact the aggregators to determine what price point works best for you.
One last important point – make sure you choose your aggregator carefully. Once you’ve chosen an aggregator any advertising of your keyword/shortcode will be showing the shared shortcode used by the aggregator – effectively locking you in. I strongly recommend you run some tests with the aggregators you’re interested in before making a final decision.
Configuring a Keyword/Shortcode
Once you have a keyword/shortcode, how do you set it up? It’s pretty easy. You give the aggregator a Web address, and the aggregator forwards any inbound messages they receive on your keyword/shortcode to the specified Web address. Depending upon the aggregator, there are two ways you can provide the Web address:
- Provide the Web address to the aggregator’s support team, who will then configure it for you (this is the only method of configuration for certain aggregators)
- Log on to the aggregator’s control panel and change the URL yourself
The second option is much preferred, as it gives you the ability to quickly change the URL yourself. The first option puts a reliance on the aggregator’s support team.
Here’s the iTagg control panel, listing available keywords:
Clicking on the keyword allows you to configure details for the keyword/shortcode. There are a number of options – you can choose to forward messages on to a URL, and/or to an e-mail address. Clicking on the E-mail/URL Forwarder option displays a screen allowing you to configure the relevant pieces of information:
Once you’ve configured these pieces of information, you’re ready to receive inbound SMS messages – as long as you have a system present to process them, of course. Let’s build that system after we’ve looked at how iTagg send inbound messages to us.
iTagg Inbound Messages
iTagg send all inbound SMS message requests over HTTP POST. Here are the parameters iTagg send to our Web listener page:
- Source This is the mobile number the message was sent from. So if you sent the message from your mobile number, the value for this field would be your mobile number
- Dest The shortcode the message was delivered to, e.g. 60300
- Dtime The delivery time of the message from the mobile network – in the format YYYYMMDDHHMM.
- Message The message body received
- Network The network the source mobile number uses. This is passed as a code:
- 10 – O2
- 15 – Vodafone
- 20 – 3
- 30 – T-Mobile/Virgin
- 33 – Orange
- 99 – Other
Different aggregators use different codes
- Message_id The unique identifier assigned to the message by the aggregator
- Message_after_match Message_after_match contains the body of the message sent after the keyword has been matched
- Matched_keyword This is the keyword that was matched. This can be used to quickly match up keywords in multi-keyword systems
- Matched_subkeyword This is usually blank. iTagg allows keywords to be set up as sub-keywords (e.g. “example” could be extended with sub-keywords 1, 2 – so the user would send “example1” and “example2”
Note that these parameters are specific to iTagg – they will differ for each individual aggregator. For example, Tanla pass the following parameters:
- ServiceID (Tanla service number)
- MOID (aggregator ID)
- Operator ID (denotes which network the sender uses)
- Date (date/time the message was received)
- From (the mobile number the message was sent from)
- To (the shortcode the message was sent to)
- Message (the message body)
It’s important to look at the API documents for the aggregator you’re planning to use (iTagg’s API documents can be found at their Web site). Using iTagg, let’s enhance our existing system to process inbound messages.
Building the Database
Here’s the SQL Server database script from the delivery receipts article, with one minor alteration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
USE master -- Delete the database if it already exists IF EXISTS(SELECT [name] FROM sys.databases WHERE [name] = 'MobiforgeMessageDemo') BEGIN DROP DATABASE MobiforgeMessageDemo END GO -- Create the database CREATE DATABASE MobiforgeMessageDemo GO USE MobiforgeMessageDemo -- Add the Messages table CREATE TABLE [Messages] ( AggregatorMessageID VARCHAR(40), InboundMessage BIT, Originator VARCHAR(12), Recipient VARCHAR(12), MessageDate DATETIME DEFAULT GETDATE(), MessageBody VARCHAR(160), MessageDelivered BIT DEFAULT 0 ) GO -- Stored procedure to insert a message -- Amended to accept an optional Message Date CREATE PROCEDURE usp_InsertMessage @AggregatorMessageID VARCHAR(40), @InboundMessage BIT, @Originator VARCHAR(12), @Recipient VARCHAR(12), @MessageBody VARCHAR(160), @MessageDate DATETIME = NULL AS BEGIN SET NOCOUNT ON IF (@MessageDate IS NULL) BEGIN INSERT INTO [Messages] (AggregatorMessageID, InboundMessage, Originator, Recipient, MessageBody) VALUES (@AggregatorMessageID, @InboundMessage, @Originator, @Recipient, @MessageBody) END ELSE BEGIN INSERT INTO [Messages] (AggregatorMessageID, InboundMessage, Originator, Recipient, MessageBody, MessageDate) VALUES (@AggregatorMessageID, @InboundMessage, @Originator, @Recipient, @MessageBody, @MessageDate) END SET NOCOUNT OFF END GO -- Stored procedure to update a message's delivery status CREATE PROCEDURE usp_MarkMessageAsDelivered @AggregatorMessageID VARCHAR(40) AS BEGIN SET NOCOUNT ON -- iTagg pass message IDs with a -3 when sending messages, -- but do not pass the -3 in delivery receipts. So we check -- if we can find the message first, and if we can't we append -- a -3 to the end of the ID IF NOT EXISTS (SELECT AggregatorMessageID FROM [Messages] WHERE AggregatorMessageID = @AggregatorMessageID) BEGIN SET @AggregatorMessageID += '-3' END UPDATE [Messages] SET MessageDelivered = 1 WHERE AggregatorMessageID = @AggregatorMessageID END |
We’ve made one change to this script – in the usp_InsertMessage stored procedure. We’ve added an optional @MessageDate parameter. If this is passed we insert the date into the table; otherwise we use the current date/time.
Amending the Listener Web Page
When we added the ability to process delivery receipts to our simple messaging system, we introduced a listener Web page. We configured this in the iTagg control panel. We’ll now amend the listener to detect whether a delivery receipt or inbound message has been received, and we’ll process it accordingly.
Here’s the original code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
protected void Page_Load(object sender, EventArgs e) { try { // Check if some POST data was passed if (Request.InputStream != null) { StreamReader sr = new StreamReader(Request.InputStream); // Convert the POST stream to a string // We remove the xml= tag at the front, otherwise the XML will not parse string inputStream = sr.ReadToEnd().Replace("xml=", string.Empty); // Create a delivery receipt iTaggDeliveryRecipt dr = new iTaggDeliveryRecipt(inputStream); // Write out the delivery receipt details to the trace Trace.Write("iTagg DR data: " + inputStream); Trace.Write("DR submission ref: " + dr.SubmissionRef); Trace.Write("DR status: " + dr.Status); // Only process if the status is "delivered" if (dr.Status == "Delivered") { // Try and update the database SqlConnection conn = new SqlConnection( "Data Source=DEVSQL;Initial Catalog=MobiforgeMessageDemo;Integrated Security=true"); SqlCommand cmd = new SqlCommand("usp_MarkMessageAsDelivered", conn); cmd.CommandType = CommandType.StoredProcedure; // Make sure we set the aggregator message ID cmd.Parameters.Add(new SqlParameter("@AggregatorMessageID", dr.SubmissionRef)); conn.Open(); cmd.ExecuteNonQuery(); conn.Close(); // Show we updated the database in the trace Trace.Write("Successfully updated database."); } // A different status has been received, so we output a message to show // this else { Response.Write("Message was not delivered. Status: " + dr.Status + "<BR>"); } // Write out a confirmation message at the end Response.Write("OK"); } } catch (Exception ex) // Output the error details { Response.Write("Error occurred whilst processing: " + ex.Message); Trace.Write("Error occurred whilst processing: " + ex.Message); } } |
The changes we’ll make are:
- Add a class to represent an inbound SMS message
- Add a method to inspect the POST data and determine whether a delivery receipt or inbound SMS message has been received
- Move the delivery receipt processing into a separate method
- Add a method to process an inbound SMS message
- Change the Page_Load event to call the appropriate methods and process inbound requests
The Inbound SMS Message Class
The inbound SMS message class does nothing more than parse the inbound data from iTagg and convert it to properties on the class. This makes it easier for us to insert the message into the database.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
using System; using System.Collections.Generic; using System.Web; /// <summary> /// Summary description for InboundSmsMessage /// </summary> public class InboundSmsMessage { private string _aggregatorMessageID = string.Empty; //The aggregator message ID private string _originator = string.Empty; // The message originator private string _recipient = string.Empty; // The message recipient private DateTime _messageDate = DateTime.MinValue; // Date/time the message was received private string _messageBody = string.Empty; // The message body // Public accessors public string AggregatorMessageID { get { return _aggregatorMessageID; } set { _aggregatorMessageID = value; } } public string Originator { get { return _originator; } set { _originator = value; } } public string Recipient { get { return _recipient; } set { _recipient = value; } } public DateTime MessageDate { get { return _messageDate; } set { _messageDate = value; } } public string MessageBody { get { return _messageBody; } set { _messageBody = value; } } /// <summary> /// Accepts Input Stream parameter as a string and populates class properties /// </summary> /// <param name="InputStream"></param> public InboundSmsMessage(string InputStream) { // Obtain the parameter values by splitting on the ampersand string[] parameters = InputStream.Split(new char[] { '&' }); // And now we can loop around the parameters and process each value // Note that we don't process every value iTagg send to us as we don't need them all for (int i = 0; i < parameters.Length; i++) { // Separate parameter name and value string[] parameterValues = parameters[i].Split(new char[] { '=' }); // Should have two values if (parameterValues.Length == 2) { // Assign the value to the appropriate class property switch (parameterValues[0]) { case "source": _originator = parameterValues[1]; break; case "dest": _recipient = parameterValues[1]; break; case "dtime": // Length should be 12 // 4 digits for YYYY // 2 digits for MM // 2 digits for DD // 2 digits for HH // 2 digits for MM if (parameterValues[1].Length == 12) { int year = Convert.ToInt32(parameterValues[1].Substring(0, 4)); int month = Convert.ToInt32(parameterValues[1].Substring(4, 2)); int day = Convert.ToInt32(parameterValues[1].Substring(6, 2)); int hour = Convert.ToInt32(parameterValues[1].Substring(8, 2)); int minute = Convert.ToInt32(parameterValues[1].Substring(10, 2)); DateTime dateToProcess = new DateTime(year, month, day, hour, minute, 0); _messageDate = dateToProcess; } break; case "message": _messageBody = parameterValues[1]; break; case "message_id": _aggregatorMessageID = parameterValues[1]; break; } } } } } |
There’s not much to say about this code; it’s self-explanatory. In the real world we’d add try/catch blocks, constants for literal values, and better processing around dates.
POST Data Processing Method
The method to check if the POST data returns a Boolean – true if we’re processing a delivery receipt, false if we’re processing an inbound SMS message.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/// <summary> /// Determines whether we should process a delivery receipt or an inbound SMS message /// </summary> /// <param name="inputStream">The data to check</param> /// <returns></returns> private bool deliveryReceiptReceived(string inputStream) { // Assume we're processing an inbound SMS message bool deliveryReceiptReceived = false; // If the input stream contains a delivery receipt XML element, // return delivery receipt if (inputStream.Contains("itagg_delivery_receipt")) { deliveryReceiptReceived = true; } return deliveryReceiptReceived; } |
We perform a simple check for a delivery receipt value, and if we find that value we know we’re processing a delivery receipt.
Move Delivery Receipt Processing Into a Separate Method
We now move the coding to process a delivery receipt from the Page_Load event into a separate method called, originally enough, processDeliveryReceipt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
/// <summary> /// Process the inbound delivery receipt and add it to the database /// </summary> /// <param name="inputStream"></param> private void processDeliveryReceipt(string inputStream) { // Create a delivery receipt iTaggDeliveryRecipt dr = new iTaggDeliveryRecipt(inputStream); // Write out the delivery receipt details to the trace Trace.Write("iTagg DR data: " + inputStream); Trace.Write("DR submission ref: " + dr.SubmissionRef); Trace.Write("DR status: " + dr.Status); // Only process if the status is "delivered" if (dr.Status == "Delivered") { // Try and update the database SqlConnection conn = new SqlConnection( "Data Source=DEVSQL;Initial Catalog=MobiforgeMessageDemo;Integrated Security=true"); SqlCommand cmd = new SqlCommand("usp_MarkMessageAsDelivered", conn); cmd.CommandType = CommandType.StoredProcedure; // Make sure we set the aggregator message ID cmd.Parameters.Add(new SqlParameter("@AggregatorMessageID", dr.SubmissionRef)); conn.Open(); cmd.ExecuteNonQuery(); conn.Close(); // Show we updated the database in the trace Trace.Write("Successfully updated database."); } else // A different status has been received, so we output a message to show this { Response.Write("Message was not delivered. Status: " + dr.Status + "<BR>"); } // Write out a confirmation message at the end Response.Write("OK"); } |
This method creates a delivery receipt object and inserts it into the database.
Add a Method to Process an Inbound SMS Message
Our penultimate step sees us write a similar method to that above, but to process an inbound SMS message.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
/// <summary> /// Processes an inbound SMS message and adds it to the database /// </summary> /// <param name="inputStream"></param> private void processInboundSmsMessage(string inputStream) { // Create an inbound SMS message InboundSmsMessage message = new InboundSmsMessage(inputStream); // Write out the SMS details to the trace Trace.Write("iTagg SMS data: " + inputStream); // Only process if we have valid data if (message.AggregatorMessageID != string.Empty) { SqlConnection conn = new SqlConnection( "Data Source=DEVSQL;Initial Catalog=MobiforgeMessageDemo;Integrated Security=true"); SqlCommand cmd = new SqlCommand("usp_InsertMessage", conn); cmd.CommandType = CommandType.StoredProcedure; // Set the parameters cmd.Parameters.Add(new SqlParameter("@AggregatorMessageID", message.AggregatorMessageID)); cmd.Parameters.Add(new SqlParameter("@InboundMessage", true)); cmd.Parameters.Add(new SqlParameter("@Originator", message.Originator)); cmd.Parameters.Add(new SqlParameter("@Recipient", message.Recipient)); cmd.Parameters.Add(new SqlParameter("@MessageBody", message.MessageBody)); cmd.Parameters.Add(new SqlParameter("@MessageDate", message.MessageDate)); conn.Open(); cmd.ExecuteNonQuery(); conn.Close(); // Show we updated the database in the trace Trace.Write("Successfully updated database."); } // Write out a confirmation message at the end Response.Write("OK"); } |
The InboundSmsMessage class takes care of parsing the input stream; once we have these values we write it to the database.
If we were following strict OOP guidelines we’d have put the database code in a method on the InboundSmsMessage class called Update(). We’d also have added a similar method to the Delivery Receipt class.
The Page_Load Event
All that’s left to do now is rewrite the Page_Load event to call the methods we’ve written.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
protected void Page_Load(object sender, EventArgs e) { try { // Check if some POST data was passed if (Request.InputStream != null) { StreamReader sr = new StreamReader(Request.InputStream); // Convert the POST stream to a string // We remove the xml= tag at the front, otherwise the XML will not parse // NOTE - "xml" is only passed if we receive a delivery receipt string inputStream = sr.ReadToEnd().Replace("xml=", string.Empty); if (deliveryReceiptReceived(inputStream)) { processDeliveryReceipt(inputStream); } else { processInboundSmsMessage(inputStream); } } } catch (Exception ex) // Output the error details { Response.Write("Error occurred whilst processing: " + ex.Message); Trace.Write("Error occurred whilst processing: " + ex.Message); } } |
This method has been greatly simplified. The first part remains the same – we convert the input stream to a string. After that, we check if we’ve received a delivery receipt and process accordingly.
We’re now ready to try it out!
Putting It All Together
Publish the listener to IIS and make sure you can access it through your Web browser. Once you’re happy visit iTagg (or your selected aggregator) and set up the URL as the inbound message path by either configuring it in the aggregator control panel, or contacting aggregator support.
Then try sending in a message. A typical message may look like:
Send this message, and after a few seconds it should appear in the Messages database table.
Summary
It’s not difficult to process inbound SMS messages – in fact, it’s quite similar to sending an inbound message. Once you have a simple database configured, it’s pretty easy to process the messages and insert them into the database. The main thing to consider is your choice of aggregator – as you’ll be tied in to their shortcode it’s vital you make the correct choice.
Our example system has some limitations. Firstly, we don’t actually do anything with the inbound message. A further article will show how to do this. Secondly, what we’ve developed can only support one keyword/shortcode. Say we have keyword1 on 60300 and keyword2 on 60300, and we wanted different messages to be sent back depending upon which keyword/shortcode was used. We’d need to add a Keywords table to the database and link this to the Messages table. We could then amend our stored procedures accordingly and change our class library and listener Web page over to process messages from different keywords. We’ll also look at how to do this in a future article.