Last updatedOct 22, 2019

Creating Jira Service Desk requests from Twitter

Increasingly, companies have been utilizing social media platforms to provide support services for their customers. Facebook and Twitter are highly popular in this regard due to their ubiquity and familiarity. By leveraging the power of Service Desk's new REST API and Atlassian Connect support, it is possible to extend Service Desk to support entirely new methods of customer interaction.

This guide will show you how to integrate Jira Service Desk with Twitter to create requests from a Twitter stream. We'll take you on a guided tour of an example app, jira-servicedesk-twitter-example, that implements this functionality and explain the key concepts. You won't be building an app yourself in this guide, but you can browse, download, and even run the source code for the example app:

Show me the code!

The code for the final output of this tutorial is here: https://bitbucket.org/atlassianlabs/jira-servicedesk-twitter-example (open source).

Before you begin

This guide is aimed at developers who are experienced with Jira Service Desk development and the Connect framework. If you are new to either of these, we recommend that you read our Getting started guide first.

GET POSTing -- The Jira Service Desk REST API

The Service Desk REST API is the cornerstone for Atlassian Connect app development, serving as the primary means by which apps communicate with Service Desk. Using the REST API, your app can interact with Jira Service Desk in a number of ways, like retrieving a list of service desks, creating customer requests, and more! Check out the Jira Service Desk Cloud REST API documentation, if you'd like to learn more.

Making REST calls

The Atlassian Connect Express framework provides a bundled version of the well-known request HTTP client, accessed via the addon object that is passed to your routes. To use it, you'd typically add code like this to the routes/index.js file of your app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = (app, addon) => {
    ...
    let httpClient;
  
    app.get('/my-route', addon.authenticate(), (req, res) => {
        // Creating when in request context
        httpClient = addon.httpClient(req);
    });
 
    // Creating when not in request context
    httpClient = addon.httpClient({
        clientKey: clientKey, // The unique client key of the tenant to make a request to
        appKey: appKey // The key specified in your atlassian-connect.json descriptor
    });
    ...
}

This HTTP client could then be used to make secure REST calls to Service Desk from the backend of your app, like this: (Note, secure requests can also be made from your app's front-end by using the Request JavaScript module)

1
2
3
httpClient.get('/some-endpoint', (err, res, body) => {
    console.log(body);
});

Creating a REST client

While it's helpful to understand how the HTTP client works with Atlassian Connect Express, in practice it's better to not use the HTTP client directly. A better approach is to wrap the usage of the HTTP client in a simple REST client, so that it's decoupled from our app code. The example below shows how you might implement this in the routes/index.js of your app (or in a file that is included by index.js):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class ServiceDeskClient {
    constructor(httpClient) {
        this.httpClient = httpClient;
    }
 
    getServiceDesks() {
        return promiseOf(
            this.httpClient.get,
            {
                url: '/rest/servicedeskapi/servicedesk',
                json: true
            }
        );
    }
 
    getRequestTypes(serviceDeskId) {
        return promiseOf(
            this.httpClient.get,
            {
                url: `/rest/servicedeskapi/servicedesk/${serviceDeskId}/requesttype`,
                json: true
            }
        );
    }
 
    getRequestTypeFields(serviceDeskId, requestTypeId) {
        return promiseOf(
            this.httpClient.get,
            {
                url: `/rest/servicedeskapi/servicedesk/${serviceDeskId}/requesttype/${requestTypeId}/field`,
                json: true
            }
        );
    }
 
    createRequest(serviceDeskId, requestTypeId, requestFieldValues) {
        return promiseOf(
            this.httpClient.post,
            {
                url: `/rest/servicedeskapi/request`,
                body: {
                    serviceDeskId,
                    requestTypeId,
                    requestFieldValues
                },
                json: true
            }
        );
    }
}

