Monday, June 17, 2013

Proxy Page for New Twitter API 1.1

The old Twitter API V1.0 is just retired. The new API V1.1 requires OAuth authentication for each Twitter request. Our SharePoint Twitter WebPart is no longer working since this API update. I created a SharePoint layout page as a proxy connecting to Twitter using the new API so the Twitter services are available in our Intranet without the troublesome OAuth authentication. Also the proxy page caches the result locally for performance consideration and avoids exceeding the new Twitter request limit:

<%@ Page Language="C#"  %>

<%@ Import Namespace="System.Web.UI" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Globalization" %>
<%@ Import Namespace="System.Security.Cryptography" %>
<%@ Import Namespace="System.Net.Security" %>
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Text" %>

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>A proxy page for Twitter API V1.1</title>
    <meta name="ROBOTS" content="NOINDEX, NOFOLLOW">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta http-equiv="Content-language" content="en">

    <script runat="server">
            
        // oauth application keys
        string consumerKey = "xxxxxxxx";
        string consumerSecret = "xxxxxxxx";
        string accessToken = "xxxxxxxx";
        string accessTokenSecret = "xxxxxxxx";

        int cacheTime = 5; // Cache time in minutes
        string baseUrl = "https://api.twitter.com/1.1/";        
                
        void Page_Load(object sender, System.EventArgs e)
        {
            // handle parameters
            bool noCache = Request.QueryString["nocache"] == "true" ? true : false;
            string endpoint= string.IsNullOrEmpty(Request.QueryString["endpoint"]) ? "statuses/user_timeline.json" : Request.QueryString["endpoint"];
            string[] resStrings = new string[] { "endpoint", "nocache", "callback", "_" };
            List<string> reservedParameters = new List<string>(resStrings); 
            
            Dictionary<string, string> parameters = new Dictionary<string, string>();
            StringBuilder keys = new StringBuilder();
            foreach (String key in Request.QueryString.AllKeys)
            {
                if (!reservedParameters.Contains(key) && !parameters.ContainsKey(key)) 
                {
                    parameters.Add(key, Request.QueryString[key]);
                    keys.Append(key + "=" + Request.QueryString[key] + "|");
                }
            }
            string cacheKey = keys.ToString();
            if (string.IsNullOrEmpty(cacheKey)) // simply return if no parameter provided 
            {
                lblInfo.Text = "Invalid parameters";
                return;            
            }     
            
            string tweets = Convert.ToString(Cache[cacheKey]);
            if (noCache || string.IsNullOrEmpty(tweets)) // check if cache exsits
            {
                string requestUrl = baseUrl + endpoint;
                try
                {
                    tweets = GetTweets(requestUrl, parameters);
                    // Update cache
                    Cache.Insert(cacheKey.ToString(), tweets, null, DateTime.Now.AddMinutes(cacheTime), System.Web.Caching.Cache.NoSlidingExpiration);
                }
                catch (Exception ex)
                {
                    lblInfo.Text = "Error occur: " + ex.Message;
                    return;
                }
            }
            
            // prepare for writting data to Response
            Response.Clear();
            Response.ContentType = "application/json; charset=utf-8";
            Response.ContentEncoding = System.Text.Encoding.UTF8;
            if (!string.IsNullOrEmpty(Request.QueryString["callback"])) // wrap data for JSONP
                Response.Write(string.Format("{0}({1})", Request.QueryString["callback"], tweets));
            else
                Response.Write(tweets);
            Response.End();
        }

        // Reference: https://dev.twitter.com/discussions/15206
        string GetTweets(string url, Dictionary<string, string> parameters)
        {
            string responseString = string.Empty;
            StringBuilder queryStrings = new StringBuilder();

            // OAuth setting
            string oauthSignatureMethod = "HMAC-SHA1";
            string oauthVersion = "1.0";
            string oauthNonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString()));
            TimeSpan timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
            string oauthTimestamp = Convert.ToInt64(timeSpan.TotalSeconds).ToString();
            string compositeKey = Uri.EscapeDataString(consumerSecret) + "&" + Uri.EscapeDataString(accessTokenSecret);

            // OAauth signature: https://dev.twitter.com/docs/auth/creating-signature
            string oauthSignature;
            SortedList<string, string> authSigBaseValues = new SortedList<string, string>();
            authSigBaseValues.Add("oauth_consumer_key", consumerKey);
            authSigBaseValues.Add("oauth_nonce", oauthNonce);
            authSigBaseValues.Add("oauth_signature_method", oauthSignatureMethod);
            authSigBaseValues.Add("oauth_timestamp", oauthTimestamp);
            authSigBaseValues.Add("oauth_token", accessToken);
            authSigBaseValues.Add("oauth_version", oauthVersion);
            foreach (string key in parameters.Keys)
            {
                string escapedKey = Uri.EscapeDataString(key);
                string escapedValue = Uri.EscapeDataString(parameters[key]);
                authSigBaseValues.Add(escapedKey, escapedValue);
                queryStrings.Append("&" + escapedKey + "=" + escapedValue);
            }

            // build signagure base string
            StringBuilder oauthSigSB = new StringBuilder();
            foreach (KeyValuePair<string, string> item in authSigBaseValues)
            {
                oauthSigSB.Append("&" + item.Key + "=" + item.Value);
            }
            string signatureBaseString = "GET&" + Uri.EscapeDataString(url) + "&" + Uri.EscapeDataString(oauthSigSB.ToString().Remove(0, 1));

            // create OAuth signature 
            using (HMACSHA1 hasher = new HMACSHA1(ASCIIEncoding.ASCII.GetBytes(compositeKey)))
            {
                oauthSignature = Convert.ToBase64String(hasher.ComputeHash(ASCIIEncoding.ASCII.GetBytes(signatureBaseString)));
            }

            // create the request header
            string headerFormat = "OAuth oauth_nonce=\"{0}\", oauth_signature_method=\"{1}\", oauth_timestamp=\"{2}\", " +
                    "oauth_consumer_key=\"{3}\", oauth_token=\"{4}\", oauth_signature=\"{5}\", oauth_version=\"{6}\"";
            string authHeader = string.Format(headerFormat,
                                    Uri.EscapeDataString(oauthNonce),
                                    Uri.EscapeDataString(oauthSignatureMethod),
                                    Uri.EscapeDataString(oauthTimestamp),
                                    Uri.EscapeDataString(consumerKey),
                                    Uri.EscapeDataString(accessToken),
                                    Uri.EscapeDataString(oauthSignature),
                                    Uri.EscapeDataString(oauthVersion)
                            );
            if (queryStrings.Length > 0)
                url = url + "?" + queryStrings.ToString().Remove(0, 1);
            ServicePointManager.ServerCertificateValidationCallback = 
                new RemoteCertificateValidationCallback(delegate { return true; });
            ServicePointManager.Expect100Continue = false;

            // create request
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Headers.Add("Authorization", authHeader);
            request.Method = "GET";
            request.ContentType = "application/x-www-form-urlencoded";

            try
            {
                WebResponse response = request.GetResponse();
                responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
            }
            catch (Exception ex)
            {
                throw  ex;
            }

            return responseString;
        }
    </script>

