Logo

Developers Portal

Integrate and Extend TargetProcess

Screen

Twitter Plugin Tutorial

Make sure that you have all prerequisites installed.

Full source code of Twitter plugin is available at GitHub.

First, let's define what we are going to build. In short, we want to send notifications via Twitter when new User Story or Bug is added. Also we want to send notifications when Bug is fixed or User Story is completed. We are going to create private Twitter account that will post such tweets. People from our company may follow this account and receive updates in any twitter client. Here is the list of plugin features:

  • Create profile for Twitter account and authorize it
  • Catch events about new stories and bugs and tweet about them
  • Catch events about closed stories and fixed bugs and tweet them

Create Plugin Project

Create new project (name it Twitter) as described is Getting Started section.

Register Twitter App

Go to https://dev.twitter.com/apps and click Register a New App button. Fill all required fields. You need Consumer Key and Consumer Secret values to send requests from Twitter Plugin.

Twitter_app

Download and Use Twitter Library

We will use Twitterizer .NET library. Navigate to Twitterizer Download page and download Full package. Put it into TwitterPlugin folder and add a reference to Twitterizer2.dll. Include namespace into TwitterPluginHandler class

	using Twitterizer;

Plugin Structure

Here is the structure of our future plugin. Only 4 files should be created to make Twitter plugin.

Plugin_files

Create Profile

Profile stores plugin settings. In our case, we need to store Twitter account, Access Token and Access Token Secret. We need this information to pass Twitter authentication and post something.

Profile consists of two parts: .NET class and JS file with UI. Here is the full listing of Profile.cs.

	using System.Runtime.Serialization;
	using Tp.Integration.Plugin.Common;

	[assembly: PluginAssembly("Twitter Plugin")]

	//If you rename or remove this file, it will be re-created during package update.
	namespace Twitter
	{
		[Profile, DataContract]
		public class Profile
		{
			// Each storable property should be marked with DataMember attributed
			[DataMember]
			public string TwitterAccount { get; set; }
			[DataMember]
			public string TwitterAccessToken { get; set; }
			[DataMember]
			public string TwitterAccessTokenSecret { get; set; }
		}
	}

In fact all you should do is to mark class and properties with some attributes.

We need UI to specify Twitter account and tokens. Plugins profile UI is based on Javascript mashups. It a nutshell, you have a very simple API to store and load profiles and can do whatever you want to create profile UI.

