feat(Microsoft Teams Node): New operation sendAndWait (#12964)

This commit is contained in:
Michael Kret 2025-02-10 09:31:45 +02:00 committed by GitHub
parent d41ca832dc
commit e92556260f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 167 additions and 27 deletions

View file

@ -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)',
},
],
},

View file

@ -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 = {

View file

@ -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<INodeExecutionDa
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.emailSend',
)}${instanceId ? '_' + instanceId : ''}`;
const link = createUtmCampaignLink('n8n-nodes-base.emailSend', instanceId);
if (emailFormat === 'html' || (emailFormat === 'both' && mailOptions.html)) {
mailOptions.html = `
${mailOptions.html}

View file

@ -10,6 +10,7 @@ import type {
import { NodeApiError } from 'n8n-workflow';
import { getSendAndWaitConfig } from '../../../utils/sendAndWait/utils';
import { createUtmCampaignLink } from '../../../utils/utilities';
import { getGoogleAccessToken } from '../GenericFunctions';
async function googleServiceAccountApiRequest(
@ -163,9 +164,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.googleChat', instanceId);
const attribution = `${attributionText} _<${link}|n8n>_`;
const buttons: string[] = config.options.map(

View file

@ -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*<http://localhost/waiting-webhook/nodeID?approved=true|Approve>*\n\n_This_ _message_ _was_ _sent_ _automatically_ _with_ _<https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_instanceId|n8n>_',
text: 'my message\n\n\n*<http://localhost/waiting-webhook/nodeID?approved=true|Approve>*\n\n_This_ _message_ _was_ _sent_ _automatically_ _with_ _<https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.googleChat_instanceId|n8n>_',
});
});
});

View file

@ -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}

View file

@ -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": [
{

View file

@ -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<IExecuteFunctions>;
beforeEach(() => {
microsoftTeamsV2 = new MicrosoftTeamsV2(versionDescription);
mockExecuteFunctions = mock<IExecuteFunctions>();
});
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<INode>({ 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<br><br><a href="http://localhost/waiting-webhook/nodeID?approved=true">Approve</a><br><br><em>This message was sent automatically with <a href="https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.microsoftTeams_instanceId">n8n</a></em>',
contentType: 'html',
},
},
);
});
});

View file

@ -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);
}

View file

@ -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,
];

View file

@ -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 = `<em>${attributionText} <a href="${link}">n8n</a></em>`;
const buttons = config.options.map(
(option) => `<a href="${config.url}?approved=${option.value}">${option.label}</a>`,
);
const content = `${config.message}<br><br>${buttons.join(' ')}<br><br>${attribution}`;
const body = {
body: {
contentType: 'html',
content,
},
};
return await microsoftApiRequest.call(this, 'POST', `/v1.0/chats/${chatId}/messages`, body);
}

View file

@ -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';
};

View file

@ -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<INodeExecutionData[][]> {
const items = this.getInputData();
@ -27,6 +29,18 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
operation,
} as MicrosoftTeamsType;
if (
microsoftTeamsTypeData.resource === 'chatMessage' &&
microsoftTeamsTypeData.operation === SEND_AND_WAIT_OPERATION
) {
await chatMessage[microsoftTeamsTypeData.operation].execute.call(this, 0, instanceId);
const waitTill = configureWaitTillDate(this);
await this.putExecutionToWait(waitTill);
return [items];
}
for (let i = 0; i < items.length; i++) {
try {
switch (microsoftTeamsTypeData.resource) {

View file

@ -5,6 +5,7 @@ import * as channel from './channel';
import * as channelMessage from './channelMessage';
import * as chatMessage from './chatMessage';
import * as task from './task';
import { sendAndWaitWebhooksDescription } from '../../../../../utils/sendAndWait/descriptions';
export const versionDescription: INodeTypeDescription = {
displayName: 'Microsoft Teams',
@ -25,6 +26,7 @@ export const versionDescription: INodeTypeDescription = {
required: true,
},
],
webhooks: sendAndWaitWebhooksDescription,
properties: [
{
displayName: 'Resource',

View file

@ -11,6 +11,7 @@ import type {
import { NodeApiError } from 'n8n-workflow';
import { getSendAndWaitConfig } from '../../utils/sendAndWait/utils';
import { createUtmCampaignLink } from '../../utils/utilities';
// Interface in n8n
export interface IMarkupKeyboard {
@ -80,9 +81,7 @@ export function addAdditionalFields(
if (operation === 'sendMessage') {
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);
if (nodeVersion && nodeVersion >= 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 = {

View file

@ -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 : ''}`;
}