Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
},
],
},
Expand Down
6 changes: 2 additions & 4 deletions packages/nodes-base/nodes/Discord/v2/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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 = {
Expand Down
6 changes: 2 additions & 4 deletions packages/nodes-base/nodes/EmailSend/v2/send.operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}
Expand Down
5 changes: 2 additions & 3 deletions packages/nodes-base/nodes/Google/Chat/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>_',
});
});
});
6 changes: 2 additions & 4 deletions packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
},
},
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,6 +23,8 @@ export class MicrosoftTeamsV2 implements INodeType {

methods = { listSearch };

webhook = sendAndWaitWebhook;

async execute(this: IExecuteFunctions) {
return await router.call(this);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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[] = [
{
Expand Down Expand Up @@ -36,11 +37,18 @@ 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',
},

...create.description,
...get.description,
...getAll.description,
...sendAndWait.description,
];
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
};

Expand Down
14 changes: 14 additions & 0 deletions packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import {
type IDataObject,
type INodeExecutionData,
NodeOperationError,
SEND_AND_WAIT_OPERATION,
} from 'n8n-workflow';

import * as channel from './channel';
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();
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -25,6 +26,7 @@ export const versionDescription: INodeTypeDescription = {
required: true,
},
],
webhooks: sendAndWaitWebhooksDescription,
properties: [
{
displayName: 'Resource',
Expand Down
9 changes: 3 additions & 6 deletions packages/nodes-base/nodes/Telegram/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down
6 changes: 6 additions & 0 deletions packages/nodes-base/utils/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 : ''}`;
}
Loading