Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Drive your application through Twitter direct messages

4.32/5 (7 votes)
15 Mar 2012CPOL6 min read 30.6K   548  
Use Twitter as a globally available persisted message queue in your distributed applciation work flow

324332/Drive_your_application_through_Twitter_direct_messages.jpg

Introduction

I am sure this concept has already been put to use by someone out there, but I haven't seen it with my limited Googling. In all simplicity, Twitter is like a huge persisted message queue with a rich API to query it. In theory, it is possible therefore to enqueue certain type of messages to the queue from any Twitter client and then dequequ them up by some kind of listener who would know how to make sense of that. The challenge of directing a message to a specific listener is easily solvable. I have put together a system (just a POC) for my work place where people can send direct messages to this listener in a certain format, which gets translated to their timesheets in our internal system. They can use any Twitter client to do that obviously. In theory, this is a freely (!) available public message queue with some amount of privacy built into it (if you use DMs instead of tweets). I am of the opinion that this would not be considered a misuse of Twitter service if used sparingly. This is a disclaimer - please use this concept using your own judgment. The included project uses the lovely TweetSharp API to converse with Twitter API, authentication using oAuth. There is a basic plug-in mechanism using MEF for the 'processors' which process the direct messages to do some meaningful work.

It is limited by our imagination as how we can use such an opportunity. A finance director may request a latest financial report to his mail box by sending a small DM like "send report 2011-12" thus keeping the information secure. An authoriser may approve an expense claim using a DM.

Background

Twitter, in theory, could be considered to be a massive and durable message queue. Evrey tweet is like a packet of message which is meaningful to someone (or is it?). What if the message is only meaningful to your system? What if the message contains information to drive a workflow in your system? What if your system processes these messages and does meaningful work? So, do you get a means to send messages to your system from anywhere in the world, from any client, any device? Yes? And you don't have to deploy anything on any server outside your work domain! Of course, Twitter can be replaced by Facebook for that matter in theory to achieve the same effect. Or even an email system can do the same in some clunky kind of way.

Of course, there isn't much of data security here. We use the direct messaging service available within Twitter to ensure relative privacy. We can maximise data safety by making the messages short and cryptic, but that goes against usability. A user would like to enter short meaningful and easy to remember phrases. This article doesn't explore how we can tackle data security issues.

Using the code

The code is quite easy to read. Please feel free to ask questions.

The solution uses snippets available in the public domain from very kind people. The application has a WPF front-end which connects to your Twitter account using oAuth and persists the keys for future calls to Twitter API. Then it polls your account for direct messages of certain signatures (formats). The signatures are defined within extension DLLs that you would write. I have added a sample extension DLL which listens to messages like "pop hello Isha" and then shows a Windows message dialog which says someone said "Isha". Here "pop" is the name of the processor and "hello" is the operation. The rest of the keywords are parameters. Check out the source code of this class.

In my set up, I have a DLLl which knows how to create a timesheet record in our internal system. It expects direct messages of the form: - ts create p="Product 02 Release 9" a="Project Team Leading" d=06/11/2011 h=7.

MEF has been used as an IOC container to plug in processors for the messages and also services for sending out notifications.

Points of interest

The process of launching the embedded WebBrowser to run the oAuth workflow is interesting to look at:

C#
private void OnUrlLoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
    if (string.Compare(e.Uri.AbsoluteUri, "https://api.twitter.com/oauth/authorize", true) == 0)
    {
        if (!e.Uri.Query.Contains("oauth_token"))
        {
            var doc = this._authWebBrowser.Document as mshtml.HTMLDocument;
            // Get the user name here, so that you can persist the keys by user name
            var user = doc.getElementById("session") as mshtml.HTMLDivElement;
            if (user != null)
            {
                string userText = user.innerText.Trim();
                string twitterName = userText.Split(' ').FirstOrDefault();
                if (twitterName.Length > 0)
                {
                    AppUser = twitterName;
                }
            }
            // Check if there is DIV with id="oauth_pin"
            var oauthPinElement = doc.getElementById("oauth_pin") as mshtml.IHTMLElement;
            if (null != oauthPinElement)
            {
                var div = oauthPinElement as mshtml.HTMLDivElement;
                if (null != div)
                {
                    var pinText = div.innerText;
                    if (!string.IsNullOrEmpty(pinText))
                    {
                        // We have validation
                        OAuthPin = pinText.Trim();
                        MatchCollection collection = Regex.Matches(OAuthPin, @"\d+");
                        if (collection.Count > 0)
                        {
                            OAuthPin = collection[0].Value;
                        }

                        Authorized = true;
                    }
                }
            }
            else
            {
                // User has deined access.
                Authorized = false;
            }

            this.DialogResult = true;
            this.Close();
        }
    }
}

