Home » Twitter

Category Archives: Twitter

Add Scripts to the PowerShell Toolbox in Sitecore PowerShell Extensions

During our ‘Take charge of your Sitecore instance using Sitecore tools’ session at Sitecore Symposium 2014 Las Vegas, Sitecore MVP Sean Holmesby and I shared how easy it is to leverage/extend popular Sitecore development tools out there, and built up a fictitious Sitecore website where we pulled in #SitecoreSelfie Tweets.

The code that pulls in these Tweets is supposed to follow a naming convention where Tweet IDs are appended to Media Library Item names, as you can see here:

sean-profile-image

Sadly, right before our talk, I mistakenly 😉 made a code change which broke our naming convention for some images:

sean-selfie-image

Upon further investigation, we had discovered our issue was much larger than anticipated: all Selfie Media Library Item names do not end with their Tweet IDs:

no-tweet-ids

To fix this, I decided to create a PowerShell Toolbox script in Sitecore PowerShell Extensions using the following script:

<#
    .SYNOPSIS
        Rename selfie image items to include tweet ID where missing.
     
    .NOTES
        Mike Reynolds
#>
$items = Get-ChildItem -Path "master:\sitecore\content\Social-Media\Twitter\Tweets" -Recurse | Where-Object { $_.TemplateName -eq "Tweet" }

$changedItems = @()
foreach($item in $items) {
	$tweetID = $item["TweetID"]
	$selfieImageField = [Sitecore.Data.Fields.ImageField]$item.Fields["SelfieImage"]
	$selfieImage = $selfieImageField.MediaItem
	if($selfieImage -ne $null -and -not $selfieImage.Name.EndsWith($tweetID)) {
		$oldName = $selfieImage.Name
		$newName = $oldName + "_" + $tweetID
		$selfieImage.Editing.BeginEdit()
		$selfieImage.Name = $newName
		$selfieImage.Editing.EndEdit()
		
		$changedItem = New-Object PSObject -Property @{            
		    Icon = $selfieImage.Appearance.Icon
			OldName = $oldName
			NewName = $newName  
			Path = $selfieImage.Paths.Path
			Alt = $selfieImage["Alt"]
			Title = $selfieImage["Title"]
			Width = $selfieImage["Width"]
			Height = $selfieImage["Height"]
			MimeType = $selfieImage["Mime Type"]
			Size = $selfieImage["Size"]           
		}
		
		$changedItems += $changedItem
	}
}

if($changedItems.Count -gt 0) {
    $changedItems |
        Show-ListView -Property @{Label="Icon"; Expression={$_.Icon} },
            @{Label="Old Name"; Expression={$_.OldName} },
    		@{Label="New Name"; Expression={$_.NewName} },
    		@{Label="Path"; Expression={$_.Path} },
            @{Label="Alt"; Expression={$_.Alt} },
    		@{Label="Title"; Expression={$_.Title} },
            @{Label="Width"; Expression={$_.Width} },
            @{Label="Height"; Expression={$_.Height} },
            @{Label="Mime Type"; Expression={$_.MimeType} },
    		@{Label="Size"; Expression={$_.Size} }
} else {
    Show-Alert "There are no selfie image items missing tweet IDs in their name."
}
Close-Window

The above PowerShell script grabs all Tweet Items in Sitecore; ascertains whether referenced Selfie images in the Media Library — these are referenced in the “SelfieImage” field on the Tweet Items — end with the Tweet IDs of their referring Tweet Items (the Tweet ID is stored in a field on the Tweet Item); and renames the Selfie images to include their Tweet IDs if not. The script also launches a dialog showing the images that have changed.

To save the above script in the PowerShell Toolbox, I launched the PowerShell Integrated Scripting Environment (ISE) in Sitecore PowerShell Extensions:

powershell-ise-context-menu

I pasted in the above script, and saved it in the PowerShell Toolbox library:

toolbox-save-as

As you can see, our new script is in the PowerShell Toolbox:

new-script-in-toolbox

I then clicked the new PowerShell Toolbox option, and was presented with the following dialog:

selfie-toolbox-script-results

