Intial Commit

This commit is contained in:
valki
2020-10-17 18:42:50 +02:00
commit 664c6d8ca3
5892 changed files with 759183 additions and 0 deletions

View File

@@ -0,0 +1,239 @@
<script type="text/javascript">
// Configuration inspired by node-red's switch node
// https://github.com/node-red/node-red/blob/master/nodes/core/logic/10-switch.html
var timeoutIdx = 99999;
RED.nodes.registerType("telegrambot-switch", {
category: "telegram",
paletteLabel: "switch",
color: "#3babdd",
icon: "switch.png",
defaults: {
name: { value: "" },
bot: { value: "", type: "telegrambot-config", required: true },
chatId: { value: "", required: false },
question: { value: "" },
answers: { value: [] },
outputs: { value: 1 },
autoAnswerCallback: { value: true, required: false },
timeoutValue: { value: null },
timeoutUnits: { value: null }
},
inputs: 1,
outputs: 1,
label: function() {
return this.name || "telegram switch";
},
labelStyle: function() {
return this.name ? "node_label_italic" : "";
},
outputLabels: function(index) {
if (index < this.answers.length) {
var answer = this.answers[index];
return (answer.length > 15) ? `${answer.substring(0,15)}...` : answer;
} else {
return "timeout";
}
},
oneditprepare: function() {
var node = this;
var outputCount = $("#node-input-outputs").val("{}");
$("#node-input-timeoutUnits").change(function(){
if ($(this).val() == "") {
$("#node-input-timeoutValue").val("");
$("#node-input-timeoutValue").prop("disabled", true);
var currentOutputs = JSON.parse(outputCount.val() || "{}");
delete currentOutputs[timeoutIdx];
outputCount.val(JSON.stringify(currentOutputs));
} else {
$("#node-input-timeoutValue").prop("disabled", false);
var currentOutputs = JSON.parse(outputCount.val() || "{}");
currentOutputs[timeoutIdx] = timeoutIdx;
outputCount.val(JSON.stringify(currentOutputs));
}
});
$("#node-input-answer-container").css("min-height","250px").css("min-width","450px").editableList({
addItem: function(container, i, opt) {
container.css({
overflow: "hidden",
whiteSpace: "nowrap"
});
if (!opt.hasOwnProperty("answer")) {
opt.answer = "";
}
if (!opt.hasOwnProperty("idx")) {
opt._idx = Math.floor((0x99999-0x10000)*Math.random()).toString();
}
var answer = opt.answer;
var row = $("<div/>").appendTo(container);
var valueField = $("<input/>",{class:"node-input-answer-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:"str",types:["str"]});
var finalspan = $("<span/>",{style:"float: right;margin-top: 6px;"}).appendTo(row);
finalspan.append(` &#8594; <span class="node-input-answer-index">${(i+1)}</span> `);
if (typeof answer != "undefined") {
valueField.typedInput("value", answer)
}
var currentOutputs = JSON.parse(outputCount.val() || "{}");
currentOutputs[opt.hasOwnProperty("idx") ? opt.idx : opt._idx] = i;
outputCount.val(JSON.stringify(currentOutputs));
},
removeItem: function(opt){
var currentOutputs = JSON.parse(outputCount.val() || "{}");
if (opt.hasOwnProperty("idx")) {
currentOutputs[opt.idx] = -1;
} else {
delete currentOutputs[opt._idx];
}
var answers = $("#node-input-answer-container").editableList("items");
answers.each(function(i) {
$(this).find(".node-input-answer-index").html(i + 1);
var data = $(this).data("data");
currentOutputs[data.hasOwnProperty("idx") ? data.idx : data._idx] = i;
});
outputCount.val(JSON.stringify(currentOutputs));
},
sortItems: function(answers) {
var currentOutputs = JSON.parse(outputCount.val() || "{}");
var answers = $("#node-input-answer-container").editableList("items");
answers.each(function(i) {
$(this).find(".node-input-answer-index").html(i + 1);
var data = $(this).data("data");
currentOutputs[data.hasOwnProperty("idx")? data.idx : data._idx] = i;
});
outputCount.val(JSON.stringify(currentOutputs));
},
sortable: true,
removable: true
});
for (var i=0; i < this.answers.length; i++) {
var answer = this.answers[i];
$("#node-input-answer-container").editableList("addItem", { idx: i, answer: answer });
}
},
oneditsave: function() {
var answers = $("#node-input-answer-container").editableList("items");
var node = this;
node.answers = [];
answers.each(function(i){
var answerData = $(this).data("data");
var answer = $(this);
var value = answer.find(".node-input-answer-value").typedInput("value");
node.answers.push(value);
});
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-input-answer-container-row)");
var height = size.height;
for (var i = 0; i < rows.size(); i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-answer-container-row");
height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom")));
$("#node-input-answer-container").editableList("height",height);
}
});
</script>
<script type="text/x-red" data-template-name="telegrambot-switch">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-bot">Bot</label>
<input type="text" id="node-input-bot" placeholder="Bot">
</div>
<div class="form-row">
<label for="node-input-chatId">Chat ID</label>
<input type="text" id="node-input-chatId" placeholder="(e.g. 1234)">
</div>
<div class="form-row node-type-duration">
<label>Timeout</label>
<input type="text" id="node-input-timeoutValue" style="text-align:end; width:70px !important">
<select id="node-input-timeoutUnits" style="width:140px !important">
<option value="">disabled</option>
<option value="ms">milliseconds</option>
<option value="s">seconds</option>
<option value="min">minutes</option>
<option value="hr">hours</option>
</select>
</div>
<div class="form-row">
<label for="node-input-autoAnswerCallback">Auto-Answer</label>
<input type="checkbox" id="node-input-autoAnswerCallback" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label for="node-input-question">Question</label>
<input type="text" id="node-input-question" placeholder="(e.g. Should I continue?)">
</div>
<div class="form-row node-input-answer-container-row">
<input type="hidden" id="node-input-outputs"/>
<ol id="node-input-answer-container"></ol>
</div>
</script>
<script type="text/x-red" data-help-name="telegrambot-switch">
<p>Route messages based on a response from a user via Telegram</p>
<h3>Details</h3>
<p>When a message arrives, a message will be sent to the specified Telegram bot + chat ID with
the configured answers presented as quick reply options. When the user selects a reply, the
message will be forwarded to the corresponding output.</p>
<dl class="message-properties">
<dt>Name <span class="property-type">string</span></dt>
<dd>Label for this node for easy reference</dd>
<dt>Bot <span class="property-type">string</span></dt>
<dd>Telegram bot configuration for sending the message</dd>
<dt class="optional">Chat ID <span class="property-type">int</span></dt>
<dd>Optional. Telegram chat ID to send the message. Specify a value to have this node send to the same chat every time. Leave blank to allow passing in the chat ID via <code>msg.telegram.chat.id</code>.</dd>
<dt class="optional">Auto-Answer <span class="property-type">list</span></dt>
<dd>When enabled, automatically respond to the Telegram callback to resolve it as finished. Otherwise, you will have to send a payload to Telegram yourself to mark the request as complete.</dd>
<dt class="optional">Timeout <span class="property-type">int</span></dt>
<dd>If provided, the message will time out after the specific time in milliseconds and the reply options will be removed from the chat.</dd>
<dt class="optional">Question <span class="property-type">string</span></dt>
<dd>Question to ask the user. If not provided, <code>msg.payload</code> will be used.</dd>
<dt>Answers <span class="property-type">list</span></dt>
<dd>The answer choices to present the user. Each answer will correspond to its designated output.</dd>
</dl>
<h3>Outputs</h3>
<dl class="message-properties">
<dt>payload</dt>
<dd>The original message that entered the node.</dd>
<dt>telegram</dt>
<dd>Telegram metadata of the sent message.</dd>
<dt>chatId <span class="property-type">deprecated</span></dt>
<dd>The chat ID the messages are being sent/received in. Use <code>msg.telegram.chat.id</code> instead.</dd>
<dt>messageId <span class="property-type">deprecated</span></dt>
<dd>The ID of the message that was sent prompting for a response. Use <code>msg.telegram.message_id</code> instead.</dd>
</dl>
<p>In the case of a timeout, the original message is sent to the last output labeled "timeout". Please note that in the case that Node-RED restarts, the timeout will silently fail.</p>
</script>

View File

@@ -0,0 +1,181 @@
var utils = require('../../lib/utils.js');
module.exports = function(RED) {
function SwitchNode(config) {
RED.nodes.createNode(this, config);
var node = this;
// Get base configuration
this.bot = RED.nodes.getNode(config.bot);
this.chatId = parseInt(config.chatId);
this.question = config.question || "";
this.answers = config.answers || [];
this.timeoutValue = config.timeoutValue || null;
this.timeoutUnits = config.timeoutUnits || null;
this.timeoutCallback = null;
this.autoAnswerCallback = config.autoAnswerCallback;
// Initialize bot
utils.initializeBot(node);
// Verify inputs
if (isNaN(this.chatId)) {
this.chatId = null;
}
if (this.timeoutValue !== null) {
if (this.timeoutUnits === null) {
utils.updateNodeStatusFailed(node, "timeout units not provided");
return;
}
this.timeoutDuration = utils.timeUnits(parseInt(this.timeoutValue, 10), this.timeoutUnits);
if (this.timeoutDuration === NaN) {
utils.updateNodeStatusFailed(node, "timeout not parsable");
return;
}
if (this.timeoutDuration <= 0) {
utils.updateNodeStatusFailed(node, "timeout should be greater than 0");
return;
}
}
// Compute output ports
var portCount = (this.timeoutValue === null) ? this.answers.length : this.answers.length + 1;
var ports = new Array(portCount);
for (var i = 0; i < portCount; i++) {
ports[i] = null;
}
this.on("input", function(msg){
if (!utils.validateChatId(node, msg)) {
utils.updateNodeStatusFailed(node, "message has no chatID");
return;
}
var chatId = node.chatId || msg.telegram.chat.id;
var question = node.question || msg.payload;
var answers = node.answers || [];
if (question && answers.length > 0) {
var listener = function(botMsg){
var username = botMsg.from.username;
var fromChatId = botMsg.message.chat.id;
var messageId = botMsg.message.message_id;
var callbackQueryId = botMsg.id;
if (botMsg.data && fromChatId === chatId && messageId === msg.telegram.messageId) {
if (node.bot.isAuthorized(chatId, username)) {
// Remove this listener since we got our reply
node.telegramBot.removeListener("callback_query", listener);
// Clear the timeout
if (node.timeoutCallback) {
clearTimeout(node.timeoutCallback);
}
// Update node status
utils.updateNodeStatusSuccess(node);
if (node.autoAnswerCallback) {
// Answer the callback so progress can stop showing
node.telegramBot.answerCallbackQuery(callbackQueryId).then(function(sent){
// nothing to do here
});
// Remove quick reply options
node.telegramBot.editMessageReplyMarkup("{}", { chat_id: chatId, message_id: messageId }).then(function(sent){
// nothing to do here
});
}
// Augment original message with additional Telegram info
msg.telegram.callbackQueryId = callbackQueryId;
// Continue with the original message
var outPorts = ports.slice(0);
var outputPort = parseInt(botMsg.data);
if (!isNaN(outputPort) && outputPort < portCount) {
outPorts[outputPort] = msg;
node.send(outPorts);
} else {
node.warn("invalid callback data received from telegram");
}
} else {
node.warn(`received callback in ${chatId} from '${username}' was unauthorized`);
}
} else {
// This is not the listener you are looking for
}
};
var timeoutListener = function(sentMsg){
utils.updateNodeStatus(node, "yellow", "dot", "timed out waiting for reply");
// Remove this listener
node.telegramBot.removeListener("callback_query", listener);
var messageId = sentMsg.message_id;
var sentChatId = sentMsg.chat.id;
// Remove reply keyboard from message
if (messageId && sentChatId) {
node.telegramBot.editMessageReplyMarkup("{}", { chat_id: sentChatId, message_id: messageId }).then(function(sent){
// nothing to do here
});
}
// output to timeout
var outPorts = ports.slice(0);
var outputPort = portCount - 1;
outPorts[outputPort] = msg;
node.send(outPorts);
};
var chunkSize = 4000;
var answerOpts = answers.map(function(answer, idx){
return { text: answer, callback_data: idx };
});
var options = {
reply_markup: {
inline_keyboard: [answerOpts]
}
};
if (question.length > chunkSize) {
utils.updateNodeStatusFailed(node, "message larger than allowed chunk size");
} else {
node.telegramBot.sendMessage(chatId, question, options).then(function(sent){
// Store sent message so we know how to respond later
msg.telegram = sent;
msg.telegram.chatId = chatId; // deprecated
msg.telegram.messageId = sent.message_id; // deprecated
if (node.timeoutDuration > 0) {
node.timeoutCallback = setTimeout(function(){
timeoutListener(sent);
}, node.timeoutDuration);
utils.updateNodeStatusPending(node, `waiting for reply (${node.timeoutValue}${node.timeoutUnits})`);
} else {
utils.updateNodeStatusPending(node, "waiting for reply");
}
});
node.telegramBot.on("callback_query", listener);
}
}
});
this.on("close", function(){
node.telegramBot.off("message");
node.status({});
});
};
RED.nodes.registerType("telegrambot-switch", SwitchNode);
};