Twitter Plugin Tutorial
Make sure that you have all prerequisites installed.
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.

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.

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.

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):