The above dialog gives information about the images along with their old and new Item names.

I then navigated to where these images live in the Media Library, and see that they were all renamed to include Tweet IDs:

selfie-images-tweet-ids

If you have any thoughts on this, or suggestions for other PowerShell Toolbox scripts, please share in a comment.

Until next time, have a #SitecoreSelfie type of day!

Embedded Tweets in Sitecore: A Proof of Concept

In a previous post, I showcased a “proof of concept” for shortcodes in Sitecore — this is a shorthand notation for embedding things like YouTube videos in your webpages without having to type up a bunch of HTML — and felt I should follow up with another “proof of concept” around incorporating Embedded Tweets in Sitecore.

You might be asking “what’s an Embedded Tweet?” An Embedded Tweet is basically the process of pasting a Tweet URL from Twitter into an editable content area of your website/blog/whatever (think Rich Text field in Sitecore), and let the code that builds the HTML for your site figure out how to display it.

For example, I had used an Embedded Tweet in a recent post:

tweet-url-wordpress

This is what is seen on the rendered page:

tweet-embedded

While doing some research via Google on how to do this in Sitecore, I found this page from Twitter that discusses how you could go about accomplishing this, and discovered how to get JSON containing information about a Tweet — including its HTML — using one of Twitter’s API URLs:

tweet-api-json

The JSON above drove me to build the following POCO class to represent data returned by that URL:

using System.Runtime.Serialization;

namespace Sitecore.Sandbox.Pipelines.RenderField.Tweets
{
    public class Tweet
    {
        [DataMember(Name = "cache_age")]
        public int CacheAgeMilliseconds { get; set; }

        [DataMember(Name = "url")]
        public string Url { get; set; }

        [DataMember(Name = "html")]
        public string Html { get; set; }
    }
}

I decided to omit some of the JSON properties returned by the Twitter URL from my class above — width and height are examples — since I felt I did not need to use them for this “proof of concept”.

I then leveraged the class above in the following class that will serve as a <renderField> pipeline processor to embed Tweets:

using System;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;

using Sitecore.Caching;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Sitecore.Sandbox.Pipelines.RenderField.Tweets
{
    public class ExpandTweets
    {
        private string TwitterWidgetScriptTag {get ; set; }

        private string TwitterApiUrlFormat { get; set; }

        private string _TweetPattern;
        private string TweetPattern 
        {
            get
            {
                return _TweetPattern;
            }
            set
            {
                _TweetPattern = value;
                if (!string.IsNullOrWhiteSpace(_TweetPattern))
                {
                    _TweetPattern = HttpUtility.HtmlDecode(_TweetPattern);
                }
            }
        }

        private HtmlCache _HtmlCache;
        private HtmlCache HtmlCache
        {
            get
            {
                if (_HtmlCache == null)
                {
                    _HtmlCache = CacheManager.GetHtmlCache(Context.Site);
                }

                return _HtmlCache;
            }
        }

        public void Process(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            AssertRequired();
            if(!ShouldFieldBeProcessed(args))
            {
                return;
            }

            args.Result.FirstPart = ExpandTweetUrls(args.Result.FirstPart);
        }

        private static bool ShouldFieldBeProcessed(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.FieldTypeKey, "args.FieldTypeKey");
            string fieldTypeKey = args.FieldTypeKey.ToLower();
            return fieldTypeKey == "text"
                    || fieldTypeKey == "rich text"
                    || fieldTypeKey == "single-line text"
                    || fieldTypeKey == "multi-line text";
        }

        private void AssertRequired()
        {
            Assert.IsNotNullOrEmpty(TwitterWidgetScriptTag, "TwitterWidgetScriptTag must be set! Check your configuration!");
            Assert.IsNotNullOrEmpty(TwitterApiUrlFormat, "TwitterApiUrlFormat must be set! Check your configuration!");
            Assert.IsNotNullOrEmpty(TweetPattern, "TweetPattern must be set! Check your configuration!");
        }

        protected virtual string ExpandTweetUrls(string html)
        {
            string htmlExpanded = html;
            MatchCollection matches = Regex.Matches(htmlExpanded, TweetPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
            foreach (Match match in matches)
            {
                string tweetHtml = GetTweetHtml(match.Groups["id"].Value);
                if (!string.IsNullOrWhiteSpace(tweetHtml))
                {
                    htmlExpanded = htmlExpanded.Replace(match.Value, tweetHtml);
                }
            }

            if (matches.Count > 0)
            {
                htmlExpanded = string.Concat(htmlExpanded, TwitterWidgetScriptTag);
            }

            return htmlExpanded;
        }

        protected virtual string GetTweetHtml(string id)
        {
            string html = GetTweetHtmlFromCache(id);
            if (!string.IsNullOrWhiteSpace(html))
            {
                return html;
            }

            Tweet tweet = GetTweetFromApi(id);
            AddTweetHtmlToCache(id, tweet);
            return tweet.Html;
        }

        private string GetTweetHtmlFromCache(string id)
        {
            return HtmlCache.GetHtml(id);
        }

        private void AddTweetHtmlToCache(string id, Tweet tweet)
        {
            if (string.IsNullOrWhiteSpace(tweet.Html))
            {
                return;
            }

            if (tweet.CacheAgeMilliseconds > 0)
            {
                HtmlCache.SetHtml(id, tweet.Html, DateTime.Now.AddMilliseconds(tweet.CacheAgeMilliseconds));
                return;
            }

            HtmlCache.SetHtml(id, tweet.Html);
        }

        protected virtual Tweet GetTweetFromApi(string id)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(string.Format(TwitterApiUrlFormat, id));
            try
            {
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                {
                    var result = reader.ReadToEnd();
                    JObject jObject = JObject.Parse(result);
                    return JsonConvert.DeserializeObject<Tweet>(jObject.ToString());
                }
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }

            return new Tweet { Html = string.Empty };
        }
    }
}

