A node-red module to interact with the Slack API
npm install node-red-contrib-slackA Node-RED node to interact with
the Slack
API.
Run the following command in the root directory of your Node-RED install:
```
npm install --save node-red-contrib-slack
Version 2.x of this package is NOT compatible with older versions. Plese
refer to the migration section for help.
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:
- slack-rtm-in
- slack-rtm-out
- slack-web-out
- slack-state
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 withNode-RED
the Slack API in it's
entirety facilitating powerful flows in . 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.
To invoke methods (slack-rtm-out,
slack-web-out) set the msg.topic to the name of themsg.payload
method and set the 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;
`
outputThe following nodes each provide
Slack API output:
- slack-rtm-in - view details of each event in the
documentation
- slack-rtm-out - view details of each event in the
documentation
- slack-web-out - view response section 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 datamsg.payload
is traversed to enrich the (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 with the lookup value will be
added.
For example, if the response contains a bot_id attribute you would seebot_idObject added, or if it found an attribute called bot it would addbotObject etc. Ultimately all the lookups come fromslackState (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": "...",
...
}
}
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 msg
`
As 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 #channel.id
the node will automatically lookup the appropriate fromslackState (see below) and set it for you.
All outputs for all nodes include a msg.slackState object which has severalmembers
properties containing the lists of /channels/bots/team/etc soslackState
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 even before the 10 minute refresh).
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 examplemessage to receive only events of type message or message::bot_message tomessage
receive only events of type which additionally have a subtype ofbot_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": {
...
}
}
Invokes a Slack RTM
method and outputs the dressed response as the
msg.payload.
Available methods:
- message: send a messageping
- : pongpresence_sub
- :presence_change
to subscribe to
presence_query
events
- :presence_change
to request a one-time
typing
status
- : 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": {
...
}
}
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 outputs a message with
msg.slackState added. If the msg.payload sent toslack-state is true then it will first do a full refresh ofmsg
the state (should not generally be necessary) and then output the .
The state events (2nd) output of the node emits a signal when the state haspresence_sub
been fully initialized after (re)connect. This can be useful if you want to
perform any post initilization tasks (ie:
).
Example input:
``
msg.payload = true; // force a refresh
return msg;
Example output (state):
``
{
"slackState": {
...
}
}
Example output (state events):
``
{
"payload": {
"type":"ready"
},
"slackState": {
...
}
}
While you can send messages using the simplified syntax (msg.topic starts@
with or # and msg.payload is the message) using either theslack-web-out or the slack-rtm-outchat.postMessage
nodes, your use-case may require more control. The most advanced message
sending can be accomplished by invoking theslack-web-out
method of the 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;
`
A simple respond to keyword example function node to place between aslack-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;
`
While slackState does not automatically subscribe to
presence_change
events for you, it will keep track of presence details inslackState if anypresence_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 theslack events output of the slack-state node and theslack-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:
1. wait for the slackState to be initialized so you have a
complete list of membersslack-rtm-out
1. iterate that list to build up the appropriate request to
slack-rtm-out
1. subscribe to presence events by sending the message to
slack-rtm-in
1. receive presence events on the node
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 thosefunction
to the above node as well triggering the same procedure when newteam
users join the . You may need to put a delay node before thefunction 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 putinterval
it on a sane 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.
or earlierIn 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 |
To replicate the slack node simply place the following function node justslack-web-out
before the new 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;
`
To replicate the Slack Bot In node simply place the following function nodeslack-rtm-in
downstream from the new 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;
`
To replicate the Slack Bot Out node simply place the following functionslack-rtm-out
node just before the new 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;
``
- 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