Posted by Will on Monday, April 07, 2008 at 10:14 PM

UPDATE #2: I've added a link to an improved version of the source code sent in by Hafthor Stefensson at the end of the article.

UPDATE: I've made a few changes to this article (at the end) based on comments kindly left by Scott Hanselman.  The project source files and compiled macro dll have the updates discussed.

Danny nagged me into getting a Twitter account a few weeks ago.  The traffic content runs from interesting to spam.  Around that same time I made the switch from Community Server to dasBlog for this site, resolving to post more (read: more than once per quarter).  The convergence of my curiosity regarding customizing dasBlog and the Twitter API along with my desire to ease getting content onto my blog led me to this post.  Danny pointed out hashtags, a service that will use the hash/pound character (#) as an indicator for a metatag in a Twitter post, similar to a code comments.  After looking a the javascript based badge for displaying your Twitter posts on a web site I though, why not write a dasBlog macro that will selectively pull in Twitter posts to show on my blog?  I could prefix the posts I want to show up with a special character.  That way visitors to my blog don't have to read "I love Kashi GoLean, but Kashi GoLean doesn't love me.", but can see the link to the VS2008 Hot-fix Roll-up.  And better yet I could use a caret (^) as the special character because then I could call it CaretStick, you know, because the caret would let me stick it in my blog.  It's too cheesy to pass up.  Anyway, none of this is particularly difficult, but it does touch on several programming concepts for those looking for working examples of HttpWebRequest, Regular Expressions, and XML in the context of .net.

I started by familiarizing myself with the dasBlog macro guide article.  It is a thorough post, and there's no point in me repeating the content here so read it first if you're not familiar with the process.  This article will assume you have.  I will point out the specifics of this implementation however and one additional modification I had to make to the web.config file at the end.

I started by creating a class library and vainly named it ThirstyCrow.DasBlogMacros.  I named the class DasCaretStick and created a single public virtual control called CaretStick with two arguments, a string for the Twitter feed URL and a boolean to indicate whether to suppress error messages. 

public virtual Control CaretStick(string twitterURL, bool suppressErrors) {
    return new LiteralControl(ParseTwitterFeed(twitterURL, suppressErrors));
}

Inside the CaretStick control I call the method that does all the work - ParseTwitterFeed.  The Twitter API has a lot of info about pulling different content out of an account and also about how to make posts to an account.  For this macro though all I'm really interested in is parsing the existing content, so rather than make an authenticated request I chose instead to parse the publicly available RSS stream output from my account.  This is done using the HttpWebRequest class. 

private string ParseTwitterFeed(string twitterURL, bool suppressErrors) {

    StringBuilder sticks = new StringBuilder();
    sticks.Append("<div class=\"caretStickWrapper\">\r\n");

    try {

        HttpWebRequest request = WebRequest.Create(twitterURL) as HttpWebRequest;

        using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) {

            StreamReader reader = new StreamReader(response.GetResponseStream());

            XmlDocument doc = new XmlDocument();
            doc.Load(reader); 

You'll notice I've also instantiated a string builder that will be used to hold the HTML that will be output to the control.  StringBuilder performs better than string when doing successive concatenation operations, which we'll be doing as we parse the Twitter feed items.  I've started the HTML output by creating a wrapper div for the entire content and applied the class caretStickWrapper.  This will allow us to style the output with CSS.  You'll also notice the opening of a try catch block used to handle any errors that may arise.  More on that later.  By the last line of this block we've requested the RSS feed (an XML document) from the twitterURL, gotten the response stream from the response, and loaded it into an instance of an XmlDocument.

The RSS specification lays out the structure of an RSS feed.  In this case we're interested in the item nodes of the document.  We can use an XPath query to select all of the item nodes into an XmlNodeList that we can loop through.  We pass in the XPath query as an argument of the SelectNodes method of the XmlDocument we're working with.  The double slashes (//) indicate to select all item nodes, regardless of where they are in the document.

XmlNodeList nodeList = doc.SelectNodes("//item");

Under an item node there are several possible child nodes.  We'll need the "description" node, which has the Twitter post in it and the "pubDate" node which has the date and time the Twitter post was made.  How did I know the Twitter post was in the description?  I simply took a look at the raw feed output by pulling it up in my browser and using the View Page Source function.  (The Twitter post is duplicated in the item's title node too).  To select the description nodes we use the SelectSingleNode method of the XmlNode we're working with and grab the node's inner text in a string variable.

foreach (XmlNode xn in nodeList) {
    string rawMessage = xn.SelectSingleNode("description").InnerText;

Next we check whether the message contains a caret.  Testing whether the message starts with a caret will fail because in the feed output all of the messages are formatted username: message.  The earliest we would see our caret would be after the colon following the username.  It is still a useful delimiter for chopping off the text we don't want to show at the front of the message.  Each message will be wrapped in an individual div of the class caretStick.

//use contains instead of startswith because message is prefixed by account name
if (rawMessage.Contains("^")) {
    //*********************************************
    //process the message
    //*********************************************

    // remove everything up to the leading caret
    rawMessage = rawMessage.Substring(rawMessage.IndexOf('^') + 1, rawMessage.Length - rawMessage.IndexOf('^') - 1);

What if we put a link in the Twitter message?  Twitter doesn't include the markup necessary to make it active on our page, but that's easily fixed using regular expressions.  The $0 notation refers to the string matching the regular expression. 

// activate any links
Regex rx = new Regex(@"(http|https|ftp)(://(\w|\.|/)+)");
string processedMessage = rx.Replace(rawMessage, "<a href=\"$0\">$0</a>");

sticks.Append("<div class=\"caretStick\">\r\n" + processedMessage + "\r\n");

At this point we've got our tagged post with all links activated in the processedMessage string.  Next we'll append the publication date information.  This is held in the pubDate subnode.  We'll select it using the same technique we used to grab the description node above then convert it to a date.  With the pubDate in hand we can subtract it from the current time do determine how old the post is and format a friendly message about the age of the post.

//parse the pubdate
DateTime pubDate;
if (DateTime.TryParse(xn.SelectSingleNode("pubDate").InnerText, out pubDate)) {

    //format a friendly message about the post's age
    TimeSpan span = DateTime.Now.Subtract(pubDate);

    sticks.Append("<div class=\"caretStickPubDate\">");

    if (span.TotalSeconds < 60.0) {
        sticks.Append(span.TotalSeconds.ToString("0") + "sec ago");
    }
    else if (span.TotalHours < 1.0) {
        sticks.Append(span.TotalMinutes.ToString("0") + "min ago");
    }
    else if (span.TotalHours < 24.0) {
        sticks.Append(span.TotalHours.ToString("0") + "hr " + span.Minutes.ToString("0") + "min ago");
    }
    else {
        sticks.Append("on " + pubDate.ToShortDateString() + " at " + pubDate.ToShortTimeString());
    }

    sticks.Append("</div>\r\n");

}

sticks.Append("</div>\r\n");

We end the loop iteration by closing the caretStick div.

In the case of an error we have a couple of options.  We can suppress the error message or show it in our output to the control.  Because there are several places that the error could occur with respect to the div's we're making it is safer to discard any HTML up to the point of the error so that we don't end up with an unclosed div that could wreck the page layout.  This code could be written to handle errors at various stages and salvage the output up to the point of failure, but I just want a simple all or nothing solution in this case.

catch (Exception exception) {
    if (suppressErrors) {
        sticks = new StringBuilder();
        sticks.Append("<div class=\"caretStickWrapper\">\r\n");
    }
    else {
        sticks = new StringBuilder();
        sticks.Append("<div class=\"caretStickWrapper\">\r\n");
        sticks.Append("<div class=\"caretStickError\">An error occurred: " + exception.Message + "</div>\r\n");
    }
}
sticks.Append("</div>\r\n");
return sticks.ToString();

We end up by closing the outermost caretStickWrapper div and returning the output from the StringBuilder to the calling method. 

To make use of the finished project modify the web.config file based on the instructions in the macro guide article.  The syntax for this macro is:

<add macro="DasCaretStick" type="ThirstyCrow.DasBlogMacros.DasCaretStick, ThirstyCrow.DasBlogMacros" />

One other modification I had to make to the web.config file was in the trust setting.  To get this macro to work since it is issuing an HttpWebRequest under Medium trust I had to specify the originUrl as follows:

<trust level="Medium" originUrl=".*" />

Drop the ThirstyCrow.DasBlogMacros.dll file in your site's bin directory and add the macro to your template.

<%CaretStick("your twitter feed URL here", "false")|dascaretstick%> (see below)

And that should be it.  Another issue I ran into is that the control output does not change the document modified date of the default.aspx page on the server so browsers will simply show the cached version of the page even though new Twitter posts may be available.  I added a no-cache directive in the response header (code not shown above) using the SharedBasePage reference.  If anyone can think of a more elegant solution please let me know and I'll update the code and this post.  You can see a working version of this macro in the sidebar under the Microblog heading.

UPDATE: Twitter allows for 70 hits per minute max so caching the content of your feed is necessary if you get more than one or two visitors.  I've restructured the code and added a method to cache the results of the ParseTwitterFeed method.  DasBlog stores posts in a directory called "content".  Create a subdirectory of the "content" directory called "caretsticks" to hold the cached results.  (The code checks for the existence of the caretsticks directory and attempts to create it.  However, at least for me, a permissions error is thrown under medium trust when attempting to create the directory). The template code has changed based on a new argument that is passed in indicating how long you want the cache to live.  I personally chose 30 minutes.  The template code syntax should now look like this:

<%CaretStick("your twitter feed URL here", "false", "30")|dascaretstick%>

Despite looking at the dasBlog source and googling forever, I still can not figure out how to force the macro's literal control to refresh without adding the no-cache directive to the header.  I'll investigate more as time permits.

You can download the complete Visual Studio solution here: DasBlogMacros.zip (722.15 KB)
You can download the compiled dll here: CompiledDasBlogMacro.zip (3.39 KB)

New improved version from reader Hafthor Stefansson: HS_DasCaretStick.zip (2.20 KB)

Comments [4]     Categories: dasBlog Macro | Twitter