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
+
+
+
+
+
+
+
+
+