Friday, September 3, 2010

Tweeting Logger (Twitter Logger Updated -- Using OAuth)

Twitter now requires OAuth in order to use their API. I thought it might be nice to provide a new Logger that tweets using Twitter's OAuth.

Unfortunately, compared to the original TwitterLogger, the code has ballooned quite a bit. However, if you want to use one of the available Twitter API libraries, then most of the code here would go away and you'd be back to a tiny Logger subclass that Tweets.

But if you want to avoid adding yet another DLL to your application and you have limited needs to integrate with Twitter--such as just tweeting, then this should get you started.

Check out the Twitter help for obtaining a Consumer Key, a Consumer Secret, an Access Token, and an Access Token Secret.

Notice that there is only one method that is overridden. The rest of the code is there to support OAuth. Remember, this is intended to be neither a complete OAuth implementation, nor a complete Twitter API integration. It's just a simple TweetingLogger. And frankly, it could be improved quite a bit. (e.g. Separating out the tweeting specifics into another class, parameterizing the constants, etc.) But it's good enough to get you started.

public class TweetingLogger : Logger
        const string TweetUrl = "";

        const string ConsumerKey = "[Your Consumer Key]";
        const string ConsumerSecret = "[Your Consumer Secret]";
        const string AccessToken = "[Your Access Token]";
        const string AccessTokenSecret = "[Your Access Token Secret]";

        protected string GetOAuthUrlEncode(string aValue)
            // Thanks to Stephen Denton for this url encode

            var newValue = System.Web.HttpUtility.UrlEncode(aValue).Replace("+", "%20");

            // UrlEncode escapes with lowercase characters (e.g. %2f) but oAuth needs %2F
            newValue = System.Text.RegularExpressions.Regex.Replace(newValue, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper());

            // these characters are not escaped by UrlEncode() but needed to be escaped
            newValue = newValue.Replace("(", "%28").Replace(")", "%29").Replace("$", "%24").Replace("!", "%21").Replace("*", "%2A").Replace("'", "%27");

            // these characters are escaped by UrlEncode() but will fail if unescaped!
            newValue = newValue.Replace("%7E", "~");

            return newValue;

        protected string GetTimeStamp()
            TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return Convert.ToInt64(ts.TotalSeconds).ToString();

        protected string GetNonce()
            return Guid.NewGuid().ToString();

        protected string GetOAuthBaseString(string anHttpMethod, string aBaseUri, SortedDictionary<string, string> someParameters)
            var paramStrings = new List<String>();
            foreach (string key in someParameters.Keys)
                paramStrings.Add(key + "=" + someParameters[key]);
            return anHttpMethod.ToUpper() + "&" + GetOAuthUrlEncode(aBaseUri) + "&" + GetOAuthUrlEncode(String.Join("&", paramStrings.ToArray()));

        protected string GetAuthSignature(SortedDictionary<string,string> someParameters)
            var hmacsha1 = new System.Security.Cryptography.HMACSHA1(Encoding.ASCII.GetBytes(ConsumerSecret + "&" + AccessTokenSecret));
            var bytes = hmacsha1.ComputeHash(Encoding.ASCII.GetBytes(GetOAuthBaseString("POST", TweetUrl, someParameters)));
            return Convert.ToBase64String(bytes);

        protected SortedDictionary<string, string> GetOAuthParameters()
            return new SortedDictionary<string,string>()
                { "oauth_nonce", GetNonce() },
                { "oauth_consumer_key", ConsumerKey },
                { "oauth_signature_method", "HMAC-SHA1" },
                { "oauth_timestamp", GetTimeStamp() },
                { "oauth_token", AccessToken },
                { "oauth_version", "1.0" },
                //{ "oauth_signature", GetAuthSignature() },    // add later in process

        protected string GetOAuthString(SortedDictionary<string, string> baseParameters, SortedDictionary<string, string> additionalParameters)
            var sb = new StringBuilder();
            sb.Append("OAuth ");

            var paramList = new List<string>();
            foreach (string key in baseParameters.Keys)
                paramList.Add(key + "=" + "\"" + baseParameters[key] + "\"");

            sb.Append(string.Join(",", paramList.ToArray()));

            var allParameters = new SortedDictionary<string, string>(baseParameters);
            foreach (KeyValuePair<string, string> kv in additionalParameters)
                allParameters.Add(kv.Key, kv.Value);

            sb.Append(",oauth_signature=\"" + GetOAuthUrlEncode(GetAuthSignature(allParameters)) + "\"");

            return sb.ToString();

        protected override bool DoLog(LogEntry aLogEntry)
            // without this, you may receive a '417 Expectation Failed' error
            ServicePointManager.Expect100Continue = false;

            using (WebClient wClient = new WebClient())
                var parameters = GetOAuthParameters();
                var additionalParameters = new SortedDictionary<string, string>() { { "status", GetOAuthUrlEncode(aLogEntry.Message) } };
                var theString = GetOAuthString(parameters, additionalParameters);
                wClient.Headers.Add("Authorization", theString);

                var nvc = new NameValueCollection();
                nvc["status"] = aLogEntry.Message;
                wClient.UploadValues(TweetUrl, nvc);

            return true;