Windows 10, UWP & Twilio for SMS retrieval

I’ve been wanting to try out Twilio for quite a long time. I’ve seen my colleague, Martin, make great use of Twilio in demonstrations at conferences for the longest time and I always fancied trying it out.

I’ve had a Twilio account for quite a while but, the other week, I went through and added a little bit of money to the account and I bought myself a Twilio phone number that supports both voice and SMS messaging.

The first thing that I found kind of ‘fun’ was that as soon as I’d purchased a phone number I could specify URLs to be invoked in order to return XML files containing ‘TwiML’ which controlled what happened when you called my number or sent an SMS to it. Here’s my SMS file;

<?xml version="1.0" encoding="UTF-8"?>
<Response>
	<Message>
		<Body>Thanks for texting Mike's Twilio number.</Body>
	</Message>
</Response>

and I found this to be a very immediate way of linking the world of SMS/voice straight through to the world of web servers and programmability and, clearly, while I’m currently returning a static response to a voice call or text message I could easily be dynamically generating that response.

The next thing that I wanted to do was to see if I could get hold of the text messages that had been sent to my Twilio number inside of a Windows 10, UWP app.

Twilio has a REST API for SMS messages documented and I went about making sure that I could;

  1. Authenticate
  2. Get the list of text messages for my account and for my phone number including chasing down the numerous pages that might involve

That wasn’t particularly difficult.

Twilio has helper libraries for these APIs across technologies like PHP, Ruby, Java and there’s a .NET library on GitHub.

Now, it may be that I didn’t look hard enough at this package but I didn’t seem to find a variant of it which supported the UWP. The Twilio package itself didn’t seem to be right and I couldn’t find the Twilio.WinRT package that was mentioned and so I figured that I’d just write some code myself as I only wanted to support a simple scenario which is;

“Poll for text messages on an interval and return the newly arrived messages”

and I didn’t need any more than that so I wrote a little UWP app to do just that starting from a blank project and adding just a piece of XAML to display a list;

<Page
  x:Class="App307.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:App307"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  mc:Ignorable="d">

  <Grid
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
      <RowDefinition />
      <RowDefinition
        Height="Auto" />
    </Grid.RowDefinitions>
    <!-- using x:Bind here just to be lazy and avoid setting datacontext -->
    <ListView Margin="40"
      Header="SMS messages arrived since start button was pressed"
              ItemsSource="{x:Bind NewSmsMessages}">
      <ListView.ItemTemplate>
        <DataTemplate>
          <StackPanel Margin="0,8,0,0">
            <TextBlock
              Text="{Binding Sid}" />
            <TextBlock
              Text="{Binding Body}" />
            <TextBlock
              Text="{Binding DateCreated}" />
            <TextBlock
              Text="{Binding From}" />
            <TextBlock
              Text="{Binding To}" />
          </StackPanel>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
    <StackPanel
      Grid.Row="1"
      HorizontalAlignment="Center"
      Orientation="Horizontal"
      Margin="20">
      <Button
        Content="Start"
        Click="OnStartAsync"
        Margin="8" />
      <Button
        Content="Stop"
        Click="OnStopAsync"
        Margin="8" />
    </StackPanel>
  </Grid>
</Page>

and some code behind it to provide the data and so on;

namespace App307
{
  using System;
  using System.Collections.ObjectModel;
  using System.Threading;
  using TwilioSmsUtils;
  using Windows.UI.Core;
  using Windows.UI.Xaml;
  using Windows.UI.Xaml.Controls;
  using Windows.UI.Xaml.Navigation;