tau.mashups.addMashup(function (config) {
	// your code here
}

It is important to note that you can do anything you want in Javascript UI inside tau.mashup.add function. Mashups do not constraint anything. We are trying to show good and structured way how to deal with profile UI here, however, you can simplify it and write any code you want. You need to call repository.getByName method to load profile and repository.update to save profile. That is it in general.

Here is the full listing of ProfileEditor.js file. It looks quite complex, but it contains several utility classes:

	//If you rename or remove this file, it will be re-created during package update.
	tau.mashups.addMashup(function (config) {
		// define profile editor 
		function twitterProfileEditor(config) {
			this._create(config);
		}
		twitterProfileEditor.prototype = {
			template: null,
			placeHolder: null,
			saveBtn: null,
			_returnUrl: null,
			_create: function (config) {
				this.placeHolder = config.placeHolder;
				this.repository = config.profileRepository;
				this._returnUrl = 'javascript:window.history.back()';
				this.template = '<div>' +
	                        '<form method="POST">' +
	                        '<h2 class="h2">Twitter</h2>' +
	                        '<p class="note">Tweets when new user story is added.</p>' +
	                        '<div class="twitter-settings">' +
	                        '    <div class="pad-box">' +
	                        '        <p class="label">Profile Name<br />' +
	                        '        <input id="profileNameTextBox" type="text" name="Name" class="input" style="width: 275px;" value="${Name}" />' +
	                        '    </div>' +
	                        '    <div class="pad-box">' +
	                        '        <p class="label pt-10">Twitter Account Name</p>' +
	                        '        <input id="twitterAccountTextBox" name="TwitterAccount" value="${Settings.TwitterAccount}" type="text" class="input" style="width: 275px;" value="{Type Twitter account}" />' +
	                        '        <p class="label">Access Token<br>' +
	                        '       <input id="twitterAccessTokenTextBox" name="TwitterAccessTokenTextBox" value="${Settings.TwitterAccessToken}" type="password" class="input" style="width: 275px;" value="{Paste Access Token}" /></p>' +
	                        '        <p class="label">Access Token Secret<br>' +
	                        '       <input id="twitterAccessTokenSecretTextBox" name="TwitterAccessTokenSecretTextBox" value="${Settings.TwitterAccessTokenSecret}" type="password" class="input" style="width: 275px;" value="{Paste Access Token Secret. Ask all to close their eyes}" /></p>' +
	                        '    </div>' +
	                        '</div>' +
	                        '<div class="save-block">' +
	                        '    <a href="javascript:void(0);" id="saveButton" class="button">Save & Exit</a>' +
	                        '    <a href="' + this._returnUrl + '">Cancel</a>' +
	                        '</div>' +
	                        '</form>' +
	                        '</div>';
			},
			// this is the main method.
			// It loads plugin profile data and register callback that will be executed after profile initialization
			render: function () {
				// getByName loads plugin profile data
				this.repository.getByName(this._getEditingProfileName(), $.proxy(this._renderProfile, this));
			},
			_getEditingProfileName: function () {
				return new Tp.URL(window.location.href).getArgumentValue('ProfileName');
			},
			// render profile UI into placeholder and enable Save button
			_renderProfile: function (profile) {
				profile = profile || { Name: null, Settings: { TwitterAccount: null, TwitterAccessToken: null, TwitterAccessTokenSecret: null} };
				this.placeHolder.html('');
				$.tmpl(this.template, profile).appendTo(this.placeHolder);
				this.saveBtn = this.placeHolder.find('#saveButton');
				this.saveBtn.click($.proxy(this._saveProfile, this));
			},
			// save twitter profile using repository update method
			_saveProfile: function () {
				var profile =
	                {
	                	Name: this.placeHolder.find('#profileNameTextBox').val(),
	                	Settings:
	                    {
	                    	TwitterAccount: this.placeHolder.find('#twitterAccountTextBox').val(),
	                    	TwitterAccessToken: this.placeHolder.find('#twitterAccessTokenTextBox').val(),
	                    	TwitterAccessTokenSecret: this.placeHolder.find('#twitterAccessTokenSecretTextBox').val()
	                    }
	                };
				this.repository.update(this._getEditingProfileName(), profile, $.proxy(this._onProfileSaved, this));
			},
			// redirect to Plugins list after profile save
			_onProfileSaved: function () {
				window.location.href = this._returnUrl;
			}
		};

		function profileRepository(config) {
			this._create(config);
		};
		
		// Utility class
		profileRepository.prototype = {
			_requestUrlBase: '/api/v1/Plugins/{PluginName}/Profiles/{ProfileName}',
			_pluginName: null,
			_create: function () {
				var url = new Tp.URL(location.href);
				this._pluginName = url.getArgumentValue('PluginName');
			},
			_getUrl: function (profileName) {
				var relativeUrl = this._requestUrlBase.replace(/{PluginName}/g, this._pluginName).replace(/{ProfileName}/g, profileName);
				return new Tp.WebServiceURL(relativeUrl).url;
			},
			getByName: function (profileName, success) {
				if (profileName) {
					$.getJSON(this._getUrl(profileName), success);
				} else {
					success(null);
				}
			},
			_post: function (profileName, data, success, error) {
				$.ajax({
					url: this._getUrl(profileName),
					data: JSON.stringify(data),
					success: success,
					error: function (response) {
						error(JSON.parse(response.responseText));
					},
					type: 'POST',
					dataType: "json"
				});
			},
			create: function (data, success, error) {
				this._post('', data, success, error);
			},
			update: function (profileName, data, success, error) {
				this._post(profileName, data, success, error);
			}
		};

		// Instantiate twitterProfileEditor and run render method.
		new twitterProfileEditor({
			placeHolder: $('#' + config.placeholderId),
			profileRepository: new profileRepository()
		}).render();
	})

Let's check how profile UI looks.

Twitter-profile

Authorize Twitter Account

We will authorize twitter account in plugin constructor. ConsumerKey and ConsumerSecret values are unique per application, so we can just put them here. However, twitter account AccessToken and AccessTokenSecret are different for various accounts. It means if you want to tweet from account1 you will use one set of tokens, and for account2 you will use another set. That is why we need to store them in Profile.

public TweetNewUserStoryHandler(IStorageRepository storage)
{
	_storage = storage;
}

Send Tweets on User Story and Bug Creation

We need to tell the plugin what it events it should catch. It is performed via IHandleMessages interface.

public class TweetNewUserStoryHandler : IHandleMessages<UserStoryCreatedMessage>, IHandleMessages<BugCreatedMessage>

Then it is required to implement interfaces.

public void Handle(UserStoryCreatedMessage message)
{
	SendTweet("New Story added: " + message.Dto.Name);    
}

public void Handle(BugCreatedMessage message) 
{
	SendTweet("New Bug found: " + message.Dto.Name);  
}

Now we only need SendTweet method to do the real job. Twitterizer has a very nice method TwitterStatus.Update:

private void SendTweet(string tweet)
{
	var profile = _storage.GetProfile();
	var tokens = new OAuthTokens
	             	{
	             		AccessToken = profile.TwitterAccessToken,
	             		AccessTokenSecret = profile.TwitterAccessTokenSecret,
	             		ConsumerKey = "xxx", //TODO : put your value here
						ConsumerSecret = "xxx" //TODO : put your value here
	             	};

	var tweetResponse = TwitterStatus.Update(tokens, tweet);
	Console.WriteLine(tweetResponse.Result == RequestResult.Success
	                  	? "Tweeted successfully!"
	                  	: tweetResponse.ErrorMessage);
}

Send Tweets on User Story and Bug Closing

It is very easy to expand Twitter plugin and send tweets for other events. We want to tweet when EntityState of a bug or a user story changed. It means we should handle UserStoryUpdated and BugUpdated messages:

	
	public class TweetNewUserStoryHandler : 
	        IHandleMessages<UserStoryCreatedMessage>, IHandleMessages<BugCreatedMessage>,
	        IHandleMessages<UserStoryUpdatedMessage>, IHandleMessages<BugUpdatedMessage>
		
		
	...
	
	public void Handle(UserStoryUpdatedMessage message)
    {
        if (message.Dto.EntityStateName == "Done") 
        {
            SendTweet("Story completed: " + message.Dto.Name); 
        }
    }

    public void Handle(BugUpdatedMessage message)
    {
        if (message.Dto.EntityStateName == "Closed")
        {
            SendTweet("Bug closed: " + message.Dto.Name);
        }
    }

See It In Action

It is time to run the plugin and see how it works. Push Ctrl + F5 in Visual Studio. Navigate to TargetProcess → Plugins and create a profile for Twitter plugin. Make sure to provide correct AccessToken keys. Then add a new user story. Open Twitter account page and check results (This is a screenshot of @tpinsider twitter account):

Tweet

Plugins Chapters

  1. Getting Started
  2. Tutorial
  3. Overview
  4. Reference

Tutorial

  1. Create Plugin Project
  2. Register Twitter App
  3. Download and Use Twitter Library
  4. Plugin Structure
  5. Create Profile
  6. Authorize Twitter Account
  7. Send Tweets on User Story and Bug Creation
  8. Send Tweets on User Story and Bug Closing
  9. See It In Action
Loading
GoogleJoin the community!
#DEV TargetProcess Group

OctocatTargetProcess at GitHub https://github.com/TargetProcess