Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
aoberoi committed Mar 15, 2017
0 parents commit 35b1ed7
Show file tree
Hide file tree
Showing 9 changed files with 677 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": "airbnb-base",
"env": {
"node": true
},
"rules": {
"no-console": ["off"],
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": ["**/*.js", "!scripts/**/*.js"],
"optionalDependencies": true,
"peerDependencies": true
}
]
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.env
tmp/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v6
145 changes: 145 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# App Unfurls API Sample for Node

[App Unfurls](https://api.slack.com/docs/message-link-unfurling) are a feature of the Slack Platform
that allow your Slack app customize the presentation of links that belong to a certain domain or
set of domains.

This sample demonstrates building an app that can unfurl links from the popular photo sharing site
[Flickr](https://www.flickr.com/). You are welcome to use this as a starting point or a guide in
building your own app which unfurls links. This sample uses Slack's own SDKs and tools. Even if you
choose to use another programming language or another set of tools, reading through the code will
help you gain an understanding of how to make use of unfurls.

## Set Up

You should start by [creating a Slack app](https://api.slack.com/slack-apps) and configuring it
to use the Events API. This sample app uses the
[Slack Event Adapter](https://github.com/slackapi/node-slack-events-api), where you can find some
configuration steps to get the Events API ready to use in your app. Set up a subscription to the
team event `link_shared`. Add an app unfurl domain for "flickr.com". Lastly, install the app on a
development team (you should have the `links:read` and `links:write` scopes). Once the installation
is complete, note the OAuth Access Token.

You also need to create a Flickr app to be able to use the API. You can create one from the
[Flickr developer site](https://www.flickr.com/services/apps/create/). Once you create an app, note
the API Key

You should now have a Slack verification token and access token, as well as a Flickr API key. Clone
this application locally. Create a new file named `.env` within the directory and place these values
as shown:

```
SLACK_VERIFICATION_TOKEN=xxxxxxxxxxxxxxxxxxx
SLACK_CLIENT_TOKEN=xoxp-0000000000-0000000000-0000000000-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FLICKR_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Lastly, download the dependencies for the application by running `npm install`. Note that this
example assumes you are using a currently supported LTS version of Node (at this time, v6 or above).

## Part 1: The basic unfurl

The example of a basic unfurl is contained in `basic.js`.

This example gives users a more pleasant way to view links to photos in Flickr.

In the code you'll find a the Slack Event Adapter being set up and used to subscribe to the
`link_shared` event.

```javascript
slackEvents.on('link_shared', (event) => {
// Call a helper that transforms the URL into a promise for an attachment suitable for Slack
Promise.all(event.links.map(messageAttachmentFromLink))
// Transform the array of attachments to an unfurls object keyed by URL
.then(attachments => keyBy(attachments, 'url'))
.then(unfurls => mapValues(unfurls, attachment => omit(attachment, 'url')))
// Invoke the Slack Web API to append the attachment
.then(unfurls => slack.chat.unfurl(event.message_ts, event.channel, unfurls))
.catch(console.error);
});
```

The event contains an array of links, which are each run through the function
`messageAttachmentFromLink()` to fetch data about the link from Flickr, and transform the link into
a message attachment. Message attachments have
[rich formatting capabilities](https://api.slack.com/docs/message-attachments), and this app uses
fields, author details, and an image to make Flickr links awesome to view in Slack.

Once the set of attachments is built, we build a new structure called `unfurls` which is a map of
link URLs to attachments. That unfurls structure is passed to the Web API method `chat.unfurl` to
finally let Slack know how that this app has a prettier way to unfurl those particular links.

## Part 2: Interactivity with unfurls

The example of adding interactivity to unfurls is in `interactive.js`.

This example builds off of `basic.js` but adds interactive message buttons to each of the unfurls.
This is an extremely powerful feature of unfurls, since buttons can but used to make updates and
*act* rather than just display information to a user. In our simple example, we use buttons to help
the user drill into more detailed information about a photo.

The main changes in this version is that the `messageAttachmentFromLink()` function now adds
an array of `actions` to each attachment it produces. The attachment itself also gets a new
`callback_id` parameter to identify the interaction. In this case we call the interaction
`photo_details`.

Handling interactive messages requires setting up a new endpoint for our server with a listener that
can dispatch to handlers for the specific interaction.

```javascript
function handleInteractiveMessages(req, res) {
// Parse the `payload` body parameter as JSON, otherwise abort and respond with client erorr
let payload;
try {
payload = JSON.parse(req.body.payload);
} catch (parseError) {
res.sendStatus(400);
return;
}

// Verify token to prove that the request originates from Slack
if (!payload.token || payload.token !== process.env.SLACK_VERIFICATION_TOKEN) {
res.sendStatus(404);
return;
}

// Decoding the callback_id (which contains an identifier for the interaction and a photo ID)
const interaction = payload.callback_id.split(':');
const interactionType = interaction.shift();

// Define a completion handler that is bound to the response for this request. Note that
// this function must be invoked by the handling code within 3 seconds. A more sophistocated
// implementation may choose to timeout before 3 seconds and send an HTTP response anyway, and
// then use the `payload.response_url` to send a request once the completion handler is invoked.
function callback(error, body) {
if (error) {
res.sendStatus(500);
} else {
res.send(body);
}
}

// This switch statement should have a case for the exhaustive set of interaction types
// this application may handle. In this sample, we only have one: `photo_details`.
switch (interactionType) {
case 'photo_details':
handlePhotoDetailsInteraction(interaction, payload, callback);
break;
default:
// As long as the above list of cases is exhaustive, there shouldn't be anything here
break;
}
}
```

Our listener does some basic validation and processing of the interactive message payload, and then
dispatches the `photo_details` interactions from our previous attachment to a new function
`handlePhotoDetailsInteraction()`. This is a very simple function that simply augments the
original attachment with a new field for either the photo's groups or albums. Once the new
attachment is built, the server respond to Slack with a new message payload, using our new attachment
as the first item in the attachments array. Slack will ignore all message content in this response
other than the first attachment in the attachment array.

Now we have beautiful interactive unfurls that allow users to drill in deeper on content that
was shared in a channel.
116 changes: 116 additions & 0 deletions basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
require('dotenv').config();

const slackEventsAPI = require('@slack/events-api');
const { WebClient } = require('@slack/client');
const { getFlickrUrlData } = require('./lib/flickr');
const keyBy = require('lodash.keyby');
const omit = require('lodash.omit');
const mapValues = require('lodash.mapvalues');
const normalizePort = require('normalize-port');

/**
* Transform a Slack link into a Slack message attachment.
*
* @param {Object} link - Slack link
* @param {string} link.url - The URL of the link
*
* @returns {Promise.<Object>} An object described by the Slack message attachment structure. In
* addition to the properties described in the API documentation, an additional `url` property is
* defined so the source of the attachment is captured.
* See: https://api.slack.com/docs/message-attachments
*/
function messageAttachmentFromLink(link) {
return getFlickrUrlData(link.url)
.then((photo) => {
// The basic attachment
const attachment = {
fallback: photo.title + (photo.description ? `: ${photo.description}` : ''),
color: '#ff0084', // Flickr logo pink
title: photo.title,
title_link: photo.url,
image_url: photo.imageUrl,
url: link.url,
};

// Slack only renders the author information if the `author_name` property is defined
// Doesn't always have a value. see: https://github.com/npm-flickr/flickr-photo-info/pull/3
const authorName = photo.owner.name || photo.owner.username;
if (authorName) {
attachment.author_name = authorName;
attachment.author_icon = photo.owner.icons.small;
attachment.author_link = photo.owner.url;
}

// Conditionally add fields as long as the data is available
const fields = [];

if (photo.description) {
fields.push({
title: 'Description',
value: photo.description,
});
}

if (photo.tags.length > 0) {
fields.push({
title: 'Tags',
value: photo.tags.map(t => t.raw).join(', '),
});
}

if (photo.takenTS) {
fields.push({
title: 'Taken',
value: (new Date(photo.takenTS)).toUTCString(),
});
}

if (photo.postTS) {
fields.push({
title: 'Posted',
value: (new Date(photo.postTS)).toUTCString(),
});
}

if (fields.length > 0) {
attachment.fields = fields;
}

return attachment;
});
}

// Initialize a Slack Event Adapter for easy use of the Events API
// See: https://github.com/slackapi/node-slack-events-api
const slackEvents = slackEventsAPI.createSlackEventAdapter(process.env.SLACK_VERIFICATION_TOKEN);

// Initialize a Web Client
const slack = new WebClient(process.env.SLACK_CLIENT_TOKEN);

// Handle the event from the Slack Events API
slackEvents.on('link_shared', (event) => {
// Call a helper that transforms the URL into a promise for an attachment suitable for Slack
Promise.all(event.links.map(messageAttachmentFromLink))
// Transform the array of attachments to an unfurls object keyed by URL
.then(attachments => keyBy(attachments, 'url'))
.then(unfurls => mapValues(unfurls, attachment => omit(attachment, 'url')))
// Invoke the Slack Web API to append the attachment
.then(unfurls => slack.chat.unfurl(event.message_ts, event.channel, unfurls))
.catch(console.error);
});

// Handle errors
const slackEventsErrorCodes = slackEventsAPI.errorCodes;
slackEvents.on('error', (error) => {
if (error.code === slackEventsErrorCodes.TOKEN_VERIFICATION_FAILURE) {
console.warn(`An unverified request was sent to the Slack events request URL: ${error.body}`);
} else {
console.error(error);
}
});

// Start the server
const port = normalizePort(process.env.PORT || '3000');
slackEvents.start(port).then(() => {
console.log(`server listening on port ${port}`);
});
Loading

0 comments on commit 35b1ed7

Please sign in to comment.