In two previous blog entries I have detailed how I set up my website to access the Tumblr API to retrieve posts and retrieve information using OAuth authentication. The recommended method of creating a new post on Tumblr is to use the "Neue Post Format", this involves passing structured JSON to the API as part of an HTTP POST. More information about the structure of the JSON can be found here:
https://www.tumblr.com/docs/npf
A Neue Post Format (NPF) post consists of a set of content blocks that define the content that will be displayed in the post. The types of content block are detailed in the above link. I decided to create a class for each content block type, which will store the information required and generate the JSON string. Each of these classes will be based on a base abstract class, so that they can be treated as the same type during processing.
public abstract class ContentBlock { public abstract JObject GetJson(); }
A "Text" content block can then be represented thus:
public enum TextSubtype { none, heading1, heading2, quirky, quote, chat, ordered_list_item, unordered_list_item } public class TextContentBlock : ContentBlock { private String Text; private TextSubtype Subtype; public TextContentBlock(String text) { Text = text; Subtype = TextSubtype.none; } public TextContentBlock(String text, TextSubtype subtype) { Text = text; Subtype = subtype; } public override JObject GetJson() { JObject json = new JObject(); json.Add(new JProperty("type", "text")); json.Add(new JProperty("text", Text)); if (Subtype != TextSubtype.none) { json.Add(new JProperty("subtype", Subtype.ToString().Replace("_","-"))); } return json; } }
In a similar manner, the "Video" content block can be created:
public enum VideoProvider { tumblr, youtube, vimeo } public class VideoContentBlock : ContentBlock { private String Url; private VideoProvider Provider; public VideoContentBlock(String url) { Url = url; Provider = VideoProvider.youtube; } public VideoContentBlock(String url, VideoProvider provider) { Url = url; Provider = provider; } private String GetVideoId() { String id = ""; if (!String.IsNullOrWhiteSpace(Url)) { int lastSlash = Url.LastIndexOf("/"); if (lastSlash >= 0 && lastSlash < Url.Length - 1) { id = Url.Substring(lastSlash + 1); } } return id; } public override JObject GetJson() { JObject videoBlock = new JObject(); videoBlock.Add(new JProperty("type", "video")); videoBlock.Add(new JProperty("url", Url)); videoBlock.Add(new JProperty("provider", Provider.ToString())); videoBlock.Add(new JProperty("metadata", new JObject(new JProperty("id", GetVideoId())))); return videoBlock; } }
As these are the only content types that I need for my Cinema Cento blog entries I didn’t create any more, but it would be simple to add the missing content block types.
I then implemented a "NeuePost" class that contains a list of content blocks and can be used to define the Neue Post and generate its JSON string for use in the API.
public enum NeuePostState { Published, Draft, Private } public class NeuePost { private List<ContentBlock> ContentBlocks; private List<String> Tags; private NeuePostState State; private String Title; public long Id { get; set; } public NeuePost(String title) { Title = title; ContentBlocks = new List<ContentBlock>(); Tags = new List<String>(); State = NeuePostState.Published; } public void AddContentBlock(ContentBlock block) { ContentBlocks.Add(block); } public void AddTag(String tag) { Tags.Add(tag); } public void SetState(NeuePostState state) { State = state; } public String GetJsonString() { JObject json = GetJson(); return json.ToString(); } public JObject GetJson() { JObject json = new JObject(); JArray contentArray = new JArray(); // Title TextContentBlock titleBlock = new TextContentBlock(Title, TextSubtype.heading2); contentArray.Add(titleBlock.GetJson()); // Content Blocks foreach (ContentBlock block in ContentBlocks) { contentArray.Add(block.GetJson()); } json.Add(new JProperty("content", contentArray)); // State json.Add(new JProperty("state", State.ToString().ToLower())); // Tags String tagCsv = String.Join(",", Tags); json.Add(new JProperty("tags", tagCsv)); return json; } }
Now that I could build the NPF JSON, I needed to send that to the API in order to create a post on Tumblr itself. Previously I used OAuth with an HTTP GET to retrieve the user information, but to create a post you need to use an HTTP POST. The DotNetOpenAuth library does help with this, but it is a bit more involved. In the end, this is the solution I found. I added this function to my TumblrOAuthClient class.
public long CreatePost(String blogIdentifier, NeuePost post) { long id = 0; String accessToken = TokenManager.GetAccessToken(); WebConsumer tumblr = new WebConsumer(Provider, TokenManager); MessageReceivingEndpoint endpoint = new MessageReceivingEndpoint("https://api.tumblr.com/v2/blog/" + blogIdentifier + "/posts", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest); HttpWebRequest request = tumblr.PrepareAuthorizedRequest(endpoint, accessToken); request.ContentType = "application/json"; byte[] bytes = Encoding.UTF8.GetBytes(post.GetJsonString()); request.ContentLength = bytes.Length; using (Stream requestStream = request.GetRequestStream()) { requestStream.Write(bytes, 0, bytes.Length); } WebResponse response = request.GetResponse(); String result = (new StreamReader(response.GetResponseStream())).ReadToEnd(); CreatedBlogStatusAndResponse obj = Newtonsoft.Json.JsonConvert.DeserializeObject<CreatedBlogStatusAndResponse>(result); if (obj.meta.status == 201) { long responseId = 0; if (long.TryParse(obj.response.id, out responseId)) { id = responseId; } } return id; }
Using this method I was able to write posts to Tumblr using the API, but they were of a different structure to my existing posts and were limited by the following issues:
For these reasons I decided to take a look at the legacy posts on the API. There is no indication that these are deprecated, just that they are "legacy". As they are much more suited to my requirements, I’ll use them as long as I can, and then resort to NPF if needed.
To create a legacy post using the API you pass the information as HTTP POST parameters. The DotNetOpenAuth library requires you to pass these as a list of "MultipartPostPart" objects. To build this list I first set up the required parameters as KeyValuePairs.
private List<KeyValuePair<String,String>> GetPostParameters(Post post, Boolean update) { List<KeyValuePair<String, String>> postParameters = new List<KeyValuePair<String, String>>(); postParameters.Add(new KeyValuePair<String,String>("type", "text")); postParameters.Add(new KeyValuePair<String, String>("state", "published")); postParameters.Add(new KeyValuePair<String, String>("format", "html")); if (post.tags != null && post.tags.Length > 0) { String tagString = String.Join(",", post.tags); postParameters.Add(new KeyValuePair<String, String>("tags", tagString)); } // Tumblr converts to another timezone - so I just add on 5 hours to compensate DateTime postDate = post.date.AddHours(5); postParameters.Add(new KeyValuePair<String, String>("date", postDate.ToString("yyyy-MM-dd HH:mm:ss 'GMT'"))); postParameters.Add(new KeyValuePair<String, String>("title", post.title)); postParameters.Add(new KeyValuePair<String, String>("body", post.body)); return postParameters; }
The function requires a "Post" object that I created for the API previously. The body will be the HTML to post, thus this method is much more flexible about what I can post. I then created a new function to convert the KeyValuePairs to MultiPostParts.
private List<MultipartPostPart> GetMultiPartParameters(List<KeyValuePair<String, String>> pairs) { List<MultipartPostPart> parts = new List<MultipartPostPart>(); foreach (KeyValuePair<String,String> pair in pairs) { MultipartPostPart part = new MultipartPostPart("form-data"); part.ContentAttributes["name"] = pair.Key; part.Content = new MemoryStream(Encoding.UTF8.GetBytes(pair.Value)); parts.Add(part); } return parts; }
These functions were created in the TumblrOAuthClient class, so I was able to add a function that creates a legacy post. Rather than write the parameters to the post directly, DotNetOpenAuth requires them when setting up the authorisation header.
public long CreateLegacyTextPost(String blogIdentifier, Post post) { long id = 0; String accessToken = TokenManager.GetAccessToken(); WebConsumer tumblr = new WebConsumer(Provider, TokenManager); var postParameters = GetPostParameters(post, false); List<MultipartPostPart> parts = GetMultiPartParameters(postParameters); MessageReceivingEndpoint endpoint = new MessageReceivingEndpoint("https://api.tumblr.com/v2/blog/" + blogIdentifier + "/post", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest); HttpWebRequest request = tumblr.PrepareAuthorizedRequest(endpoint, accessToken, parts); request.ContentType = "application/x-www-form-urlencoded"; WebResponse response = request.GetResponse(); String result = (new StreamReader(response.GetResponseStream())).ReadToEnd(); LegacyPostStatusAndResponse obj = Newtonsoft.Json.JsonConvert.DeserializeObject<LegacyPostStatusAndResponse>(result); if (obj.meta.status == 201) { id = obj.response.id; } return id; }
This provides me with all of the flexibility that I need. For both legacy and NPF I also created “update” functions that are very similar. They both need the id of the Tumblr post to be included.
Finally, I could use these functions for creating a Cinema Cento post.
public long CreateTumblrLegacyPost(String title, String description, String videoUrl, float numberStars, String tagString, DateTime date) { List<String> tags = new List<String>(); tags.Add(title.ToLower()); tags.AddRange(tagString.Split(',').ToList<String>()); Post post = new Post(); post.tags = tags.ToArray(); post.date = date; post.title = title; post.body = BuildLegacyHTML(description, videoUrl, numberStars); String blogIdentifier = ConfigHelper.GetAppSetting("TumblrBlogIdentifier"); TumblrOAuthClient tumblrClient = GetTumblrClient(); long returnId = tumblrClient.CreateLegacyTextPost(blogIdentifier, post); return returnId; }
Here is the function for building the HTML body of the post.
private String BuildLegacyHTML(String description, String videoUrl, float numberStars) { String html = "<p>"; // Star rating html += "<div class=\"sjm-icons\">"; String stars = ""; for (int i = 1; i <= 5; i++) { if (i <= (int)numberStars) { // Black star stars += "<i class=\"fa fa-star fa-2x\"></i> "; } else if (i == (int)(numberStars + 0.5)) { // Half stars += "<i class=\"fa fa-star-half-o fa-2x\"></i> "; } else { // Clear star stars += "<i class=\"fa fa-star-o fa-2x\"></i> "; } } html += stars + "</div>"; // Video if (!String.IsNullOrWhiteSpace(videoUrl)) { String videoId = GetVideoId(videoUrl); html += "<figure class=\"tmblr-embed tmblr-full\" data-provider=\"youtube\" data-orig-width=\"540\" data-orig-height=\"304\" data-url=\"https%3A%2F%2Fyoutu.be%2F" + videoId + "\">"; html += "<iframe id=\"youtube_iframe\" src=\"https://www.youtube.com/embed/" + videoId + "?feature=oembed&enablejsapi=1&origin=https://safe.txmblr.com&wmode=opaque\" allowfullscreen=\"\" width=\"540\" height=\"304\" frameborder=\"0\"></iframe>"; html += "</figure>"; } html += description; html += "</p>"; return html; } private String GetVideoId(String videoUrl) { String id = ""; if (!String.IsNullOrWhiteSpace(videoUrl)) { int lastSlash = videoUrl.LastIndexOf("/"); if (lastSlash >= 0 && lastSlash < videoUrl.Length - 1) { id = videoUrl.Substring(lastSlash + 1); } } return id; }
To automatically post my Umbraco content to Tumblr I hooked into the ContentService Saving event to call the above functions.
private void ContentService_Saving(IContentService sender, Umbraco.Core.Events.SaveEventArgs<IContent> e) { Boolean createTumblrPost = ConfigHelper.GetAppSettingBoolean("TumblrPostToCinemaCento", false); foreach (IContent entity in e.SavedEntities) { if (entity.HasProperty("title") && entity.Properties["title"] != null) { String title = ""; if (entity.Properties["title"].Value != null) { title = (String)entity.Properties["title"].Value; } if (String.IsNullOrWhiteSpace(title)) { entity.SetValue("title", entity.Name); } } if (createTumblrPost && entity.ContentType.Alias == "cinemaCento") { String title = GetProperty(entity, "title", entity.Name); String videoUrl = GetProperty(entity, "videoURL"); String description = GetProperty(entity, "description"); String tumblrId = GetProperty(entity, "tumblrId"); float numberStars = GetPropertyFloat(entity, "starRating"); String tagString = GetProperty(entity, "tags"); DateTime date = GetPropertyDate(entity, "date"); TumblrHelper tumblrHelper = new TumblrHelper(ApplicationContext.Current); if (String.IsNullOrWhiteSpace(tumblrId)) { long id = tumblrHelper.CreateTumblrLegacyPost(title, description, videoUrl, numberStars, tagString, date); entity.SetValue("tumblrId", id.ToString()); } else { long id = long.Parse(tumblrId); long returnId = tumblrHelper.UpdateTumblrLegacyPost(id, title, description, videoUrl, numberStars, tagString, date); } } } }