Methods in the class above find all Tweet URLs in the Rich Text, Single-Line Text, or Multi-Line Text field being processed — the code determines if it’s a Tweet URL based on a pattern that is supplied by a configuration setting (you will see this below in this post); extract Tweets’ Twitter identifiers (these are located at the end of the Tweet URLs); and attempt to find the Tweets’ HTML in Sitecore’s HTML cache.

If the HTML is found in cache for a Tweet, we return it. Otherwise, we make a request to Twitter’s API to get it, put it in cache one we have it (it is set to expire after a specified number of milliseconds from the time it was retrieved: Twitter returns the number of milliseconds in one full year by default), and then we return it.

If the returned HTML is not empty, we replace it in the field’s value for display.

If the HTML returned is empty — this could happen when an exception is encountered during the Twitter API call (of course we log the exception in the Sitecore log when this happens 😉 ) — we don’t touch the Tweet URL in the field’s value.

Once all Tweet URLs have been processed, we append a script tag referencing Twitter’s widget.js file — this is supplied through a configuration setting, and it does the heavy lifting on making the Tweet HTML look Twitterific 😉 — to the field’s rendered HTML.

I then tied everything together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderField>
        <processor type="Sitecore.Sandbox.Pipelines.RenderField.Tweets.ExpandTweets, Sitecore.Sandbox"
					patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetTextFieldValue, Sitecore.Kernel']">
          <TwitterWidgetScriptTag>&lt;script async src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</TwitterWidgetScriptTag>
          <TwitterApiUrlFormat>https://api.twitter.com/1/statuses/oembed.json?id={0}&amp;omit_script=true</TwitterApiUrlFormat>
          <TweetPattern>https://twitter.com/.+/status/(?&lt;id&gt;\d*)</TweetPattern>
        </processor>
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Let’s see this in action!

I created a test Item, and added some legitimate and bogus Tweet URLs into one of its Rich Text fields (please pardon the grammatical issues in the following screenshots :-/):

tweets-rich-text

This isn’t the most aesthetically pleasing HTML, but it will serve its purpose for testing:

tweets-rich-text-html

After saving and publishing, I navigated to my test Item’s page, and saw this:

tweets-front-end

If you have any suggestions on making this better, or have other ideas for embedding Tweets in Sitecore, please share in a comment.