Posting an Image via Tumblr V2 API inside Unity

May 24th, 2014

For a project I needed to upload screenshots from my Unity project to Tumblr, so I got into the whole OAuth & Tumblr encoding mess. I am posting the final working source code, with the hope that it will save someone the two days of struggle.

using UnityEngine;
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Security.Cryptography;
using System.IO;
 
public class URLUtils {
	private static string _UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
	public static string URLEncodeBytes(byte[] value, bool encodePercent)
	{
		var osb = new StringBuilder(value.Length * 3);
		for (var x = 0; x < value.Length; x++)
		{
			var b = value[x];
 
 
			if( (char)b == '%') {
				osb.Append ("%25");
			} else if ((_UnreservedChars.IndexOf((char)b) == -1) && ((char)b) != '~' && (((char)b) != '%'))
			{
				osb.AppendFormat("%{0:X2}", b);
			}
			else
			{
				osb.Append((char)b);
			}
			
		}
		
		if(encodePercent)	{
			osb = osb.Replace("%", "%25");// Revisit to encode actual %'s
		}
		
		return osb.ToString();
	}
}
 
public class TumblrOAuth : MonoBehaviour {
	
	private string consumerKey = "";
	private string consumerSecret = "";
 
	private string accessToken = "";
	private string accessTokenSecret = "";
 
	void Start () {
		string imagePath = "file://"+Application.dataPath + "/../image.png";
		StartCoroutine(UploadImage("xxxx.tumblr.com", imagePath));
	}
 
	private static string GenerateTimeStamp()
	{
		// Default implementation of UNIX time of the current UTC time
		TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
		return Convert.ToInt64(ts.TotalSeconds, CultureInfo.CurrentCulture).ToString(CultureInfo.CurrentCulture);
	}
	
	private static string GenerateNonce()
	{
		// Just a simple implementation of a random number between 123400 and 9999999
		return new System.Random().Next(123400, int.MaxValue).ToString();
	}
 
	
	IEnumerator UploadImage(string blogAddress, string imagePath) {
		string uri =  string.Format("http://api.tumblr.com/v2/blog/{0}/post",blogAddress);
 
		// download image
		WWW img = new WWW(imagePath);
		yield return img;
 
 
		// signature
		Dictionary<string, string> options = new Dictionary<string, string>();
		
		string timestamp = GenerateTimeStamp();
		string nonce = timestamp+GenerateNonce();
		string method = "POST";
 
		options.Add("oauth_nonce", nonce);
		options.Add("oauth_timestamp", timestamp);
 
		var stringBuilder = new StringBuilder();
		
		stringBuilder.Append(method+"&");
		stringBuilder.Append(Uri.EscapeDataString(uri));
		stringBuilder.Append("&");
		
		//the key value pairs have to be sorted by encoded key
		var dictionary = new SortedDictionary<string, string>
		{
			//oauth
			{"oauth_token", accessToken},
			{"oauth_consumer_key", consumerKey},
			{"oauth_timestamp", timestamp},
			{"oauth_nonce", nonce},
			{"oauth_signature_method", "HMAC-SHA1"},
			{"oauth_version", "1.0"},
 
			// post params
			{"type","photo"},
			{"data[0]",""}
		};
 
 
		// encode all params
		foreach (var keyValuePair in dictionary)
		{
			
			byte[] bytesValue = System.Text.Encoding.ASCII.GetBytes(keyValuePair.Value);
			string encodedValue;
			
			if(keyValuePair.Key.IndexOf("data") > -1) {
				// encode image data directly from the image bytes
				// when creating the signature the % must be changed with %25, hence the true param for URLEncodeBytes
	
				encodedValue = URLUtils.URLEncodeBytes(img.bytes, true);
			} else {
				encodedValue = URLUtils.URLEncodeBytes(bytesValue,false);
			}
			
			byte[] bytesKey = System.Text.Encoding.ASCII.GetBytes(keyValuePair.Key);
			string encodedKey = URLUtils.URLEncodeBytes(bytesKey,false);
			
			//append a = between the key and the value and a & after the value
			stringBuilder.Append(string.Format("{0}%3D{1}%26", encodedKey, encodedValue));
		}
 
 
		// the holy signature base!
		string signatureBaseString = stringBuilder.ToString().Substring(0, stringBuilder.Length - 3);
 
		string signatureKey =
			Uri.EscapeDataString(consumerSecret) + "&" +
				Uri.EscapeDataString(accessTokenSecret);
		
		var hmacsha1 = new HMACSHA1(
			System.Text.Encoding.ASCII.GetBytes(signatureKey)
			);
		
		// teh signature string
		string signatureString = Convert.ToBase64String(
			hmacsha1.ComputeHash(
			System.Text.Encoding.ASCII.GetBytes(signatureBaseString)));
 
		signatureString = Uri.EscapeDataString(signatureString);
 
 
		// the authorize header
		string auth =  "OAuth oauth_token=\""+accessToken+"\", oauth_consumer_key=\""+consumerKey+"\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\""+timestamp+"\", oauth_nonce=\""+nonce+"\", oauth_version=\"1.0\", oauth_signature=\""+signatureString+"\"";
 
 
		// the post data
		// we have to build it ourselves, because if we use WWWForm, it will enforce it's own way of
		// encoding the image data, and it will break things
 
		// we don't change % to %25 when adding the post data
		string imgDataEncoded = URLUtils.URLEncodeBytes(img.bytes, false);
 
		StringBuilder postData = new StringBuilder();
		postData.Append("data%5B0%5D=");
		postData.Append (imgDataEncoded);
		postData.Append ("&type=photo");
 
 
		// send request
		Hashtable headers = new Hashtable();
		headers["Authorization"] = auth;
		headers["Content-type"] = "application/x-www-form-urlencoded";
 
		WWW web = new WWW(uri, System.Text.Encoding.ASCII.GetBytes(postData.ToString()), headers);
		
		yield return web;
		
		if (!string.IsNullOrEmpty(web.error))
		{
			Debug.Log(string.Format("GetAccessToken - failed. error : {0}", web.error));
		} else {
			Debug.Log (web.text);
		}
 
	}
}

The main problem was how to encode the raw data from the image, so we could generate the OAuth signature base and the Tumblr post params correctly. The code is pretty self explanatory, so I won’t go into the details.

To get the initial access token and secret, I used a great command line tool: https://code.google.com/p/oacurl/