Edit: I’ve posted a Dart port of this code here.
For a few years I’ve been meaning to automate the posting of tweets to a couple of twitter accounts I run. I’ve always been too lazybusy but this week I had some time and decided to get it done. For many reasons (including that my parents taught me not to take binary dependencies from strangers on the internet ;)) I decided not to use a 5,000 line-of-code third party library but just instead write the simplest code for what I needed.
While Googling for other peoples implementations of “simplest code to post tweets” to see what I was getting myself into (I didn’t actually find any!) I stumbled across a page on the Twitter site about Single-user OAuth. This allows you to generate tokens for authenticating without having to go through the normal OAuth ballache. Obviously you can only do this for your own account but it massively simplifies things.
By using a single access token, you don’t need to implement the entire OAuth token acquisition dance. Instead, you can pick up from the point where you are working with an access token to make signed requests for Twitter resources.
Most of the work is fairly straight forward but it also requires an OAuth signature in the OAuth header. This was a bit of a pain in the ass… You have to build a big string of all the data in a specific order with specific encoding and then hash it with the secret keys you got. I implemented this (or so I thought) and sent it off to Twitter only go get a BAD AUTHENTICATION DATA response with zero information on what exactly was wrong. Great :(
After much keyboard-bashing I ragequit for the night. Stupid Twitter. After some much-required sleep I realised that on the Creating a signature page, Twitter actually gave an example including the secret keys and timestamp. This is a perfect test case for testing out hashing code! It even gives the values at each step of the algorithm to help track down where you’re going wrong. My issues turned out to mostly be encoding - I’d tried using WebUtility.UrlEncode
and Uri.EscapeUriString
but neither encoded spaces and pluses the way that Twitter excepted. It turned out that Uri.EscapeDataString
does encode exactly as Twitter requires.
In total, my class turned out to be around 80 lines of code without comments. It’s provided here in its entirety for easy copy/pasting (no, there’s no NuGet package.. didn’t anyone tell you that you shouldn’t trust binary dependencies from strangers on the internet?).
/// <summary>
/// Simple class for sending tweets to Twitter using Single-user OAuth.
/// https://dev.twitter.com/oauth/overview/single-user
///
/// Get your access keys by creating an app at apps.twitter.com then visiting the
/// "Keys and Access Tokens" section for your app. They can be found under the
/// "Your Access Token" heading.
/// </summary>
class TwitterApi
{
const string TwitterApiBaseUrl = "https://api.twitter.com/1.1/";
readonly string consumerKey, consumerKeySecret, accessToken, accessTokenSecret;
readonly HMACSHA1 sigHasher;
readonly DateTime epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Creates an object for sending tweets to Twitter using Single-user OAuth.
///
/// Get your access keys by creating an app at apps.twitter.com then visiting the
/// "Keys and Access Tokens" section for your app. They can be found under the
/// "Your Access Token" heading.
/// </summary>
public TwitterApi(string consumerKey, string consumerKeySecret, string accessToken, string accessTokenSecret)
{
this.consumerKey = consumerKey;
this.consumerKeySecret = consumerKeySecret;
this.accessToken = accessToken;
this.accessTokenSecret = accessTokenSecret;
sigHasher = new HMACSHA1(new ASCIIEncoding().GetBytes(string.Format("{0}&{1}", consumerKeySecret, accessTokenSecret)));
}
/// <summary>
/// Sends a tweet with the supplied text and returns the response from the Twitter API.
/// </summary>
public Task<string> Tweet(string text)
{
var data = new Dictionary<string, string> {
{ "status", text },
{ "trim_user", "1" }
};
return SendRequest("statuses/update.json", data);
}
Task<string> SendRequest(string url, Dictionary<string, string> data)
{
var fullUrl = TwitterApiBaseUrl + url;
// Timestamps are in seconds since 1/1/1970.
var timestamp = (int)((DateTime.UtcNow - epochUtc).TotalSeconds);
// Add all the OAuth headers we'll need to use when constructing the hash.
data.Add("oauth_consumer_key", consumerKey);
data.Add("oauth_signature_method", "HMAC-SHA1");
data.Add("oauth_timestamp", timestamp.ToString());
data.Add("oauth_nonce", "a"); // Required, but Twitter doesn't appear to use it, so "a" will do.
data.Add("oauth_token", accessToken);
data.Add("oauth_version", "1.0");
// Generate the OAuth signature and add it to our payload.
data.Add("oauth_signature", GenerateSignature(fullUrl, data));
// Build the OAuth HTTP Header from the data.
string oAuthHeader = GenerateOAuthHeader(data);
// Build the form data (exclude OAuth stuff that's already in the header).
var formData = new FormUrlEncodedContent(data.Where(kvp => !kvp.Key.StartsWith("oauth_")));
return SendRequest(fullUrl, oAuthHeader, formData);
}
/// <summary>
/// Generate an OAuth signature from OAuth header values.
/// </summary>
string GenerateSignature(string url, Dictionary<string, string> data)
{
var sigString = string.Join(
"&",
data
.Union(data)
.Select(kvp => string.Format("{0}={1}", Uri.EscapeDataString(kvp.Key), Uri.EscapeDataString(kvp.Value)))
.OrderBy(s => s)
);
var fullSigData = string.Format(
"{0}&{1}&{2}",
"POST",
Uri.EscapeDataString(url),
Uri.EscapeDataString(sigString.ToString())
);
return Convert.ToBase64String(sigHasher.ComputeHash(new ASCIIEncoding().GetBytes(fullSigData.ToString())));
}
/// <summary>
/// Generate the raw OAuth HTML header from the values (including signature).
/// </summary>
string GenerateOAuthHeader(Dictionary<string, string> data)
{
return "OAuth " + string.Join(
", ",
data
.Where(kvp => kvp.Key.StartsWith("oauth_"))
.Select(kvp => string.Format("{0}=\"{1}\"", Uri.EscapeDataString(kvp.Key), Uri.EscapeDataString(kvp.Value)))
.OrderBy(s => s)
);
}
/// <summary>
/// Send HTTP Request and return the response.
/// </summary>
async Task<string> SendRequest(string fullUrl, string oAuthHeader, FormUrlEncodedContent formData)
{
using (var http = new HttpClient())
{
http.DefaultRequestHeaders.Add("Authorization", oAuthHeader);
var httpResp = await http.PostAsync(fullUrl, formData);
var respBody = await httpResp.Content.ReadAsStringAsync();
return respBody;
}
}
}
And to use it:
var twitter = new TwitterApi(ConsumerKey, ConsumerKeySecret, AccessToken, AccessTokenSecret);
var response = await twitter.Tweet("This is my first automated tweet!");
Console.WriteLine(response);
You’re free to do as you please with this code. If you improve anything significantly, do leave a comment!