  public sealed partial class MainPage : Page
  {
    public ObservableCollection<TwilioSmsDetails> NewSmsMessages
    {
      get;
      private set;
    }
    public MainPage()
    {
      this.InitializeComponent();

      this.NewSmsMessages = new ObservableCollection<TwilioSmsDetails>();
    }
    protected async override void OnNavigatedTo(NavigationEventArgs e)
    {
      base.OnNavigatedTo(e);
    }
    private async void OnStartAsync(object sender, RoutedEventArgs e)
    {
      this.NewSmsMessages.Clear();

      this.tokenSource = new CancellationTokenSource();

      this.twilioWatcher = new TwilioSmsWatcher(
        Constants.ACCOUNT_SID,
        Constants.ACCOUNT_TOKEN,
        Constants.PHONE_NUMBER);

      await this.twilioWatcher.InitialiseAsync(this.tokenSource.Token);

      // We don't await this, we let it go.
      try
      {
        await this.twilioWatcher.PollForNewMessagesAsync(
          TimeSpan.FromSeconds(30),
          OnNewTwilioMessage,
          this.tokenSource.Token);
      }
      catch (OperationCanceledException)
      {
        this.twilioWatcher = null;
        this.tokenSource.Dispose();
        this.tokenSource = null;
      }
    }
    void OnNewTwilioMessage(TwilioSmsDetails message)
    {
      this.Dispatcher.RunAsync(
        CoreDispatcherPriority.Normal,
        () =>
        {
          this.NewSmsMessages.Add(message);
        }
      );
    }
    void OnStopAsync(object sender, RoutedEventArgs e)
    {
      this.tokenSource.Cancel();
    }
    TwilioSmsWatcher twilioWatcher;
    CancellationTokenSource tokenSource;
  }
}

and that relies on 3 fairly simple (and quickly written) I should stress classes for talking to Twilio – one to represent an SMS;

namespace TwilioSmsUtils
{
  using System;
  using System.Xml.Linq;
  public class TwilioSmsDetails
  {
    public string Sid { get; private set; }
    public DateTimeOffset DateCreated { get; private set; }
    public DateTimeOffset DateUpdated { get; private set; }
    public string Body { get; private set; }
    public string To { get; private set; }
    public string From { get; private set; }
    public string Uri { get; private set; }
    internal static TwilioSmsDetails FromXElement(XElement xElement)
    {
      return (new TwilioSmsDetails()
      {
        Sid = (string)xElement.Element("Sid"),
        DateCreated = DateTimeOffset.Parse(xElement.Element("DateCreated").Value),
        DateUpdated = DateTimeOffset.Parse(xElement.Element("DateUpdated").Value),
        Body = (string)xElement.Element("Body"),
        To = (string)xElement.Element("To"),
        From = (string)xElement.Element("From"),
        Uri = (string)xElement.Element("Uri")
      });
    }
  }
}

and one to represent a page of SMS messages;

namespace TwilioSmsUtils
{
  using System.Collections.Generic;

  internal class TwilioMessageListPage
  {
    public string NextPageUri { get; set; }
    public List<TwilioSmsDetails> SmsList { get; set; }
  }
}

and a watcher class which polls;

namespace TwilioSmsUtils
{
  using System;
  using System.Linq;
  using System.Net;
  using System.Net.Http;
  using System.Threading;
  using System.Threading.Tasks;
  using System.Xml.Linq;