</head>
<body>
    <form id="form1" runat="server">
    <div>
        <p>
            <asp:Label ID="lblInfo" runat="server"></asp:Label>
        </p>
    </div>
    </form>
</body>
</html>

The proxy page can work as a regular ASP.NET page inside IIS. It can also run as a layout page in SharePoint: simply copy the file to the layouts folder under SharePoint 12/14 hive. and it will just work.

The proxy accepts different endpoint and parameter where the endpoint is the twitter service endpoint such as "statuses/user_timeline.json" or "search/tweets.json", and parameter is the query strings you pass to twitter such as "screen_name=mytwittername". The easiest way to consume the proxy Twitter servcie is using JavaScript which can be inserted into SharePoint OOB content editor WebPart:

<script type="text/javascript">
<!-- 
    $(document).ready(function() {
        // twitter proxy URL
        var url = 'http://twitterproxy/twitter.aspx?endpoint=statuses/user_timeline.json&screen_name=myname&count=10';
       // http://twitterproxy/twitter.aspx?endpoint=search/tweets.json&q=from:myname&result_type=recent
        $.ajax({url : url, cache: false, crossDomain: true, dataType: 'jsonp'}).done(function(data) {
            $('#tweeters').html("");
            $.each(data, function(i, tweet) {
                if(tweet.text) {
                    var date = parseDate(tweet.created_at);
                    var tweet_html = '<div><div class="tweetText">' + tweet.text + '</div>';
                    tweet_html += '<div class="tweetDate">'+ date.toString().substring(0, 24) +'</div></div>';
                    $('#tweeters').append(tweet_html);
                }
            });
        });
    });
    
    // Fix IE DateTime format issue
    function parseDate(str) {
        var v=str.split(' ');
        return new Date(Date.parse(v[1]+" "+v[2]+", "+v[5]+" "+v[3]+" UTC"));
    }
// -->  
</script>