You'll notice that the methods in the code above use a promiseOf function. This simply wraps all of the HTTP client methods in a small utility function, promiseOf, allowing us to use promises rather than callbacks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function promiseOf(fn, arg) {
    return new Promise((resolve, reject) => {
        function callback(err, res) {
            if (err) {
                reject(err);
            } else if (res.statusCode < 200 || res.statusCode > 299) {
                reject({error: `${res.statusCode} ${res.statusMessage}`});
            } else {
                resolve(res.body);
            }
        }
 
        fn(arg, callback);
    });
}

Once you have implemented a REST client, it's easy to make REST calls to your Jira Service Desk instance. Have a look at the following code, which shows how you can use the REST client to make a GET call to a servicedesk resource:

1
2
3
4
5
6
7
8
9
10
const httpClient = addon.httpClient(req);
const serviceDeskClient = new ServiceDeskClient(httpClient);
  
serviceDeskClient.getServiceDesks()
                 .then(serviceDesksResponse => {
                           console.log(serviceDeskResponse);
                       },
                       err => {
                           console.error(err);
                       });

#AllTheThings -- The Twitter Streaming APIs

The Twitter Streaming APIs offer near real-time access to Twitter's global tweet stream, allowing consumers to receive and process tweets in a reactive manner rather than polling REST endpoints. Several different streaming endpoints are available for use, each offering different capabilities and requiring different levels of access. For full details regarding the Streaming APIs and their capabilities, see the Twitter developer documentation.

If you are developing with Twitter's APIs, you must have Twitter API credentials. To obtain these credentials, you can either:

  • Register a Twitter account and attach a phone number
  • Create a Twitter app at https://apps.twitter.com/ and create an access token for the app.

In this section, we'll have a look at the Twitter Streaming APIs and how to create Twitter Streaming API client for Jira Service Desk.

Listening for tweets

For our Twitter integration, we'll use the Twit library. Twit is fairly straightforward to use. Once you've added the dependency to your project via

1
npm install twit --save-exact

a client can be instantiated with your Twitter API credentials, as shown in the example below:

1
2
3
4
5
6
7
8
const Twit = require('twit');
  
const twitterClient = new Twit({
    consumer_key: 'Twitter consumer key here',
    consumer_secret: 'Twitter consumer secret here',
    access_token: 'Twitter access token here',
    access_token_secret: 'Twitter access token secret here'
});

You can use this Twit instance to do different things, like get the list of a user's followers or search for tweets matching certain criteria. The example code below uses the .stream() method to create a tweet-stream object for a given endpoint and begin listening for tweets. All tweets that contain the text specified in the track property will be streamed to the app, allowing us to react to and process them.

1
2
3
4
5
const tweetStream = twitterClient.stream('statuses/filter', {track: '#AtlasCamp'});
  
tweetStream.on('tweet', tweet => {
    console.log(tweet.text);
});

Just like we wrapped the HTTP client, it's good practice to write a simple wrapper client for the Twit library, so that the usage of external libraries is decoupled from your app code. See the example code below. The code below also shows how you can get your Twitter API credentials from environment variables, rather than storing them in the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const DEFAULT_TWITTER_CONFIG = {
    consumer_key: process.env.TWITTER_CONSUMER_KEY,
    consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
    access_token: process.env.TWITTER_ACCESS_TOKEN,
    access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET
};
 
class TwitterClient {
    constructor(config) {
        this.twit = new Twit(config || DEFAULT_TWITTER_CONFIG);
    }
 
    streamTweets(options) {
        return new TweetStream(this.twit.stream('statuses/filter', options || {}));
    }
}
 
class TweetStream {
    constructor(twitStream) {
        this.twitStream = twitStream;
    }
 
    start() {
        this.twitStream.start();
        return this;
    }
 
    stop() {
        this.twitStream.stop();
        return this;
    }
 
    onTweetReceived(handler) {
        this.twitStream.on('tweet', handler);
        return this;
    }
}

