-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 35b1ed7
Showing
9 changed files
with
677 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
.env | ||
tmp/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
v6 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
}); |
Oops, something went wrong.