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:
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;
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;
}
}
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))
{
OAuthPin = pinText.Trim();
MatchCollection collection = Regex.Matches(OAuthPin, @"\d+");
if (collection.Count > 0)
{
OAuthPin = collection[0].Value;
}
Authorized = true;
}
}
}
else
{
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.
[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; }
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:
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.
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.
var directMessagesRequest = FluentTwitter.CreateRequest().AuthenticateWith(consumerKey, consumerSecret,
accessToken, accessTokenSecret).DirectMessages().Received();
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();
FluentTwitter.CreateRequest().AuthenticateWith(consumerKey, consumerSecret, accessToken,
accessTokenSecret).DirectMessages().Destroy(directMessage.Id).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:
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)
{
List<string> fragments = narrative.Split('"').ToList();
for (int i = 1; i < fragments.Count(); i += 2)
{
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.