This gives us a Twitter Streaming API client that is easy to use, but also allows us to change the underlying library if we want to. Have a look at the following code, which shows how you can use the client to track tweets with the #AtlasCamp hashtag:

1
2
3
4
5
const twitterClient = new TwitterClient();
const tweetStream = twitterClient.streamTweets({track: '#AtlasCamp'})
                                 .onTweetReceived(tweet => {
                                     console.log(tweet.text);
                                 });

Putting it all together

You have a REST client. You have a Twitter Streaming client. All that's left is adding the glue to join the two together!

Creating customer requests from your app

One of the pieces that your app is still missing is the ability to create a customer request, so that it can create customer requests from incoming tweets. The following function in the routes/index.js file takes a tweet, creates a customer request payload, then creates a customer request using the Service Desk REST API.

1
2
3
4
5
6
7
8
9
10
11
12
function createRequestFromTweet(serviceDeskClient, serviceDeskId, requestTypeId, tweet) {
    // Normally, you'd use the serviceDeskId and requestTypeId to get the
    // list of fields for the request-type, then populate them.
    // For now, let's just create a payload directly and use that.
  
    const requestPayload = {
        summary: tweet.text,
        description: 'Created via Twitter'
    };
  
    return serviceDeskClient.createRequest(serviceDeskId, requestTypeId, requestPayload);
}

A few things to be aware of:

  • In order to create a customer request using the Service Desk REST API, we'd usually need to get the list of fields associated with a given request-type and service desk, then populate the required fields. For the purpose of this example however, we've assumed that our target service desk has only one request type, which requires only two fields: summary and description.
  • In order to be able to create customer requests from your app, you must add the WRITE scope to the scopes list in your atlassian-connect.json descriptor.

Kicking everything off

We now have everything we need to seamlessly pipe tweets into Service Desk as customer requests. We just need a way of kicking the whole process off! Luckily for us, Atlassian Connect gives us a very easy method of doing this.

The atlassian-connect.json descriptor contains a lifecycle property, allowing us to specify endpoints within our app that will be hit at various stages of its lifecycle (see Lifecycle). The following code shows how we've added entries for the enabled and disabled events, which get fired when our app is enabled and disabled respectively by Service Desk:

1
2
3
4
5
6
7
...
"lifecycle": {
    "installed": "/installed",
    "enabled": "/enabled",
    "disabled": "/disabled"
}
...

Finally, we just need to implement the associated route. You can see this in the index.js file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
...
// You will need to set these values correctly for your instance
const serviceDeskSettings = {
    serviceDeskId: 9,
    requestTypeId: 36
};
 
let tweetStream;
  
module.exports = (app, addon) => {
    ...
    app.post('/enabled', (req, res) => {
        const appKey = req.body.key;
        const clientKey = req.body.clientKey;
 
        const httpClient = addon.httpClient({
            appKey,
            clientKey
        });
        const serviceDeskClient = new ServiceDeskClient(httpClient);
        const twitterClient = new TwitterClient();
 
        tweetStream = twitterClient.streamTweets({track: '#AtlasCamp'})
                                   .onTweetReceived(tweet => {
                                       createRequestFromTweet(serviceDeskClient,
                                                              serviceDeskSettings.serviceDeskId,
                                                              serviceDeskSettings.requestTypeId,
                                                              tweet);
                                   });
    });
  
    app.post('/disabled', (req, res) => {
        tweetStream.stop();
    });
    ...
}

See it action

If you'd like to see this integration in action, grab the code for our example app and run it: https://bitbucket.org/atlassianlabs/jira-servicedesk-twitter-example (instructions in the repository). Once it's running and installed in your development instance, try posting a tweet with the hashtag #AtlasCamp and you should see something like this:

Customer request created from tweet

Congratulations!

You now know how to build a working Twitter integration with Jira Service Desk.

Next steps

If you've finished this tutorial, check out more Jira Service Desk tutorials.