node-red-contrib-slack
Version:
A node-red module to interact with the Slack API
1,390 lines (1,257 loc) • 40.1 kB
JavaScript
/**
* TODO: create channels transparently when sending to `@` channels?
* TODO: disable the `interval` to refreshState() since we should have all
* RTM listeners in place now?
*
* https://nodered.org/blog/2019/09/20/node-done
*/
module.exports = function (RED) {
"use strict";
const { RTMClient } = require("@slack/rtm-api");
const { WebClient } = require("@slack/web-api");
/**
* https://nodered.org/docs/creating-nodes/status
* The shape property can be: ring or dot
* The fill property can be: red, green, yellow, blue or grey
*/
var statuses = {
connecting: { fill: "yellow", shape: "ring", text: "connecting" },
connected: { fill: "green", shape: "dot", text: "connected" },
disconnected: { fill: "red", shape: "ring", text: "disconnected" },
misconfigured: { fill: "red", shape: "ring", text: "misconfigured" },
error: { fill: "red", shape: "dot", text: "error" },
sending: { fill: "blue", shape: "dot", text: "sending" },
blank: {},
};
function SlackDebug() {
var debug = process.env.SLACK_DEBUG ? true : false;
if (!debug) {
return;
}
// 16-5-2015 9:50:11
var d = new Date();
var datestring =
d.getFullYear() +
"-" +
(d.getMonth() + 1) +
"-" +
d.getDate() +
" " +
d.getHours() +
":" +
d.getMinutes() +
":" +
d.getSeconds();
var prefix = datestring + " - [debug] ";
console.log(
"---------------------------------------------------------------------"
);
console.log(prefix + "slack");
if (arguments.length > 1) {
arguments[0] = arguments[0] + "\n";
}
console.log(...arguments);
console.log(
"---------------------------------------------------------------------"
);
}
/**
* turn any value into a string
* https://webbjocke.com/javascript-check-data-types/
*
* @param {*} value
*/
function ValueToString(value) {
if (value && typeof value === "object" && value.constructor === Array) {
return JSON.stringify(value);
}
if (value && typeof value === "object" && value.constructor === Object) {
return JSON.stringify(value);
}
/**
* seeing some really weird cases where the above fail but "" + value
* returns '[object Object]'
*/
if (value && typeof value === "object") {
return JSON.stringify(value);
}
if (value === null || typeof value === "undefined") {
return "";
}
if (typeof value === "boolean") {
if (value) {
return "true";
} else {
return "false";
}
}
if (typeof value === "number" && isFinite(value)) {
return value.toString();
}
if (!!(value && value.constructor && value.call && value.apply)) {
return value.toString();
}
return "" + value;
}
/**
* Common event listeners to set status indicators
*
* @param {*} node
*/
function SetConfigNodeConnectionListeners(node) {
node.clientNode.rtmClient.on("connecting", () => {
node.status(statuses.connecting);
});
node.clientNode.rtmClient.on("ready", () => {
node.status(statuses.connected);
});
node.clientNode.rtmClient.on("disconnected", (e) => {
var status = statuses.disconnected;
/**
* {
* code: 'slackclient_platform_error',
* data: { ok: false, error: 'invalid_auth' }
* }
*/
if (e.code == "slackclient_platform_error" && e.data.ok === false) {
status.text = e.data.error;
}
node.status(status);
});
/**
* if connectivity is dropped we go from connected -> reconnecting
* directly bypassing disconnected
*/
node.clientNode.rtmClient.on("reconnecting", () => {
node.status(statuses.disconnected);
});
}
/**
* Slack client helper to recursively request a resource using pagination
* https://slack.dev/node-slack-sdk/web_api#pagination
*
* @param {*} client
* @param {*} method
* @param {*} property
* @param {*} options
*/
function RecursivePaginatedWebCall(client, method, property, options = {}) {
/**
* store all entries
*/
var data = [];
/**
* count of recursive calls required
*/
var rcount = 0;
return new Promise((resolve, reject) => {
function recurse() {
rcount++;
client
.apiCall(method, options)
.then((res) => {
data = data.concat(res[property]);
/**
* if more pages, keep going otherwise resolve the promise
*/
if (
res.response_metadata &&
res.response_metadata.next_cursor &&
res.response_metadata.next_cursor !== ""
) {
options.cursor = res.response_metadata.next_cursor;
// keep recursing
recurse();
} else {
resolve(data);
}
})
.catch((e) => {
reject(e);
});
}
// start recursion
recurse();
});
}
/**
* Connection/configuration NODE for slack api
* make note that when the flows are deployed this function will be
* re-ran (along with other nodes)
*
* @param {*} n
*/
function SlackConfig(n) {
RED.nodes.createNode(this, n);
var node = this;
this.options = {};
this.state = {};
this.webClient = null;
this.rtmClient = null;
this.shortToken = function () {
if (this.credentials && this.credentials.hasOwnProperty("token")) {
var token = this.credentials.token;
return token.substr(0, 8) + "..." + token.substr(token.length - 3);
}
};
if (this.credentials && this.credentials.hasOwnProperty("token")) {
/**
* This can be used to pre-emptively valid API calls when check against
* this scopes the user has
*
* https://api.slack.com/docs/token-types
*/
var tokenType = null;
if (this.credentials.token.startsWith("xoxp-")) {
tokenType = "user";
} else if (this.credentials.token.startsWith("xoxb-")) {
tokenType = "bot";
} else {
// TODO: validate this on the config node
tokenType = "invalid";
}
/**
* meta method to refresh all state data
*/
this.refreshState = function () {
SlackDebug("refreshing state " + this.shortToken());
var promises = [];
promises.push(this.refreshTeam());
promises.push(this.refreshChannels());
promises.push(this.refreshMembers());
promises.push(this.refreshBots());
promises.push(this.refreshUser());
promises.push(this.refreshDnd());
return Promise.all(promises)
.then(() => {
this.emit("state_refreshed", {});
})
.catch((e) => {
this.error(e);
});
};
/**
* update local cache of channels
*/
this.refreshChannels = function () {
/**
* TODO: ensure this does not happen too frequent
* get all channels
* https://api.slack.com/methods/conversations.list
*/
return RecursivePaginatedWebCall(
this.webClient,
"conversations.list",
"channels",
{
limit: 1000,
exclude_archived: true,
types: "public_channel,private_channel,im,mpim",
}
)
.then((res) => {
var channels = {};
res.forEach((item) => {
channels[item.id] = item;
});
this.state.channels = channels;
})
.catch((e) => {
this.error(e);
});
};
/**
* update local cache of members
*/
this.refreshMembers = function () {
/**
* TODO: ensure this does not happen too frequent
* get all members
* https://api.slack.com/methods/users.list
*/
return RecursivePaginatedWebCall(
this.webClient,
"users.list",
"members",
{
limit: 1000,
}
)
.then((res) => {
this.state.presence = this.state.presence || {};
var members = {};
res.forEach((item) => {
members[item.id] = item;
});
this.state.members = members;
})
.catch((e) => {
this.error(e);
});
};
/**
* update local cache of bots
*/
this.refreshBots = function () {
var promises = [];
this.state.bots = this.state.bots || {};
for (var id in this.state.bots) {
if (this.state.bots.hasOwnProperty(id)) {
SlackDebug("webrequest to find bot: " + id);
var p = this.webClient
.apiCall("bots.info", { bot: id })
.then((res) => {
this.state.bots[id] = res.bot;
})
.catch((e) => {
this.error(e);
});
promises.push(p);
}
}
return Promise.all(promises)
.then(() => {
// TODO: update any internal flags?
})
.catch((e) => {
this.error(e);
});
};
/**
* update local cache of team
*/
this.refreshTeam = function () {
/**
* TODO: ensure this does not happen too frequent
* https://api.slack.com/methods/team.info
*/
return this.webClient
.apiCall("team.info")
.then((res) => {
this.state.team = res.team;
})
.catch((e) => {
this.error(e);
});
};
/**
* update local cache of identity
*/
this.refreshUser = function () {
/**
* TODO: ensure this does not happen too frequent
* https://api.slack.com/methods/users.identity
*/
if (this.state.connection.self.id) {
return this.webClient
.apiCall("users.identity", { user: this.state.connection.self.id })
.then((res) => {
this.state.user = res.user;
})
.catch((e) => {
//this.error(e);
});
}
};
/**
* update local cache of dnd
*/
this.refreshDnd = function () {
return this.webClient
.apiCall("dnd.teamInfo")
.then((res) => {
this.state.dnd = res.users;
})
.catch((e) => {
//this.error(e);
});
};
/**
* lookup member based off of id
*/
this.findMemberById = function (id) {
SlackDebug("looking up member: " + id);
this.state.members = this.state.members || {};
return this.state.members[id];
};
/**
* lookup member based off of name or @name syntax
*/
this.findMemberByName = function (name) {
SlackDebug("looking up member: " + name);
this.state.members = this.state.members || {};
for (var id in this.state.members) {
if (this.state.members.hasOwnProperty(id)) {
if (
this.state.members[id].deleted === false &&
this.state.members[id].name == name.replace(/^@/, "")
) {
return this.state.members[id];
}
}
}
};
/**
* lookup channel based off of id
*/
this.findChannelById = function (id) {
SlackDebug("looking up channel: " + id);
this.state.channels = this.state.channels || {};
return this.state.channels[id];
};
/**
* channels start with:
* C - Conference (normal rooms)
* G - Group (multiuser, and private channels)
* D - Direct (user to user)
*/
this.findChannelByName = function (name, type = null) {
SlackDebug("looking up channel: " + name);
this.state.channels = this.state.channels || {};
/**
* this purposely does a lookup on both regardless to handle
* use-cases where the # or @ is ommitted completely
*/
var directChannel = null;
var roomChannel = null;
// do lockup by member name
var member = this.findMemberByName(name);
if (member) {
for (var id in this.state.channels) {
if (this.state.channels.hasOwnProperty(id)) {
if (
this.state.channels[id].id[0] == "D" &&
this.state.channels[id].is_user_deleted === false &&
this.state.channels[id].user == member.id
) {
directChannel = this.state.channels[id];
break;
}
}
}
}
// do lookup by channel name
for (var id in this.state.channels) {
if (this.state.channels.hasOwnProperty(id)) {
if (
this.state.channels[id].is_archived === false &&
this.state.channels[id].name == name.replace(/^#/, "")
) {
roomChannel = this.state.channels[id];
break;
}
}
}
if (name[0] == "@") {
return directChannel;
} else if (name[0] == "#") {
return roomChannel;
} else {
return directChannel || roomChannel;
}
};
/**
* lookup bot based off of id
*/
this.findBotById = function (id) {
SlackDebug("looking up bot: " + id);
this.state.bots = this.state.bots || {};
var bot = this.state.bots[id];
if (!bot) {
SlackDebug("webrequest to find bot: " + id);
this.webClient
.apiCall("bots.info", { bot: id })
.then((res) => {
this.state.bots[id] = res.bot;
})
.catch((e) => {
//ignore
});
}
return bot;
};
/**
* Takes raw responses from RTM/Web API and adds additional properties
* to help looking up names/etc as msg flows through nodered
*/
this.dressResponseMessage = function (res) {
var node = this;
var depth = 0;
function recurse(data) {
depth++;
for (var key in data) {
if (data.hasOwnProperty(key)) {
var value = data[key];
/**
* prevent infinite recursion by only going 2 deep
*/
if (typeof value === "object" && depth < 2) {
recurse(value);
continue;
}
/**
* dress users, channels, teams, etc
*/
if (key.includes("user")) {
if (typeof value === "string" || value instanceof String) {
if (!data.hasOwnProperty(key + "Object")) {
var member = node.findMemberById(value);
if (member) {
data[key + "Object"] = member;
}
}
}
} else if (key.includes("channel")) {
if (typeof value === "string" || value instanceof String) {
if (!data.hasOwnProperty(key + "Object")) {
var channel = node.findChannelById(value);
if (channel) {
data[key + "Object"] = channel;
}
}
}
} else if (key.includes("team")) {
if (typeof value === "string" || value instanceof String) {
if (!data.hasOwnProperty(key + "Object")) {
if (value == node.state.team.id)
data[key + "Object"] = node.state.team;
}
}
} else if (key.includes("bot")) {
if (typeof value === "string" || value instanceof String) {
if (!data.hasOwnProperty(key + "Object")) {
var bot = node.findBotById(value);
if (bot) {
data[key + "Object"] = bot;
}
}
}
}
}
}
--depth;
}
try {
recurse(res);
} catch (e) {
node.error(e);
}
return res;
};
/**
* https://api.slack.com/methods
* https://slack.dev/node-slack-sdk/web_api
* https://github.com/slackapi/node-slack-sdk/blob/master/src/WebClient.ts
*/
this.webClient = new WebClient(this.credentials.token);
/**
* https://slack.dev/node-slack-sdk/rtm_api
* https://slack.dev/node-slack-sdk/rtm_api#lifecycle-events
* https://api.slack.com/rtm#events
* https://github.com/slackapi/node-slack-sdk/blob/master/src/RTMClient.ts
*
* by default autoReconnect=true and keepAlive settings are enabled
*/
this.rtmClient = new RTMClient(this.credentials.token, {
logLevel: process.env.SLACK_DEBUG ? "debug" : "info",
retryConfig: {
forever: true,
minTimeout: 1 * 1000,
maxTimeout: 5 * 1000,
},
});
this.rtmClient.on("disconnected", (e) => {
SlackDebug("disconnected " + this.shortToken(), e);
this.log(
RED._("node-red:common.status.disconnected") +
" from slack with token: " +
this.shortToken()
);
clearInterval(this.refreshIntervalId);
});
this.rtmClient.on("connecting", () => {
SlackDebug("connecting " + this.shortToken());
clearInterval(this.refreshIntervalId);
});
this.rtmClient.on("authenticated", () => {
SlackDebug("authenticated " + this.shortToken());
});
this.rtmClient.on("connected", () => {
SlackDebug("connected " + this.shortToken());
this.log(
RED._("node-red:common.status.connected") +
" to slack with token: " +
this.shortToken()
);
// what is worse, no data or potentially stale data?
this.state.presence = {};
this.refreshState()
.then(() => {
this.emit("state_initialized", {});
})
.catch((e) => {
this.error(e);
});
// TODO: make this a node configurable item
var interval = 10 * 60 * 1000;
this.refreshIntervalId = setInterval(function () {
node.refreshState();
}, interval);
this.on("close", () => {
clearInterval(this.refreshIntervalId);
});
});
this.rtmClient.on("ready", () => {
SlackDebug("ready " + this.shortToken());
});
this.rtmClient.on("disconnecting", () => {
SlackDebug("disconnecting " + this.shortToken());
clearInterval(this.refreshIntervalId);
});
/**
* if connectivity is dropped we go from connected -> reconnecting
* directly bypassing disconnected
*/
this.rtmClient.on("reconnecting", () => {
SlackDebug("reconnecting " + this.shortToken());
this.log(
RED._("node-red:common.status.disconnected") +
" from slack with token: " +
this.shortToken()
);
clearInterval(this.refreshIntervalId);
});
this.rtmClient.on("error", (e) => {
SlackDebug("error " + this.shortToken(), e);
});
this.rtmClient.on("unable_to_rtm_start", (e) => {
SlackDebug("unable_to_rtm_start " + this.shortToken(), e);
});
/**
* This is meant to keep state current
* - bots
* - members
* - channels
* - team
* - dnd
* - presence (only listen to what the user has subscribed to manually)
*/
this.rtmClient.on("slack_event", (eventType, event) => {
switch (eventType) {
case "user_change":
case "team_join":
this.state.members = this.state.members || {};
this.state.members[event.user.id] = event.user;
break;
case "bot_added":
case "bot_changed":
this.state.bots = this.state.bots || {};
this.state.bots[event.bot.id] = event.bot;
break;
case "channel_deleted":
case "group_deleted":
this.state.channels = this.state.channels || {};
delete this.state.channels[event.channel];
break;
case "channel_created":
case "channel_joined":
case "channel_rename":
case "group_joined":
case "group_rename":
case "im_created":
this.webClient
.apiCall("channels.info", { channel: event.channel.id })
.then((res) => {
this.state.channels = this.state.channels || {};
this.state.channels[res.channel.id] = res.channel;
})
.catch((e) => {
console.log("failed getting channel info", e);
});
break;
case "channel_archive":
case "channel_left":
case "channel_unarchive":
case "group_archive":
case "group_close":
case "group_left":
case "group_open":
case "group_unarchive":
case "im_close":
case "im_open":
case "member_joined_channel":
case "member_left_channel":
this.webClient
.apiCall("channels.info", { channel: event.channel })
.then((res) => {
this.state.channels = this.state.channels || {};
this.state.channels[res.channel.id] = res.channel;
})
.catch((e) => {
console.log("failed getting channel info", e);
});
break;
case "presence_change":
this.state.presence = this.state.presence || {};
if (event.user) {
var res = {
type: event.type,
presence: event.presence,
user: event.user,
};
this.state.presence[event.user] = res;
}
if (event.users) {
event.users.forEach((user) => {
var res = {
type: event.type,
presence: event.presence,
user: user,
};
this.state.presence[user] = res;
});
}
break;
case "manual_presence_change":
this.state.presence = this.state.presence || {};
this.this.state.presence[this.state.connection.self.id] = {
type: event.type,
presence: event.presence,
user: this.state.connection.self.id,
};
break;
case "dnd_updated":
case "dnd_updated_user":
this.state.dnd = this.state.dnd || {};
this.state.dnd[event.user] = event.dnd_status;
break;
case "team_rename":
case "team_domain_change":
case "team_plan_change":
case "team_pref_change":
case "team_profile_change":
case "team_profile_delete":
case "team_profile_reorder":
this.webClient
.apiCall("team.info")
.then((res) => {
this.state.team = res.team;
})
.catch((e) => {
console.log("failed getting team info", e);
});
break;
default:
// do nothing
break;
}
});
this.startClient = function () {
/**
* may consider using the .connect() method instead
* https://api.slack.com/methods/rtm.start
* https://api.slack.com/methods/rtm.connect
*/
this.rtmClient
.start()
.then((res) => {
SlackDebug("start result " + this.shortToken(), res);
this.state.connection = {};
this.state.connection.self = res.self;
this.state.connection.url = res.url;
this.state.connection.scopes = res.scopes;
this.state.connection.acceptedScopes = res.acceptedScopes;
})
.catch((e) => {
console.log("failed starting slack session", e);
});
};
this.startClient();
//start watchdog
this.connectionWatchdog = function () {
//var interval = (5 * 60 * 1000);
var interval = 15 * 1000;
var lastState = null;
var stateAt = null;
var maxUnconnectedStateSeconds = 30;
var intervalId = setInterval(() => {
SlackDebug("watchdog connection details", {
connected: this.rtmClient.connected,
currentState: this.rtmClient.stateMachine.getCurrentState(),
currentStateAt: stateAt,
});
if (lastState === null) {
lastState = this.rtmClient.stateMachine.getCurrentState();
stateAt = Date.now();
}
if (
!this.rtmClient.connected &&
lastState == this.rtmClient.stateMachine.getCurrentState() &&
(Date.now() - stateAt) / 1000 > maxUnconnectedStateSeconds
) {
console.log(
"sitting in " +
lastState +
" for longer than " +
maxUnconnectedStateSeconds +
" seconds"
);
//console.log("slack watchdog attempting to force reconnect");
//console.log(this.rtmClient);
}
if (lastState != this.rtmClient.stateMachine.getCurrentState()) {
stateAt = Date.now();
}
lastState = this.rtmClient.stateMachine.getCurrentState();
}, interval);
this.on("close", () => {
clearInterval(intervalId);
});
};
//this.connectionWatchdog();
this.on("close", (done) => {
/**
* attempt a disconnect regardless of current state
* and simply catch invalid transitions etc
*/
if (this.rtmClient.connected) {
this.rtmClient
.disconnect()
.then(() => {
this.rtmClient.removeAllListeners();
done();
})
.catch((e) => {
done();
});
} else {
done();
}
});
}
}
RED.nodes.registerType("slack-config", SlackConfig, {
credentials: {
token: { type: "text" },
},
});
/**
* Send the current state of the config object out
*
* @param {*} n
*/
function SlackState(n) {
RED.nodes.createNode(this, n);
var node = this;
this.client = n.client;
this.clientNode = RED.nodes.getNode(this.client);
node.status(statuses.disconnected);
if (node.client) {
SetConfigNodeConnectionListeners(node);
node.on("input", function (msg, send, done) {
SlackDebug("slack-state incoming message", msg);
// support of 1.0+ and pre-1.0
send =
send ||
function () {
node.send.apply(node, arguments);
};
node.status(statuses.sending);
/**
* support for forcing a refresh of data
*/
if (msg.payload === true) {
node.clientNode
.refreshState()
.then(() => {
msg.slackState = node.clientNode.state;
send(msg);
node.status(statuses.connected);
if (done) {
done();
}
})
.catch((e) => {
SlackDebug("slack-state error response", e.data);
msg.payload = e.data;
send(msg);
node.status(statuses.connected);
if (done) {
done();
}
});
} else {
msg.slackState = node.clientNode.state;
send(msg);
node.status(statuses.connected);
if (done) {
done();
}
}
});
["state_initialized"].forEach((eventName) => {
node.clientNode.on(eventName, (e) => {
node.status(statuses.sending);
var msg = {};
switch (eventName) {
case "state_initialized":
eventName = "ready";
break;
}
msg.slackState = node.clientNode.state;
msg.payload = {
type: eventName,
};
node.send([null, msg]);
node.status(statuses.connected);
});
});
} else {
node.status(statuses.misconfigured);
}
}
RED.nodes.registerType("slack-state", SlackState);
/**
* Read all 'slack_events' from the slack RTM api
* https://api.slack.com/rtm#events
*
* @param {*} n
*/
function SlackRtmIn(n) {
RED.nodes.createNode(this, n);
var node = this;
this.client = n.client;
this.events = n.events;
this.clientNode = RED.nodes.getNode(this.client);
var eventNames;
if (this.events) {
eventNames = this.events.replace(/[\s]+/g, "").split(",").filter(Boolean);
}
node.status(statuses.disconnected);
if (node.client) {
SetConfigNodeConnectionListeners(node);
node.clientNode.rtmClient.on("slack_event", (eventType, event) => {
/**
* responses to websocket commands come over the without an eventType
*/
if (eventType === undefined) {
eventType = "ws_response";
event.type = eventType;
}
/**
* ignore non-subscribed events
*
* Since the message events are bound to the same object as connection
* events this feature has been implemented in this fashion vs directly
* subscribing to prevent stupid (ie: someone setting a value of
* 'connected' or the like) in node properties. Crude but effective.
*
* It also decouples the feature from the semantics of the client.
*/
if (this.events && eventNames.length > 0) {
if (
!(
eventNames.includes(event.type) ||
(event.subtype &&
eventNames.includes(`${event.type}::${event.subtype}`))
)
) {
return null;
}
}
/**
* pong: triggered in response to a ping keep-alive
*
* hello: triggered on connect
*
* ws_response: triggered after data has been sent over the ws
*
* channel_marked: triggered when the bot user has 'read' a channel
*
* desktop_notification: triggered when the bot user is @mentioned or
* otherwise would receive an alert
*
* pretty sure nobody is interested in the below
* note that ws_response is sent on the output side of the output node
* so receiving it here is double processing
*/
if (eventType == "pong" || eventType == "ws_response") {
return null;
}
SlackDebug("slack-rtm-in incoming event", event);
var msg = {};
msg.payload = node.clientNode.dressResponseMessage(event);
msg.slackState = node.clientNode.state;
node.send(msg);
});
} else {
node.status(statuses.misconfigured);
}
}
RED.nodes.registerType("slack-rtm-in", SlackRtmIn);
/**
* Write to the slack RTM api
* https://api.slack.com/rtm
* https://slack.dev/node-slack-sdk/rtm_api
*
* @param {*} n
*/
function SlackRtmOut(n) {
RED.nodes.createNode(this, n);
var node = this;
this.client = n.client;
this.clientNode = RED.nodes.getNode(this.client);
node.status(statuses.disconnected);
if (node.client) {
SetConfigNodeConnectionListeners(node);
node.on("input", function (msg, send, done) {
SlackDebug("slack-rtm-out incoming message", msg);
// support of 1.0+ and pre-1.0
send =
send ||
function () {
node.send.apply(node, arguments);
};
/**
* message, presence_sub, presence_query, ping, and typing.
*/
var method = msg.topic;
var options = msg.payload;
if (
!method ||
typeof method !== "string" ||
!method instanceof String
) {
if (done) {
done("invalid msg.topic");
} else {
node.error("invalid msg.topic", msg);
}
return null;
}
/**
* Try to help with common use case, set channel based on lookup if starts with @ or #
*/
if (
method == "message" &&
(options.channel[0] == "@" || options.channel[0] == "#")
) {
var channel = node.clientNode.findChannelByName(options.channel);
if (channel && channel.id) {
options.channel = channel.id;
}
}
/**
* special handling for simple use-case
*/
if (method[0] == "@" || method[0] == "#") {
var channel = node.clientNode.findChannelByName(method);
if (!channel || !channel.id) {
if (done) {
done("invalid channel: " + method);
} else {
node.error("invalid channel: " + method, msg);
}
return null;
}
method = "message";
options = {
text: msg.payload,
channel: channel.id,
};
}
/**
* custom per-method logic
*/
switch (method) {
case "message":
// force text to be a string
options.text = ValueToString(options.text);
break;
}
/**
* something seems to block when it's not message events
* keeping a hard-coded list for now until the 'bug' is squashed
*
* https://github.com/slackapi/node-slack-sdk/issues/706
*
* There's only 5 (public) writable events on RTM: message,
* presence_sub, presence_query, ping, and typing.
*
* The only two that expect responses are message and ping.
*/
var awaitReply = false;
switch (method) {
case "message":
case "ping":
awaitReply = true;
break;
case "presence_sub":
case "presence_query":
case "typing":
awaitReply = false;
break;
default:
/**
* go ahead and let invalid methods await so a negative/error
* response is returned
*/
awaitReply = true;
break;
}
node.status(statuses.sending);
msg.slackState = node.clientNode.state;
SlackDebug("slack-rtm-out call", method, options);
node.clientNode.rtmClient
.addOutgoingEvent(awaitReply, method, options)
.then((res) => {
/**
* mock a response for methods which return nothing
* ie: everything but message and ping
*/
if (typeof res === "undefined") {
res = {
ok: true,
type: method,
};
}
SlackDebug("slack-rtm-out call response", res);
msg.payload = res;
send(msg);
node.status(statuses.connected);
if (done) {
done();
}
})
.catch((e) => {
SlackDebug("slack-rtm-out error response", e.data);
msg.payload = e.data;
send(msg);
node.status(statuses.connected);
if (done) {
done();
}
});
});
} else {
node.status(statuses.misconfigured);
}
}
RED.nodes.registerType("slack-rtm-out", SlackRtmOut);
/**
* Interact with the slack web/REST api
* https://api.slack.com/methods
* https://slack.dev/node-slack-sdk/web_api
*
* @param {*} n
*/
function SlackWebOut(n) {
RED.nodes.createNode(this, n);
var node = this;
this.client = n.client;
this.clientNode = RED.nodes.getNode(this.client);
node.status(statuses.disconnected);
if (node.client) {
SetConfigNodeConnectionListeners(node);
node.on("input", function (msg, send, done) {
SlackDebug("slack-web-out incoming message", msg);
// support of 1.0+ and pre-1.0
send =
send ||
function () {
node.send.apply(node, arguments);
};
var method = msg.topic;
var options = msg.payload;
if (
!method ||
typeof method !== "string" ||
!method instanceof String
) {
if (done) {
done("invalid msg.topic");
} else {
node.error("invalid msg.topic", msg);
}
return null;
}
/**
* Try to help with common use case, set channel based on lookup if starts with @ or #
*
* https://api.slack.com/methods/chat.postMessage
*/
if (
method == "chat.postMessage" ||
method == "chat.postEphemeral" ||
method == "chat.meMessage" ||
method == "chat.scheduleMessage"
) {
if (options.channel[0] == "@" || options.channel[0] == "#") {
var channel = node.clientNode.findChannelByName(options.channel);
if (channel && channel.id) {
options.channel = channel.id;
}
}
}
/**
* special handling for simple use-case
*/
if (method[0] == "@" || method[0] == "#") {
var channel = node.clientNode.findChannelByName(method);
if (!channel || !channel.id) {
if (done) {
done("invalid channel: " + method);
} else {
node.error("invalid channel: " + method, msg);
}
return null;
}
method = "chat.postMessage";
options = {
channel: channel.id,
text: msg.payload,
//as_user: true
};
}
/**
* custom per-method logic
*/
switch (method) {
case "chat.postMessage":
case "chat.postEphemeral":
case "chat.meMessage":
case "chat.scheduleMessage":
// force text to be a string
options.text = ValueToString(options.text);
break;
}
node.status(statuses.sending);
msg.slackState = node.clientNode.state;
SlackDebug("slack-web-out call", method, options);
node.clientNode.webClient
.apiCall(method, options)
.then((res) => {
SlackDebug("slack-web-out call response", res);
msg.payload = node.clientNode.dressResponseMessage(res);
send(msg);
node.status(statuses.connected);
if (done) {
done();
}
})
.catch((e) => {
SlackDebug("slack-web-out error response", e.data);
msg.payload = e.data;
send(msg);
node.status(statuses.connected);
if (done) {
done();
}
});
});
} else {
node.status(statuses.misconfigured);
}
}
RED.nodes.registerType("slack-web-out", SlackWebOut);
};