  public class TwilioSmsWatcher
  {
    public TwilioSmsWatcher(
      string accountSid, 
      string accountToken,
      string phoneNumber)
    {
      this.accountSid = accountSid;
      this.accountToken = accountToken;
      this.phoneNumber = phoneNumber;
    }
    public async Task<bool> InitialiseAsync(CancellationToken token)
    {
      var messageListUri = MakeTwilioUri(TWILIO_MESSAGE_LIST);

      var firstPage = await this.ReadSingleTwilioSmsPageAsync(messageListUri,
        token);

      if (firstPage != null)
      {
        if (firstPage.SmsList?.Count > 0)
        {
          this.latestMessageSeenTime = firstPage.SmsList[0].DateCreated;
        }
        this.initialised = true;
      }
      return (this.initialised);
    }
    public async Task PollForNewMessagesAsync(
      TimeSpan pollInterval,
      Action<TwilioSmsDetails> messageHandler,
      CancellationToken cancellationToken)
    {
      this.CheckInitialised();

      while (true)
      {
        await Task.Delay(pollInterval, cancellationToken);

        var uri = MakeTwilioUri(TWILIO_MESSAGE_LIST);

        var updatedLatestMessageSeenTime = this.latestMessageSeenTime;

        while (uri != null)
        {
          var page = await ReadSingleTwilioSmsPageAsync(uri, cancellationToken);

          // clear the URI so that we don't loop unless we find a whole page
          // of new results and a next page to move to.
          uri = null;

          if (page != null)
          {
            // filter to the messages that are newer than our current
            // latest message (if we have one)
            var newMessageList = page.SmsList.Where(
              sms => IsNewerMessage(sms, this.latestMessageSeenTime)).ToList();

            foreach (var newMessage in newMessageList)
            {
              messageHandler(newMessage);

              if (IsNewerMessage(newMessage, updatedLatestMessageSeenTime))
              {
                updatedLatestMessageSeenTime = newMessage.DateCreated;
              }
              cancellationToken.ThrowIfCancellationRequested();
            }
            // if everything on this page was new, we might have more to do
            // so see if there's another page.
            if (newMessageList.Count == page.SmsList.Count)
            {
              uri = new Uri(page.NextPageUri);
            }
          }
          cancellationToken.ThrowIfCancellationRequested();
        }
        cancellationToken.ThrowIfCancellationRequested();

        this.latestMessageSeenTime = updatedLatestMessageSeenTime;
      }
    }
    bool IsNewerMessage(TwilioSmsDetails sms, DateTimeOffset? currentLatest)
    {
      return (
        !currentLatest.HasValue ||
        sms.DateCreated > currentLatest);
    }
    HttpClient HttpClient
    {
      get
      {
        if (this.httpClient == null)
        {
          HttpClientHandler handler = new HttpClientHandler()
          {
            Credentials = new NetworkCredential(this.accountSid, this.accountToken)
          };
          this.httpClient = new HttpClient(handler);
        }
        return (this.httpClient);
      }
    }
    async Task<TwilioMessageListPage> ReadSingleTwilioSmsPageAsync(Uri uri,
      CancellationToken cancellationToken)
    {
      TwilioMessageListPage page = null;

      var response = await this.HttpClient.GetAsync(uri, cancellationToken);

      if (response.IsSuccessStatusCode)
      {
        using (var responseStream = await response.Content.ReadAsStreamAsync())
        {
          var xElement = XElement.Load(responseStream);

          var twilioResponse = xElement.DescendantsAndSelf(XML_TWILIO_RESPONSE_NODE);

          if (twilioResponse != null)
          {
            page = new TwilioMessageListPage();
            var twilioNextPage = 
              twilioResponse.Attributes(XML_TWILIO_NEXT_PAGE_URI_ATTRIBUTE).FirstOrDefault();

            page.NextPageUri = twilioNextPage?.Value;

            page.SmsList =
              twilioResponse.DescendantsAndSelf(XML_TWILIO_SMS_MESSAGE_NODE).Select(
                xml => TwilioSmsDetails.FromXElement(xml)).ToList();
          }
        }
      }
      return (page);
    }
    Uri MakeTwilioUri(string path)
    {
      return (new Uri(
        $"{TWILIO_HOST}{this.accountSid}/{path}?To={this.phoneNumber}"));
    }
    void CheckInitialised()
    {
      if (!this.initialised)
      {
        throw new InvalidOperationException("Not initialised");
      }
    }
    static readonly string TWILIO_HOST = "https://api.twilio.com/2010-04-01/Accounts/";
    static readonly string TWILIO_MESSAGE_LIST = "Messages";
    static readonly string XML_TWILIO_RESPONSE_NODE = "TwilioResponse";
    static readonly string XML_TWILIO_NEXT_PAGE_URI_ATTRIBUTE = "nextpageuri";
    static readonly string XML_TWILIO_SMS_MESSAGE_NODE = "Message";

    HttpClient httpClient;
    DateTimeOffset? latestMessageSeenTime;
    string phoneNumber;
    string accountSid;
    string accountToken;
    bool initialised;
  }
}

and I can run up my little app and watch SMS messages come in;

image

and that all works really nicely for me and I really like the simplicity/immediacy of being able to quickly add SMS message integration into an app and especially when I think of this coupled with Windows IoT Core and how (e.g.) you could build some kind of kiosk that easily accepts text messaging as a form of input.

The code for this post is here for download. You would need to edit the Constants.cs file in order to provide your own Twilio account details and phone number.