The MEF framework expects the processor and services DLLs. The names of the extension DLLs for the processors must be of the form *.TweetProcessor.dll and must reside in the \extension folder relative to the executable. Similarly, the names of the extension DLLs for the services must be of the form *.Service.dll and must reside in the \extension folder relative to the executable.

C#
[InheritedExport(typeof(IMessagingService))]
public interface IMessagingService
{
    bool SendEMail(string from, string[] toList, string subject, 
                   string body, out string error);
}

[InheritedExport(typeof(IProcessingElement))]
public interface IProcessingElement
{
    [Description("Command name")]
    string Moniker { get; }
    //List<CommandOptions> CommandOptionsCollection { get; }

    IEnumerable<imessagingservice /> MessagingServices { get; set; }
}

public class TweetProcess
{
    [ImportMany(typeof(IProcessingElement))]
    IEnumerable<IProcessingElement> ListProcessors { get; set; }

    [ImportMany(typeof(IMessagingService))]
    IEnumerable<imessagingservice /> MessagingServices { get; set; }

    public TweetProcess()
    {
        string path = System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        ListProcessors = new CompositionContainer(
            new DirectoryCatalog(path + @"\Extensions", 
            "*.TweetProcessor.dll")).GetExportedValues<iprocessingelement />();

        MessagingServices = new CompositionContainer(
            new DirectoryCatalog(path + @"\Extensions", 
            "*.Service.dll")).GetExportedValues<imessagingservice />();

        foreach (IProcessingElement element in ListProcessors)
        {
            element.MessagingServices = MessagingServices;
        }
    }

TweetSharp is a fantastic library for accessing Twitter. I am not sure if this is maintained anymore. TweetSharp makes it a doddle to query Twitter:

C#
public static bool Authenticate(string consumerKey, string consumerSecret, string accessToken, string accessTokenSecret)
{
    return FluentTwitter.CreateRequest().AuthenticateWith(consumerKey, consumerSecret, 
           accessToken, accessTokenSecret).Statuses().OnUserTimeline().Request().ResponseHttpStatusCode != 401;
}

public void DeleteAllDirectMessages(string consumerKey, string consumerSecret, 
            string accessToken, string accessTokenSecret)
{
    var directMessages = FluentTwitter.CreateRequest().AuthenticateWith(consumerKey, consumerSecret, 
        accessToken, accessTokenSecret).DirectMessages().Sent().Take(200).Request().AsDirectMessages();
    foreach (TwitterDirectMessage directMessage in directMessages)
    {
        var x = FluentTwitter.CreateRequest().AuthenticateWith(consumerKey, consumerSecret, 
                accessToken, accessTokenSecret).DirectMessages().Destroy(directMessage.Id).Request();
    }
}

Finally, I am polling Twitter every 10 seconds for any new direct messages. You may want to use the Twitter Streaming API to do this more efficiently perhaps, but I haven't explored that option.

C#
internal void Start(string consumerKey, string consumerSecret, string accessToken, 
         string accessTokenSecret, TweetProcessor.MainWindow.UpdateListDelegate dg)
{
    while (true)
    {
        ProcessDiretMessages(consumerKey, consumerSecret, accessToken, accessTokenSecret, dg);
        System.Threading.Thread.Sleep(10000);
    }
}

ProcessDirectMessages gets the direct messages since the last read, parses the messages, and then invokes a relevant message processor for every message, depending on the keywords used in the message.

C#
var directMessagesRequest = FluentTwitter.CreateRequest().AuthenticateWith(consumerKey, consumerSecret, 
    accessToken, accessTokenSecret).DirectMessages().Received();//.Take(200).Request().AsDirectMessages();
IEnumerable<TwitterDirectMessage> directMessages = null; ;
if (uLastId > 0)
{
    directMessages = directMessagesRequest.Since(uLastId).Request().AsDirectMessages();
}
else
{
    directMessages = directMessagesRequest.Take(200).Request().AsDirectMessages();
}

bool success = true;
string lastTweetId = string.Empty;
string message = string.Empty;

if (directMessages != null)
{
    foreach (TwitterDirectMessage directMessage in directMessages.OrderBy(s => s.Id))
    {
        success = ProcessTweet(directMessage, out message);
        lastTweetId = directMessage.Id.ToString();
        
        //Delete the direct message
        FluentTwitter.CreateRequest().AuthenticateWith(consumerKey, consumerSecret, accessToken, 
                      accessTokenSecret).DirectMessages().Destroy(directMessage.Id).Request();
        
        //TwitterResult result = FluentTwitter.CreateRequest().AuthenticateWith(consumerKey,
        //   consumerSecret, accessToken, accessTokenSecret).DirectMessages().Send(directMessage.SenderScreenName,
        //    (message.Length > 110 ? message.Substring(0, 110) : message) + " " + DateTime.Now.ToString()).Request();

        if (!success)
        {
            string temp = string.Empty;
            TrySendMessage("rahul.kumar@sage.com", "rahul.kumar@sage.com", 
                           directMessage.Sender.ScreenName, 
                           directMessage.Text + Environment.NewLine + message, out temp);
        }
        List<string /> list = new List<string />();
        list.Add(string.Format("{0}:{1} - {2}", directMessage.Sender.ScreenName,
            directMessage.Text, message));
        dg.DynamicInvoke(new object[] { list });
    }
}

Please take a look at the CommandOptionsFactory class for the code which parses the direct messages for extracting command fragments for execution:

C#
public class CommandOptionsFactory
{
    public static T ParseOptions<t>(string narrative) where T : new()
    {
        if (typeof(T).BaseType != typeof(CommandOptions))
            throw new System.ArgumentException("Only works with CommandOptions objects");
        narrative = ProcessQuotes(narrative);

        List<string> fragments = 
          narrative.Split(' ').Select(s => s.Trim()).Where(s => s.Length > 0).ToList();
        T options = new T();
        Type type = typeof(T);
        PropertyInfo[] infos = type.GetProperties();
        bool success = true;
        foreach (string fragment in fragments)
        {
            success &= ProcessOption(fragment, options as CommandOptions, infos);
        }

        (options as CommandOptions).Initialised = success;

        return options;
    }

