diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts index 8432544cbe..b6c117296c 100644 --- a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts @@ -13,6 +13,7 @@ import { labelFields, labelOperations } from './LabelDescription'; import { getGmailAliases, getLabels, getThreadMessages } from './loadOptions'; import { messageFields, messageOperations } from './MessageDescription'; import { threadFields, threadOperations } from './ThreadDescription'; +import { sendAndWaitWebhooks } from '../../../../utils/sendAndWait/descriptions'; import type { IEmail } from '../../../../utils/sendAndWait/interfaces'; import { configureWaitTillDate, @@ -68,26 +69,7 @@ const versionDescription: INodeTypeDescription = { }, }, ], - webhooks: [ - { - name: 'default', - httpMethod: 'GET', - responseMode: 'onReceived', - responseData: '', - path: '={{ $nodeId }}', - restartWebhook: true, - isFullPath: true, - }, - { - name: 'default', - httpMethod: 'POST', - responseMode: 'onReceived', - responseData: '', - path: '={{ $nodeId }}', - restartWebhook: true, - isFullPath: true, - }, - ], + webhooks: sendAndWaitWebhooks, properties: [ { displayName: 'Authentication', diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts index 2224ea673e..5b9596c9d1 100644 --- a/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v2/actions/node.description.ts @@ -9,6 +9,7 @@ import * as folder from './folder'; import * as folderMessage from './folderMessage'; import * as message from './message'; import * as messageAttachment from './messageAttachment'; +import { sendAndWaitWebhooks } from '../../../../../utils/sendAndWait/descriptions'; export const description: INodeTypeDescription = { displayName: 'Microsoft Outlook', @@ -30,26 +31,7 @@ export const description: INodeTypeDescription = { required: true, }, ], - webhooks: [ - { - name: 'default', - httpMethod: 'GET', - responseMode: 'onReceived', - responseData: '', - path: '={{ $nodeId }}', - restartWebhook: true, - isFullPath: true, - }, - { - name: 'default', - httpMethod: 'POST', - responseMode: 'onReceived', - responseData: '', - path: '={{ $nodeId }}', - restartWebhook: true, - isFullPath: true, - }, - ], + webhooks: sendAndWaitWebhooks, properties: [ { displayName: 'Resource', diff --git a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts index 7b1484487f..1ff61e3372 100644 --- a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts +++ b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts @@ -41,6 +41,7 @@ import { reactionFields, reactionOperations } from './ReactionDescription'; import { starFields, starOperations } from './StarDescription'; import { userFields, userOperations } from './UserDescription'; import { userGroupFields, userGroupOperations } from './UserGroupDescription'; +import { sendAndWaitWebhooks } from '../../../utils/sendAndWait/descriptions'; import { configureWaitTillDate, getSendAndWaitProperties, @@ -80,26 +81,7 @@ export class SlackV2 implements INodeType { }, }, ], - webhooks: [ - { - name: 'default', - httpMethod: 'GET', - responseMode: 'onReceived', - responseData: '', - path: '={{ $nodeId }}', - restartWebhook: true, - isFullPath: true, - }, - { - name: 'default', - httpMethod: 'POST', - responseMode: 'onReceived', - responseData: '', - path: '={{ $nodeId }}', - restartWebhook: true, - isFullPath: true, - }, - ], + webhooks: sendAndWaitWebhooks, properties: [ { displayName: 'Authentication', diff --git a/packages/nodes-base/nodes/Telegram/GenericFunctions.ts b/packages/nodes-base/nodes/Telegram/GenericFunctions.ts index 309b23c176..2a65a4acce 100644 --- a/packages/nodes-base/nodes/Telegram/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Telegram/GenericFunctions.ts @@ -10,6 +10,8 @@ import type { } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; +import { getSendAndWaitConfig } from '../../utils/sendAndWait/utils'; + // Interface in n8n export interface IMarkupKeyboard { rows?: IMarkupKeyboardRow[]; @@ -252,3 +254,36 @@ export function getSecretToken(this: IHookFunctions | IWebhookFunctions) { const secret_token = `${this.getWorkflow().id}_${this.getNode().id}`; return secret_token.replace(/[^a-zA-Z0-9\_\-]+/g, ''); } + +export function createSendAndWaitMessageBody(context: IExecuteFunctions) { + const chat_id = context.getNodeParameter('chatId', 0) as string; + + const config = getSendAndWaitConfig(context); + let text = config.message; + + const instanceId = context.getInstanceId(); + const attributionText = 'This message was sent automatically with '; + const link = `https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=${encodeURIComponent( + 'n8n-nodes-base.telegram', + )}${instanceId ? '_' + instanceId : ''}`; + text = `${text}\n\n_${attributionText}_[n8n](${link})`; + + const body = { + chat_id, + text, + disable_web_page_preview: true, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + config.options.map((option) => { + return { + text: option.label, + url: `${config.url}?approved=${option.value}`, + }; + }), + ], + }, + }; + + return body; +} diff --git a/packages/nodes-base/nodes/Telegram/Telegram.node.ts b/packages/nodes-base/nodes/Telegram/Telegram.node.ts index 7d1d37b09f..a51a5f3f3b 100644 --- a/packages/nodes-base/nodes/Telegram/Telegram.node.ts +++ b/packages/nodes-base/nodes/Telegram/Telegram.node.ts @@ -6,11 +6,27 @@ import type { INodeTypeDescription, IHttpRequestMethods, } from 'n8n-workflow'; -import { BINARY_ENCODING, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; +import { + BINARY_ENCODING, + SEND_AND_WAIT_OPERATION, + NodeConnectionType, + NodeOperationError, +} from 'n8n-workflow'; import type { Readable } from 'stream'; -import { addAdditionalFields, apiRequest, getPropertyName } from './GenericFunctions'; +import { + addAdditionalFields, + apiRequest, + createSendAndWaitMessageBody, + getPropertyName, +} from './GenericFunctions'; import { appendAttributionOption } from '../../utils/descriptions'; +import { sendAndWaitWebhooks } from '../../utils/sendAndWait/descriptions'; +import { + configureWaitTillDate, + getSendAndWaitProperties, + sendAndWaitWebhook, +} from '../../utils/sendAndWait/utils'; export class Telegram implements INodeType { description: INodeTypeDescription = { @@ -33,6 +49,7 @@ export class Telegram implements INodeType { required: true, }, ], + webhooks: sendAndWaitWebhooks, properties: [ { displayName: 'Resource', @@ -263,6 +280,12 @@ export class Telegram implements INodeType { description: 'Send a text message', action: 'Send a text message', }, + { + name: 'Send and Wait for Response', + value: SEND_AND_WAIT_OPERATION, + description: 'Send a message and wait for response', + action: 'Send message and wait for response', + }, { name: 'Send Photo', value: 'sendPhoto', @@ -1735,9 +1758,31 @@ export class Telegram implements INodeType { }, ], }, + ...getSendAndWaitProperties( + [ + { + displayName: 'Chat ID', + name: 'chatId', + type: 'string', + default: '', + required: true, + description: + 'Unique identifier for the target chat or username of the target channel (in the format @channelusername). To find your chat ID ask @get_id_bot.', + }, + ], + 'message', + undefined, + { + noButtonStyle: true, + defaultApproveLabel: '✅ Approve', + defaultDisapproveLabel: '❌ Decline', + }, + ).filter((p) => p.name !== 'subject'), ], }; + webhook = sendAndWaitWebhook; + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: INodeExecutionData[] = []; @@ -1757,6 +1802,17 @@ export class Telegram implements INodeType { const nodeVersion = this.getNode().typeVersion; const instanceId = this.getInstanceId(); + if (resource === 'message' && operation === SEND_AND_WAIT_OPERATION) { + body = createSendAndWaitMessageBody(this); + + await apiRequest.call(this, 'POST', 'sendMessage', body); + + const waitTill = configureWaitTillDate(this); + + await this.putExecutionToWait(waitTill); + return [this.getInputData()]; + } + for (let i = 0; i < items.length; i++) { try { // Reset all values diff --git a/packages/nodes-base/nodes/Telegram/tests/node/sendAndWait.operation.test.ts b/packages/nodes-base/nodes/Telegram/tests/node/sendAndWait.operation.test.ts new file mode 100644 index 0000000000..8acd58eed5 --- /dev/null +++ b/packages/nodes-base/nodes/Telegram/tests/node/sendAndWait.operation.test.ts @@ -0,0 +1,71 @@ +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; +import { type INode, SEND_AND_WAIT_OPERATION, type IExecuteFunctions } from 'n8n-workflow'; + +import * as genericFunctions from '../../GenericFunctions'; +import { Telegram } from '../../Telegram.node'; + +jest.mock('../../GenericFunctions', () => { + const originalModule = jest.requireActual('../../GenericFunctions'); + return { + ...originalModule, + apiRequest: jest.fn(), + }; +}); + +describe('Test Telegram, message => sendAndWait', () => { + let telegram: Telegram; + let mockExecuteFunctions: MockProxy; + + beforeEach(() => { + telegram = new Telegram(); + mockExecuteFunctions = mock(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should send message and put execution to wait', async () => { + const items = [{ json: { data: 'test' } }]; + //node + mockExecuteFunctions.getInputData.mockReturnValue(items); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message'); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(false); + mockExecuteFunctions.getNode.mockReturnValue(mock()); + mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId'); + + //createSendAndWaitMessageBody + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('chatID'); + + //getSendAndWaitConfig + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message'); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject'); + mockExecuteFunctions.evaluateExpression.mockReturnValueOnce('http://localhost/waiting-webhook'); + mockExecuteFunctions.evaluateExpression.mockReturnValueOnce('nodeID'); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval'); + + // configureWaitTillDate + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); //options.limitWaitTime.values + + const result = await telegram.execute.call(mockExecuteFunctions); + + expect(result).toEqual([items]); + expect(genericFunctions.apiRequest).toHaveBeenCalledTimes(1); + expect(mockExecuteFunctions.putExecutionToWait).toHaveBeenCalledTimes(1); + + expect(genericFunctions.apiRequest).toHaveBeenCalledWith('POST', 'sendMessage', { + chat_id: 'chatID', + disable_web_page_preview: true, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: 'Approve', url: 'http://localhost/waiting-webhook/nodeID?approved=true' }], + ], + }, + text: 'my message\n\n_This message was sent automatically with _[n8n](https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_instanceId)', + }); + }); +}); diff --git a/packages/nodes-base/utils/sendAndWait/descriptions.ts b/packages/nodes-base/utils/sendAndWait/descriptions.ts new file mode 100644 index 0000000000..3ec38e6ddd --- /dev/null +++ b/packages/nodes-base/utils/sendAndWait/descriptions.ts @@ -0,0 +1,22 @@ +import type { IWebhookDescription } from 'n8n-workflow'; + +export const sendAndWaitWebhooks: IWebhookDescription[] = [ + { + name: 'default', + httpMethod: 'GET', + responseMode: 'onReceived', + responseData: '', + path: '={{ $nodeId }}', + restartWebhook: true, + isFullPath: true, + }, + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + responseData: '', + path: '={{ $nodeId }}', + restartWebhook: true, + isFullPath: true, + }, +]; diff --git a/packages/nodes-base/utils/sendAndWait/utils.ts b/packages/nodes-base/utils/sendAndWait/utils.ts index 2135f92a84..218ca3c216 100644 --- a/packages/nodes-base/utils/sendAndWait/utils.ts +++ b/packages/nodes-base/utils/sendAndWait/utils.ts @@ -137,6 +137,11 @@ export function getSendAndWaitProperties( targetProperties: INodeProperties[], resource: string = 'message', additionalProperties: INodeProperties[] = [], + options?: { + noButtonStyle?: boolean; + defaultApproveLabel?: string; + defaultDisapproveLabel?: string; + }, ) { const buttonStyle: INodeProperties = { displayName: 'Button Style', @@ -154,6 +159,77 @@ export function getSendAndWaitProperties( }, ], }; + const approvalOptionsValues = [ + { + displayName: 'Type of Approval', + name: 'approvalType', + type: 'options', + placeholder: 'Add option', + default: 'single', + options: [ + { + name: 'Approve Only', + value: 'single', + }, + { + name: 'Approve and Disapprove', + value: 'double', + }, + ], + }, + { + displayName: 'Approve Button Label', + name: 'approveLabel', + type: 'string', + default: options?.defaultApproveLabel || 'Approve', + displayOptions: { + show: { + approvalType: ['single', 'double'], + }, + }, + }, + ...[ + options?.noButtonStyle + ? ({} as INodeProperties) + : { + ...buttonStyle, + displayName: 'Approve Button Style', + name: 'buttonApprovalStyle', + displayOptions: { + show: { + approvalType: ['single', 'double'], + }, + }, + }, + ], + { + displayName: 'Disapprove Button Label', + name: 'disapproveLabel', + type: 'string', + default: options?.defaultDisapproveLabel || 'Decline', + displayOptions: { + show: { + approvalType: ['double'], + }, + }, + }, + ...[ + options?.noButtonStyle + ? ({} as INodeProperties) + : { + ...buttonStyle, + displayName: 'Disapprove Button Style', + name: 'buttonDisapprovalStyle', + default: 'secondary', + displayOptions: { + show: { + approvalType: ['double'], + }, + }, + }, + ], + ].filter((p) => Object.keys(p).length) as INodeProperties[]; + const sendAndWait: INodeProperties[] = [ ...targetProperties, { @@ -216,68 +292,7 @@ export function getSendAndWaitProperties( { displayName: 'Values', name: 'values', - values: [ - { - displayName: 'Type of Approval', - name: 'approvalType', - type: 'options', - placeholder: 'Add option', - default: 'single', - options: [ - { - name: 'Approve Only', - value: 'single', - }, - { - name: 'Approve and Disapprove', - value: 'double', - }, - ], - }, - { - displayName: 'Approve Button Label', - name: 'approveLabel', - type: 'string', - default: 'Approve', - displayOptions: { - show: { - approvalType: ['single', 'double'], - }, - }, - }, - { - ...buttonStyle, - displayName: 'Approve Button Style', - name: 'buttonApprovalStyle', - displayOptions: { - show: { - approvalType: ['single', 'double'], - }, - }, - }, - { - displayName: 'Disapprove Button Label', - name: 'disapproveLabel', - type: 'string', - default: 'Decline', - displayOptions: { - show: { - approvalType: ['double'], - }, - }, - }, - { - ...buttonStyle, - displayName: 'Disapprove Button Style', - name: 'buttonDisapprovalStyle', - default: 'secondary', - displayOptions: { - show: { - approvalType: ['double'], - }, - }, - }, - ], + values: approvalOptionsValues, }, ], displayOptions: {