From e92556260f2b95022a852825f8475be369f0440a Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:31:45 +0200 Subject: [PATCH] feat(Microsoft Teams Node): New operation sendAndWait (#12964) --- .../test/v2/node/message/sendAndWait.test.ts | 2 +- .../nodes/Discord/v2/helpers/utils.ts | 6 +- .../nodes/EmailSend/v2/send.operation.ts | 6 +- .../nodes/Google/Chat/GenericFunctions.ts | 5 +- .../test/node/sendAndWait.operation.test.ts | 2 +- .../nodes/Google/Gmail/GenericFunctions.ts | 6 +- .../Microsoft/Teams/MicrosoftTeams.node.json | 6 +- .../v2/node/chatMessage/sendAndWait.test.ts | 69 +++++++++++++++++++ .../Teams/v2/MicrosoftTeamsV2.node.ts | 3 + .../Teams/v2/actions/chatMessage/index.ts | 12 +++- .../chatMessage/sendAndWait.operation.ts | 44 ++++++++++++ .../Microsoft/Teams/v2/actions/node.type.ts | 2 +- .../Microsoft/Teams/v2/actions/router.ts | 14 ++++ .../Teams/v2/actions/versionDescription.ts | 2 + .../nodes/Telegram/GenericFunctions.ts | 9 +-- packages/nodes-base/utils/utilities.ts | 6 ++ 16 files changed, 167 insertions(+), 27 deletions(-) create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/sendAndWait.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/sendAndWait.operation.ts diff --git a/packages/nodes-base/nodes/Discord/test/v2/node/message/sendAndWait.test.ts b/packages/nodes-base/nodes/Discord/test/v2/node/message/sendAndWait.test.ts index c405651e1e..1a2ef8d857 100644 --- a/packages/nodes-base/nodes/Discord/test/v2/node/message/sendAndWait.test.ts +++ b/packages/nodes-base/nodes/Discord/test/v2/node/message/sendAndWait.test.ts @@ -84,7 +84,7 @@ describe('Test DiscordV2, message => sendAndWait', () => { { color: 5814783, description: - '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)', + '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.discord_instanceId)', }, ], }, diff --git a/packages/nodes-base/nodes/Discord/v2/helpers/utils.ts b/packages/nodes-base/nodes/Discord/v2/helpers/utils.ts index 1824149348..f3f51cb778 100644 --- a/packages/nodes-base/nodes/Discord/v2/helpers/utils.ts +++ b/packages/nodes-base/nodes/Discord/v2/helpers/utils.ts @@ -11,7 +11,7 @@ import type { import { jsonParse, NodeApiError, NodeOperationError } from 'n8n-workflow'; import { getSendAndWaitConfig } from '../../../../utils/sendAndWait/utils'; -import { capitalize } from '../../../../utils/utilities'; +import { capitalize, createUtmCampaignLink } from '../../../../utils/utilities'; import { discordApiMultiPartRequest, discordApiRequest } from '../transport'; export const createSimplifyFunction = @@ -395,9 +395,7 @@ export function createSendAndWaitMessageBody(context: IExecuteFunctions) { 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 : ''}`; + const link = createUtmCampaignLink('n8n-nodes-base.discord', instanceId); const description = `${config.message}\n\n_${attributionText}_[n8n](${link})`; const body = { diff --git a/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts b/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts index 32f33566fb..ba94d270ef 100644 --- a/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts +++ b/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts @@ -7,7 +7,7 @@ import type { } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; -import { updateDisplayOptions } from '@utils/utilities'; +import { createUtmCampaignLink, updateDisplayOptions } from '@utils/utilities'; import { fromEmailProperty, toEmailProperty } from './descriptions'; import { configureTransport, type EmailSendOptions } from './utils'; @@ -218,9 +218,7 @@ export async function execute(this: IExecuteFunctions): Promise_`; const buttons: string[] = config.options.map( diff --git a/packages/nodes-base/nodes/Google/Chat/test/node/sendAndWait.operation.test.ts b/packages/nodes-base/nodes/Google/Chat/test/node/sendAndWait.operation.test.ts index b03930541b..0f18928c7c 100644 --- a/packages/nodes-base/nodes/Google/Chat/test/node/sendAndWait.operation.test.ts +++ b/packages/nodes-base/nodes/Google/Chat/test/node/sendAndWait.operation.test.ts @@ -54,7 +54,7 @@ describe('Test GoogleChat, message => sendAndWait', () => { expect(mockExecuteFunctions.putExecutionToWait).toHaveBeenCalledTimes(1); expect(genericFunctions.googleApiRequest).toHaveBeenCalledWith('POST', '/v1/spaceID/messages', { - text: 'my message\n\n\n**\n\n_This_ _message_ _was_ _sent_ _automatically_ _with_ __', + text: 'my message\n\n\n**\n\n_This_ _message_ _was_ _sent_ _automatically_ _with_ __', }); }); }); diff --git a/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts index 6993ab1930..2169876d38 100644 --- a/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts @@ -17,7 +17,7 @@ import { NodeApiError, NodeOperationError } from 'n8n-workflow'; import MailComposer from 'nodemailer/lib/mail-composer'; import type { IEmail } from '../../../utils/sendAndWait/interfaces'; -import { escapeHtml } from '../../../utils/utilities'; +import { createUtmCampaignLink, escapeHtml } from '../../../utils/utilities'; import { getGoogleAccessToken } from '../GenericFunctions'; export interface IAttachments { @@ -433,9 +433,7 @@ export function prepareEmailBody( if (appendAttribution) { const attributionText = 'This email was sent automatically with '; - const link = `https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=${encodeURIComponent( - 'n8n-nodes-base.gmail', - )}${instanceId ? '_' + instanceId : ''}`; + const link = createUtmCampaignLink('n8n-nodes-base.gmail', instanceId); if (emailType === 'html') { message = ` ${message} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.json b/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.json index 03d322e99f..4163f0118b 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.json +++ b/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.json @@ -2,7 +2,11 @@ "node": "n8n-nodes-base.microsoftTeams", "nodeVersion": "1.0", "codexVersion": "1.0", - "categories": ["Communication"], + "categories": ["Communication", "HITL"], + "subcategories": { + "HITL": ["Human in the Loop"] + }, + "alias": ["human", "form", "wait", "hitl", "approval"], "resources": { "credentialDocumentation": [ { diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/sendAndWait.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/sendAndWait.test.ts new file mode 100644 index 0000000000..2a803b11c2 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/sendAndWait.test.ts @@ -0,0 +1,69 @@ +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; +import { SEND_AND_WAIT_OPERATION, type IExecuteFunctions, type INode } from 'n8n-workflow'; + +import { versionDescription } from '../../../../v2/actions/versionDescription'; +import { MicrosoftTeamsV2 } from '../../../../v2/MicrosoftTeamsV2.node'; +import * as transport from '../../../../v2/transport'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(), + }; +}); + +describe('Test MicrosoftTeamsV2, chatMessage => sendAndWait', () => { + let microsoftTeamsV2: MicrosoftTeamsV2; + let mockExecuteFunctions: MockProxy; + + beforeEach(() => { + microsoftTeamsV2 = new MicrosoftTeamsV2(versionDescription); + mockExecuteFunctions = mock(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should send message and put execution to wait', async () => { + const items = [{ json: { data: 'test' } }]; + mockExecuteFunctions.getNodeParameter.mockImplementation((key: string) => { + if (key === 'operation') return SEND_AND_WAIT_OPERATION; + if (key === 'resource') return 'chatMessage'; + if (key === 'chatId') return 'chatID'; + if (key === 'message') return 'my message'; + if (key === 'subject') return ''; + if (key === 'approvalOptions.values') return {}; + if (key === 'responseType') return 'approval'; + if (key === 'options.limitWaitTime.values') return {}; + }); + + mockExecuteFunctions.putExecutionToWait.mockImplementation(); + mockExecuteFunctions.getInputData.mockReturnValue(items); + mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId'); + mockExecuteFunctions.getNode.mockReturnValue(mock({ typeVersion: 2 })); + + mockExecuteFunctions.evaluateExpression.mockReturnValueOnce('http://localhost/waiting-webhook'); + mockExecuteFunctions.evaluateExpression.mockReturnValueOnce('nodeID'); + + const result = await microsoftTeamsV2.execute.call(mockExecuteFunctions); + + expect(result).toEqual([items]); + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(mockExecuteFunctions.putExecutionToWait).toHaveBeenCalledTimes(1); + + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/v1.0/chats/chatID/messages', + { + body: { + content: + 'my message

Approve

This message was sent automatically with n8n', + contentType: 'html', + }, + }, + ); + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/MicrosoftTeamsV2.node.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/MicrosoftTeamsV2.node.ts index 79003ce2cb..f8a4fa6559 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/v2/MicrosoftTeamsV2.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/MicrosoftTeamsV2.node.ts @@ -8,6 +8,7 @@ import type { import { router } from './actions/router'; import { versionDescription } from './actions/versionDescription'; import { listSearch } from './methods'; +import { sendAndWaitWebhook } from '../../../../utils/sendAndWait/utils'; export class MicrosoftTeamsV2 implements INodeType { description: INodeTypeDescription; @@ -22,6 +23,8 @@ export class MicrosoftTeamsV2 implements INodeType { methods = { listSearch }; + webhook = sendAndWaitWebhook; + async execute(this: IExecuteFunctions) { return await router.call(this); } diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/index.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/index.ts index 1ae403bddc..955357ea47 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/index.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/index.ts @@ -1,10 +1,11 @@ -import type { INodeProperties } from 'n8n-workflow'; +import { SEND_AND_WAIT_OPERATION, type INodeProperties } from 'n8n-workflow'; import * as create from './create.operation'; import * as get from './get.operation'; import * as getAll from './getAll.operation'; +import * as sendAndWait from './sendAndWait.operation'; -export { create, get, getAll }; +export { create, get, getAll, sendAndWait }; export const description: INodeProperties[] = [ { @@ -36,6 +37,12 @@ export const description: INodeProperties[] = [ description: 'Get many messages from a chat', action: 'Get many chat messages', }, + { + 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', + }, ], default: 'create', }, @@ -43,4 +50,5 @@ export const description: INodeProperties[] = [ ...create.description, ...get.description, ...getAll.description, + ...sendAndWait.description, ]; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/sendAndWait.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/sendAndWait.operation.ts new file mode 100644 index 0000000000..e62309f9a8 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/sendAndWait.operation.ts @@ -0,0 +1,44 @@ +import type { INodeProperties, IExecuteFunctions } from 'n8n-workflow'; + +import { + getSendAndWaitConfig, + getSendAndWaitProperties, +} from '../../../../../../utils/sendAndWait/utils'; +import { createUtmCampaignLink } from '../../../../../../utils/utilities'; +import { chatRLC } from '../../descriptions'; +import { microsoftApiRequest } from '../../transport'; + +export const description: INodeProperties[] = getSendAndWaitProperties( + [chatRLC], + 'chatMessage', + undefined, + { + noButtonStyle: true, + defaultApproveLabel: '✓ Approve', + defaultDisapproveLabel: '✗ Decline', + }, +).filter((p) => p.name !== 'subject'); + +export async function execute(this: IExecuteFunctions, i: number, instanceId: string) { + const chatId = this.getNodeParameter('chatId', i, '', { extractValue: true }) as string; + const config = getSendAndWaitConfig(this); + + const attributionText = 'This message was sent automatically with'; + const link = createUtmCampaignLink('n8n-nodes-base.microsoftTeams', instanceId); + const attribution = `${attributionText} n8n`; + + const buttons = config.options.map( + (option) => `${option.label}`, + ); + + const content = `${config.message}

${buttons.join(' ')}

${attribution}`; + + const body = { + body: { + contentType: 'html', + content, + }, + }; + + return await microsoftApiRequest.call(this, 'POST', `/v1.0/chats/${chatId}/messages`, body); +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/node.type.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/node.type.ts index f4c7753709..42afa7a2bf 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/node.type.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/node.type.ts @@ -3,7 +3,7 @@ import type { AllEntities } from 'n8n-workflow'; type NodeMap = { channel: 'create' | 'deleteChannel' | 'get' | 'getAll' | 'update'; channelMessage: 'create' | 'getAll'; - chatMessage: 'create' | 'get' | 'getAll'; + chatMessage: 'create' | 'get' | 'getAll' | 'sendAndWait'; task: 'create' | 'deleteTask' | 'get' | 'getAll' | 'update'; }; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts index 6e4d966b3e..28c0028f3e 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts @@ -3,6 +3,7 @@ import { type IDataObject, type INodeExecutionData, NodeOperationError, + SEND_AND_WAIT_OPERATION, } from 'n8n-workflow'; import * as channel from './channel'; @@ -10,6 +11,7 @@ import * as channelMessage from './channelMessage'; import * as chatMessage from './chatMessage'; import type { MicrosoftTeamsType } from './node.type'; import * as task from './task'; +import { configureWaitTillDate } from '../../../../../utils/sendAndWait/utils'; export async function router(this: IExecuteFunctions): Promise { const items = this.getInputData(); @@ -27,6 +29,18 @@ export async function router(this: IExecuteFunctions): Promise= 1.1 && additionalFields.appendAttribution === undefined) { additionalFields.appendAttribution = true; @@ -263,9 +262,7 @@ export function createSendAndWaitMessageBody(context: IExecuteFunctions) { 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 : ''}`; + const link = createUtmCampaignLink('n8n-nodes-base.telegram', instanceId); text = `${text}\n\n_${attributionText}_[n8n](${link})`; const body = { diff --git a/packages/nodes-base/utils/utilities.ts b/packages/nodes-base/utils/utilities.ts index 74834b2aab..0ff2b4212c 100644 --- a/packages/nodes-base/utils/utilities.ts +++ b/packages/nodes-base/utils/utilities.ts @@ -461,3 +461,9 @@ export function sortItemKeysByPriorityList(data: INodeExecutionData[], priorityL return item; }); } + +export function createUtmCampaignLink(nodeType: string, instanceId?: string) { + return `https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=${encodeURIComponent( + nodeType, + )}${instanceId ? '_' + instanceId : ''}`; +}