Files
n8n-enterprise-unlocked/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeamsTrigger.node.ts

399 lines
10 KiB
TypeScript

import type {
IExecuteFunctions,
INodeType,
INodeTypeDescription,
IHookFunctions,
IWebhookFunctions,
IWebhookResponseData,
IDataObject,
ILoadOptionsFunctions,
JsonObject,
INodeExecutionData,
} from 'n8n-workflow';
import { NodeApiError, NodeConnectionTypes } from 'n8n-workflow';
import type { WebhookNotification, SubscriptionResponse } from './v2/helpers/types';
import { createSubscription, getResourcePath } from './v2/helpers/utils-trigger';
import { listSearch } from './v2/methods';
import { microsoftApiRequest, microsoftApiRequestAllItems } from './v2/transport';
export class MicrosoftTeamsTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Microsoft Teams Trigger',
name: 'microsoftTeamsTrigger',
icon: 'file:teams.svg',
group: ['trigger'],
version: 1,
description:
'Triggers workflows in n8n based on events from Microsoft Teams, such as new messages or team updates, using specified configurations.',
subtitle: 'Microsoft Teams Trigger',
defaults: {
name: 'Microsoft Teams Trigger',
},
credentials: [
{
name: 'microsoftTeamsOAuth2Api',
required: true,
},
],
inputs: [],
outputs: [NodeConnectionTypes.Main],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Trigger On',
name: 'event',
type: 'options',
default: 'newChannelMessage',
options: [
{
name: 'New Channel',
value: 'newChannel',
description: 'A new channel is created',
},
{
name: 'New Channel Message',
value: 'newChannelMessage',
description: 'A message is posted to a channel',
},
{
name: 'New Chat',
value: 'newChat',
description: 'A new chat is created',
},
{
name: 'New Chat Message',
value: 'newChatMessage',
description: 'A message is posted to a chat',
},
{
name: 'New Team Member',
value: 'newTeamMember',
description: 'A new member is added to a team',
},
],
description: 'Select the event to trigger the workflow',
},
{
displayName: 'Watch All Teams',
name: 'watchAllTeams',
type: 'boolean',
default: false,
description: 'Whether to watch for the event in all the available teams',
displayOptions: {
show: {
event: ['newChannel', 'newChannelMessage', 'newTeamMember'],
},
},
},
{
displayName: 'Team',
name: 'teamId',
type: 'resourceLocator',
default: {
mode: 'list',
value: '',
},
required: true,
description: 'Select a team from the list, enter an ID or a URL',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a team...',
typeOptions: {
searchListMethod: 'getTeams',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g., 61165b04-e4cc-4026-b43f-926b4e2a7182',
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder:
'e.g., https://teams.microsoft.com/l/team/19%3A...groupId=your-team-id&tenantId=...',
extractValue: {
type: 'regex',
regex: /groupId=([0-9a-fA-F-]{36})/,
},
},
],
displayOptions: {
show: {
event: ['newChannel', 'newChannelMessage', 'newTeamMember'],
watchAllTeams: [false],
},
},
},
{
displayName: 'Watch All Channels',
name: 'watchAllChannels',
type: 'boolean',
default: false,
description: 'Whether to watch for the event in all the available channels',
displayOptions: {
show: {
event: ['newChannelMessage'],
watchAllTeams: [false],
},
},
},
{
displayName: 'Channel',
name: 'channelId',
type: 'resourceLocator',
default: {
mode: 'list',
value: '',
},
required: true,
description: 'Select a channel from the list, enter an ID or a URL',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a channel...',
typeOptions: {
searchListMethod: 'getChannels',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g., 19:-xlxyqXNSCxpI1SDzgQ_L9ZvzSR26pgphq1BJ9y7QJE1@thread.tacv2',
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder: 'e.g., https://teams.microsoft.com/l/channel/19%3A...@thread.tacv2/...',
extractValue: {
type: 'regex',
regex: /channel\/([^\/?]+)/,
},
},
],
displayOptions: {
show: {
event: ['newChannelMessage'],
watchAllTeams: [false],
watchAllChannels: [false],
},
},
},
{
displayName: 'Watch All Chats',
name: 'watchAllChats',
type: 'boolean',
default: false,
description: 'Whether to watch for the event in all the available chats',
displayOptions: {
show: {
event: ['newChatMessage'],
},
},
},
{
displayName: 'Chat',
name: 'chatId',
type: 'resourceLocator',
default: {
mode: 'list',
value: '',
},
required: true,
description: 'Select a chat from the list, enter an ID or a URL',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a chat...',
typeOptions: {
searchListMethod: 'getChats',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: '19:7e2f1174-e8ee-4859-b8b1-a8d1cc63d276@unq.gbl.spaces',
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder: 'https://teams.microsoft.com/_#/conversations/CHAT_ID',
extractValue: {
type: 'regex',
regex: /conversations\/([^\/?]+)/i,
},
},
],
displayOptions: {
show: {
event: ['newChatMessage'],
watchAllChats: [false],
},
},
},
],
};
methods = {
listSearch,
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const event = this.getNodeParameter('event', 0) as string;
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
try {
const subscriptions = (await microsoftApiRequestAllItems.call(
this as unknown as ILoadOptionsFunctions,
'value',
'GET',
'/v1.0/subscriptions',
)) as SubscriptionResponse[];
const matchingSubscriptions = subscriptions.filter(
(subscription) => subscription.notificationUrl === webhookUrl,
);
const now = new Date();
const thresholdMs = 5 * 60 * 1000;
const validSubscriptions = matchingSubscriptions.filter((subscription) => {
const expiration = new Date(subscription.expirationDateTime);
return expiration.getTime() - now.getTime() > thresholdMs;
});
const resourcePaths = await getResourcePath.call(this, event);
const requiredResources = Array.isArray(resourcePaths) ? resourcePaths : [resourcePaths];
const subscribedResources = validSubscriptions.map((sub) => sub.resource);
const allResourcesSubscribed = requiredResources.every((resource) =>
subscribedResources.includes(resource),
);
if (allResourcesSubscribed) {
webhookData.subscriptionIds = validSubscriptions.map((sub) => sub.id);
return true;
}
return false;
} catch (error) {
return false;
}
},
async create(this: IHookFunctions): Promise<boolean> {
const event = this.getNodeParameter('event', 0) as string;
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
if (!webhookUrl?.startsWith('https://')) {
throw new NodeApiError(this.getNode(), {
message: 'Invalid Notification URL',
description: `The webhook URL "${webhookUrl}" is invalid. Microsoft Graph requires an HTTPS URL.`,
});
}
const resourcePaths = await getResourcePath.call(this, event);
const subscriptionIds: string[] = [];
if (Array.isArray(resourcePaths)) {
await Promise.all(
resourcePaths.map(async (resource) => {
const subscription = await createSubscription.call(this, webhookUrl, resource);
subscriptionIds.push(subscription.id);
return subscription;
}),
);
webhookData.subscriptionIds = subscriptionIds;
} else {
const subscription = await createSubscription.call(this, webhookUrl, resourcePaths);
webhookData.subscriptionIds = [subscription.id];
}
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const storedIds = webhookData.subscriptionIds as string[] | undefined;
if (!Array.isArray(storedIds)) {
return false;
}
try {
await Promise.all(
storedIds.map(async (subscriptionId) => {
try {
await microsoftApiRequest.call(
this as unknown as IExecuteFunctions,
'DELETE',
`/v1.0/subscriptions/${subscriptionId}`,
);
} catch (error) {
if ((error as JsonObject).httpStatusCode !== 404) {
throw error;
}
}
}),
);
delete webhookData.subscriptionIds;
return true;
} catch (error) {
return false;
}
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const req = this.getRequestObject();
const res = this.getResponseObject();
// Handle Microsoft Graph validation request
if (req.query.validationToken) {
res.status(200).send(req.query.validationToken);
return { noWebhookResponse: true };
}
const eventNotifications = req.body.value as WebhookNotification[];
const response: IWebhookResponseData = {
workflowData: eventNotifications.map((event) => [
{
json: (event.resourceData as IDataObject) ?? event,
} as INodeExecutionData,
]),
};
return response;
}
}