...

Building a Twitter Workflow

For a long time I’ve wanted to play more with Windows Workflow Foundation (WWF) in conjunction with EPiServer, and today I finally did  it. And luckily it turned out to be a lot less scary than I had imagined.

First of all, I decided to check out a couple of guides online:

http://world.episerver.com/en/Articles/Items/Creating-and-Using-a-Workflow-in-EPiServer-CMS-5-R1/

http://sdk.episerver.com/library/CMS5/Developers%20Guide/How%20to/Create%20and%20Use%20a%20Workflow.htm

http://world.episerver.com/Blogs/Andreas-Ek/Dates/2009/3/Workflows-with-EPiServer-is-really-easy/

As you can tell, lots of good stuff in the guides. However, there was still a couple of things I was uncertain about after reading them – like how to interact with episerver pages and workflow parameters. So here’s a description of the steps I took.

 

Twittering

I’ve noticed that a lot of people tweet it, whenever they have written a blog post – and I thought: Why not automate that with a workflow. True, it’ll be a fairly simple one – but then again, it felt fairly safe to start out small. Some people are opposed to automated tweets and regard it spam – but in my eyes the API and the ability to automate tweets is a key feature of Twitter. And I appreciate being updated when there are relevant blog posts for me to read – I don’t always check my rss reader – but I usually follow twitter pretty closely. Anyway – tweeps (people on twitter) who complain about other peoples tweets should probably just stop following the sources of their frustration – much like people who get offended by certain tv broadcasts should consider changing the channel :-)

 

  1. First I created a Workflow Library project in visual studio, referenced a couple of EPiServer assemblies (including EPiServer.WorkflowFoundation) and I connected the project to my web site to make sure the code would be deployed when I compiled.
  2. In the workflow project I created a SequentialWorkflowActivity called “SendTwitterNotification”.
  3. In the designer I dragged in an IfElse (because we might only want to tweet when the page is published for the first time – not in any subsequent fixes). In one of the IfElseBranches I put a custom code activity. In the end it looked like this:
    image
  4. Then I created empty handler methods for both the IfElseBranch and the custom code block. The IfElse branch requires you to create a method matching a certain signature and then assign it as a property, while the custom code let’s just setup an event-handler for when it should execute.
  5. I’m the kind of developer who likes to see stuff working as soon as possible, so already at this point I put in some dummy code (in the IfElse I just returned true, and in the custom code I wrote a line to a file), compiled and decided to try it out in Admin mode:
    image
    image
  6. And to my big surprise it seemed to work in the first try. I published a page in edit-mode and could see the file the dummy code was writing to, grow!
  7. Next step was to make sure I from my code would know which page it was being invoked on. This turned out to be really, really simple. Just add a public property to your workflow class of the type WorkflowPageEventArgs and call it “PageArgs”. It will automatically be set when the workflow is invoked. Just the same way you could create a property called FileSystemArgs of the type WorkflowFileSystemEventArgs and it would be assigned filesystem info if the workflow was started from a filesystem event.
  8.    1: public WorkflowPageEventArgs PageArgs { get; set; }
  9. Now, that the page information was here, it wasn’t that hard to write the method for the IfElseBranch – basically it checks if there are previouslypublished versions of the page.
  10.    1: protected void CheckIfItIsFirstPublish(object sender, ConditionalEventArgs args)
       2: {
       3:      args.Result = (bool)(DataFactory.Instance.ListVersions(PageArgs.PageLink).Where(pv => pv.Status == VersionStatus.PreviouslyPublished).Count() == 0);
       4: }
  11. Now I reached a point where I needed to access some parameters for the workflow. Like which username and password to use for Twitter, and how the tweets should look like. So, first I created a user control to hold the UI (initially I created the user control in the Web Site project – and then moved it to my Workflow project), added asp.net code for showing the input boxes and implemented the IWorkflowStartParameterHandler interface on the user control. That lets you define a method for loading parameters and one for saving them – in a dictionary. Fairly easy stuff. I ended up with a code-behind for the UserControl that looked like this:
  12.    1: using System;
       2: using System.Collections.Generic;
       3: using System.Linq;
       4: using System.Web;
       5: using System.Web.UI;
       6: using System.Web.UI.WebControls;
       7: using EPiServer.WorkflowFoundation.UI;
       8:  
       9: namespace TwitterWorkflow.WorkflowUI
      10: {
      11:     public partial class TwitterWorkflowUI : System.Web.UI.UserControl, IWorkflowStartParameterHandler
      12:     {
      13:         protected void Page_Load(object sender, EventArgs e)
      14:         {
      15:  
      16:         }
      17:  
      18:         #region IWorkflowStartParameterHandler Members
      19:  
      20:         public void LoadStartParameters(Guid definitionId, EPiServer.Core.PageReference pageLink, bool definitionMode, IDictionary<string, object> parameters)
      21:         {
      22:             if (parameters.ContainsKey("TwitterUsername"))
      23:             {
      24:                 this.TwitterUser.Text = parameters["TwitterUsername"] as string;
      25:             }
      26:             if (parameters.ContainsKey("TwitterPassword"))
      27:             {
      28:                 this.TwitterPassword.Text = parameters["TwitterPassword"] as string;
      29:                 TwitterPassword.Attributes.Add("value", parameters["TwitterPassword"] as string);
      30:             }
      31:             if (parameters.ContainsKey("TwitterMessage"))
      32:             {
      33:                 this.Message.Text = parameters["TwitterMessage"] as string;
      34:             }
      35:             if (parameters.ContainsKey("TweetAlways"))
      36:             {
      37:                 TweetAlways.Checked = (bool) parameters["TweetAlways"];
      38:             }
      39:         }
      40:  
      41:         public IDictionary<string, object> SaveStartParameters()
      42:         {
      43:             Dictionary<string, object> rt = new Dictionary<string, object>();
      44:             rt.Add("TwitterUsername", TwitterUser.Text);
      45:             rt.Add("TwitterPassword", TwitterPassword.Text);
      46:             rt.Add("TwitterMessage", Message.Text);
      47:             rt.Add("TweetAlways", TweetAlways.Checked);
      48:             return rt;
      49:         }
      50:  
      51:         #endregion
      52:     }
      53: }
    And the .ascx like this:
       1: <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="TwitterWorkflowUI.ascx.cs"
       2:     Inherits="TwitterWorkflow.WorkflowUI.TwitterWorkflowUI" %>
       3: <h2>
       4:     Twitter Workflow</h2>
       5: <div style="width: 100%">
       6:     <table class="epistandardtable">
       7:         <tr>
       8:             <td>
       9:                 Twitter Username
      10:             </td>
      11:             <td>
      12:                 <asp:TextBox ID="TwitterUser" runat="server" CssClass="episize240" Width="400px"  />
      13:                 <asp:RequiredFieldValidator runat="server" ControlToValidate="TwitterUser" ErrorMessage="Required" />
      14:             </td>
      15:         </tr>
      16:         <tr>
      17:             <td>
      18:                 Twitter Password
      19:             </td>
      20:             <td>
      21:                 <asp:TextBox ID="TwitterPassword" runat="server" TextMode="Password" CssClass="episize240" Width="400px" />
      22:                 <asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" ControlToValidate="TwitterPassword" ErrorMessage="Required" />
      23:             </td>
      24:         </tr>
      25:         <tr>
      26:             <td>
      27:                 Twitter Message
      28:                 <br />
      29:                 <span style="font-size: xx-small">Place Holders:</span>
      30:                 <ul style="font-size: xx-small">
      31:                     <li>{0} Author / Username of Creator</li>
      32:                     <li>{1} Page Name</li>
      33:                     <li>{2} Shortened URL</li>
      34:                     <li>{3} Full friendly URL</li>
      35:                 </ul>
      36:             </td>
      37:             <td>
      38:                 <asp:TextBox ID="Message" runat="server" CssClass="episize240" Width="400px" MaxLength="256" />
      39:                 <asp:RequiredFieldValidator ID="RequiredFieldValidator2" runat="server" ControlToValidate="Message" ErrorMessage="Required" />
      40:             </td>
      41:         </tr>
      42:                 <tr>
      43:             <td>
      44:                 Tweet every time page is published
      45:             </td>
      46:             <td>
      47:                 <asp:CheckBox runat="server" ID="TweetAlways" />
      48:             </td>
      49:         </tr>
      50:     </table>
      51: </div>
    Of course this needs to be polished off a bit, language translations and all – but I’m sure you get the point. End result:
    image
    You’ll note that to test this with, I created a test-account on twitter – and logged it so only approved readers can follow. The message uses regular place-holders to insert various elements one might image people would want in their tweets, like Author name, page name, shortened url and the full friendly url. Finally I put in an option to send out tweets every time the page is published – as opposed to only tweet it once.
  13. In order to access these parameters in the workflow we need to do 2 things:
    Create public properties in our workflow with the same name and type as the dictionary entries (then they will magically be assigned)
  14.    1: public string TwitterUsername { get; set; }
       2: public string TwitterPassword { get; set; }
       3: public string TwitterMessage { get; set; }
       4: public bool TweetAlways { get; set; }
    and attach the user control to the workflow through an attribute.
       1: [WorkflowPlugIn(Area = PlugInArea.None, Url = "~/WorkflowUI/TwitterWorkflowUI.ascx")]
       2: public sealed partial class SendTwitterNotification : SequentialWorkflowActivity
       3: {
  15. Now, all I had to do was to write the code that actually creates the tweet, shortens the URL and sends out the tweet.
    In this post there is a nice example of sending tweets from C# without external libraries and it turns out that both TinyUrl and bit.ly has pretty decent API’s for shortening urls. I went with TinyUrls – mostly because it was so simple – just send a request to http://tinyurl.com/api-create.php?url=http://labs.episerver.com and see how it returns a short url. I also wrote a bit of code to try to keep the messages under 140 – if they are too big, it will try to cut the end of the PageName – but leave the url intact.
  16. In the end, my Workflow class ended up looking like this:
  17.    1: [WorkflowPlugIn(Area = PlugInArea.None, Url = "~/WorkflowUI/TwitterWorkflowUI.ascx")]
       2:    public sealed partial class SendTwitterNotification : SequentialWorkflowActivity
       3:    {
       4:        //TODO: Digg, Reddit och Delicious, stumpleupon
       5:  
       6:        public string TwitterUsername { get; set; }
       7:        public string TwitterPassword { get; set; }
       8:        public string TwitterMessage { get; set; }
       9:        public bool TweetAlways { get; set; }
      10:  
      11:        public WorkflowPageEventArgs PageArgs { get; set; }
      12:  
      13:        public static string tinyurl = "http://tinyurl.com/api-create.php?url={0}";
      14:  
      15:        public SendTwitterNotification()
      16:        {
      17:            InitializeComponent();
      18:        }
      19:  
      20:        protected void CheckIfItIsFirstPublish(object sender, ConditionalEventArgs args)
      21:        {
      22:            if (TweetAlways)
      23:            {
      24:                args.Result = true;
      25:            }
      26:            else
      27:            {
      28:                args.Result = (bool)(DataFactory.Instance.ListVersions(PageArgs.PageLink).Where(pv => pv.Status == VersionStatus.PreviouslyPublished).Count() == 0);
      29:            }
      30:        }
      31:  
      32:  
      33:        /// <summary>
      34:        /// Post an update to a Twitter acount
      35:        /// </summary>
      36:        /// <param name="username">The username of the account</param>
      37:        /// <param name="password">The password of the account</param>
      38:        /// <param name="tweet">The status to post</param>
      39:        public static void PostTweet(string username, string password, string tweet)
      40:        {
      41:            try
      42:            {
      43:                // encode the username/password
      44:                string user = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(username + ":" + password));
      45:                // determine what we want to upload as a status
      46:                byte[] bytes = System.Text.Encoding.ASCII.GetBytes("status=" + tweet);
      47:                // connect with the update page
      48:                HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://twitter.com/statuses/update.xml");
      49:                // set the method to POST
      50:                request.Method = "POST";
      51:                request.ServicePoint.Expect100Continue = false; // thanks to argodev for this recent change!
      52:                // set the authorisation levels
      53:                request.Headers.Add("Authorization", "Basic " + user);
      54:                request.ContentType = "application/x-www-form-urlencoded";
      55:                // set the length of the content
      56:                request.ContentLength = bytes.Length;
      57:  
      58:                // set up the stream
      59:                Stream reqStream = request.GetRequestStream();
      60:                // write to the stream
      61:                reqStream.Write(bytes, 0, bytes.Length);
      62:                // close the stream
      63:                reqStream.Close();
      64:            }
      65:            catch (Exception ex) {/* DO NOTHING */}
      66:        }
      67:  
      68:        private string ShortenUrl(string url)
      69:        {
      70:            WebClient wc = new WebClient();
      71:            return wc.DownloadString(string.Format(tinyurl, url)).Trim();
      72:        }
      73:  
      74:        private void sendTwitterAnnounce_ExecuteCode(object sender, EventArgs e)
      75:        {
      76:            PageData pd=DataFactory.Instance.GetPage(PageArgs.PageLink);
      77:            string author = (pd["Author"] as string) ?? pd.CreatedBy;
      78:            string title = pd.PageName;
      79:            UrlBuilder url = new UrlBuilder(pd.LinkURL); 
      80:            EPiServer.Global.UrlRewriteProvider.ConvertToExternal(url, pd.PageLink, System.Text.UTF8Encoding.UTF8);
      81:            string linkurl = UriSupport.AbsoluteUrlBySettings(url.ToString());
      82:            string shorturl = ShortenUrl(linkurl);
      83:            
      84:            string msg = string.Format(TwitterMessage,author,title,shorturl,linkurl);
      85:            //Max 140 chars
      86:            if (msg.Length > 140)
      87:            {
      88:                int toomuch = msg.Length - 140;
      89:                if (toomuch < title.Length)
      90:                {
      91:                    msg=string.Format(TwitterMessage, author, title.Substring(0,title.Length-toomuch), shorturl, linkurl);
      92:                }
      93:            }
      94:  
      95:            PostTweet(TwitterUsername, TwitterPassword, HttpUtility.UrlEncode(msg));
      96:        }
      97:  
      98:    }
  18. And finally, after publishing a page:
    image

 

So – in the end, it turns out to be fairly easy to create workflows. I’ll admit this was a very basic one, but definitely not impossible. One might argue that this would have been significantly easier to put directly as an eventhandler to the PagePublished event in the system, but you have to keep in mind that this way I get:

  • Admin UI for selecting which pages / page types that should be affected
  • Admin UI for setting up start parameters – and even defining different tweets for different pages
  • A workflow, that can be used as a building block in other workflows.
  • A learning experience :-)

 

Download User Control + assembly here – just register them in Admin mode and you are good to go.

Download full source here – notice this is only a code sample. No Guarantees.

 

Enjoy it!

 

P.S.

Naturally this post was automatically tweeted by @EPiServerLabs when it was published :-)

Post Comments()