node-red-contrib-slack v2.0.0
node-red-contrib-slack
A Node-RED node to interact with the Slack API.
Install
Run the following command in the root directory of your Node-RED install:
npm install --save node-red-contrib-slackVersion 2.x of this package is NOT compatible with older versions. Plese
refer to the migration section for help.
Usage
The nodes included in this package are purposely generic in nature. The usage very closely mimics the Slack API. Your best source of reference for input/output specifics will be from:
- https://api.slack.com
- https://api.slack.com/rtm
- https://api.slack.com/web
- https://api.slack.com/methods
4 nodes are provided:
The rtm API/node(s) are connected to slack via web sockets and are useful for
receiving a real-time stream of events/data.
The web API/node(s) are useful for making traditional web service calls and
have a much broader use-case.
Combining both rtm and web APIs provides a full solution to interact with
the Slack API in it's
entirety facilitating powerful flows in Node-RED. Which nodes are
appropriate to use for any given use-case can be subjective so familiarizing
youself with the documentation links above is extremely beneficial.
invoking methods
To invoke methods (slack-rtm-out,
slack-web-out) set the msg.topic to the name of the
method and set the msg.payload to the args/params.
The token property is NOT required to be set in any msg.payload.
As an example of invoking the
chat.meMessage
method with the slack-web-out node you would do the
following:
msg.topic = "chat.meMessage";
msg.payload = {
channel: "...",
text: "..."
}
return msg;dressed output
The following nodes each provide Slack API output:
slack-rtm-in- view details of each event in the documentationslack-rtm-out- view details of each event in the documentationslack-web-out- viewresponsesection of each method in the documentation
The respective events/responses are generally left unaltered and are directly
passed through as msg.payload. However, before outputting the msg the data
is traversed to enrich the msg.payload (examples provided below) with
complete object data where otherwise only internal Slack IDs are present.
All of the lookups are done dynamically/generically so regardless of what API
response you get if the node finds an attribute that appears to be a
supported object (user/channel/team/bot) in some shape or form, a
corresponding <attribute>Object attribute with the lookup value will be
added.
For example, if the response contains a bot_id attribute you would see
bot_idObject added, or if it found an attribute called bot it would add
botObject etc. Ultimately all the lookups come from
slackState (see below) so it could be done on your own but
it's added to simplify and for convenience.
An example
user_typing
event from the slack-rtm-in node:
{
"type": "user_typing",
"channel": "...",
"user": "..."
}is dressed to be sent as:
{
"type": "user_typing",
"channel": "...",
"user": "...",
"channelObject": {
"id": "...",
"name": "...",
"is_channel": true,
"is_group": false,
"is_im": false,
"created": 1434735155,
...
},
"userObject": {
"id": "...",
"name": "...",
"real_name": "...",
...
}
}Helpers
As a convenience for both the slack-web-out and
slack-rtm-out nodes a special interface is supported that
allows you to send a message with a simplified structure (msg.topic starts
with @ or #):
msg.topic = "@some_user";
# or
msg.topic = "#some_channel";
msg.payload = "a special message just for you"
return msgAs an additional convenience, if you are invoking the
chat.meMessage,
chat.postEphemeral,
chat.postMessage
methods (slack-web-out), or
message method
(slack-rtm-out) and the channel starts with @ or #
the node will automatically lookup the appropriate channel.id from
slackState (see below) and set it for you.
slackState
All outputs for all nodes include a msg.slackState object which has several
properties containing the lists of members/channels/bots/team/etc so
downstream nodes can do 'lookups' without re-hitting the API (this data is
currently refreshed in the background every 10 minutes). Additionally the
internal state is connected to all relevant slack events and updates are
reflected real-time in any new messages (ie: a user getting created
automatically updates the slackState even before the 10 minute refresh).
nodes
slack-rtm-in
The slack-rtm-in node listens to
Slack RTM events and
outputs the dressed response as the msg.payload.
By default the node will listen to ALL events. You can however filter
event types by setting the node Slack Events property to a value
taking the form of type[::subtype][,type[::subtype],...]. For example
message to receive only events of type message or message::bot_message to
receive only events of type message which additionally have a subtype of
bot_message.
Example output:
{
"payload" {
"type": "user_typing",
"channel": "...",
"user": "...",
"channelObject": {
"id": "...",
"name": "...",
"is_channel": true,
"is_group": false,
"is_im": false,
"created": 1434735155,
...
},
"userObject": {
"id": "...",
"name": "...",
"real_name": "...",
...
}
},
"slackState": {
...
}
}slack-rtm-out
Invokes a Slack RTM
method and outputs the dressed response as the
msg.payload.
Available methods:
message: send a messageping: pongpresence_sub: to subscribe topresence_changeeventspresence_query: to request a one-timepresence_changestatustyping: to send typing indicators
Using slack-rtm-out for sending messages should only be
used for very basic messages, preference would be to use the
chat.postMessage,
method of the slack-web-out node for anything beyond the
simplest messaging use-case as it supports
attachments
as well as many other features.
presence_sub
is a powerful slack-rtm-out method that allows you to
receive
presence_change
events on the slack-rtm-in node. See the
presence example below for further details.
Example input:
msg.topic = 'presence_query';
msg.payload = {
ids: [
'...'
]
}
return msg;Example output:
{
"topic": "presence_query",
"payload": {
"ok":true,
"type":"presence_query"
},
"slackState": {
...
}
}slack-web-out
Invokes a Slack Web
method and outputs the dressed response as the
msg.payload.
See the sending a message example for advanced message sending.
Example input:
msg.topic = "chat.meMessage";
msg.payload = {
channel: "...",
text: "..."
}
return msg;Example output:
{
"topic": "chat.meMessage",
"payload": {
"channel": "...",
"ts": "1552705036.049000",
"ok": true,
"scopes": [
"identify",
"read",
"post",
"client",
"apps"
],
"acceptedScopes": [
"chat:write:user",
"post"
],
"channelObject": {
"id": "...",
...
"userObject": {
"id": "...",
...
}
}
},
"slackState": {
...
}
}slack-state
slack-state outputs a message with
msg.slackState added. If the msg.payload sent to
slack-state is true then it will first do a full refresh of
the state (should not generally be necessary) and then output the msg.
The state events (2nd) output of the node emits a signal when the state has
been fully initialized after (re)connect. This can be useful if you want to
perform any post initilization tasks (ie:
presence_sub
).
Example input:
msg.payload = true; // force a refresh
return msg;Example output (state):
{
"slackState": {
...
}
}Example output (state events):
{
"payload": {
"type":"ready"
},
"slackState": {
...
}
}Examples / Advanced
sending a message
While you can send messages using the simplified syntax (msg.topic starts
with @ or # and msg.payload is the message) using either the
slack-web-out or the slack-rtm-out
nodes, your use-case may require more control. The most advanced message
sending can be accomplished by invoking the
chat.postMessage
method of the slack-web-out node:
var topic = "chat.postMessage";
var payload = {
// channel: "@someuser",
// or
// channel: "#somechannel",
text: "hi from bot",
...
// review linked documentation for all options
}
msg = {
topic: topic,
payload: payload
}
return msg;respond to keyword
A simple respond to keyword example function node to place between a
slack-rtm-in node and a slack-web-out
node:
// ignore anything but messages
if (msg.payload.type != "message") {
return null;
}
// ignore deleted messages
if (msg.payload.subtype == "message_deleted") {
return null;
}
// ignore messages from bots
if (msg.payload.bot_id || (msg.payload.userObject && msg.payload.userObject.is_bot)) {
return null;
}
// if you only want to watch a specific channel put name here
var channel = "";
if (channel && !msg.payload.channelObject) {
return null;
}
if (channel && msg.payload.channelObject.name != channel.replace(/^@/, "").replace(/^#/, "")) {
return null;
}
// only specific users
var username = "";
if (username && !msq.payload.userObject) {
return null;
}
if (username && msq.payload.userObject.name.replace(/^@/, "") != username)) {
return null;
}
// check for keyword
// could use regex etc
if (!msg.payload.text.includes("keyword")) {
return null;
}
// prepare outbound response
var topic = "chat.postMessage";
var payload = {
channel: msg.payload.channel, // respond to same channel
//text: '<@' + msg.payload.userObject.name + '>, thanks for chatting',
text: '<@' + msg.payload.user + '>, thanks for chatting',
//as_user: false,
//username: "",
//attachments: [],
//icon_emoji: "",
}
msg = {
topic: topic,
payload: payload
}
return msg;presence
While slackState does not automatically subscribe to
presence_change
events for you, it will keep track of presence details in
slackState if any
presence_change
events are received (this is all done behind the scenes).
To subscribe to
presence_change
events for all your users place the following function node between the
slack events output of the slack-state node and the
slack-rtm-out node:
msg.topic = 'presence_sub';
var ids = [];
for (var id in msg.slackState.members) {
if (msg.slackState.members.hasOwnProperty(id)) {
ids.push(id)
}
}
msg.payload = {
ids: ids
}
return msg;The theory of operation is:
- wait for the
slackStateto be initialized so you have a complete list ofmembers - iterate that list to build up the appropriate request to
slack-rtm-out - subscribe to presence events by sending the message to
slack-rtm-out - receive presence events on the
slack-rtm-innode
Immediately after the request is sent you will see a flood of
presence_change
events emitted on the slack-rtm-in node. Once the initial
flood of messages has passed continued updates will come through as
appropriate. Again, behind the scenes the slack-state nodes
are listening for these events and updating the
slackState.presence values appropriately for general
usage/consumption in your flow(s).
If you are really interested in keeping the data updated you could capture
team_join events from a slack-rtm-in node and wire those
to the above function node as well triggering the same procedure when new
users join the team. You may need to put a delay node before the
function node just to give slackState enough time to process
this same event and update.
An alternative would be to wire an inject node to the function node and put
it on a sane interval such as every 10 minutes.
If you wanted to be really sure you are receiving all
presence_change
events for the whole team do all the above.
migration from 0.1.2 or earlier
In order to replicate the previous behavior it is possible to introduce simple
function nodes.
Roughly speaking the node equivalents are:
0.1.2 | 2.x |
|---|---|
slack | slack-web-out |
Slack Bot In | slack-rtm-in |
Slack Bot Out | slack-rtm-out |
slack
To replicate the slack node simply place the following function node just
before the new slack-web-out node:
// https://api.slack.com/methods/chat.postMessage
msg.topic = "chat.postMessage"
var payload = {
text: msg.payload
};
// set default username (replicate the node configuration value)
var username = "";
if (username) {
payload.username = username;
payload.as_user = false;
} else if (msg.username) {
payload.username = msg.username;
payload.as_user = false;
}
// set default emojiIcon (replicate the node configuration value)
var emojiIcon = "";
if (emojiIcon) {
payload.icon_emoji = emojiIcon;
} else if (msg.emojiIcon) {
payload.icon_emoji = msg.emojiIcon;
}
// set default channel (replicate the node configuration value)
var channel = "";
if (channel) {
payload.channel = channel;
} else if (msg.channel) {
payload.channel = msg.channel
}
if (msg.attachments) {
payload.attachments = msg.attachments;
}
msg.payload = payload;
return msg;Slack Bot In
To replicate the Slack Bot In node simply place the following function node
downstream from the new slack-rtm-in node:
// https://api.slack.com/events/message
if (msg.payload.type != "message") {
return null;
}
// if you only want to watch a specific channel put name here
var channel = "";
if (channel && !msg.payload.channelObject) {
return null;
}
if (channel && msg.payload.channelObject.name != channel.replace(/^@/, "").replace(/^#/, "")) {
return null;
}
var payload = "";
if (msg.payload.text) {
payload += msg.payload.text;
}
if (msg.payload.attachments) {
if (payload) {
payload += "\n";
}
msg.payload.attachments.forEach((attachment, index) => {
if (index > 0) {
payload += "\n";
}
payload += attachment.fallback;
})
}
var slackObj = {
id: msg.payload.client_msg_id,
type: msg.payload.type,
text: msg.payload.text,
channelName: msg.payload.channelObject.name,
channel: msg.payload.channelObject,
fromUser: (msg.payload.userObject) ? msg.payload.userObject.name : "",
attachments: msg.payload.attachments
};
msg = {
payload: payload,
slackObj: slackObj
}
return msg;Slack Bot Out
To replicate the Slack Bot Out node simply place the following function
node just before the new slack-rtm-out node:
// set channel
var channel = "";
if (channel) {
// do nothing, use the provided channel
} else if (msg.channel) {
channel = msg.channel;
} else if (msg.slackObj && msg.slackObj.channel) {
channel = msg.slackObj.channel
} else {
node.error("'slackChannel' is not defined, check you are specifying a channel in the message (msg.channel) or the node config.");
node.error("Message: '" + JSON.stringify(msg));
return null;
}
msg = {
topic: channel,
payload: msg.payload
}
return msg;Additional Resources
- Emoji Cheat Sheet
- Slack Attachments
- https://slack.dev/node-slack-sdk/
- https://slack.dev/node-slack-sdk/rtm_api
- https://slack.dev/node-slack-sdk/web_api