diff --git a/src/botPage/bot/Interface/ToolsInterface.js b/src/botPage/bot/Interface/ToolsInterface.js index 2067f0889b..5a863bf733 100644 --- a/src/botPage/bot/Interface/ToolsInterface.js +++ b/src/botPage/bot/Interface/ToolsInterface.js @@ -1,11 +1,12 @@ import CandleInterface from './CandleInterface'; import MiscInterface from './MiscInterface'; import IndicatorsInterface from './IndicatorsInterface'; +import WebhookInterface from './WebhookInterface'; import { translate } from '../../../common/i18n'; // prettier-ignore export default Interface => class extends IndicatorsInterface( - MiscInterface(CandleInterface(Interface))) { + MiscInterface(CandleInterface(WebhookInterface(Interface)))) { getToolsInterface() { return { getTime : () => parseInt(new Date().getTime() / 1000), @@ -76,6 +77,7 @@ export default Interface => class extends IndicatorsInterface( ...this.getCandleInterface(), ...this.getMiscInterface(), ...this.getIndicatorsInterface(), + ...this.getWebhookInterface(), }; } }; diff --git a/src/botPage/bot/Interface/WebhookInterface.js b/src/botPage/bot/Interface/WebhookInterface.js new file mode 100644 index 0000000000..b7002445f7 --- /dev/null +++ b/src/botPage/bot/Interface/WebhookInterface.js @@ -0,0 +1,33 @@ +import { notify } from '../broadcast'; +import { translate } from '../../../common/i18n'; + +export default Interface => + class extends Interface { + // eslint-disable-next-line class-methods-use-this + sendWebhook(url, payload) { + const onError = () => notify('warn', translate('Unable to send webhook')); + const fetchOption = { + method : 'POST', + mode : 'cors', + headers: { 'Content-Type': 'application/json' }, + }; + + if (payload) { + fetchOption.body = JSON.stringify(payload); + } + + fetch(url, fetchOption) + .then(response => { + if (!response.ok) { + onError(); + } + }) + .catch(onError); + } + + getWebhookInterface() { + return { + sendWebhook: this.sendWebhook, + }; + } + }; diff --git a/src/botPage/view/blockly/blocks/tools/index.js b/src/botPage/view/blockly/blocks/tools/index.js index 699e51210a..7f13bb1768 100644 --- a/src/botPage/view/blockly/blocks/tools/index.js +++ b/src/botPage/view/blockly/blocks/tools/index.js @@ -7,3 +7,5 @@ import './block_holder'; import './loader'; import './candle'; import './time'; +import './webhook'; +import './key_value_pair'; diff --git a/src/botPage/view/blockly/blocks/tools/key_value_pair.js b/src/botPage/view/blockly/blocks/tools/key_value_pair.js new file mode 100644 index 0000000000..d7b5cdb484 --- /dev/null +++ b/src/botPage/view/blockly/blocks/tools/key_value_pair.js @@ -0,0 +1,34 @@ +import { translate } from '../../../../../common/i18n'; + +Blockly.Blocks.key_value_pair = { + init() { + this.jsonInit({ + message0: translate('Key: %1 Value: %2'), + args0 : [ + { + type: 'field_input', + name: 'KEY', + text: 'default', + }, + { + type: 'input_value', + name: 'VALUE', + }, + ], + colour : '#dedede', + output : null, + tooltip: translate('Returns a string representation of a key value pair'), + }); + }, +}; + +Blockly.JavaScript.key_value_pair = block => { + const key = block.getFieldValue('KEY') || ''; + const value = Blockly.JavaScript.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_ATOMIC) || null; + + if (!key) { + return ''; + } + + return [`{"${key}":${value}}`, Blockly.JavaScript.ORDER_ATOMIC]; +}; diff --git a/src/botPage/view/blockly/blocks/tools/webhook.js b/src/botPage/view/blockly/blocks/tools/webhook.js new file mode 100644 index 0000000000..f50fd2da72 --- /dev/null +++ b/src/botPage/view/blockly/blocks/tools/webhook.js @@ -0,0 +1,151 @@ +/* eslint-disable no-underscore-dangle */ +import { translate } from '../../../../../common/i18n'; +import { expectValue } from '../shared'; + +Blockly.Blocks.webhook = { + init() { + this.jsonInit({ + message0: translate('Webhook URL: %1'), + args0 : [ + { + type: 'input_value', + name: 'WEBHOOK_URL', + }, + ], + colour : '#dedede', + previousStatement: null, + nextStatement : null, + tooltip : translate('Sends a POST request to a URL'), + }); + + this.itemCount_ = 1; + this.updateShape_(false); + this.setMutator(new Blockly.Mutator(['lists_create_with_item'])); + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom() { + const container = document.createElement('mutation'); + container.setAttribute('items', this.itemCount_); + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation(xmlElement) { + this.itemCount_ = parseInt(xmlElement.getAttribute('items')); + this.updateShape_(false); + }, + /** + * Populate the mutator's dialog with this block's components. + * @param {!Blockly.Workspace} workspace Mutator's workspace. + * @return {!Blockly.Block} Root block in mutator. + * @this Blockly.Block + */ + decompose(workspace) { + const containerBlock = workspace.newBlock('lists_create_with_container'); + containerBlock.initSvg(); + + let { connection } = containerBlock.getInput('STACK'); + for (let i = 0; i < this.itemCount_; i++) { + const itemBlock = workspace.newBlock('lists_create_with_item'); + itemBlock.initSvg(); + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + compose(containerBlock) { + let itemBlock = containerBlock.getInputTargetBlock('STACK'); + // Count number of inputs. + const connections = []; + while (itemBlock) { + connections.push(itemBlock.valueConnection_); + itemBlock = itemBlock.nextConnection && itemBlock.nextConnection.targetBlock(); + } + this.itemCount_ = connections.length; + this.updateShape_(true); + }, + /** + * Modify this block to have the correct number of inputs. + * @private + * @this Blockly.Block + */ + updateShape_(attachInput) { + if (this.itemCount_ && this.getInput('EMPTY')) { + this.removeInput('EMPTY'); + } else if (!this.itemCount_ && !this.getInput('EMPTY')) { + this.appendDummyInput('EMPTY').appendField(translate('Empty payload')); + } + let i; + for (i = 0; i < this.itemCount_; i++) { + if (!this.getInput(`ADD${i}`)) { + const input = this.appendValueInput(`ADD${i}`); + + if (i === 0) { + input.appendField(translate('Payload:')); + } + + if (!attachInput) { + return; + } + const { connection } = input; + const keypair = this.workspace.newBlock('key_value_pair', `keyvalue${i}`); + keypair.initSvg(); + keypair.render(); + keypair.outputConnection.connect(connection); + } + } + // Remove deleted inputs. + while (this.getInput(`ADD${i}`)) { + this.removeInput(`ADD${i}`); + i++; + } + }, + onchange: function onchange(ev) { + if (!this.workspace || this.isInFlyout || this.workspace.isDragging()) { + return; + } + + if (ev.type === Blockly.Events.MOVE) { + for (let i = 0; i < this.itemCount_; i++) { + const currentBlock = this.getInputTargetBlock(`ADD${i}`); + if (currentBlock && currentBlock.type !== 'key_value_pair') { + currentBlock.unplug(true); + } + } + } + }, +}; + +Blockly.JavaScript.webhook = block => { + const url = expectValue(block, 'WEBHOOK_URL'); + + if (!block.itemCount_) { + return `Bot.sendWebhook(${url}, null);\n`; + } + + const keypairs = new Array(block.itemCount_); + for (let i = 0; i < block.itemCount_; i++) { + keypairs[i] = Blockly.JavaScript.valueToCode(block, `ADD${i}`, Blockly.JavaScript.ORDER_ATOMIC) || null; + } + + const params = keypairs + .filter(item => item !== null) + .map(item => { + const regExp = /^{(.*?)}$/; + return item && item.match(regExp)[1]; + }); + + return `Bot.sendWebhook(${url}, {${params}});\n`; +}; diff --git a/static/xml/toolbox.xml b/static/xml/toolbox.xml index faa4aea50d..2dbce11e0b 100644 --- a/static/xml/toolbox.xml +++ b/static/xml/toolbox.xml @@ -469,6 +469,19 @@ + + + + https://example.com + + + + + + + + +