diff --git a/Projects/ProjectsSchema.json b/Projects/ProjectsSchema.json index 2fbb81987..339cc1576 100644 --- a/Projects/ProjectsSchema.json +++ b/Projects/ProjectsSchema.json @@ -682,6 +682,13 @@ "fileName": "DiscordBot.js", "folderName": "Trading-Bot/Announcements" }, + { + "name": "Slack Bot", + "propertyName": "slackBot", + "functionName": "newSuperalgosBotModulesSlackBot", + "fileName": "SlackBot.js", + "folderName": "Trading-Bot/Announcements" + }, { "name": "Trading Process", "propertyName": "tradingProcess", diff --git a/Projects/Superalgos/Icons-SVG-Sources/nodes/slack-bot.svg b/Projects/Superalgos/Icons-SVG-Sources/nodes/slack-bot.svg new file mode 100644 index 000000000..c37dc5eb4 --- /dev/null +++ b/Projects/Superalgos/Icons-SVG-Sources/nodes/slack-bot.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Superalgos/Icons/slack-bot.png b/Projects/Superalgos/Icons/slack-bot.png new file mode 100644 index 000000000..1422ddd50 Binary files /dev/null and b/Projects/Superalgos/Icons/slack-bot.png differ diff --git a/Projects/Superalgos/Schemas/App-Schema/slack-bot.json b/Projects/Superalgos/Schemas/App-Schema/slack-bot.json new file mode 100644 index 000000000..d672e4b2d --- /dev/null +++ b/Projects/Superalgos/Schemas/App-Schema/slack-bot.json @@ -0,0 +1,41 @@ +{ + "type": "Slack Bot", + "editors": { + "config": true + }, + "menuItems": [ + { + "action": "Configure", + "actionFunction": "uiObject.configEditor.activate", + "label": "Configure", + "iconPathOn": "configuration", + "iconPathOff": "configuration" + }, + { + "action": "Send Slack Test Message", + "label": "Send Slack Test Message", + "iconPathOn": "test-entity", + "iconPathOff": "test-entity", + "actionFunction": "payload.executeAction" + }, + { + "action": "Delete UI Object", + "askConfirmation": true, + "confirmationLabel": "Confirm to Delete", + "label": "Delete", + "iconPathOn": "delete-entity", + "iconPathOff": "delete-entity", + "actionFunction": "payload.executeAction" + } + ], + "initialValues": { + "config": "{\n\"webhookURL\": \"https://hooks.slack.com/services/abc/abc/abc\"\n}" + }, + "addLeftIcons": true, + "level": 3, + "attachingRules": { + "compatibleTypes": "->Social Bots->" + }, + "childrenNodesProperties": [], + "propertyNameAtParent": "slackBots" +} diff --git a/Projects/Superalgos/Schemas/App-Schema/social-bots.json b/Projects/Superalgos/Schemas/App-Schema/social-bots.json index 8b1011846..1e08f0fc4 100644 --- a/Projects/Superalgos/Schemas/App-Schema/social-bots.json +++ b/Projects/Superalgos/Schemas/App-Schema/social-bots.json @@ -13,6 +13,12 @@ "relatedUiObject": "Discord Bot", "actionFunction": "payload.executeAction" }, + { + "action": "Add UI Object", + "label": "Add Slack Bot", + "relatedUiObject": "Slack Bot", + "actionFunction": "payload.executeAction" + }, { "action": "Delete UI Object", "askConfirmation": true, @@ -40,6 +46,12 @@ "type": "array", "childType": "Discord Bot", "autoAdd": true + }, + { + "name": "slackBots", + "type": "array", + "childType": "Slack Bot", + "autoAdd": true } ], "propertyNameAtParent": "socialBots" diff --git a/Projects/Superalgos/Schemas/Docs-Nodes/slack-bot.json b/Projects/Superalgos/Schemas/Docs-Nodes/slack-bot.json new file mode 100644 index 000000000..b745a1206 --- /dev/null +++ b/Projects/Superalgos/Schemas/Docs-Nodes/slack-bot.json @@ -0,0 +1,58 @@ +{ + "type": "Discord Bot", + "definition": { + "text": "The Discord Bot node is used for defining the configuration for a Discord Bot. This bot allows Superalgos to send announcements to a Discord Channel.", + "updated": 1619546769461 + }, + "paragraphs": [ + { + "style": "Text", + "text": "In order to use the Discord Bot, you will need to create a new Webhook Integration in Discord. The only configuration that is required within Superalgos is the webhookURL.", + "updated": 1619546875758 + }, + { + "style": "Text", + "text": "Parameters:" + }, + { + "style": "Text", + "text": "- webhookURL - required - obtained from Integrations > Webhooks in the channel settings." + }, + { + "style": "Text", + "text": "- format - optional - allows custom formatting of the message being sent into Discord." + }, + { + "style": "Text", + "text": "Recognized Format Variables:" + }, + { + "style": "Text", + "text": "- `%{EXCHANGE}` the exchange which is operating, i.e. Binance" + }, + { + "style": "Text", + "text": "- `%{MARKET}` the market which is operating. i.e. BTC/USDT" + }, + { + "style": "Text", + "text": "- `%{MESSAGE}` the text being sent" + }, + { + "style": "Text", + "text": "An example format is:" + }, + { + "style": "Javascript", + "text": "\"%{EXCHANGE} %{MARKET}: %{MESSAGE}\"" + }, + { + "style": "Text", + "text": "Which results in a final message in Discord that looks like:" + }, + { + "style": "Javascript", + "text": "\"Binance BTC/USDT: Backtesting Session 'Lightning Strike' is starting.\"" + } + ] +} diff --git a/Projects/Superalgos/Schemas/Docs-Nodes/social-bots.json b/Projects/Superalgos/Schemas/Docs-Nodes/social-bots.json index 36f35b99d..6d14ab739 100644 --- a/Projects/Superalgos/Schemas/Docs-Nodes/social-bots.json +++ b/Projects/Superalgos/Schemas/Docs-Nodes/social-bots.json @@ -8,7 +8,7 @@ }, { "style": "Note", - "text": "At present Superalgos only supports Telegram and Discord messaging applications.", + "text": "At present Superalgos only supports Telegram, Discord, and Slack messaging applications.", "updated": 1619547181067 } ], @@ -16,4 +16,4 @@ "text": "A Social Bots node is used to define the bots for each messaging application. These messaging bots are used to send announcements and receive commands used to interact with Superalgos.", "updated": 1618222853055 } -} \ No newline at end of file +} diff --git a/Projects/Superalgos/TS/Bot-Modules/Trading-Bot/Announcements/SlackBot.js b/Projects/Superalgos/TS/Bot-Modules/Trading-Bot/Announcements/SlackBot.js new file mode 100644 index 000000000..497fd84a9 --- /dev/null +++ b/Projects/Superalgos/TS/Bot-Modules/Trading-Bot/Announcements/SlackBot.js @@ -0,0 +1,87 @@ +exports.newSuperalgosBotModulesSlackBot = function (processIndex) { + + const MODULE_NAME = 'Slack Bot' + + let thisObject = { + webhookURL: undefined, + taskParameters: undefined, + format: undefined, + sendMessage: sendMessage, + initialize: initialize, + finalize: finalize + } + + return thisObject + + function initialize(config) { + /* Slack Bot Initialization */ + + try { + thisObject.webhookURL = config.webhookURL + thisObject.taskParameters = { + exchange: TS.projects.superalgos.globals.taskConstants.TASK_NODE.parentNode.parentNode.parentNode.referenceParent.parentNode.parentNode.name, + market: TS.projects.superalgos.globals.taskConstants.TASK_NODE.parentNode.parentNode.parentNode.referenceParent.baseAsset.referenceParent.config.codeName + + '/' + + TS.projects.superalgos.globals.taskConstants.TASK_NODE.parentNode.parentNode.parentNode.referenceParent.quotedAsset.referenceParent.config.codeName + } + if (config.format === undefined) { + thisObject.format = "%{MESSAGE}" + } else { + thisObject.format = config.format + } + } catch (err) { + logError("initialize -> err = " + err.stack); + } + + } + + function finalize() {} + + function sendMessage(message) { + const https = require('https') + + try { + message = formatMessage(message) + message = JSON.stringify({text: message}) + } catch (err) { + logError("announce -> Slack JSON message error -> err = " + err) + } + + return new Promise((resolve, reject) => { + const requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + } + + const req = https.request(thisObject.webhookURL, requestOptions, (res) => { + let response = '' + res.on('data', (data) => { response += data }) + res.on('end', () => { resolve(response) }) + }) + + req.on('error', (err) => { + reject(err) + logWarn("announce -> Slack request error -> err = " + err) + }) + + req.write(message) + req.end() + }) + } + + function formatMessage(message) { + formattedMessage = thisObject.format + formattedMessage = formattedMessage.replace('%{EXCHANGE}', thisObject.taskParameters.exchange) + formattedMessage = formattedMessage.replace('%{MARKET}', thisObject.taskParameters.market) + formattedMessage = formattedMessage.replace('%{MESSAGE}', message) + return formattedMessage + } + + function logWarn(message) { + TS.projects.superalgos.globals.loggerVariables.VARIABLES_BY_PROCESS_INDEX_MAP.get(processIndex).BOT_MAIN_LOOP_LOGGER_MODULE_OBJECT.write(MODULE_NAME, '[WARN] ' + message) + } + + function logError(message) { + TS.projects.superalgos.globals.loggerVariables.VARIABLES_BY_PROCESS_INDEX_MAP.get(processIndex).BOT_MAIN_LOOP_LOGGER_MODULE_OBJECT.write(MODULE_NAME, '[ERROR] ' + message) + } +} diff --git a/Projects/Superalgos/TS/Bot-Modules/Trading-Bot/Announcements/SocialBots.js b/Projects/Superalgos/TS/Bot-Modules/Trading-Bot/Announcements/SocialBots.js index 4fd29c7f2..fe7736edd 100644 --- a/Projects/Superalgos/TS/Bot-Modules/Trading-Bot/Announcements/SocialBots.js +++ b/Projects/Superalgos/TS/Bot-Modules/Trading-Bot/Announcements/SocialBots.js @@ -24,6 +24,10 @@ exports.newSuperalgosBotModulesSocialBots = function (processIndex) { let config = socialBot.config socialBot.botInstance = TS.projects.superalgos.botModules.discordBot.newSuperalgosBotModulesDiscordBot(processIndex) socialBot.botInstance.initialize(config) + } else if (socialBot.type === "Slack Bot") { + let config = socialBot.config + socialBot.botInstance = TS.projects.superalgos.botModules.slackBot.newSuperalgosBotModulesSlackBot(processIndex) + socialBot.botInstance.initialize(config) } } } @@ -38,7 +42,7 @@ exports.newSuperalgosBotModulesSocialBots = function (processIndex) { try { if (socialBot.type === "Telegram Bot") { socialBot.botInstance.telegramAPI.sendMessage(socialBot.botInstance.chatId, text).catch(err => TS.projects.superalgos.globals.loggerVariables.VARIABLES_BY_PROCESS_INDEX_MAP.get(processIndex).BOT_MAIN_LOOP_LOGGER_MODULE_OBJECT.write(MODULE_NAME, "[WARN] initialize -> initializeSocialBots -> announce -> Telegram API error -> err = " + err)) - } else if (socialBot.type === "Discord Bot") { + } else if (socialBot.type === "Discord Bot" ||socialBot.type === "Slack Bot" ) { socialBot.botInstance.sendMessage(text).catch(err => TS.projects.superalgos.globals.loggerVariables.VARIABLES_BY_PROCESS_INDEX_MAP.get(processIndex).BOT_MAIN_LOOP_LOGGER_MODULE_OBJECT.write(MODULE_NAME, "[WARN] initialize -> initializeSocialBots -> announce -> Discord Bot error -> err = " + err)) } } catch (err) { @@ -61,7 +65,7 @@ exports.newSuperalgosBotModulesSocialBots = function (processIndex) { if (TS.projects.superalgos.globals.processConstants.CONSTANTS_BY_PROCESS_INDEX_MAP.get(processIndex).SESSION_NODE.socialBots[prop] !== undefined) { for (let i = 0; i < TS.projects.superalgos.globals.processConstants.CONSTANTS_BY_PROCESS_INDEX_MAP.get(processIndex).SESSION_NODE.socialBots[prop].length; i++) { let socialBot = TS.projects.superalgos.globals.processConstants.CONSTANTS_BY_PROCESS_INDEX_MAP.get(processIndex).SESSION_NODE.socialBots[prop][i] - if (socialBot.type === "Telegram Bot" || socialBot.type === "Discord Bot") { + if (socialBot.type === "Telegram Bot" || socialBot.type === "Discord Bot" || socialBot.type === "Slack Bot") { socialBot.botInstance.finalize() } } @@ -77,7 +81,7 @@ exports.newSuperalgosBotModulesSocialBots = function (processIndex) { if (TS.projects.superalgos.globals.processConstants.CONSTANTS_BY_PROCESS_INDEX_MAP.get(processIndex).SESSION_NODE.socialBots[prop] !== undefined) { for (let i = 0; i < TS.projects.superalgos.globals.processConstants.CONSTANTS_BY_PROCESS_INDEX_MAP.get(processIndex).SESSION_NODE.socialBots[prop].length; i++) { let socialBot = TS.projects.superalgos.globals.processConstants.CONSTANTS_BY_PROCESS_INDEX_MAP.get(processIndex).SESSION_NODE.socialBots[prop][i] - if (socialBot.type === "Telegram Bot" || socialBot.type === "Discord Bot") { + if (socialBot.type === "Telegram Bot" || socialBot.type === "Discord Bot" || socialBot.type === "Slack Bot") { socialBot.botInstance.sendMessage(message) } } diff --git a/Projects/Superalgos/UI/Function-Libraries/ActionSwitch.js b/Projects/Superalgos/UI/Function-Libraries/ActionSwitch.js index 3b1016f58..1ad6b714c 100644 --- a/Projects/Superalgos/UI/Function-Libraries/ActionSwitch.js +++ b/Projects/Superalgos/UI/Function-Libraries/ActionSwitch.js @@ -722,6 +722,11 @@ function newSuperalgosActionSwitch() { UI.projects.superalgos.functionLibraries.socialBotsFunctions.sendDiscordTestMessage(action.node, action.callBackFunction) } break + case 'Send Slack Test Message': + { + UI.projects.superalgos.functionLibraries.socialBotsFunctions.sendSlackTestMessage(action.node, action.callBackFunction) + } + break } } } diff --git a/Projects/Superalgos/UI/Function-Libraries/LearningSessionFunctions.js b/Projects/Superalgos/UI/Function-Libraries/LearningSessionFunctions.js index fb3835fba..6620c9b33 100644 --- a/Projects/Superalgos/UI/Function-Libraries/LearningSessionFunctions.js +++ b/Projects/Superalgos/UI/Function-Libraries/LearningSessionFunctions.js @@ -129,7 +129,7 @@ function newSuperalgosFunctionLibraryLearningSessionFunctions() { 'Back Learning Session->Live Learning Session->' + 'Learning Parameters->' + 'Learning Algorithm->Time Range->Time Frame->Heartbeats->User Defined Parameters->' + - 'Social Bots->Telegram Bot->Discord Bot->' + 'Social Bots->Telegram Bot->Discord Bot->Slack Bot->' let session = UI.projects.superalgos.functionLibraries.protocolNode.getProtocolNode(node, false, true, true, false, false, lightingPath) diff --git a/Projects/Superalgos/UI/Function-Libraries/SocialBotsFunctions.js b/Projects/Superalgos/UI/Function-Libraries/SocialBotsFunctions.js index 31a387c2b..4b20d5443 100644 --- a/Projects/Superalgos/UI/Function-Libraries/SocialBotsFunctions.js +++ b/Projects/Superalgos/UI/Function-Libraries/SocialBotsFunctions.js @@ -1,7 +1,8 @@ function newSuperalgosFunctionLibrarySocialBotsFunctions() { thisObject = { sendTelegramTestMessage: sendTelegramTestMessage, - sendDiscordTestMessage: sendDiscordTestMessage + sendDiscordTestMessage: sendDiscordTestMessage, + sendSlackTestMessage: sendSlackTestMessage } return thisObject @@ -127,4 +128,59 @@ function newSuperalgosFunctionLibrarySocialBotsFunctions() { } } } + + function sendSlackTestMessage(node, callBackFunction) { + + let webhookURL = UI.projects.superalgos.utilities.nodeConfig.loadPropertyFromNodeConfig(node.payload, 'webhookURL') + + if (webhookURL === undefined) { + node.payload.uiObject.setErrorMessage('webhookURL parameter not defined.') + callBackFunction(GLOBAL.DEFAULT_FAIL_RESPONSE) + return + } + + let message = "Test message from Superalgos!" + + httpRequestJSON(JSON.stringify({text: message}), webhookURL, onResponse) + + function onResponse(err) { + if (err.result === GLOBAL.DEFAULT_OK_RESPONSE.result) { + node.payload.uiObject.setInfoMessage('Slack message sent.') + callBackFunction(GLOBAL.DEFAULT_OK_RESPONSE) + } else { + node.payload.uiObject.setErrorMessage('Could not send Slack message. Error Response: ' + err.message) + callBackFunction(GLOBAL.DEFAULT_FAIL_RESPONSE) + } + } + + function httpRequestJSON(pContentToSend, pPath, callBackFunction) { + let xmlHttpRequest = new XMLHttpRequest(); + xmlHttpRequest.onreadystatechange = function () { + if (this.readyState === 4 && this.status === 200) { + callBackFunction(GLOBAL.DEFAULT_OK_RESPONSE, xmlHttpRequest.responseText); + return; + } else if (this.readyState === 4 && (this.status === 404 || this.status === 401 || this.status === 400)) { + callBackFunction({ + result: "Fail", + message: xmlHttpRequest.responseText + }); + return; + } + }; + + if (pContentToSend === undefined) { + xmlHttpRequest.open("GET", pPath, true); + xmlHttpRequest.send(); + } else { + try { + let blob = new Blob([pContentToSend]); + xmlHttpRequest.open("POST", pPath, true); + xmlHttpRequest.send(blob); + } catch (err) { + if (ERROR_LOG === true) { console.log(spacePad(MODULE_NAME, 50) + " : " + "[ERROR] callServer -> err.message = " & err.message); } + callBackFunction({ result: "Fail", message: err.message }) + } + } + } + } } diff --git a/Projects/Superalgos/UI/Function-Libraries/TradingSessionFunctions.js b/Projects/Superalgos/UI/Function-Libraries/TradingSessionFunctions.js index 3adc57019..a48b4b0ff 100644 --- a/Projects/Superalgos/UI/Function-Libraries/TradingSessionFunctions.js +++ b/Projects/Superalgos/UI/Function-Libraries/TradingSessionFunctions.js @@ -153,7 +153,7 @@ function newSuperalgosFunctionLibraryTradingSessionFunctions() { 'Backtesting Session->Paper Trading Session->Forward Testing Session->Live Trading Session->' + 'Trading Parameters->' + 'Session Base Asset->Session Quoted Asset->Time Range->Time Frame->Slippage->Fee Structure->Snapshots->Heartbeats->User Defined Parameters->' + - 'Social Bots->Telegram Bot->Discord Bot->' + + 'Social Bots->Telegram Bot->Discord Bot->Slack Bot->' + 'Exchange Account Asset->Asset->' let session = UI.projects.superalgos.functionLibraries.protocolNode.getProtocolNode(node, false, true, true, false, false, lightingPath)