    public const string SPACE = "_|_";

    private static string ProcessQuotes(string narrative)
    {
        // Get the quotes
        List<string> fragments = narrative.Split('"').ToList();

        for (int i = 1; i < fragments.Count(); i += 2)
        {
            //Convert the spaces to a symbol - SPACE
            fragments[i] = fragments[i].Split(' ').Select(
              s => s.Trim()).Where(s => s.Length > 0).Aggregate((a, b) => a + SPACE + b);
        }

        return fragments.Aggregate((a, b) => a + b);
    }

    private static bool ProcessOption(string fragment, CommandOptions options, PropertyInfo[] infos)
    {
        bool success = false;
        List<string> pair = fragment.Split('=').ToList();
        if (pair.Count > 1)
        {
            foreach (PropertyInfo info in infos)
            {
                object[] attribs = info.GetCustomAttributes(typeof(ParamAttribute), false);
                if (attribs != null && attribs.FirstOrDefault() != null)
                {
                    ParamAttribute param = attribs.First() as ParamAttribute;
                    if (param != null)
                    {
                        if (string.Compare(pair.First(), param.Name, true) == 0)
                        {
                            info.SetValue(options, pair[1], new object[] { });
                            success = true;
                            break;
                        }
                    }
                }
            }
        }
        return success;
    }
}</t>

To be able to run the solution, you must have two Twitter accounts - one for the server and another for the client. The two accounts must follow each other for the DM (direct message) to work. When you run the application, go through the oAuth work flow to authorise the application to read/delete messages from the Twitter account you selected to be the server. Now use the second Twitter account to send a direct message to the server Twitter account. Send a message like pop hello Isha. This will make a window popup dialog to appear with "hello Isha" on it. BTW, Isha is my lovely 15 month old daughter.

This demo covers the process of getting direct messages to the server. It is now up to you to write a MEF extension to do meaningful work with it. Also, you would have to define the syntax for driving your workflow.

You must create an application on your Twitter login (in developer mode) and register this application (TweetProcessor or any other name you want to give). Get the ConsumerKey and ConsumerSecret from this new application on Twitter and stick it in the Settings.settings of this demo solution. Now you should be good to go. Please let me know if you have a question.

History

  • 30/01/2012: First posted. Waiting for feedback.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)