From 2c146cca62ec605f6d722fe6c4b90c7df9cf77f7 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Mon, 22 Jan 2024 18:35:09 +0200 Subject: [PATCH] feat(Microsoft Teams Node): Overhaul (#7477) Co-authored-by: Giulio Andreini --- .../Microsoft/Teams/MicrosoftTeams.node.ts | 692 +----------------- .../Teams/test/v2/node/channel/create.test.ts | 71 ++ .../test/v2/node/channel/create.workflow.json | 107 +++ .../v2/node/channel/deleteChannel.test.ts | 52 ++ .../node/channel/deleteChannel.workflow.json | 102 +++ .../Teams/test/v2/node/channel/get.test.ts | 65 ++ .../test/v2/node/channel/get.workflow.json | 111 +++ .../Teams/test/v2/node/channel/getAll.test.ts | 91 +++ .../test/v2/node/channel/getAll.workflow.json | 129 ++++ .../Teams/test/v2/node/channel/update.test.ts | 53 ++ .../test/v2/node/channel/update.workflow.json | 106 +++ .../v2/node/channelMessage/create.test.ts | 95 +++ .../node/channelMessage/create.workflow.json | 145 ++++ .../v2/node/channelMessage/getAll.test.ts | 107 +++ .../node/channelMessage/getAll.workflow.json | 152 ++++ .../test/v2/node/chatMessage/create.test.ts | 91 +++ .../v2/node/chatMessage/create.workflow.json | 134 ++++ .../test/v2/node/chatMessage/get.test.ts | 91 +++ .../v2/node/chatMessage/get.workflow.json | 133 ++++ .../test/v2/node/chatMessage/getAll.test.ts | 131 ++++ .../v2/node/chatMessage/getAll.workflow.json | 171 +++++ .../Teams/test/v2/node/task/create.test.ts | 111 +++ .../test/v2/node/task/create.workflow.json | 167 +++++ .../test/v2/node/task/deleteTask.test.ts | 65 ++ .../v2/node/task/deleteTask.workflow.json | 91 +++ .../Teams/test/v2/node/task/get.test.ts | 102 +++ .../Teams/test/v2/node/task/get.workflow.json | 139 ++++ .../Teams/test/v2/node/task/getAll.test.ts | 201 +++++ .../test/v2/node/task/getAll.workflow.json | 232 ++++++ .../Teams/test/v2/node/task/update.test.ts | 65 ++ .../test/v2/node/task/update.workflow.json | 96 +++ .../Microsoft/Teams/test/v2/utils.test.ts | 25 + .../Teams/{ => v1}/ChannelDescription.ts | 0 .../{ => v1}/ChannelMessageDescription.ts | 0 .../Teams/{ => v1}/ChatMessageDescription.ts | 0 .../Teams/{ => v1}/GenericFunctions.ts | 0 .../Teams/v1/MicrosoftTeamsV1.node.ts | 683 +++++++++++++++++ .../Teams/{ => v1}/TaskDescription.ts | 0 .../Teams/v2/MicrosoftTeamsV2.node.ts | 28 + .../v2/actions/channel/create.operation.ts | 81 ++ .../channel/deleteChannel.operation.ts | 35 + .../Teams/v2/actions/channel/get.operation.ts | 38 + .../v2/actions/channel/getAll.operation.ts | 41 ++ .../Teams/v2/actions/channel/index.ts | 62 ++ .../v2/actions/channel/update.operation.ts | 69 ++ .../channelMessage/create.operation.ts | 120 +++ .../channelMessage/getAll.operation.ts | 43 ++ .../Teams/v2/actions/channelMessage/index.ts | 38 + .../actions/chatMessage/create.operation.ts | 91 +++ .../v2/actions/chatMessage/get.operation.ts | 49 ++ .../actions/chatMessage/getAll.operation.ts | 42 ++ .../Teams/v2/actions/chatMessage/index.ts | 46 ++ .../Microsoft/Teams/v2/actions/node.type.ts | 10 + .../Microsoft/Teams/v2/actions/router.ts | 82 +++ .../Teams/v2/actions/task/create.operation.ts | 109 +++ .../v2/actions/task/deleteTask.operation.ts | 41 ++ .../Teams/v2/actions/task/get.operation.ts | 31 + .../Teams/v2/actions/task/getAll.operation.ts | 97 +++ .../Microsoft/Teams/v2/actions/task/index.ts | 62 ++ .../Teams/v2/actions/task/update.operation.ts | 155 ++++ .../Teams/v2/actions/versionDescription.ts | 60 ++ .../v2/descriptions/common.description.ts | 22 + .../Microsoft/Teams/v2/descriptions/index.ts | 2 + .../Teams/v2/descriptions/rlc.description.ts | 269 +++++++ .../nodes/Microsoft/Teams/v2/helpers/utils.ts | 44 ++ .../nodes/Microsoft/Teams/v2/methods/index.ts | 1 + .../Microsoft/Teams/v2/methods/listSearch.ts | 279 +++++++ .../Microsoft/Teams/v2/transport/index.ts | 103 +++ 68 files changed, 6288 insertions(+), 668 deletions(-) create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/create.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/create.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/deleteChannel.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/deleteChannel.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/get.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/get.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/getAll.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/getAll.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/update.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/update.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/create.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/create.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/create.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/create.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/get.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/get.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/create.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/create.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/deleteTask.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/deleteTask.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/get.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/get.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/getAll.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/getAll.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/update.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/update.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/utils.test.ts rename packages/nodes-base/nodes/Microsoft/Teams/{ => v1}/ChannelDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/Teams/{ => v1}/ChannelMessageDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/Teams/{ => v1}/ChatMessageDescription.ts (100%) rename packages/nodes-base/nodes/Microsoft/Teams/{ => v1}/GenericFunctions.ts (100%) create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v1/MicrosoftTeamsV1.node.ts rename packages/nodes-base/nodes/Microsoft/Teams/{ => v1}/TaskDescription.ts (100%) create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/MicrosoftTeamsV2.node.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/create.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/deleteChannel.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/get.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/update.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/create.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/create.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/get.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/node.type.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/create.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/deleteTask.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/get.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/update.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/versionDescription.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/common.description.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/rlc.description.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/helpers/utils.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/methods/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/methods/listSearch.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/transport/index.ts diff --git a/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts b/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts index 04a1833a618d3..5912d7a0c8248 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts @@ -1,670 +1,26 @@ -import type { - IExecuteFunctions, - IDataObject, - ILoadOptionsFunctions, - INodeExecutionData, - INodePropertyOptions, - INodeType, - INodeTypeDescription, -} from 'n8n-workflow'; - -import { - microsoftApiRequest, - microsoftApiRequestAllItems, - prepareMessage, -} from './GenericFunctions'; - -import { channelFields, channelOperations } from './ChannelDescription'; - -import { channelMessageFields, channelMessageOperations } from './ChannelMessageDescription'; - -import { chatMessageFields, chatMessageOperations } from './ChatMessageDescription'; - -import { taskFields, taskOperations } from './TaskDescription'; - -export class MicrosoftTeams implements INodeType { - description: INodeTypeDescription = { - displayName: 'Microsoft Teams', - name: 'microsoftTeams', - icon: 'file:teams.svg', - group: ['input'], - version: [1, 1.1], - subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Microsoft Teams API', - defaults: { - name: 'Microsoft Teams', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'microsoftTeamsOAuth2Api', - required: true, - }, - ], - properties: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Channel', - value: 'channel', - }, - { - name: 'Channel Message (Beta)', - value: 'channelMessage', - }, - { - name: 'Chat Message', - value: 'chatMessage', - }, - { - name: 'Task', - value: 'task', - }, - ], - default: 'channel', - }, - // CHANNEL - ...channelOperations, - ...channelFields, - /// MESSAGE - ...channelMessageOperations, - ...channelMessageFields, - ...chatMessageOperations, - ...chatMessageFields, - ///TASK - ...taskOperations, - ...taskFields, - ], - }; - - methods = { - loadOptions: { - // Get all the team's channels to display them to user so that they can - // select them easily - async getChannels(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const teamId = this.getCurrentNodeParameter('teamId') as string; - const { value } = await microsoftApiRequest.call( - this, - 'GET', - `/v1.0/teams/${teamId}/channels`, - ); - for (const channel of value) { - const channelName = channel.displayName; - const channelId = channel.id; - returnData.push({ - name: channelName, - value: channelId, - }); - } - return returnData; - }, - // Get all the teams to display them to user so that they can - // select them easily - async getTeams(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const { value } = await microsoftApiRequest.call(this, 'GET', '/v1.0/me/joinedTeams'); - for (const team of value) { - const teamName = team.displayName; - const teamId = team.id; - returnData.push({ - name: teamName, - value: teamId, - }); - } - return returnData; - }, - // Get all the groups to display them to user so that they can - // select them easily - async getGroups(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const groupSource = this.getCurrentNodeParameter('groupSource') as string; - let requestUrl = '/v1.0/groups' as string; - if (groupSource === 'mine') { - requestUrl = '/v1.0/me/transitiveMemberOf'; - } - const { value } = await microsoftApiRequest.call(this, 'GET', requestUrl); - for (const group of value) { - returnData.push({ - name: group.displayName || group.mail || group.id, - value: group.id, - description: group.mail, - }); - } - return returnData; - }, - // Get all the plans to display them to user so that they can - // select them easily - async getPlans(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - let groupId = this.getCurrentNodeParameter('groupId') as string; - const operation = this.getNodeParameter('operation', 0); - if (operation === 'update' && (groupId === undefined || groupId === null)) { - // groupId not found at base, check updateFields for the groupId - groupId = this.getCurrentNodeParameter('updateFields.groupId') as string; - } - const { value } = await microsoftApiRequest.call( - this, - 'GET', - `/v1.0/groups/${groupId}/planner/plans`, - ); - for (const plan of value) { - returnData.push({ - name: plan.title, - value: plan.id, - }); - } - return returnData; - }, - // Get all the plans to display them to user so that they can - // select them easily - async getBuckets(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - let planId = this.getCurrentNodeParameter('planId') as string; - const operation = this.getNodeParameter('operation', 0); - if (operation === 'update' && (planId === undefined || planId === null)) { - // planId not found at base, check updateFields for the planId - planId = this.getCurrentNodeParameter('updateFields.planId') as string; - } - const { value } = await microsoftApiRequest.call( - this, - 'GET', - `/v1.0/planner/plans/${planId}/buckets`, - ); - for (const bucket of value) { - returnData.push({ - name: bucket.name, - value: bucket.id, - }); - } - return returnData; - }, - // Get all the plans to display them to user so that they can - // select them easily - async getMembers(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - let groupId = this.getCurrentNodeParameter('groupId') as string; - const operation = this.getNodeParameter('operation', 0); - if (operation === 'update' && (groupId === undefined || groupId === null)) { - // groupId not found at base, check updateFields for the groupId - groupId = this.getCurrentNodeParameter('updateFields.groupId') as string; - } - const { value } = await microsoftApiRequest.call( - this, - 'GET', - `/v1.0/groups/${groupId}/members`, - ); - for (const member of value) { - returnData.push({ - name: member.displayName, - value: member.id, - }); - } - return returnData; - }, - // Get all the labels to display them to user so that they can - // select them easily - async getLabels(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - - let planId = this.getCurrentNodeParameter('planId') as string; - const operation = this.getNodeParameter('operation', 0); - if (operation === 'update' && (planId === undefined || planId === null)) { - // planId not found at base, check updateFields for the planId - planId = this.getCurrentNodeParameter('updateFields.planId') as string; - } - const { categoryDescriptions } = await microsoftApiRequest.call( - this, - 'GET', - `/v1.0/planner/plans/${planId}/details`, - ); - for (const key of Object.keys(categoryDescriptions as IDataObject)) { - if (categoryDescriptions[key] !== null) { - returnData.push({ - name: categoryDescriptions[key], - value: key, - }); - } - } - return returnData; - }, - // Get all the chats to display them to user so that they can - // select them easily - async getChats(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const qs: IDataObject = { - $expand: 'members', - }; - const { value } = await microsoftApiRequest.call(this, 'GET', '/v1.0/chats', {}, qs); - for (const chat of value) { - if (!chat.topic) { - chat.topic = chat.members - .filter((member: IDataObject) => member.displayName) - .map((member: IDataObject) => member.displayName) - .join(', '); - } - const chatName = `${chat.topic || '(no title) - ' + (chat.id as string)} (${ - chat.chatType - })`; - const chatId = chat.id; - returnData.push({ - name: chatName, - value: chatId, - }); - } - return returnData; - }, - }, - }; - - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - const length = items.length; - const qs: IDataObject = {}; - let responseData; - const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); - const nodeVersion = this.getNode().typeVersion; - const instanceId = this.getInstanceId(); - - for (let i = 0; i < length; i++) { - try { - if (resource === 'channel') { - //https://docs.microsoft.com/en-us/graph/api/channel-post?view=graph-rest-beta&tabs=http - if (operation === 'create') { - const teamId = this.getNodeParameter('teamId', i) as string; - const name = this.getNodeParameter('name', i) as string; - const options = this.getNodeParameter('options', i); - const body: IDataObject = { - displayName: name, - }; - if (options.description) { - body.description = options.description as string; - } - if (options.type) { - body.membershipType = options.type as string; - } - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/v1.0/teams/${teamId}/channels`, - body, - ); - } - //https://docs.microsoft.com/en-us/graph/api/channel-delete?view=graph-rest-beta&tabs=http - if (operation === 'delete') { - const teamId = this.getNodeParameter('teamId', i) as string; - const channelId = this.getNodeParameter('channelId', i) as string; - responseData = await microsoftApiRequest.call( - this, - 'DELETE', - `/v1.0/teams/${teamId}/channels/${channelId}`, - ); - responseData = { success: true }; - } - //https://docs.microsoft.com/en-us/graph/api/channel-get?view=graph-rest-beta&tabs=http - if (operation === 'get') { - const teamId = this.getNodeParameter('teamId', i) as string; - const channelId = this.getNodeParameter('channelId', i) as string; - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/v1.0/teams/${teamId}/channels/${channelId}`, - ); - } - //https://docs.microsoft.com/en-us/graph/api/channel-list?view=graph-rest-beta&tabs=http - if (operation === 'getAll') { - const teamId = this.getNodeParameter('teamId', i) as string; - const returnAll = this.getNodeParameter('returnAll', i); - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/v1.0/teams/${teamId}/channels`, - ); - } else { - qs.limit = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/v1.0/teams/${teamId}/channels`, - {}, - ); - responseData = responseData.splice(0, qs.limit); - } - } - //https://docs.microsoft.com/en-us/graph/api/channel-patch?view=graph-rest-beta&tabs=http - if (operation === 'update') { - const teamId = this.getNodeParameter('teamId', i) as string; - const channelId = this.getNodeParameter('channelId', i) as string; - const updateFields = this.getNodeParameter('updateFields', i); - const body: IDataObject = {}; - if (updateFields.name) { - body.displayName = updateFields.name as string; - } - if (updateFields.description) { - body.description = updateFields.description as string; - } - responseData = await microsoftApiRequest.call( - this, - 'PATCH', - `/v1.0/teams/${teamId}/channels/${channelId}`, - body, - ); - responseData = { success: true }; - } - } - if (resource === 'channelMessage') { - //https://docs.microsoft.com/en-us/graph/api/channel-post-messages?view=graph-rest-beta&tabs=http - //https://docs.microsoft.com/en-us/graph/api/channel-post-messagereply?view=graph-rest-beta&tabs=http - if (operation === 'create') { - const teamId = this.getNodeParameter('teamId', i) as string; - const channelId = this.getNodeParameter('channelId', i) as string; - const messageType = this.getNodeParameter('messageType', i) as string; - const message = this.getNodeParameter('message', i) as string; - const options = this.getNodeParameter('options', i); - - let includeLinkToWorkflow = options.includeLinkToWorkflow; - if (includeLinkToWorkflow === undefined) { - includeLinkToWorkflow = nodeVersion >= 1.1; - } - - const body: IDataObject = prepareMessage.call( - this, - message, - messageType, - includeLinkToWorkflow as boolean, - instanceId, - ); - - if (options.makeReply) { - const replyToId = options.makeReply as string; - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/beta/teams/${teamId}/channels/${channelId}/messages/${replyToId}/replies`, - body, - ); - } else { - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/beta/teams/${teamId}/channels/${channelId}/messages`, - body, - ); - } - } - //https://docs.microsoft.com/en-us/graph/api/channel-list-messages?view=graph-rest-beta&tabs=http - if (operation === 'getAll') { - const teamId = this.getNodeParameter('teamId', i) as string; - const channelId = this.getNodeParameter('channelId', i) as string; - const returnAll = this.getNodeParameter('returnAll', i); - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/beta/teams/${teamId}/channels/${channelId}/messages`, - ); - } else { - qs.limit = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/beta/teams/${teamId}/channels/${channelId}/messages`, - {}, - ); - responseData = responseData.splice(0, qs.limit); - } - } - } - if (resource === 'chatMessage') { - // https://docs.microsoft.com/en-us/graph/api/channel-post-messages?view=graph-rest-1.0&tabs=http - if (operation === 'create') { - const chatId = this.getNodeParameter('chatId', i) as string; - const messageType = this.getNodeParameter('messageType', i) as string; - const message = this.getNodeParameter('message', i) as string; - const options = this.getNodeParameter('options', i, {}); - - const includeLinkToWorkflow = - options.includeLinkToWorkflow !== false && nodeVersion >= 1.1; - - const body: IDataObject = prepareMessage.call( - this, - message, - messageType, - includeLinkToWorkflow, - instanceId, - ); - - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/v1.0/chats/${chatId}/messages`, - body, - ); - } - // https://docs.microsoft.com/en-us/graph/api/chat-list-messages?view=graph-rest-1.0&tabs=http - if (operation === 'get') { - const chatId = this.getNodeParameter('chatId', i) as string; - const messageId = this.getNodeParameter('messageId', i) as string; - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/v1.0/chats/${chatId}/messages/${messageId}`, - ); - } - // https://docs.microsoft.com/en-us/graph/api/chat-list-messages?view=graph-rest-1.0&tabs=http - if (operation === 'getAll') { - const chatId = this.getNodeParameter('chatId', i) as string; - const returnAll = this.getNodeParameter('returnAll', i); - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/v1.0/chats/${chatId}/messages`, - ); - } else { - qs.limit = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/v1.0/chats/${chatId}/messages`, - {}, - ); - responseData = responseData.splice(0, qs.limit); - } - } - } - if (resource === 'task') { - //https://docs.microsoft.com/en-us/graph/api/planner-post-tasks?view=graph-rest-1.0&tabs=http - if (operation === 'create') { - const planId = this.getNodeParameter('planId', i) as string; - const bucketId = this.getNodeParameter('bucketId', i) as string; - const title = this.getNodeParameter('title', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i); - const body: IDataObject = { - planId, - bucketId, - title, - }; - Object.assign(body, additionalFields); - - if (body.assignedTo) { - body.assignments = { - [body.assignedTo as string]: { - '@odata.type': 'microsoft.graph.plannerAssignment', - orderHint: ' !', - }, - }; - delete body.assignedTo; - } - - if (Array.isArray(body.labels)) { - body.appliedCategories = (body.labels as string[]).map((label) => ({ - [label]: true, - })); - } - - responseData = await microsoftApiRequest.call( - this, - 'POST', - '/v1.0/planner/tasks', - body, - ); - } - //https://docs.microsoft.com/en-us/graph/api/plannertask-delete?view=graph-rest-1.0&tabs=http - if (operation === 'delete') { - const taskId = this.getNodeParameter('taskId', i) as string; - const task = await microsoftApiRequest.call( - this, - 'GET', - `/v1.0/planner/tasks/${taskId}`, - ); - responseData = await microsoftApiRequest.call( - this, - 'DELETE', - `/v1.0/planner/tasks/${taskId}`, - {}, - {}, - undefined, - { 'If-Match': task['@odata.etag'] }, - ); - responseData = { success: true }; - } - //https://docs.microsoft.com/en-us/graph/api/plannertask-get?view=graph-rest-1.0&tabs=http - if (operation === 'get') { - const taskId = this.getNodeParameter('taskId', i) as string; - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/v1.0/planner/tasks/${taskId}`, - ); - } - if (operation === 'getAll') { - const tasksFor = this.getNodeParameter('tasksFor', i) as string; - const returnAll = this.getNodeParameter('returnAll', i); - if (tasksFor === 'member') { - //https://docs.microsoft.com/en-us/graph/api/planneruser-list-tasks?view=graph-rest-1.0&tabs=http - const memberId = this.getNodeParameter('memberId', i) as string; - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/v1.0/users/${memberId}/planner/tasks`, - ); - } else { - qs.limit = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/v1.0/users/${memberId}/planner/tasks`, - {}, - ); - responseData = responseData.splice(0, qs.limit); - } - } else { - //https://docs.microsoft.com/en-us/graph/api/plannerplan-list-tasks?view=graph-rest-1.0&tabs=http - const planId = this.getNodeParameter('planId', i) as string; - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/v1.0/planner/plans/${planId}/tasks`, - ); - } else { - qs.limit = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/v1.0/planner/plans/${planId}/tasks`, - {}, - ); - responseData = responseData.splice(0, qs.limit); - } - } - } - //https://docs.microsoft.com/en-us/graph/api/plannertask-update?view=graph-rest-1.0&tabs=http - if (operation === 'update') { - const taskId = this.getNodeParameter('taskId', i) as string; - const updateFields = this.getNodeParameter('updateFields', i); - const body: IDataObject = {}; - Object.assign(body, updateFields); - - if (body.assignedTo) { - body.assignments = { - [body.assignedTo as string]: { - '@odata.type': 'microsoft.graph.plannerAssignment', - orderHint: ' !', - }, - }; - delete body.assignedTo; - } - - if (body.groupId) { - // tasks are assigned to a plan and bucket, group is used for filtering - delete body.groupId; - } - - if (Array.isArray(body.labels)) { - body.appliedCategories = (body.labels as string[]).map((label) => ({ - [label]: true, - })); - } - - const task = await microsoftApiRequest.call( - this, - 'GET', - `/v1.0/planner/tasks/${taskId}`, - ); - - responseData = await microsoftApiRequest.call( - this, - 'PATCH', - `/v1.0/planner/tasks/${taskId}`, - body, - {}, - undefined, - { 'If-Match': task['@odata.etag'] }, - ); - - responseData = { success: true }; - } - } - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionErrorData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionErrorData); - continue; - } - throw error; - } - } - return [returnData]; +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; + +import { MicrosoftTeamsV1 } from './v1/MicrosoftTeamsV1.node'; +import { MicrosoftTeamsV2 } from './v2/MicrosoftTeamsV2.node'; + +export class MicrosoftTeams extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Microsoft Teams', + name: 'microsoftTeams', + icon: 'file:teams.svg', + group: ['input'], + description: 'Consume Microsoft Teams API', + defaultVersion: 2, + }; + + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new MicrosoftTeamsV1(baseDescription), + 1.1: new MicrosoftTeamsV1(baseDescription), + 2: new MicrosoftTeamsV2(baseDescription), + }; + + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/create.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/create.test.ts new file mode 100644 index 0000000000000..b863e75856133 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/create.test.ts @@ -0,0 +1,71 @@ +import type { INodeTypes } from 'n8n-workflow'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +import nock from 'nock'; + +import * as transport from '../../../../v2/transport'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'POST') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#teams('1644e7fe-547e-4223-a24f-922395865343')/channels/$entity", + id: '19:16259efabba44a66916d91dd91862a6f@thread.tacv2', + createdDateTime: '2023-10-26T05:37:43.4798824Z', + displayName: 'New Channel', + description: 'new channel description', + isFavoriteByDefault: null, + email: '', + webUrl: + 'https://teams.microsoft.com/l/channel/19%3a16259efabba44a66916d91dd91862a6f%40thread.tacv2/New+Channel?groupId=1644e7fe-547e-4223-a24f-922395865343&tenantId=tenantId-111-222-333', + membershipType: 'private', + }; + } +}); + +describe('Test MicrosoftTeamsV2, channel => create', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/channel/create.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'POST', + '/v1.0/teams/1644e7fe-547e-4223-a24f-922395865343/channels', + { + description: 'new channel description', + displayName: 'New Channel', + membershipType: 'private', + }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/create.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/create.workflow.json new file mode 100644 index 0000000000000..75443299f2085 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/create.workflow.json @@ -0,0 +1,107 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "teamId": { + "__rl": true, + "value": "1644e7fe-547e-4223-a24f-922395865343", + "mode": "list", + "cachedResultName": "5w1hb7" + }, + "name": "New Channel", + "options": { + "description": "new channel description", + "type": "private" + } + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#teams('1644e7fe-547e-4223-a24f-922395865343')/channels/$entity", + "id": "19:16259efabba44a66916d91dd91862a6f@thread.tacv2", + "createdDateTime": "2023-10-26T05:37:43.4798824Z", + "displayName": "New Channel", + "description": "new channel description", + "isFavoriteByDefault": null, + "email": "", + "webUrl": "https://teams.microsoft.com/l/channel/19%3a16259efabba44a66916d91dd91862a6f%40thread.tacv2/New+Channel?groupId=1644e7fe-547e-4223-a24f-922395865343&tenantId=tenantId-111-222-333", + "membershipType": "private" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "ec0b4e3d-2fd7-4fac-90e5-f18ecd620a8f", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/deleteChannel.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/deleteChannel.test.ts new file mode 100644 index 0000000000000..8e972d8b0df75 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/deleteChannel.test.ts @@ -0,0 +1,52 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'DELETE') { + return {}; + } +}); + +describe('Test MicrosoftTeamsV2, channel => deleteChannel', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/channel/deleteChannel.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'DELETE', + '/v1.0/teams/1644e7fe-547e-4223-a24f-922395865343/channels/19:16259efabba44a66916d91dd91862a6f@thread.tacv2', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/deleteChannel.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/deleteChannel.workflow.json new file mode 100644 index 0000000000000..2447b2dc25ac5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/deleteChannel.workflow.json @@ -0,0 +1,102 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "operation": "deleteChannel", + "teamId": { + "__rl": true, + "value": "1644e7fe-547e-4223-a24f-922395865343", + "mode": "list", + "cachedResultName": "5w1hb7" + }, + "channelId": { + "__rl": true, + "value": "19:16259efabba44a66916d91dd91862a6f@thread.tacv2", + "mode": "list", + "cachedResultName": "New Channel", + "cachedResultUrl": "https://teams.microsoft.com/l/channel/19%3A16259efabba44a66916d91dd91862a6f%40thread.tacv2/New%20Channel?groupId=1644e7fe-547e-4223-a24f-922395865343&tenantId=tenantId-111-222-333&allowXTenantAccess=False" + } + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "fb9028a2-b502-45f1-b907-825e8d754991", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/get.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/get.test.ts new file mode 100644 index 0000000000000..b7ec5e9e0e2a0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/get.test.ts @@ -0,0 +1,65 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'GET') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#teams('e25bae35-7bcc-4fb7-b4f2-0d5caef251fd')/channels/$entity", + id: '19:dff84a49e5124cc89dff0192c621ea0f@thread.tacv2', + createdDateTime: '2022-03-26T17:16:51Z', + displayName: 'General', + description: 'Description of Retail', + isFavoriteByDefault: null, + email: 'Retail@5w1hb7.onmicrosoft.com', + tenantId: 'tenantId-111-222-333', + webUrl: + 'https://teams.microsoft.com/l/channel/19%3Adff84a49e5124cc89dff0192c621ea0f%40thread.tacv2/General?groupId=e25bae35-7bcc-4fb7-b4f2-0d5caef251fd&tenantId=tenantId-111-222-333&allowXTenantAccess=True', + membershipType: 'standard', + }; + } +}); + +describe('Test MicrosoftTeamsV2, channel => get', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/channel/get.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'GET', + '/v1.0/teams/e25bae35-7bcc-4fb7-b4f2-0d5caef251fd/channels/19:dff84a49e5124cc89dff0192c621ea0f@thread.tacv2', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/get.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/get.workflow.json new file mode 100644 index 0000000000000..ecf6e6b9ade02 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/get.workflow.json @@ -0,0 +1,111 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "operation": "get", + "teamId": { + "__rl": true, + "value": "e25bae35-7bcc-4fb7-b4f2-0d5caef251fd", + "mode": "list", + "cachedResultName": "Retail" + }, + "channelId": { + "__rl": true, + "value": "19:dff84a49e5124cc89dff0192c621ea0f@thread.tacv2", + "mode": "list", + "cachedResultName": "General", + "cachedResultUrl": "https://teams.microsoft.com/l/channel/19%3Adff84a49e5124cc89dff0192c621ea0f%40thread.tacv2/Retail?groupId=e25bae35-7bcc-4fb7-b4f2-0d5caef251fd&tenantId=tenantId-111-222-333&allowXTenantAccess=False" + } + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#teams('e25bae35-7bcc-4fb7-b4f2-0d5caef251fd')/channels/$entity", + "id": "19:dff84a49e5124cc89dff0192c621ea0f@thread.tacv2", + "createdDateTime": "2022-03-26T17:16:51Z", + "displayName": "General", + "description": "Description of Retail", + "isFavoriteByDefault": null, + "email": "Retail@5w1hb7.onmicrosoft.com", + "tenantId": "tenantId-111-222-333", + "webUrl": "https://teams.microsoft.com/l/channel/19%3Adff84a49e5124cc89dff0192c621ea0f%40thread.tacv2/General?groupId=e25bae35-7bcc-4fb7-b4f2-0d5caef251fd&tenantId=tenantId-111-222-333&allowXTenantAccess=True", + "membershipType": "standard" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "021e8e46-fd28-4dfb-bd94-b288a8541940", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/getAll.test.ts new file mode 100644 index 0000000000000..1b26d0b8f8705 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/getAll.test.ts @@ -0,0 +1,91 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequestAllItems'); + +microsoftApiRequestSpy.mockImplementation(async (_, method: string) => { + if (method === 'GET') { + return [ + { + id: '42:aaabbbccc.tacv2', + createdDateTime: '2022-03-26T17:18:33Z', + displayName: 'Sales West', + description: 'Description of Sales West', + isFavoriteByDefault: null, + email: null, + tenantId: 'tenantId-111-222-333', + webUrl: + 'https://teams.microsoft.com/l/channel/threadId/Sales%20West?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&allowXTenantAccess=False', + membershipType: 'standard', + }, + { + id: '19:8662cdf2d8ff49eabdcf6364bc0fe3a2@thread.tacv2', + createdDateTime: '2022-03-26T17:18:30Z', + displayName: 'Sales East', + description: 'Description of Sales West', + isFavoriteByDefault: null, + email: null, + tenantId: 'tenantId-111-222-333', + webUrl: + 'https://teams.microsoft.com/l/channel/19%3A8662cdf2d8ff49eabdcf6364bc0fe3a2%40thread.tacv2/Sales%20East?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&allowXTenantAccess=False', + membershipType: 'standard', + }, + { + id: '19:a95209ede91f4d5595ac944aeb172124@thread.tacv2', + createdDateTime: '2022-03-26T17:18:16Z', + displayName: 'General', + description: 'Description of U.S. Sales', + isFavoriteByDefault: null, + email: 'U.S.Sales@5w1hb7.onmicrosoft.com', + tenantId: 'tenantId-111-222-333', + webUrl: + 'https://teams.microsoft.com/l/channel/19%3Aa95209ede91f4d5595ac944aeb172124%40thread.tacv2/U.S.%20Sales?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&allowXTenantAccess=False', + membershipType: 'standard', + }, + ]; + } +}); + +describe('Test MicrosoftTeamsV2, channel => getAll', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/channel/getAll.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'value', + 'GET', + '/v1.0/teams/1111-2222-3333/channels', + {}, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/getAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/getAll.workflow.json new file mode 100644 index 0000000000000..3b6796c5b15b9 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/getAll.workflow.json @@ -0,0 +1,129 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "operation": "getAll", + "teamId": { + "__rl": true, + "value": "1111-2222-3333", + "mode": "list", + "cachedResultName": "U.S. Sales" + } + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "id": "42:aaabbbccc.tacv2", + "createdDateTime": "2022-03-26T17:18:33Z", + "displayName": "Sales West", + "description": "Description of Sales West", + "isFavoriteByDefault": null, + "email": null, + "tenantId": "tenantId-111-222-333", + "webUrl": "https://teams.microsoft.com/l/channel/threadId/Sales%20West?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&allowXTenantAccess=False", + "membershipType": "standard" + } + }, + { + "json": { + "id": "19:8662cdf2d8ff49eabdcf6364bc0fe3a2@thread.tacv2", + "createdDateTime": "2022-03-26T17:18:30Z", + "displayName": "Sales East", + "description": "Description of Sales West", + "isFavoriteByDefault": null, + "email": null, + "tenantId": "tenantId-111-222-333", + "webUrl": "https://teams.microsoft.com/l/channel/19%3A8662cdf2d8ff49eabdcf6364bc0fe3a2%40thread.tacv2/Sales%20East?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&allowXTenantAccess=False", + "membershipType": "standard" + } + }, + { + "json": { + "id": "19:a95209ede91f4d5595ac944aeb172124@thread.tacv2", + "createdDateTime": "2022-03-26T17:18:16Z", + "displayName": "General", + "description": "Description of U.S. Sales", + "isFavoriteByDefault": null, + "email": "U.S.Sales@5w1hb7.onmicrosoft.com", + "tenantId": "tenantId-111-222-333", + "webUrl": "https://teams.microsoft.com/l/channel/19%3Aa95209ede91f4d5595ac944aeb172124%40thread.tacv2/U.S.%20Sales?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&allowXTenantAccess=False", + "membershipType": "standard" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "08f365b7-a03d-4d38-979e-edca8194d045", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/update.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/update.test.ts new file mode 100644 index 0000000000000..f3114f5978660 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/update.test.ts @@ -0,0 +1,53 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'PATCH') { + return {}; + } +}); + +describe('Test MicrosoftTeamsV2, channel => update', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/channel/update.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'PATCH', + '/v1.0/teams/e25bae35-7bcc-4fb7-b4f2-0d5caef251fd/channels/19:b9daa3647ff8450bacaf39490d3e05e2@thread.tacv2', + { description: 'new channel description', displayName: 'New Deals' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/update.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/update.workflow.json new file mode 100644 index 0000000000000..54530e4b62ab9 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channel/update.workflow.json @@ -0,0 +1,106 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "operation": "update", + "teamId": { + "__rl": true, + "value": "e25bae35-7bcc-4fb7-b4f2-0d5caef251fd", + "mode": "list", + "cachedResultName": "Retail" + }, + "channelId": { + "__rl": true, + "value": "19:b9daa3647ff8450bacaf39490d3e05e2@thread.tacv2", + "mode": "list", + "cachedResultName": "Deals", + "cachedResultUrl": "https://teams.microsoft.com/l/channel/19%3Ab9daa3647ff8450bacaf39490d3e05e2%40thread.tacv2/Deals?groupId=e25bae35-7bcc-4fb7-b4f2-0d5caef251fd&tenantId=tenantId-111-222-333&allowXTenantAccess=False" + }, + "name": "New Deals", + "options": { + "description": "new channel description" + } + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "75151dab-2cd1-42ee-9a01-e61cf6a1245e", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/create.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/create.test.ts new file mode 100644 index 0000000000000..4cf330bf3e4fd --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/create.test.ts @@ -0,0 +1,95 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'POST') { + return { + '@odata.context': + "https://graph.microsoft.com/beta/$metadata#teams('1111-2222-3333')/channels('threadId')/messages/$entity", + id: '1698324478896', + replyToId: null, + etag: '1698324478896', + messageType: 'message', + createdDateTime: '2023-10-26T12:47:58.896Z', + lastModifiedDateTime: '2023-10-26T12:47:58.896Z', + lastEditedDateTime: null, + deletedDateTime: null, + subject: null, + summary: null, + chatId: null, + importance: 'normal', + locale: 'en-us', + webUrl: + 'https://teams.microsoft.com/l/message/threadId/1698324478896?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&createdTime=1698324478896&parentMessageId=1698324478896', + onBehalfOf: null, + policyViolation: null, + eventDetail: null, + from: { + application: null, + device: null, + user: { + '@odata.type': '#microsoft.graph.teamworkUserIdentity', + id: '11111-2222-3333', + displayName: 'My Name', + userIdentityType: 'aadUser', + }, + }, + body: { + contentType: 'html', + content: 'new sale', + }, + channelIdentity: { + teamId: '1111-2222-3333', + channelId: '42:aaabbbccc.tacv2', + }, + attachments: [], + mentions: [], + reactions: [], + }; + } +}); + +describe('Test MicrosoftTeamsV2, channelMessage => create', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/channelMessage/create.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'POST', + '/beta/teams/1111-2222-3333/channels/42:aaabbbccc.tacv2/messages', + { body: { content: 'new sale', contentType: 'html' } }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/create.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/create.workflow.json new file mode 100644 index 0000000000000..863131d5b17b9 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/create.workflow.json @@ -0,0 +1,145 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "resource": "channelMessage", + "teamId": { + "__rl": true, + "value": "1111-2222-3333", + "mode": "list", + "cachedResultName": "U.S. Sales" + }, + "channelId": { + "__rl": true, + "value": "42:aaabbbccc.tacv2", + "mode": "list", + "cachedResultName": "Sales West", + "cachedResultUrl": "https://teams.microsoft.com/l/channel/threadId/Sales%20West?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&allowXTenantAccess=False" + }, + "contentType": "html", + "message": "new sale", + "options": { + "includeLinkToWorkflow": false + } + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/beta/$metadata#teams('1111-2222-3333')/channels('threadId')/messages/$entity", + "id": "1698324478896", + "replyToId": null, + "etag": "1698324478896", + "messageType": "message", + "createdDateTime": "2023-10-26T12:47:58.896Z", + "lastModifiedDateTime": "2023-10-26T12:47:58.896Z", + "lastEditedDateTime": null, + "deletedDateTime": null, + "subject": null, + "summary": null, + "chatId": null, + "importance": "normal", + "locale": "en-us", + "webUrl": "https://teams.microsoft.com/l/message/threadId/1698324478896?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&createdTime=1698324478896&parentMessageId=1698324478896", + "onBehalfOf": null, + "policyViolation": null, + "eventDetail": null, + "from": { + "application": null, + "device": null, + "user": { + "@odata.type": "#microsoft.graph.teamworkUserIdentity", + "id": "11111-2222-3333", + "displayName": "My Name", + "userIdentityType": "aadUser" + } + }, + "body": { + "contentType": "html", + "content": "new sale" + }, + "channelIdentity": { + "teamId": "1111-2222-3333", + "channelId": "42:aaabbbccc.tacv2" + }, + "attachments": [], + "mentions": [], + "reactions": [] + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "30cec397-a737-41b8-8da2-dff4d293ce70", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.test.ts new file mode 100644 index 0000000000000..db13528329d29 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.test.ts @@ -0,0 +1,107 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequestAllItems'); + +microsoftApiRequestSpy.mockImplementation(async (_, method: string) => { + if (method === 'GET') { + return [ + { + id: '1698130964682', + replyToId: null, + etag: '1698130964682', + messageType: 'message', + createdDateTime: '2023-10-24T07:02:44.682Z', + lastModifiedDateTime: '2023-10-24T07:02:44.682Z', + lastEditedDateTime: null, + deletedDateTime: null, + subject: '', + summary: null, + chatId: null, + importance: 'normal', + locale: 'en-us', + webUrl: + 'https://teams.microsoft.com/l/message/threadId/1698130964682?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&createdTime=1698130964682&parentMessageId=1698130964682', + onBehalfOf: null, + policyViolation: null, + eventDetail: null, + from: { + application: null, + device: null, + user: { + '@odata.type': '#microsoft.graph.teamworkUserIdentity', + id: '11111-2222-3333', + displayName: 'My Name', + userIdentityType: 'aadUser', + tenantId: 'tenantId-111-222-333', + }, + }, + body: { + contentType: 'html', + content: + '
I added a tab at the top of this channel. Check it out!
', + }, + channelIdentity: { + teamId: '1111-2222-3333', + channelId: '42:aaabbbccc.tacv2', + }, + attachments: [ + { + id: 'tab::f22a0494-6f7c-4512-85c5-e4ce72ce142a', + contentType: 'tabReference', + contentUrl: null, + content: null, + name: 'Tasks', + thumbnailUrl: null, + teamsAppId: null, + }, + ], + mentions: [], + reactions: [], + }, + ]; + } +}); + +describe('Test MicrosoftTeamsV2, channelMessage => getAll', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'value', + 'GET', + '/beta/teams/1111-2222-3333/channels/42:aaabbbccc.tacv2/messages', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.workflow.json new file mode 100644 index 0000000000000..24e797f6847cd --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/channelMessage/getAll.workflow.json @@ -0,0 +1,152 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "resource": "channelMessage", + "operation": "getAll", + "teamId": { + "__rl": true, + "value": "1111-2222-3333", + "mode": "list", + "cachedResultName": "U.S. Sales" + }, + "channelId": { + "__rl": true, + "value": "42:aaabbbccc.tacv2", + "mode": "list", + "cachedResultName": "Sales West", + "cachedResultUrl": "https://teams.microsoft.com/l/channel/threadId/Sales%20West?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&allowXTenantAccess=False" + }, + "returnAll": true + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "id": "1698130964682", + "replyToId": null, + "etag": "1698130964682", + "messageType": "message", + "createdDateTime": "2023-10-24T07:02:44.682Z", + "lastModifiedDateTime": "2023-10-24T07:02:44.682Z", + "lastEditedDateTime": null, + "deletedDateTime": null, + "subject": "", + "summary": null, + "chatId": null, + "importance": "normal", + "locale": "en-us", + "webUrl": "https://teams.microsoft.com/l/message/threadId/1698130964682?groupId=1111-2222-3333&tenantId=tenantId-111-222-333&createdTime=1698130964682&parentMessageId=1698130964682", + "onBehalfOf": null, + "policyViolation": null, + "eventDetail": null, + "from": { + "application": null, + "device": null, + "user": { + "@odata.type": "#microsoft.graph.teamworkUserIdentity", + "id": "11111-2222-3333", + "displayName": "My Name", + "userIdentityType": "aadUser", + "tenantId": "tenantId-111-222-333" + } + }, + "body": { + "contentType": "html", + "content": "
I added a tab at the top of this channel. Check it out!
" + }, + "channelIdentity": { + "teamId": "1111-2222-3333", + "channelId": "42:aaabbbccc.tacv2" + }, + "attachments": [ + { + "id": "tab::f22a0494-6f7c-4512-85c5-e4ce72ce142a", + "contentType": "tabReference", + "contentUrl": null, + "content": null, + "name": "Tasks", + "thumbnailUrl": null, + "teamsAppId": null + } + ], + "mentions": [], + "reactions": [] + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "d89de79a-1819-4d29-b781-a1f3f00b4a2e", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/create.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/create.test.ts new file mode 100644 index 0000000000000..5fac7c0d662df --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/create.test.ts @@ -0,0 +1,91 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'POST') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#chats('19%3Aebed9ad42c904d6c83adf0db360053ec%40thread.v2')/messages/$entity", + id: '1698378560692', + replyToId: null, + etag: '1698378560692', + messageType: 'message', + createdDateTime: '2023-10-27T03:49:20.692Z', + lastModifiedDateTime: '2023-10-27T03:49:20.692Z', + lastEditedDateTime: null, + deletedDateTime: null, + subject: null, + summary: null, + chatId: '19:ebed9ad42c904d6c83adf0db360053ec@thread.v2', + importance: 'normal', + locale: 'en-us', + webUrl: null, + channelIdentity: null, + policyViolation: null, + eventDetail: null, + from: { + application: null, + device: null, + user: { + '@odata.type': '#microsoft.graph.teamworkUserIdentity', + id: '11111-2222-3333', + displayName: 'Michael Kret', + userIdentityType: 'aadUser', + }, + }, + body: { + contentType: 'html', + content: + 'Hello!
\n
\n Powered by this n8n workflow ', + }, + attachments: [], + mentions: [], + reactions: [], + }; + } +}); + +describe('Test MicrosoftTeamsV2, chatMessage => create', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/chatMessage/create.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'POST', + '/v1.0/chats/19:ebed9ad42c904d6c83adf0db360053ec@thread.v2/messages', + expect.anything(), + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/create.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/create.workflow.json new file mode 100644 index 0000000000000..9398def87f089 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/create.workflow.json @@ -0,0 +1,134 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "resource": "chatMessage", + "chatId": { + "__rl": true, + "value": "19:ebed9ad42c904d6c83adf0db360053ec@thread.v2", + "mode": "list", + "cachedResultName": "Grady Archie, Adele Vance, Henrietta Mueller, Patti Fernandez, Diego Siciliani, Michael Kret (group)", + "cachedResultUrl": "https://teams.microsoft.com/l/chat/19%3Aebed9ad42c904d6c83adf0db360053ec%40thread.v2/0?tenantId=23786ca6-7ff2-4672-87d0-5c649ee0a337" + }, + "message": "Hello!", + "options": { + "includeLinkToWorkflow": true + } + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#chats('19%3Aebed9ad42c904d6c83adf0db360053ec%40thread.v2')/messages/$entity", + "id": "1698378560692", + "replyToId": null, + "etag": "1698378560692", + "messageType": "message", + "createdDateTime": "2023-10-27T03:49:20.692Z", + "lastModifiedDateTime": "2023-10-27T03:49:20.692Z", + "lastEditedDateTime": null, + "deletedDateTime": null, + "subject": null, + "summary": null, + "chatId": "19:ebed9ad42c904d6c83adf0db360053ec@thread.v2", + "importance": "normal", + "locale": "en-us", + "webUrl": null, + "channelIdentity": null, + "policyViolation": null, + "eventDetail": null, + "from": { + "application": null, + "device": null, + "user": { + "@odata.type": "#microsoft.graph.teamworkUserIdentity", + "id": "11111-2222-3333", + "displayName": "Michael Kret", + "userIdentityType": "aadUser" + } + }, + "body": { + "contentType": "html", + "content": "Hello!
\n
\n Powered by this n8n workflow " + }, + "attachments": [], + "mentions": [], + "reactions": [] + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "4b3813fc-dee5-4560-becc-9e2c7fe881d6", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/get.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/get.test.ts new file mode 100644 index 0000000000000..8f9d2a660f90e --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/get.test.ts @@ -0,0 +1,91 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'GET') { + return { + '@odata.context': + "https://graph.microsoft.com/v1.0/$metadata#chats('19%3Aebed9ad42c904d6c83adf0db360053ec%40thread.v2')/messages/$entity", + id: '1698378560692', + replyToId: null, + etag: '1698378560692', + messageType: 'message', + createdDateTime: '2023-10-27T03:49:20.692Z', + lastModifiedDateTime: '2023-10-27T03:49:20.692Z', + lastEditedDateTime: null, + deletedDateTime: null, + subject: null, + summary: null, + chatId: '19:ebed9ad42c904d6c83adf0db360053ec@thread.v2', + importance: 'normal', + locale: 'en-us', + webUrl: null, + channelIdentity: null, + policyViolation: null, + eventDetail: null, + from: { + application: null, + device: null, + user: { + '@odata.type': '#microsoft.graph.teamworkUserIdentity', + id: '11111-2222-3333', + displayName: 'Michael Kret', + userIdentityType: 'aadUser', + tenantId: '23786ca6-7ff2-4672-87d0-5c649ee0a337', + }, + }, + body: { + contentType: 'html', + content: + 'Hello!
\n
\n Powered by this n8n workflow ', + }, + attachments: [], + mentions: [], + reactions: [], + }; + } +}); + +describe('Test MicrosoftTeamsV2, chatMessage => get', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/chatMessage/get.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'GET', + '/v1.0/chats/19:ebed9ad42c904d6c83adf0db360053ec@thread.v2/messages/1698378560692', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/get.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/get.workflow.json new file mode 100644 index 0000000000000..7c37bcdcb994c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/get.workflow.json @@ -0,0 +1,133 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "resource": "chatMessage", + "operation": "get", + "chatId": { + "__rl": true, + "value": "19:ebed9ad42c904d6c83adf0db360053ec@thread.v2", + "mode": "list", + "cachedResultName": "Grady Archie, Adele Vance, Henrietta Mueller, Patti Fernandez, Diego Siciliani, Michael Kret (group)", + "cachedResultUrl": "https://teams.microsoft.com/l/chat/19%3Aebed9ad42c904d6c83adf0db360053ec%40thread.v2/0?tenantId=23786ca6-7ff2-4672-87d0-5c649ee0a337" + }, + "messageId": "1698378560692" + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#chats('19%3Aebed9ad42c904d6c83adf0db360053ec%40thread.v2')/messages/$entity", + "id": "1698378560692", + "replyToId": null, + "etag": "1698378560692", + "messageType": "message", + "createdDateTime": "2023-10-27T03:49:20.692Z", + "lastModifiedDateTime": "2023-10-27T03:49:20.692Z", + "lastEditedDateTime": null, + "deletedDateTime": null, + "subject": null, + "summary": null, + "chatId": "19:ebed9ad42c904d6c83adf0db360053ec@thread.v2", + "importance": "normal", + "locale": "en-us", + "webUrl": null, + "channelIdentity": null, + "policyViolation": null, + "eventDetail": null, + "from": { + "application": null, + "device": null, + "user": { + "@odata.type": "#microsoft.graph.teamworkUserIdentity", + "id": "11111-2222-3333", + "displayName": "Michael Kret", + "userIdentityType": "aadUser", + "tenantId": "23786ca6-7ff2-4672-87d0-5c649ee0a337" + } + }, + "body": { + "contentType": "html", + "content": "Hello!
\n
\n Powered by this n8n workflow " + }, + "attachments": [], + "mentions": [], + "reactions": [] + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "4de0815e-b1b7-463a-a627-c55ac71b70e4", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.test.ts new file mode 100644 index 0000000000000..a4c89a077723c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.test.ts @@ -0,0 +1,131 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequestAllItems'); + +microsoftApiRequestSpy.mockImplementation(async (_, method: string) => { + if (method === 'GET') { + return [ + { + id: '1698378560692', + replyToId: null, + etag: '1698378560692', + messageType: 'message', + createdDateTime: '2023-10-27T03:49:20.692Z', + lastModifiedDateTime: '2023-10-27T03:49:20.692Z', + lastEditedDateTime: null, + deletedDateTime: null, + subject: null, + summary: null, + chatId: '19:ebed9ad42c904d6c83adf0db360053ec@thread.v2', + importance: 'normal', + locale: 'en-us', + webUrl: null, + channelIdentity: null, + policyViolation: null, + eventDetail: null, + from: { + application: null, + device: null, + user: { + '@odata.type': '#microsoft.graph.teamworkUserIdentity', + id: '11111-2222-3333', + displayName: 'Michael Kret', + userIdentityType: 'aadUser', + tenantId: '23786ca6-7ff2-4672-87d0-5c649ee0a337', + }, + }, + body: { + contentType: 'html', + content: + 'Hello!
\n
\n Powered by this n8n workflow ', + }, + attachments: [], + mentions: [], + reactions: [], + }, + { + id: '1698129297101', + replyToId: null, + etag: '1698129297101', + messageType: 'message', + createdDateTime: '2023-10-24T06:34:57.101Z', + lastModifiedDateTime: '2023-10-24T06:34:57.101Z', + lastEditedDateTime: null, + deletedDateTime: null, + subject: null, + summary: null, + chatId: '19:ebed9ad42c904d6c83adf0db360053ec@thread.v2', + importance: 'normal', + locale: 'en-us', + webUrl: null, + channelIdentity: null, + policyViolation: null, + eventDetail: null, + from: { + application: null, + device: null, + user: { + '@odata.type': '#microsoft.graph.teamworkUserIdentity', + id: '11111-2222-3333', + displayName: 'Michael Kret', + userIdentityType: 'aadUser', + tenantId: '23786ca6-7ff2-4672-87d0-5c649ee0a337', + }, + }, + body: { + contentType: 'html', + content: + 'tada
\n
\n Powered by this n8n workflow ', + }, + attachments: [], + mentions: [], + reactions: [], + }, + ]; + } +}); + +describe('Test MicrosoftTeamsV2, chatMessage => getAll', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'value', + 'GET', + '/v1.0/chats/19:ebed9ad42c904d6c83adf0db360053ec@thread.v2/messages', + {}, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.workflow.json new file mode 100644 index 0000000000000..a995cae609bcc --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMessage/getAll.workflow.json @@ -0,0 +1,171 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "resource": "chatMessage", + "operation": "getAll", + "chatId": { + "__rl": true, + "value": "19:ebed9ad42c904d6c83adf0db360053ec@thread.v2", + "mode": "list", + "cachedResultName": "Grady Archie, Adele Vance, Henrietta Mueller, Patti Fernandez, Diego Siciliani, Michael Kret (group)", + "cachedResultUrl": "https://teams.microsoft.com/l/chat/19%3Aebed9ad42c904d6c83adf0db360053ec%40thread.v2/0?tenantId=23786ca6-7ff2-4672-87d0-5c649ee0a337" + }, + "limit": 2 + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "id": "1698378560692", + "replyToId": null, + "etag": "1698378560692", + "messageType": "message", + "createdDateTime": "2023-10-27T03:49:20.692Z", + "lastModifiedDateTime": "2023-10-27T03:49:20.692Z", + "lastEditedDateTime": null, + "deletedDateTime": null, + "subject": null, + "summary": null, + "chatId": "19:ebed9ad42c904d6c83adf0db360053ec@thread.v2", + "importance": "normal", + "locale": "en-us", + "webUrl": null, + "channelIdentity": null, + "policyViolation": null, + "eventDetail": null, + "from": { + "application": null, + "device": null, + "user": { + "@odata.type": "#microsoft.graph.teamworkUserIdentity", + "id": "11111-2222-3333", + "displayName": "Michael Kret", + "userIdentityType": "aadUser", + "tenantId": "23786ca6-7ff2-4672-87d0-5c649ee0a337" + } + }, + "body": { + "contentType": "html", + "content": "Hello!
\n
\n Powered by this n8n workflow " + }, + "attachments": [], + "mentions": [], + "reactions": [] + } + }, + { + "json": { + "id": "1698129297101", + "replyToId": null, + "etag": "1698129297101", + "messageType": "message", + "createdDateTime": "2023-10-24T06:34:57.101Z", + "lastModifiedDateTime": "2023-10-24T06:34:57.101Z", + "lastEditedDateTime": null, + "deletedDateTime": null, + "subject": null, + "summary": null, + "chatId": "19:ebed9ad42c904d6c83adf0db360053ec@thread.v2", + "importance": "normal", + "locale": "en-us", + "webUrl": null, + "channelIdentity": null, + "policyViolation": null, + "eventDetail": null, + "from": { + "application": null, + "device": null, + "user": { + "@odata.type": "#microsoft.graph.teamworkUserIdentity", + "id": "11111-2222-3333", + "displayName": "Michael Kret", + "userIdentityType": "aadUser", + "tenantId": "23786ca6-7ff2-4672-87d0-5c649ee0a337" + } + }, + "body": { + "contentType": "html", + "content": "tada
\n
\n Powered by this n8n workflow " + }, + "attachments": [], + "mentions": [], + "reactions": [] + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "324b8a88-47a1-453e-906f-79f9be836e17", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/create.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/create.test.ts new file mode 100644 index 0000000000000..2ee3a02f19b32 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/create.test.ts @@ -0,0 +1,111 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'POST') { + return { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#planner/tasks/$entity', + '@odata.etag': 'W/"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBARCc="', + planId: 'THwgIivuyU26ki8qS7ufcJgAB6zf', + bucketId: 'CO-ZsX1s4kO7FtO6ZHZdDpgAFL1m', + title: 'do this', + orderHint: '8584964728139267910', + assigneePriority: '', + percentComplete: 25, + startDateTime: null, + createdDateTime: '2024-01-13T08:21:11.5507897Z', + dueDateTime: '2023-10-30T22:00:00Z', + hasDescription: false, + previewType: 'automatic', + completedDateTime: null, + completedBy: null, + referenceCount: 0, + checklistItemCount: 0, + activeChecklistItemCount: 0, + conversationThreadId: null, + priority: 5, + id: 'mYxTKaD9VkqWaBCJE5v4E5gAHcPB', + createdBy: { + user: { + displayName: null, + id: 'b834447b-6848-4af9-8390-d2259ce46b74', + }, + application: { + displayName: null, + id: '66bdd989-4a29-465d-86fb-d94ed8fd86ed', + }, + }, + appliedCategories: {}, + assignments: { + 'ba4a422e-bdce-4795-b4b6-579287363f0e': { + '@odata.type': '#microsoft.graph.plannerAssignment', + assignedDateTime: '2024-01-13T08:21:11.5507897Z', + orderHint: '8584964728740986700PZ', + assignedBy: { + user: { + displayName: null, + id: 'b834447b-6848-4af9-8390-d2259ce46b74', + }, + application: { + displayName: null, + id: '66bdd989-4a29-465d-86fb-d94ed8fd86ed', + }, + }, + }, + }, + }; + } +}); + +describe('Test MicrosoftTeamsV2, task => create', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/task/create.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith('POST', '/v1.0/planner/tasks', { + assignments: { + 'ba4a422e-bdce-4795-b4b6-579287363f0e': { + '@odata.type': 'microsoft.graph.plannerAssignment', + orderHint: ' !', + }, + }, + bucketId: 'CO-ZsX1s4kO7FtO6ZHZdDpgAFL1m', + dueDateTime: '2023-10-30T22:00:00.000Z', + percentComplete: 25, + planId: 'THwgIivuyU26ki8qS7ufcJgAB6zf', + title: 'do this', + }); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/create.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/create.workflow.json new file mode 100644 index 0000000000000..5653628858f8a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/create.workflow.json @@ -0,0 +1,167 @@ +{ + "name": "My workflow 69", + "nodes": [ + { + "parameters": {}, + "id": "28f1f78e-0d50-4bfe-aa16-1a53f0832793", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 520, + 300 + ] + }, + { + "parameters": { + "resource": "task", + "groupId": { + "__rl": true, + "value": "1644e7fe-547e-4223-a24f-922395865343", + "mode": "list", + "cachedResultName": "5w1hb7" + }, + "planId": { + "__rl": true, + "value": "THwgIivuyU26ki8qS7ufcJgAB6zf", + "mode": "list", + "cachedResultName": "my best plan" + }, + "bucketId": { + "__rl": true, + "value": "CO-ZsX1s4kO7FtO6ZHZdDpgAFL1m", + "mode": "list", + "cachedResultName": "To do" + }, + "title": "do this", + "options": { + "assignedTo": { + "__rl": true, + "value": "ba4a422e-bdce-4795-b4b6-579287363f0e", + "mode": "list", + "cachedResultName": "Henrietta Mueller" + }, + "dueDateTime": "2023-10-30T22:00:00.000Z", + "percentComplete": 25 + } + }, + "id": "e1c2eafd-4a1e-48aa-bc0e-d5a03644fedc", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 740, + 300 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "a05a3079-3431-44b8-a317-79e5d8babe25", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1040, + 300 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#planner/tasks/$entity", + "@odata.etag": "W/\"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBARCc=\"", + "planId": "THwgIivuyU26ki8qS7ufcJgAB6zf", + "bucketId": "CO-ZsX1s4kO7FtO6ZHZdDpgAFL1m", + "title": "do this", + "orderHint": "8584964728139267910", + "assigneePriority": "", + "percentComplete": 25, + "startDateTime": null, + "createdDateTime": "2024-01-13T08:21:11.5507897Z", + "dueDateTime": "2023-10-30T22:00:00Z", + "hasDescription": false, + "previewType": "automatic", + "completedDateTime": null, + "completedBy": null, + "referenceCount": 0, + "checklistItemCount": 0, + "activeChecklistItemCount": 0, + "conversationThreadId": null, + "priority": 5, + "id": "mYxTKaD9VkqWaBCJE5v4E5gAHcPB", + "createdBy": { + "user": { + "displayName": null, + "id": "b834447b-6848-4af9-8390-d2259ce46b74" + }, + "application": { + "displayName": null, + "id": "66bdd989-4a29-465d-86fb-d94ed8fd86ed" + } + }, + "appliedCategories": {}, + "assignments": { + "ba4a422e-bdce-4795-b4b6-579287363f0e": { + "@odata.type": "#microsoft.graph.plannerAssignment", + "assignedDateTime": "2024-01-13T08:21:11.5507897Z", + "orderHint": "8584964728740986700PZ", + "assignedBy": { + "user": { + "displayName": null, + "id": "b834447b-6848-4af9-8390-d2259ce46b74" + }, + "application": { + "displayName": null, + "id": "66bdd989-4a29-465d-86fb-d94ed8fd86ed" + } + } + } + } + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "8cb42e14-a12c-4c24-8374-4105664065c3", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "id": "73ZPNCHsvTvFBx1V", + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/deleteTask.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/deleteTask.test.ts new file mode 100644 index 0000000000000..f991d916978ff --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/deleteTask.test.ts @@ -0,0 +1,65 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'DELETE') { + return {}; + } + if (method === 'GET') { + return { + '@odata.etag': 'W/"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBARCc="', + }; + } +}); + +describe('Test MicrosoftTeamsV2, task => deleteTask', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/task/deleteTask.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(2); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'GET', + '/v1.0/planner/tasks/lDrRJ7N_-06p_26iKBtJ6ZgAKffD', + ); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'DELETE', + '/v1.0/planner/tasks/lDrRJ7N_-06p_26iKBtJ6ZgAKffD', + {}, + {}, + undefined, + { 'If-Match': 'W/"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBARCc="' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/deleteTask.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/deleteTask.workflow.json new file mode 100644 index 0000000000000..084ae196e0da7 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/deleteTask.workflow.json @@ -0,0 +1,91 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "resource": "task", + "operation": "deleteTask", + "taskId": "lDrRJ7N_-06p_26iKBtJ6ZgAKffD" + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "ae5ed0d2-4513-457a-80a4-262126523553", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/get.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/get.test.ts new file mode 100644 index 0000000000000..7d768ee2aef39 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/get.test.ts @@ -0,0 +1,102 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'GET') { + return { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#planner/tasks/$entity', + '@odata.etag': 'W/"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBARCc="', + planId: 'THwgIivuyU26ki8qS7ufcJgAB6zf', + bucketId: 'CO-ZsX1s4kO7FtO6ZHZdDpgAFL1m', + title: 'do this', + orderHint: '8585032308935758184', + assigneePriority: '', + percentComplete: 25, + startDateTime: null, + createdDateTime: '2023-10-27T03:06:31.9017623Z', + dueDateTime: '2023-10-30T22:00:00Z', + hasDescription: false, + previewType: 'automatic', + completedDateTime: null, + completedBy: null, + referenceCount: 0, + checklistItemCount: 0, + activeChecklistItemCount: 0, + conversationThreadId: null, + priority: 5, + id: 'lDrRJ7N_-06p_26iKBtJ6ZgAKffD', + createdBy: { + user: { + displayName: null, + id: '11111-2222-3333', + }, + application: { + displayName: null, + id: '11111-2222-3333-44444', + }, + }, + appliedCategories: {}, + assignments: { + 'ba4a422e-bdce-4795-b4b6-579287363f0e': { + '@odata.type': '#microsoft.graph.plannerAssignment', + assignedDateTime: '2023-10-27T03:06:31.9017623Z', + orderHint: '8585032309536070726PE', + assignedBy: { + user: { + displayName: null, + id: '11111-2222-3333', + }, + application: { + displayName: null, + id: '11111-2222-3333-44444', + }, + }, + }, + }, + }; + } +}); + +describe('Test MicrosoftTeamsV2, task => get', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/task/get.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'GET', + '/v1.0/planner/tasks/lDrRJ7N_-06p_26iKBtJ6ZgAKffD', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/get.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/get.workflow.json new file mode 100644 index 0000000000000..001b14871e83b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/get.workflow.json @@ -0,0 +1,139 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "resource": "task", + "operation": "get", + "taskId": "lDrRJ7N_-06p_26iKBtJ6ZgAKffD" + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#planner/tasks/$entity", + "@odata.etag": "W/\"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBARCc=\"", + "planId": "THwgIivuyU26ki8qS7ufcJgAB6zf", + "bucketId": "CO-ZsX1s4kO7FtO6ZHZdDpgAFL1m", + "title": "do this", + "orderHint": "8585032308935758184", + "assigneePriority": "", + "percentComplete": 25, + "startDateTime": null, + "createdDateTime": "2023-10-27T03:06:31.9017623Z", + "dueDateTime": "2023-10-30T22:00:00Z", + "hasDescription": false, + "previewType": "automatic", + "completedDateTime": null, + "completedBy": null, + "referenceCount": 0, + "checklistItemCount": 0, + "activeChecklistItemCount": 0, + "conversationThreadId": null, + "priority": 5, + "id": "lDrRJ7N_-06p_26iKBtJ6ZgAKffD", + "createdBy": { + "user": { + "displayName": null, + "id": "11111-2222-3333" + }, + "application": { + "displayName": null, + "id": "11111-2222-3333-44444" + } + }, + "appliedCategories": {}, + "assignments": { + "ba4a422e-bdce-4795-b4b6-579287363f0e": { + "@odata.type": "#microsoft.graph.plannerAssignment", + "assignedDateTime": "2023-10-27T03:06:31.9017623Z", + "orderHint": "8585032309536070726PE", + "assignedBy": { + "user": { + "displayName": null, + "id": "11111-2222-3333" + }, + "application": { + "displayName": null, + "id": "11111-2222-3333-44444" + } + } + } + } + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "8147cd45-b1e6-44b3-abd2-232b44102660", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/getAll.test.ts new file mode 100644 index 0000000000000..0e6f63804e2c0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/getAll.test.ts @@ -0,0 +1,201 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestAllItemsSpy = jest.spyOn(transport, 'microsoftApiRequestAllItems'); + +microsoftApiRequestAllItemsSpy.mockImplementation(async (_, method: string) => { + if (method === 'GET') { + return [ + { + '@odata.etag': 'W/"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBAZCc="', + planId: 'coJdCqzqNUKULQtTRWDa6pgACTln', + bucketId: null, + title: 'tada', + orderHint: '8585516884147534440', + assigneePriority: '8585516882706975451', + percentComplete: 10, + startDateTime: null, + createdDateTime: '2022-04-14T06:41:10.7241367Z', + dueDateTime: '2022-04-24T21:00:00Z', + hasDescription: false, + previewType: 'automatic', + completedDateTime: null, + completedBy: null, + referenceCount: 0, + checklistItemCount: 0, + activeChecklistItemCount: 0, + conversationThreadId: null, + priority: 5, + id: '1KgwUqOmbU2C9mZWiqxiv5gAPp8Q', + createdBy: { + user: { + displayName: null, + id: '11111-2222-3333', + }, + }, + appliedCategories: {}, + assignments: { + '11111-2222-3333': { + '@odata.type': '#microsoft.graph.plannerAssignment', + assignedDateTime: '2022-04-14T06:43:34.7800356Z', + orderHint: '8585516882406130277PO', + assignedBy: { + user: { + displayName: null, + id: '11111-2222-3333', + }, + }, + }, + }, + }, + { + '@odata.etag': 'W/"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBAWCc="', + planId: 'coJdCqzqNUKULQtTRWDa6pgACTln', + bucketId: '2avE1BwPmEKp7Lxh0E-EmZgALF72', + title: '1', + orderHint: '8585516897613076919P1', + assigneePriority: '8585516890164965803', + percentComplete: 0, + startDateTime: null, + createdDateTime: '2022-04-14T06:19:44.2011467Z', + dueDateTime: null, + hasDescription: false, + previewType: 'automatic', + completedDateTime: null, + completedBy: null, + referenceCount: 0, + checklistItemCount: 0, + activeChecklistItemCount: 0, + conversationThreadId: null, + priority: 5, + id: 'J3MLUgtmJ06YJgenyujiYpgANMF1', + createdBy: { + user: { + displayName: null, + id: '11111-2222-3333', + }, + }, + appliedCategories: {}, + assignments: { + '11111-2222-3333': { + '@odata.type': '#microsoft.graph.plannerAssignment', + assignedDateTime: '2022-04-14T06:31:08.9810004Z', + orderHint: '8585516890765590890Pw', + assignedBy: { + user: { + displayName: null, + id: '11111-2222-3333', + }, + }, + }, + }, + }, + { + '@odata.etag': 'W/"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBAVCc="', + planId: 'THwgIivuyU26ki8qS7ufcJgAB6zf', + bucketId: 'CO-ZsX1s4kO7FtO6ZHZdDpgAFL1m', + title: 'td 54', + orderHint: '8585034751365009589', + assigneePriority: '8585034751365009589', + percentComplete: 0, + startDateTime: null, + createdDateTime: '2023-10-24T07:15:48.9766218Z', + dueDateTime: null, + hasDescription: true, + previewType: 'automatic', + completedDateTime: null, + completedBy: null, + referenceCount: 0, + checklistItemCount: 0, + activeChecklistItemCount: 0, + conversationThreadId: null, + priority: 5, + id: 'silreUDQskqFYfrO4EObD5gAKt_G', + createdBy: { + user: { + displayName: null, + id: '11111-2222-3333', + }, + application: { + displayName: null, + id: '11111-2222-3333-44444', + }, + }, + appliedCategories: {}, + assignments: { + '11111-2222-3333': { + '@odata.type': '#microsoft.graph.plannerAssignment', + assignedDateTime: '2023-10-24T07:15:48.9766218Z', + orderHint: '8585034751965947109Pc', + assignedBy: { + user: { + displayName: null, + id: '11111-2222-3333', + }, + application: { + displayName: null, + id: '11111-2222-3333-44444', + }, + }, + }, + }, + }, + ]; + } +}); + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'GET') { + return { + id: '123456789', + }; + } +}); + +describe('Test MicrosoftTeamsV2, task => getAll', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/task/getAll.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith('GET', '/v1.0/me'); + + expect(microsoftApiRequestAllItemsSpy).toHaveBeenCalledTimes(1); + expect(microsoftApiRequestAllItemsSpy).toHaveBeenCalledWith( + 'value', + 'GET', + '/v1.0/users/123456789/planner/tasks', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/getAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/getAll.workflow.json new file mode 100644 index 0000000000000..3f277f31b023b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/getAll.workflow.json @@ -0,0 +1,232 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "resource": "task", + "operation": "getAll", + "groupId": { + "__rl": true, + "value": "1644e7fe-547e-4223-a24f-922395865343", + "mode": "list", + "cachedResultName": "5w1hb7" + }, + "returnAll": true + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "@odata.etag": "W/\"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBAZCc=\"", + "planId": "coJdCqzqNUKULQtTRWDa6pgACTln", + "bucketId": null, + "title": "tada", + "orderHint": "8585516884147534440", + "assigneePriority": "8585516882706975451", + "percentComplete": 10, + "startDateTime": null, + "createdDateTime": "2022-04-14T06:41:10.7241367Z", + "dueDateTime": "2022-04-24T21:00:00Z", + "hasDescription": false, + "previewType": "automatic", + "completedDateTime": null, + "completedBy": null, + "referenceCount": 0, + "checklistItemCount": 0, + "activeChecklistItemCount": 0, + "conversationThreadId": null, + "priority": 5, + "id": "1KgwUqOmbU2C9mZWiqxiv5gAPp8Q", + "createdBy": { + "user": { + "displayName": null, + "id": "11111-2222-3333" + } + }, + "appliedCategories": {}, + "assignments": { + "11111-2222-3333": { + "@odata.type": "#microsoft.graph.plannerAssignment", + "assignedDateTime": "2022-04-14T06:43:34.7800356Z", + "orderHint": "8585516882406130277PO", + "assignedBy": { + "user": { + "displayName": null, + "id": "11111-2222-3333" + } + } + } + } + } + }, + { + "json": { + "@odata.etag": "W/\"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBAWCc=\"", + "planId": "coJdCqzqNUKULQtTRWDa6pgACTln", + "bucketId": "2avE1BwPmEKp7Lxh0E-EmZgALF72", + "title": "1", + "orderHint": "8585516897613076919P1", + "assigneePriority": "8585516890164965803", + "percentComplete": 0, + "startDateTime": null, + "createdDateTime": "2022-04-14T06:19:44.2011467Z", + "dueDateTime": null, + "hasDescription": false, + "previewType": "automatic", + "completedDateTime": null, + "completedBy": null, + "referenceCount": 0, + "checklistItemCount": 0, + "activeChecklistItemCount": 0, + "conversationThreadId": null, + "priority": 5, + "id": "J3MLUgtmJ06YJgenyujiYpgANMF1", + "createdBy": { + "user": { + "displayName": null, + "id": "11111-2222-3333" + } + }, + "appliedCategories": {}, + "assignments": { + "11111-2222-3333": { + "@odata.type": "#microsoft.graph.plannerAssignment", + "assignedDateTime": "2022-04-14T06:31:08.9810004Z", + "orderHint": "8585516890765590890Pw", + "assignedBy": { + "user": { + "displayName": null, + "id": "11111-2222-3333" + } + } + } + } + } + }, + { + "json": { + "@odata.etag": "W/\"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBAVCc=\"", + "planId": "THwgIivuyU26ki8qS7ufcJgAB6zf", + "bucketId": "CO-ZsX1s4kO7FtO6ZHZdDpgAFL1m", + "title": "td 54", + "orderHint": "8585034751365009589", + "assigneePriority": "8585034751365009589", + "percentComplete": 0, + "startDateTime": null, + "createdDateTime": "2023-10-24T07:15:48.9766218Z", + "dueDateTime": null, + "hasDescription": true, + "previewType": "automatic", + "completedDateTime": null, + "completedBy": null, + "referenceCount": 0, + "checklistItemCount": 0, + "activeChecklistItemCount": 0, + "conversationThreadId": null, + "priority": 5, + "id": "silreUDQskqFYfrO4EObD5gAKt_G", + "createdBy": { + "user": { + "displayName": null, + "id": "11111-2222-3333" + }, + "application": { + "displayName": null, + "id": "11111-2222-3333-44444" + } + }, + "appliedCategories": {}, + "assignments": { + "11111-2222-3333": { + "@odata.type": "#microsoft.graph.plannerAssignment", + "assignedDateTime": "2023-10-24T07:15:48.9766218Z", + "orderHint": "8585034751965947109Pc", + "assignedBy": { + "user": { + "displayName": null, + "id": "11111-2222-3333" + }, + "application": { + "displayName": null, + "id": "11111-2222-3333-44444" + } + } + } + } + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "34bcdc66-9dad-4c93-8456-4019f94c2f88", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/update.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/update.test.ts new file mode 100644 index 0000000000000..3f763dd307fd5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/update.test.ts @@ -0,0 +1,65 @@ +import type { INodeTypes } from 'n8n-workflow'; +import nock from 'nock'; +import * as transport from '../../../../v2/transport'; +import { getResultNodeData, setup, workflowToTests } from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +const microsoftApiRequestSpy = jest.spyOn(transport, 'microsoftApiRequest'); + +microsoftApiRequestSpy.mockImplementation(async (method: string) => { + if (method === 'PATCH') { + return {}; + } + if (method === 'GET') { + return { + '@odata.etag': 'W/"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBARCc="', + }; + } +}); + +describe('Test MicrosoftTeamsV2, task => update', () => { + const workflows = ['nodes/Microsoft/Teams/test/v2/node/task/update.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.resetAllMocks(); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(microsoftApiRequestSpy).toHaveBeenCalledTimes(2); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'GET', + '/v1.0/planner/tasks/lDrRJ7N_-06p_26iKBtJ6ZgAKffD', + ); + expect(microsoftApiRequestSpy).toHaveBeenCalledWith( + 'PATCH', + '/v1.0/planner/tasks/lDrRJ7N_-06p_26iKBtJ6ZgAKffD', + { dueDateTime: '2023-10-24T21:00:00.000Z', percentComplete: 78, title: 'do that' }, + {}, + undefined, + { 'If-Match': 'W/"JzEtVGFzayAgQEBAQEBAQEBAQEBAQEBARCc="' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => await testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/update.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/update.workflow.json new file mode 100644 index 0000000000000..1a1d76b00aa50 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/task/update.workflow.json @@ -0,0 +1,96 @@ +{ + "name": "My workflow 35", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 880, + 380 + ] + }, + { + "parameters": { + "resource": "task", + "operation": "update", + "taskId": "lDrRJ7N_-06p_26iKBtJ6ZgAKffD", + "updateFields": { + "dueDateTime": "2023-10-24T21:00:00.000Z", + "percentComplete": 78, + "title": "do that" + } + }, + "id": "6666-5555-77777", + "name": "Microsoft Teams", + "type": "n8n-nodes-base.microsoftTeams", + "typeVersion": 2, + "position": [ + 1100, + 380 + ], + "credentials": { + "microsoftTeamsOAuth2Api": { + "id": "6isd5ytvA0qV78eK", + "name": "Microsoft Teams account" + } + } + }, + { + "parameters": {}, + "id": "9d1a2e59-c71c-486c-b3ac-dec6adbc26b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1400, + 380 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Teams", + "type": "main", + "index": 0 + } + ] + ] + }, + "Microsoft Teams": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "74d2873c-c1e1-4628-b398-e2a72c6eed9c", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/utils.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/utils.test.ts new file mode 100644 index 0000000000000..fa7a3fb06e2ac --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/utils.test.ts @@ -0,0 +1,25 @@ +import { filterSortSearchListItems } from '../../v2/helpers/utils'; + +describe('Test MicrosoftTeamsV2, filterSortSearchListItems', () => { + it('should filter, sort and search list items', () => { + const items = [ + { + name: 'Test1', + value: 'test1', + }, + { + name: 'Test2', + value: 'test2', + }, + ]; + + const result = filterSortSearchListItems(items, 'test1'); + + expect(result).toEqual([ + { + name: 'Test1', + value: 'test1', + }, + ]); + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/ChannelDescription.ts b/packages/nodes-base/nodes/Microsoft/Teams/v1/ChannelDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Teams/ChannelDescription.ts rename to packages/nodes-base/nodes/Microsoft/Teams/v1/ChannelDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Teams/ChannelMessageDescription.ts b/packages/nodes-base/nodes/Microsoft/Teams/v1/ChannelMessageDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Teams/ChannelMessageDescription.ts rename to packages/nodes-base/nodes/Microsoft/Teams/v1/ChannelMessageDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Teams/ChatMessageDescription.ts b/packages/nodes-base/nodes/Microsoft/Teams/v1/ChatMessageDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Teams/ChatMessageDescription.ts rename to packages/nodes-base/nodes/Microsoft/Teams/v1/ChatMessageDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Teams/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Teams/v1/GenericFunctions.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Teams/GenericFunctions.ts rename to packages/nodes-base/nodes/Microsoft/Teams/v1/GenericFunctions.ts diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v1/MicrosoftTeamsV1.node.ts b/packages/nodes-base/nodes/Microsoft/Teams/v1/MicrosoftTeamsV1.node.ts new file mode 100644 index 0000000000000..b394010e19e70 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v1/MicrosoftTeamsV1.node.ts @@ -0,0 +1,683 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { + IExecuteFunctions, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + INodeTypeBaseDescription, +} from 'n8n-workflow'; + +import { + microsoftApiRequest, + microsoftApiRequestAllItems, + prepareMessage, +} from './GenericFunctions'; + +import { channelFields, channelOperations } from './ChannelDescription'; + +import { channelMessageFields, channelMessageOperations } from './ChannelMessageDescription'; + +import { chatMessageFields, chatMessageOperations } from './ChatMessageDescription'; + +import { taskFields, taskOperations } from './TaskDescription'; +import { oldVersionNotice } from '../../../../utils/descriptions'; + +const versionDescription: INodeTypeDescription = { + displayName: 'Microsoft Teams', + name: 'microsoftTeams', + icon: 'file:teams.svg', + group: ['input'], + version: [1, 1.1], + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Teams API', + defaults: { + name: 'Microsoft Teams', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftTeamsOAuth2Api', + required: true, + }, + ], + properties: [ + oldVersionNotice, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Channel', + value: 'channel', + }, + { + name: 'Channel Message (Beta)', + value: 'channelMessage', + }, + { + name: 'Chat Message', + value: 'chatMessage', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: 'channel', + }, + // CHANNEL + ...channelOperations, + ...channelFields, + /// MESSAGE + ...channelMessageOperations, + ...channelMessageFields, + ...chatMessageOperations, + ...chatMessageFields, + ///TASK + ...taskOperations, + ...taskFields, + ], +}; + +export class MicrosoftTeamsV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { + loadOptions: { + // Get all the team's channels to display them to user so that they can + // select them easily + async getChannels(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const teamId = this.getCurrentNodeParameter('teamId') as string; + const { value } = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/teams/${teamId}/channels`, + ); + for (const channel of value) { + const channelName = channel.displayName; + const channelId = channel.id; + returnData.push({ + name: channelName, + value: channelId, + }); + } + return returnData; + }, + // Get all the teams to display them to user so that they can + // select them easily + async getTeams(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { value } = await microsoftApiRequest.call(this, 'GET', '/v1.0/me/joinedTeams'); + for (const team of value) { + const teamName = team.displayName; + const teamId = team.id; + returnData.push({ + name: teamName, + value: teamId, + }); + } + return returnData; + }, + // Get all the groups to display them to user so that they can + // select them easily + async getGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const groupSource = this.getCurrentNodeParameter('groupSource') as string; + let requestUrl = '/v1.0/groups' as string; + if (groupSource === 'mine') { + requestUrl = '/v1.0/me/transitiveMemberOf'; + } + const { value } = await microsoftApiRequest.call(this, 'GET', requestUrl); + for (const group of value) { + returnData.push({ + name: group.displayName || group.mail || group.id, + value: group.id, + description: group.mail, + }); + } + return returnData; + }, + // Get all the plans to display them to user so that they can + // select them easily + async getPlans(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let groupId = this.getCurrentNodeParameter('groupId') as string; + const operation = this.getNodeParameter('operation', 0); + if (operation === 'update' && (groupId === undefined || groupId === null)) { + // groupId not found at base, check updateFields for the groupId + groupId = this.getCurrentNodeParameter('updateFields.groupId') as string; + } + const { value } = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/groups/${groupId}/planner/plans`, + ); + for (const plan of value) { + returnData.push({ + name: plan.title, + value: plan.id, + }); + } + return returnData; + }, + // Get all the plans to display them to user so that they can + // select them easily + async getBuckets(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let planId = this.getCurrentNodeParameter('planId') as string; + const operation = this.getNodeParameter('operation', 0); + if (operation === 'update' && (planId === undefined || planId === null)) { + // planId not found at base, check updateFields for the planId + planId = this.getCurrentNodeParameter('updateFields.planId') as string; + } + const { value } = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/planner/plans/${planId}/buckets`, + ); + for (const bucket of value) { + returnData.push({ + name: bucket.name, + value: bucket.id, + }); + } + return returnData; + }, + // Get all the plans to display them to user so that they can + // select them easily + async getMembers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let groupId = this.getCurrentNodeParameter('groupId') as string; + const operation = this.getNodeParameter('operation', 0); + if (operation === 'update' && (groupId === undefined || groupId === null)) { + // groupId not found at base, check updateFields for the groupId + groupId = this.getCurrentNodeParameter('updateFields.groupId') as string; + } + const { value } = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/groups/${groupId}/members`, + ); + for (const member of value) { + returnData.push({ + name: member.displayName, + value: member.id, + }); + } + return returnData; + }, + // Get all the labels to display them to user so that they can + // select them easily + async getLabels(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + let planId = this.getCurrentNodeParameter('planId') as string; + const operation = this.getNodeParameter('operation', 0); + if (operation === 'update' && (planId === undefined || planId === null)) { + // planId not found at base, check updateFields for the planId + planId = this.getCurrentNodeParameter('updateFields.planId') as string; + } + const { categoryDescriptions } = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/planner/plans/${planId}/details`, + ); + for (const key of Object.keys(categoryDescriptions as IDataObject)) { + if (categoryDescriptions[key] !== null) { + returnData.push({ + name: categoryDescriptions[key], + value: key, + }); + } + } + return returnData; + }, + // Get all the chats to display them to user so that they can + // select them easily + async getChats(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = { + $expand: 'members', + }; + const { value } = await microsoftApiRequest.call(this, 'GET', '/v1.0/chats', {}, qs); + for (const chat of value) { + if (!chat.topic) { + chat.topic = chat.members + .filter((member: IDataObject) => member.displayName) + .map((member: IDataObject) => member.displayName) + .join(', '); + } + const chatName = `${chat.topic || '(no title) - ' + (chat.id as string)} (${ + chat.chatType + })`; + const chatId = chat.id; + returnData.push({ + name: chatName, + value: chatId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const length = items.length; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + const nodeVersion = this.getNode().typeVersion; + const instanceId = this.getInstanceId(); + + for (let i = 0; i < length; i++) { + try { + if (resource === 'channel') { + //https://docs.microsoft.com/en-us/graph/api/channel-post?view=graph-rest-beta&tabs=http + if (operation === 'create') { + const teamId = this.getNodeParameter('teamId', i) as string; + const name = this.getNodeParameter('name', i) as string; + const options = this.getNodeParameter('options', i); + const body: IDataObject = { + displayName: name, + }; + if (options.description) { + body.description = options.description as string; + } + if (options.type) { + body.membershipType = options.type as string; + } + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/v1.0/teams/${teamId}/channels`, + body, + ); + } + //https://docs.microsoft.com/en-us/graph/api/channel-delete?view=graph-rest-beta&tabs=http + if (operation === 'delete') { + const teamId = this.getNodeParameter('teamId', i) as string; + const channelId = this.getNodeParameter('channelId', i) as string; + responseData = await microsoftApiRequest.call( + this, + 'DELETE', + `/v1.0/teams/${teamId}/channels/${channelId}`, + ); + responseData = { success: true }; + } + //https://docs.microsoft.com/en-us/graph/api/channel-get?view=graph-rest-beta&tabs=http + if (operation === 'get') { + const teamId = this.getNodeParameter('teamId', i) as string; + const channelId = this.getNodeParameter('channelId', i) as string; + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/teams/${teamId}/channels/${channelId}`, + ); + } + //https://docs.microsoft.com/en-us/graph/api/channel-list?view=graph-rest-beta&tabs=http + if (operation === 'getAll') { + const teamId = this.getNodeParameter('teamId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/teams/${teamId}/channels`, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/teams/${teamId}/channels`, + {}, + ); + responseData = responseData.splice(0, qs.limit); + } + } + //https://docs.microsoft.com/en-us/graph/api/channel-patch?view=graph-rest-beta&tabs=http + if (operation === 'update') { + const teamId = this.getNodeParameter('teamId', i) as string; + const channelId = this.getNodeParameter('channelId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i); + const body: IDataObject = {}; + if (updateFields.name) { + body.displayName = updateFields.name as string; + } + if (updateFields.description) { + body.description = updateFields.description as string; + } + responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/v1.0/teams/${teamId}/channels/${channelId}`, + body, + ); + responseData = { success: true }; + } + } + if (resource === 'channelMessage') { + //https://docs.microsoft.com/en-us/graph/api/channel-post-messages?view=graph-rest-beta&tabs=http + //https://docs.microsoft.com/en-us/graph/api/channel-post-messagereply?view=graph-rest-beta&tabs=http + if (operation === 'create') { + const teamId = this.getNodeParameter('teamId', i) as string; + const channelId = this.getNodeParameter('channelId', i) as string; + const messageType = this.getNodeParameter('messageType', i) as string; + const message = this.getNodeParameter('message', i) as string; + const options = this.getNodeParameter('options', i); + + let includeLinkToWorkflow = options.includeLinkToWorkflow; + if (includeLinkToWorkflow === undefined) { + includeLinkToWorkflow = nodeVersion >= 1.1; + } + + const body: IDataObject = prepareMessage.call( + this, + message, + messageType, + includeLinkToWorkflow as boolean, + instanceId, + ); + + if (options.makeReply) { + const replyToId = options.makeReply as string; + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/beta/teams/${teamId}/channels/${channelId}/messages/${replyToId}/replies`, + body, + ); + } else { + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/beta/teams/${teamId}/channels/${channelId}/messages`, + body, + ); + } + } + //https://docs.microsoft.com/en-us/graph/api/channel-list-messages?view=graph-rest-beta&tabs=http + if (operation === 'getAll') { + const teamId = this.getNodeParameter('teamId', i) as string; + const channelId = this.getNodeParameter('channelId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/beta/teams/${teamId}/channels/${channelId}/messages`, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/beta/teams/${teamId}/channels/${channelId}/messages`, + {}, + ); + responseData = responseData.splice(0, qs.limit); + } + } + } + if (resource === 'chatMessage') { + // https://docs.microsoft.com/en-us/graph/api/channel-post-messages?view=graph-rest-1.0&tabs=http + if (operation === 'create') { + const chatId = this.getNodeParameter('chatId', i) as string; + const messageType = this.getNodeParameter('messageType', i) as string; + const message = this.getNodeParameter('message', i) as string; + const options = this.getNodeParameter('options', i, {}); + + const includeLinkToWorkflow = + options.includeLinkToWorkflow !== false && nodeVersion >= 1.1; + + const body: IDataObject = prepareMessage.call( + this, + message, + messageType, + includeLinkToWorkflow, + instanceId, + ); + + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/v1.0/chats/${chatId}/messages`, + body, + ); + } + // https://docs.microsoft.com/en-us/graph/api/chat-list-messages?view=graph-rest-1.0&tabs=http + if (operation === 'get') { + const chatId = this.getNodeParameter('chatId', i) as string; + const messageId = this.getNodeParameter('messageId', i) as string; + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/chats/${chatId}/messages/${messageId}`, + ); + } + // https://docs.microsoft.com/en-us/graph/api/chat-list-messages?view=graph-rest-1.0&tabs=http + if (operation === 'getAll') { + const chatId = this.getNodeParameter('chatId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/chats/${chatId}/messages`, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/chats/${chatId}/messages`, + {}, + ); + responseData = responseData.splice(0, qs.limit); + } + } + } + if (resource === 'task') { + //https://docs.microsoft.com/en-us/graph/api/planner-post-tasks?view=graph-rest-1.0&tabs=http + if (operation === 'create') { + const planId = this.getNodeParameter('planId', i) as string; + const bucketId = this.getNodeParameter('bucketId', i) as string; + const title = this.getNodeParameter('title', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i); + const body: IDataObject = { + planId, + bucketId, + title, + }; + Object.assign(body, additionalFields); + + if (body.assignedTo) { + body.assignments = { + [body.assignedTo as string]: { + '@odata.type': 'microsoft.graph.plannerAssignment', + orderHint: ' !', + }, + }; + delete body.assignedTo; + } + + if (Array.isArray(body.labels)) { + body.appliedCategories = (body.labels as string[]).map((label) => ({ + [label]: true, + })); + } + + responseData = await microsoftApiRequest.call( + this, + 'POST', + '/v1.0/planner/tasks', + body, + ); + } + //https://docs.microsoft.com/en-us/graph/api/plannertask-delete?view=graph-rest-1.0&tabs=http + if (operation === 'delete') { + const taskId = this.getNodeParameter('taskId', i) as string; + const task = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/planner/tasks/${taskId}`, + ); + responseData = await microsoftApiRequest.call( + this, + 'DELETE', + `/v1.0/planner/tasks/${taskId}`, + {}, + {}, + undefined, + { 'If-Match': task['@odata.etag'] }, + ); + responseData = { success: true }; + } + //https://docs.microsoft.com/en-us/graph/api/plannertask-get?view=graph-rest-1.0&tabs=http + if (operation === 'get') { + const taskId = this.getNodeParameter('taskId', i) as string; + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/planner/tasks/${taskId}`, + ); + } + if (operation === 'getAll') { + const tasksFor = this.getNodeParameter('tasksFor', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + if (tasksFor === 'member') { + //https://docs.microsoft.com/en-us/graph/api/planneruser-list-tasks?view=graph-rest-1.0&tabs=http + const memberId = this.getNodeParameter('memberId', i) as string; + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/users/${memberId}/planner/tasks`, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/users/${memberId}/planner/tasks`, + {}, + ); + responseData = responseData.splice(0, qs.limit); + } + } else { + //https://docs.microsoft.com/en-us/graph/api/plannerplan-list-tasks?view=graph-rest-1.0&tabs=http + const planId = this.getNodeParameter('planId', i) as string; + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/planner/plans/${planId}/tasks`, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/planner/plans/${planId}/tasks`, + {}, + ); + responseData = responseData.splice(0, qs.limit); + } + } + } + //https://docs.microsoft.com/en-us/graph/api/plannertask-update?view=graph-rest-1.0&tabs=http + if (operation === 'update') { + const taskId = this.getNodeParameter('taskId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i); + const body: IDataObject = {}; + Object.assign(body, updateFields); + + if (body.assignedTo) { + body.assignments = { + [body.assignedTo as string]: { + '@odata.type': 'microsoft.graph.plannerAssignment', + orderHint: ' !', + }, + }; + delete body.assignedTo; + } + + if (body.groupId) { + // tasks are assigned to a plan and bucket, group is used for filtering + delete body.groupId; + } + + if (Array.isArray(body.labels)) { + body.appliedCategories = (body.labels as string[]).map((label) => ({ + [label]: true, + })); + } + + const task = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/planner/tasks/${taskId}`, + ); + + responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/v1.0/planner/tasks/${taskId}`, + body, + {}, + undefined, + { 'If-Match': task['@odata.etag'] }, + ); + + responseData = { success: true }; + } + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + return [returnData]; + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/TaskDescription.ts b/packages/nodes-base/nodes/Microsoft/Teams/v1/TaskDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Teams/TaskDescription.ts rename to packages/nodes-base/nodes/Microsoft/Teams/v1/TaskDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/MicrosoftTeamsV2.node.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/MicrosoftTeamsV2.node.ts new file mode 100644 index 0000000000000..86124dc822c9a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/MicrosoftTeamsV2.node.ts @@ -0,0 +1,28 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { + IExecuteFunctions, + INodeType, + INodeTypeDescription, + INodeTypeBaseDescription, +} from 'n8n-workflow'; + +import { listSearch } from './methods'; +import { router } from './actions/router'; +import { versionDescription } from './actions/versionDescription'; + +export class MicrosoftTeamsV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { listSearch }; + + async execute(this: IExecuteFunctions) { + return await router.call(this); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/create.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/create.operation.ts new file mode 100644 index 0000000000000..dd150cbb41bc6 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/create.operation.ts @@ -0,0 +1,81 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { teamRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + teamRLC, + { + displayName: 'New Channel Name', + name: 'name', + required: true, + type: 'string', + default: '', + placeholder: 'e.g. My New Channel', + description: 'The name of the new channel you want to create', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + default: {}, + placeholder: 'Add Option', + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The description of the channel', + typeOptions: { + rows: 2, + }, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Private', + value: 'private', + }, + { + name: 'Standard', + value: 'standard', + }, + ], + default: 'standard', + description: + 'Standard: Accessible to everyone on the team. Private: Accessible only to a specific group of people within the team.', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['channel'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + //https://docs.microsoft.com/en-us/graph/api/channel-post?view=graph-rest-beta&tabs=http + const teamId = this.getNodeParameter('teamId', i, '', { extractValue: true }) as string; + const name = this.getNodeParameter('name', i) as string; + const options = this.getNodeParameter('options', i); + + const body: IDataObject = { + displayName: name, + }; + if (options.description) { + body.description = options.description as string; + } + if (options.type) { + body.membershipType = options.type as string; + } + return await microsoftApiRequest.call(this, 'POST', `/v1.0/teams/${teamId}/channels`, body); +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/deleteChannel.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/deleteChannel.operation.ts new file mode 100644 index 0000000000000..b4451351c8884 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/deleteChannel.operation.ts @@ -0,0 +1,35 @@ +import { type INodeProperties, type IExecuteFunctions, NodeOperationError } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { channelRLC, teamRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [teamRLC, channelRLC]; + +const displayOptions = { + show: { + resource: ['channel'], + operation: ['deleteChannel'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + //https://docs.microsoft.com/en-us/graph/api/channel-delete?view=graph-rest-beta&tabs=http + + try { + const teamId = this.getNodeParameter('teamId', i, '', { extractValue: true }) as string; + const channelId = this.getNodeParameter('channelId', i, '', { extractValue: true }) as string; + + await microsoftApiRequest.call(this, 'DELETE', `/v1.0/teams/${teamId}/channels/${channelId}`); + return { success: true }; + } catch (error) { + throw new NodeOperationError( + this.getNode(), + "The channel you are trying to delete doesn't exist", + { + description: "Check that the 'Channel' parameter is correctly set", + }, + ); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/get.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/get.operation.ts new file mode 100644 index 0000000000000..5212c1d9adf01 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/get.operation.ts @@ -0,0 +1,38 @@ +import { type INodeProperties, type IExecuteFunctions, NodeOperationError } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { channelRLC, teamRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [teamRLC, channelRLC]; + +const displayOptions = { + show: { + resource: ['channel'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + //https://docs.microsoft.com/en-us/graph/api/channel-get?view=graph-rest-beta&tabs=http + + try { + const teamId = this.getNodeParameter('teamId', i, '', { extractValue: true }) as string; + const channelId = this.getNodeParameter('channelId', i, '', { extractValue: true }) as string; + + return await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/teams/${teamId}/channels/${channelId}`, + ); + } catch (error) { + throw new NodeOperationError( + this.getNode(), + "The channel you are trying to get doesn't exist", + { + description: "Check that the 'Channel' parameter is correctly set", + }, + ); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/getAll.operation.ts new file mode 100644 index 0000000000000..574f7c9fa43a9 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/getAll.operation.ts @@ -0,0 +1,41 @@ +import type { INodeProperties, IExecuteFunctions } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { returnAllOrLimit } from '@utils/descriptions'; +import { microsoftApiRequestAllItems } from '../../transport'; +import { teamRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [teamRLC, ...returnAllOrLimit]; + +const displayOptions = { + show: { + resource: ['channel'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + //https://docs.microsoft.com/en-us/graph/api/channel-list?view=graph-rest-beta&tabs=http + + const teamId = this.getNodeParameter('teamId', i, '', { extractValue: true }) as string; + const returnAll = this.getNodeParameter('returnAll', i); + if (returnAll) { + return await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/teams/${teamId}/channels`, + ); + } else { + const limit = this.getNodeParameter('limit', i); + const responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/teams/${teamId}/channels`, + {}, + ); + return responseData.splice(0, limit); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/index.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/index.ts new file mode 100644 index 0000000000000..e2e164df8a26c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/index.ts @@ -0,0 +1,62 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as deleteChannel from './deleteChannel.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; +import * as update from './update.operation'; + +export { create, deleteChannel, get, getAll, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['channel'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a channel', + action: 'Create channel', + }, + { + name: 'Delete', + value: 'deleteChannel', + description: 'Delete a channel', + action: 'Delete channel', + }, + { + name: 'Get', + value: 'get', + description: 'Get a channel', + action: 'Get channel', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many channels', + action: 'Get many channels', + }, + { + name: 'Update', + value: 'update', + description: 'Update a channel', + action: 'Update channel', + }, + ], + default: 'create', + }, + + ...create.description, + ...deleteChannel.description, + ...get.description, + ...getAll.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/update.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/update.operation.ts new file mode 100644 index 0000000000000..8ea3a622923d7 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channel/update.operation.ts @@ -0,0 +1,69 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { channelRLC, teamRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + teamRLC, + channelRLC, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. My New Channel name', + description: 'The name of the channel', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + default: {}, + placeholder: 'Add Option', + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The description of the channel', + typeOptions: { + rows: 2, + }, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['channel'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + //https://docs.microsoft.com/en-us/graph/api/channel-patch?view=graph-rest-beta&tabs=http + + const teamId = this.getNodeParameter('teamId', i, '', { extractValue: true }) as string; + const channelId = this.getNodeParameter('channelId', i, '', { extractValue: true }) as string; + const newName = this.getNodeParameter('name', i) as string; + const newDescription = this.getNodeParameter('options.description', i, '') as string; + + const body: IDataObject = {}; + if (newName) { + body.displayName = newName; + } + if (newDescription) { + body.description = newDescription; + } + await microsoftApiRequest.call( + this, + 'PATCH', + `/v1.0/teams/${teamId}/channels/${channelId}`, + body, + ); + return { success: true }; +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/create.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/create.operation.ts new file mode 100644 index 0000000000000..d0a02655fc3d4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/create.operation.ts @@ -0,0 +1,120 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { prepareMessage } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { channelRLC, teamRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + teamRLC, + channelRLC, + { + displayName: 'Content Type', + name: 'contentType', + required: true, + type: 'options', + options: [ + { + name: 'Text', + value: 'text', + }, + { + name: 'HTML', + value: 'html', + }, + ], + default: 'text', + description: 'Whether the message is plain text or HTML', + }, + { + displayName: 'Message', + name: 'message', + required: true, + type: 'string', + default: '', + description: 'The content of the message to be sent', + typeOptions: { + rows: 2, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Include Link to Workflow', + name: 'includeLinkToWorkflow', + type: 'boolean', + default: true, + description: + 'Whether to append a link to this workflow at the end of the message. This is helpful if you have many workflows sending messages.', + }, + { + displayName: 'Reply to ID', + name: 'makeReply', + type: 'string', + default: '', + placeholder: 'e.g. 1673348720590', + description: + 'An optional ID of the message you want to reply to. The message ID is the number before "?tenantId" in the message URL.', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['channelMessage'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, + nodeVersion: number, + instanceId: string, +) { + //https://docs.microsoft.com/en-us/graph/api/channel-post-messages?view=graph-rest-beta&tabs=http + //https://docs.microsoft.com/en-us/graph/api/channel-post-messagereply?view=graph-rest-beta&tabs=http + + const teamId = this.getNodeParameter('teamId', i, '', { extractValue: true }) as string; + const channelId = this.getNodeParameter('channelId', i, '', { extractValue: true }) as string; + const contentType = this.getNodeParameter('contentType', i) as string; + const message = this.getNodeParameter('message', i) as string; + const options = this.getNodeParameter('options', i); + + let includeLinkToWorkflow = options.includeLinkToWorkflow; + if (includeLinkToWorkflow === undefined) { + includeLinkToWorkflow = nodeVersion >= 1.1; + } + + const body: IDataObject = prepareMessage.call( + this, + message, + contentType, + includeLinkToWorkflow as boolean, + instanceId, + ); + + if (options.makeReply) { + const replyToId = options.makeReply as string; + return await microsoftApiRequest.call( + this, + 'POST', + `/beta/teams/${teamId}/channels/${channelId}/messages/${replyToId}/replies`, + body, + ); + } else { + return await microsoftApiRequest.call( + this, + 'POST', + `/beta/teams/${teamId}/channels/${channelId}/messages`, + body, + ); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/getAll.operation.ts new file mode 100644 index 0000000000000..0f44e916ae289 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/getAll.operation.ts @@ -0,0 +1,43 @@ +import type { INodeProperties, IExecuteFunctions } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { returnAllOrLimit } from '@utils/descriptions'; +import { microsoftApiRequestAllItems } from '../../transport'; +import { channelRLC, teamRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [teamRLC, channelRLC, ...returnAllOrLimit]; + +const displayOptions = { + show: { + resource: ['channelMessage'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + //https://docs.microsoft.com/en-us/graph/api/channel-list-messages?view=graph-rest-beta&tabs=http + + const teamId = this.getNodeParameter('teamId', i, '', { extractValue: true }) as string; + const channelId = this.getNodeParameter('channelId', i, '', { extractValue: true }) as string; + const returnAll = this.getNodeParameter('returnAll', i); + + if (returnAll) { + return await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/beta/teams/${teamId}/channels/${channelId}/messages`, + ); + } else { + const limit = this.getNodeParameter('limit', i); + const responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/beta/teams/${teamId}/channels/${channelId}/messages`, + {}, + ); + return responseData.splice(0, limit); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/index.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/index.ts new file mode 100644 index 0000000000000..402872b4ec0a6 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/channelMessage/index.ts @@ -0,0 +1,38 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as getAll from './getAll.operation'; + +export { create, getAll }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['channelMessage'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a message in a channel', + action: 'Create message', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many messages from a channel', + action: 'Get many messages', + }, + ], + default: 'create', + }, + + ...create.description, + ...getAll.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/create.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/create.operation.ts new file mode 100644 index 0000000000000..fd19b6f493cb4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/create.operation.ts @@ -0,0 +1,91 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { prepareMessage } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { chatRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + chatRLC, + { + displayName: 'Content Type', + name: 'contentType', + required: true, + type: 'options', + options: [ + { + name: 'Text', + value: 'text', + }, + { + name: 'HTML', + value: 'html', + }, + ], + default: 'text', + description: 'Whether the message is plain text or HTML', + }, + { + displayName: 'Message', + name: 'message', + required: true, + type: 'string', + default: '', + description: 'The content of the message to be sent', + typeOptions: { + rows: 2, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Include Link to Workflow', + name: 'includeLinkToWorkflow', + type: 'boolean', + default: true, + description: + 'Whether to append a link to this workflow at the end of the message. This is helpful if you have many workflows sending messages.', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['chatMessage'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + i: number, + nodeVersion: number, + instanceId: string, +) { + // https://docs.microsoft.com/en-us/graph/api/channel-post-messages?view=graph-rest-1.0&tabs=http + + const chatId = this.getNodeParameter('chatId', i, '', { extractValue: true }) as string; + const contentType = this.getNodeParameter('contentType', i) as string; + const message = this.getNodeParameter('message', i) as string; + const options = this.getNodeParameter('options', i, {}); + + const includeLinkToWorkflow = options.includeLinkToWorkflow !== false; + + const body: IDataObject = prepareMessage.call( + this, + message, + contentType, + includeLinkToWorkflow, + instanceId, + ); + + return await microsoftApiRequest.call(this, 'POST', `/v1.0/chats/${chatId}/messages`, body); +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/get.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/get.operation.ts new file mode 100644 index 0000000000000..74e675725bc0f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/get.operation.ts @@ -0,0 +1,49 @@ +import { type INodeProperties, type IExecuteFunctions, NodeOperationError } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { chatRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [ + chatRLC, + { + displayName: 'Message ID', + name: 'messageId', + required: true, + type: 'string', + default: '', + placeholder: 'e.g. 1673355049064', + description: 'The ID of the message to retrieve', + }, +]; + +const displayOptions = { + show: { + resource: ['chatMessage'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + // https://docs.microsoft.com/en-us/graph/api/chat-list-messages?view=graph-rest-1.0&tabs=http + + try { + const chatId = this.getNodeParameter('chatId', i, '', { extractValue: true }) as string; + const messageId = this.getNodeParameter('messageId', i) as string; + + return await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/chats/${chatId}/messages/${messageId}`, + ); + } catch (error) { + throw new NodeOperationError( + this.getNode(), + "The message you are trying to get doesn't exist", + { + description: "Check that the 'Message ID' parameter is correctly set", + }, + ); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/getAll.operation.ts new file mode 100644 index 0000000000000..fe54bff3e81c8 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/getAll.operation.ts @@ -0,0 +1,42 @@ +import type { INodeProperties, IExecuteFunctions } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { returnAllOrLimit } from '@utils/descriptions'; +import { microsoftApiRequestAllItems } from '../../transport'; +import { chatRLC } from '../../descriptions'; + +const properties: INodeProperties[] = [chatRLC, ...returnAllOrLimit]; + +const displayOptions = { + show: { + resource: ['chatMessage'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + // https://docs.microsoft.com/en-us/graph/api/chat-list-messages?view=graph-rest-1.0&tabs=http + + const chatId = this.getNodeParameter('chatId', i, '', { extractValue: true }) as string; + const returnAll = this.getNodeParameter('returnAll', i); + + if (returnAll) { + return await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/chats/${chatId}/messages`, + ); + } else { + const limit = this.getNodeParameter('limit', i); + const responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/chats/${chatId}/messages`, + {}, + ); + return responseData.splice(0, limit); + } +} 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 new file mode 100644 index 0000000000000..1ae403bddc84a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMessage/index.ts @@ -0,0 +1,46 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; + +export { create, get, getAll }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['chatMessage'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a message in a chat', + action: 'Create chat message', + }, + { + name: 'Get', + value: 'get', + description: 'Get a message from a chat', + action: 'Get chat message', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many messages from a chat', + action: 'Get many chat messages', + }, + ], + default: 'create', + }, + + ...create.description, + ...get.description, + ...getAll.description, +]; 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 new file mode 100644 index 0000000000000..f4c7753709d58 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/node.type.ts @@ -0,0 +1,10 @@ +import type { AllEntities } from 'n8n-workflow'; + +type NodeMap = { + channel: 'create' | 'deleteChannel' | 'get' | 'getAll' | 'update'; + channelMessage: 'create' | 'getAll'; + chatMessage: 'create' | 'get' | 'getAll'; + task: 'create' | 'deleteTask' | 'get' | 'getAll' | 'update'; +}; + +export type MicrosoftTeamsType = AllEntities; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts new file mode 100644 index 0000000000000..8497958357fea --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts @@ -0,0 +1,82 @@ +import { + type IExecuteFunctions, + type IDataObject, + type INodeExecutionData, + NodeOperationError, +} from 'n8n-workflow'; + +import type { MicrosoftTeamsType } from './node.type'; + +import * as channel from './channel'; +import * as channelMessage from './channelMessage'; +import * as chatMessage from './chatMessage'; +import * as task from './task'; + +export async function router(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + let responseData; + + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + const nodeVersion = this.getNode().typeVersion; + const instanceId = this.getInstanceId(); + + const microsoftTeamsTypeData = { + resource, + operation, + } as MicrosoftTeamsType; + + for (let i = 0; i < items.length; i++) { + try { + switch (microsoftTeamsTypeData.resource) { + case 'channel': + responseData = await channel[microsoftTeamsTypeData.operation].execute.call(this, i); + break; + case 'channelMessage': + responseData = await channelMessage[microsoftTeamsTypeData.operation].execute.call( + this, + i, + nodeVersion, + instanceId, + ); + break; + case 'chatMessage': + responseData = await chatMessage[microsoftTeamsTypeData.operation].execute.call( + this, + i, + nodeVersion, + instanceId, + ); + break; + case 'task': + responseData = await task[microsoftTeamsTypeData.operation].execute.call(this, i); + break; + default: + throw new NodeOperationError( + this.getNode(), + `The operation "${operation}" is not supported!`, + ); + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + return [returnData]; +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/create.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/create.operation.ts new file mode 100644 index 0000000000000..0f66c03ab7e85 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/create.operation.ts @@ -0,0 +1,109 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { bucketRLC, groupRLC, memberRLC, planRLC } from '../../descriptions'; +import { microsoftApiRequest } from '../../transport'; +import { DateTime } from 'luxon'; + +const properties: INodeProperties[] = [ + groupRLC, + planRLC, + bucketRLC, + { + displayName: 'Title', + name: 'title', + required: true, + type: 'string', + default: '', + placeholder: 'e.g. new task', + description: 'Title of the task', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + default: {}, + placeholder: 'Add Option', + options: [ + { + ...memberRLC, + displayName: 'Assigned To', + name: 'assignedTo', + description: 'Who the task should be assigned to', + typeOptions: { + loadOptionsDependsOn: ['groupId.balue'], + }, + }, + { + displayName: 'Due Date Time', + name: 'dueDateTime', + type: 'string', + validateType: 'dateTime', + default: '', + description: + 'Date and time at which the task is due. The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time.', + }, + { + displayName: 'Percent Complete', + name: 'percentComplete', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 100, + }, + default: 0, + placeholder: 'e.g. 75', + description: + 'Percentage of task completion. When set to 100, the task is considered completed.', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['task'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + //https://docs.microsoft.com/en-us/graph/api/planner-post-tasks?view=graph-rest-1.0&tabs=http + + const planId = this.getNodeParameter('planId', i, '', { extractValue: true }) as string; + const bucketId = this.getNodeParameter('bucketId', i, '', { extractValue: true }) as string; + + const title = this.getNodeParameter('title', i) as string; + const options = this.getNodeParameter('options', i); + + const body: IDataObject = { + planId, + bucketId, + title, + }; + + if (options.assignedTo) { + options.assignedTo = this.getNodeParameter('options.assignedTo', i, '', { + extractValue: true, + }) as string; + } + + if (options.dueDateTime && options.dueDateTime instanceof DateTime) { + options.dueDateTime = options.dueDateTime.toISO(); + } + + Object.assign(body, options); + + if (body.assignedTo) { + body.assignments = { + [body.assignedTo as string]: { + '@odata.type': 'microsoft.graph.plannerAssignment', + orderHint: ' !', + }, + }; + delete body.assignedTo; + } + + return await microsoftApiRequest.call(this, 'POST', '/v1.0/planner/tasks', body); +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/deleteTask.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/deleteTask.operation.ts new file mode 100644 index 0000000000000..a1599e05daed8 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/deleteTask.operation.ts @@ -0,0 +1,41 @@ +import type { INodeProperties, IExecuteFunctions } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { microsoftApiRequest } from '../../transport'; + +const properties: INodeProperties[] = [ + { + displayName: 'Task ID', + name: 'taskId', + required: true, + type: 'string', + placeholder: 'e.g. h3ufgLvXPkSRzYm-zO5cY5gANtBQ', + description: 'The ID of the task to delete', + default: '', + }, +]; + +const displayOptions = { + show: { + resource: ['task'], + operation: ['deleteTask'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + //https://docs.microsoft.com/en-us/graph/api/plannertask-delete?view=graph-rest-1.0&tabs=http + + const taskId = this.getNodeParameter('taskId', i) as string; + const task = await microsoftApiRequest.call(this, 'GET', `/v1.0/planner/tasks/${taskId}`); + await microsoftApiRequest.call( + this, + 'DELETE', + `/v1.0/planner/tasks/${taskId}`, + {}, + {}, + undefined, + { 'If-Match': task['@odata.etag'] }, + ); + return { success: true }; +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/get.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/get.operation.ts new file mode 100644 index 0000000000000..7510264d1a6bf --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/get.operation.ts @@ -0,0 +1,31 @@ +import type { INodeProperties, IExecuteFunctions } from 'n8n-workflow'; +import { updateDisplayOptions } from '@utils/utilities'; +import { microsoftApiRequest } from '../../transport'; + +const properties: INodeProperties[] = [ + { + displayName: 'Task ID', + name: 'taskId', + required: true, + type: 'string', + description: 'The ID of the task to retrieve', + placeholder: 'e.g. h3ufgLvXPkSRzYm-zO5cY5gANtBQ', + default: '', + }, +]; + +const displayOptions = { + show: { + resource: ['task'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + //https://docs.microsoft.com/en-us/graph/api/plannertask-get?view=graph-rest-1.0&tabs=http + + const taskId = this.getNodeParameter('taskId', i) as string; + return await microsoftApiRequest.call(this, 'GET', `/v1.0/planner/tasks/${taskId}`); +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/getAll.operation.ts new file mode 100644 index 0000000000000..d7a2894e19e31 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/getAll.operation.ts @@ -0,0 +1,97 @@ +import type { INodeProperties, IExecuteFunctions } from 'n8n-workflow'; +import { groupRLC, planRLC } from '../../descriptions'; +import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { returnAllOrLimit } from '@utils/descriptions'; + +const properties: INodeProperties[] = [ + { + displayName: 'Tasks For', + name: 'tasksFor', + default: 'member', + required: true, + type: 'options', + description: 'Whether to retrieve the tasks for a user or for a plan', + options: [ + { + name: 'Group Member', + value: 'member', + description: 'Tasks assigned to group member', + }, + { + name: 'Plan', + value: 'plan', + description: 'Tasks in group plan', + }, + ], + }, + groupRLC, + { + ...planRLC, + displayOptions: { + show: { + tasksFor: ['plan'], + }, + }, + }, + ...returnAllOrLimit, +]; + +const displayOptions = { + show: { + resource: ['task'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + const tasksFor = this.getNodeParameter('tasksFor', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + + if (tasksFor === 'member') { + //https://docs.microsoft.com/en-us/graph/api/planneruser-list-tasks?view=graph-rest-1.0&tabs=http + const memberId = ((await microsoftApiRequest.call(this, 'GET', '/v1.0/me')) as { id: string }) + .id; + if (returnAll) { + return await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/users/${memberId}/planner/tasks`, + ); + } else { + const limit = this.getNodeParameter('limit', i); + const responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/users/${memberId}/planner/tasks`, + {}, + ); + return responseData.splice(0, limit); + } + } else { + //https://docs.microsoft.com/en-us/graph/api/plannerplan-list-tasks?view=graph-rest-1.0&tabs=http + const planId = this.getNodeParameter('planId', i, '', { extractValue: true }) as string; + if (returnAll) { + return await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/planner/plans/${planId}/tasks`, + ); + } else { + const limit = this.getNodeParameter('limit', i); + const responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/v1.0/planner/plans/${planId}/tasks`, + {}, + ); + return responseData.splice(0, limit); + } + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/index.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/index.ts new file mode 100644 index 0000000000000..b71c9d7731cdf --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/index.ts @@ -0,0 +1,62 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as deleteTask from './deleteTask.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; +import * as update from './update.operation'; + +export { create, deleteTask, get, getAll, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['task'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a task', + action: 'Create task', + }, + { + name: 'Delete', + value: 'deleteTask', + description: 'Delete a task', + action: 'Delete task', + }, + { + name: 'Get', + value: 'get', + description: 'Get a task', + action: 'Get task', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many tasks', + action: 'Get many tasks', + }, + { + name: 'Update', + value: 'update', + description: 'Update a task', + action: 'Update task', + }, + ], + default: 'create', + }, + + ...create.description, + ...deleteTask.description, + ...get.description, + ...getAll.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/update.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/update.operation.ts new file mode 100644 index 0000000000000..43a3ca788f8cb --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/task/update.operation.ts @@ -0,0 +1,155 @@ +import type { INodeProperties, IExecuteFunctions, IDataObject } from 'n8n-workflow'; +import { bucketRLC, groupRLC, memberRLC, planRLC } from '../../descriptions'; +import { microsoftApiRequest } from '../../transport'; +import { updateDisplayOptions } from '@utils/utilities'; +import { DateTime } from 'luxon'; + +const properties: INodeProperties[] = [ + { + displayName: 'Task ID', + name: 'taskId', + required: true, + type: 'string', + default: '', + placeholder: 'e.g. h3ufgLvXPkSRzYm-zO5cY5gANtBQ', + description: 'The ID of the task to update', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + default: {}, + placeholder: 'Add Field', + options: [ + { + ...memberRLC, + displayName: 'Assigned To', + name: 'assignedTo', + description: 'Who the task should be assigned to', + hint: "Select 'Team' from options first", + required: false, + typeOptions: { + loadOptionsDependsOn: ['updateFields.groupId.value'], + }, + }, + { + ...bucketRLC, + required: false, + typeOptions: { + loadOptionsDependsOn: ['updateFields.planId.value'], + }, + }, + { + displayName: 'Due Date Time', + name: 'dueDateTime', + type: 'string', + validateType: 'dateTime', + default: '', + description: + 'Date and time at which the task is due. The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time.', + }, + { + ...groupRLC, + required: false, + typeOptions: { + loadOptionsDependsOn: ['/groupSource'], + }, + }, + { + displayName: 'Percent Complete', + name: 'percentComplete', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 100, + }, + default: 0, + placeholder: 'e.g. 75', + description: + 'Percentage of task completion. When set to 100, the task is considered completed.', + }, + { + ...planRLC, + required: false, + hint: "Select 'Team' from options first", + typeOptions: { + loadOptionsDependsOn: ['updateFields.groupId.value'], + }, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + placeholder: 'e.g. my task', + description: 'Title of the task', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['task'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + //https://docs.microsoft.com/en-us/graph/api/plannertask-update?view=graph-rest-1.0&tabs=http + + const taskId = this.getNodeParameter('taskId', i, '', { extractValue: true }) as string; + const updateFields = this.getNodeParameter('updateFields', i); + + for (const key of Object.keys(updateFields)) { + if (key === 'groupId') { + // tasks are assigned to a plan and bucket, group is used for filtering + delete updateFields.groupId; + continue; + } + + if (key === 'assignedTo') { + const assignedTo = this.getNodeParameter('updateFields.assignedTo', i, '', { + extractValue: true, + }) as string; + + updateFields.assignments = { + [assignedTo]: { + '@odata.type': 'microsoft.graph.plannerAssignment', + orderHint: ' !', + }, + }; + delete updateFields.assignedTo; + continue; + } + + if (['bucketId', 'planId'].includes(key)) { + updateFields[key] = this.getNodeParameter(`updateFields.${key}`, i, '', { + extractValue: true, + }) as string; + } + + if (key === 'dueDateTime' && updateFields.dueDateTime instanceof DateTime) { + updateFields.dueDateTime = updateFields.dueDateTime.toISO(); + } + } + + const body: IDataObject = {}; + Object.assign(body, updateFields); + + const task = await microsoftApiRequest.call(this, 'GET', `/v1.0/planner/tasks/${taskId}`); + + await microsoftApiRequest.call( + this, + 'PATCH', + `/v1.0/planner/tasks/${taskId}`, + body, + {}, + undefined, + { 'If-Match': task['@odata.etag'] }, + ); + + return { success: true }; +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/versionDescription.ts new file mode 100644 index 0000000000000..a3d764687a927 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/versionDescription.ts @@ -0,0 +1,60 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { INodeTypeDescription } from 'n8n-workflow'; + +import * as channel from './channel'; +import * as channelMessage from './channelMessage'; +import * as chatMessage from './chatMessage'; +import * as task from './task'; + +export const versionDescription: INodeTypeDescription = { + displayName: 'Microsoft Teams', + name: 'microsoftTeams', + icon: 'file:teams.svg', + group: ['input'], + version: 2, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Teams API', + defaults: { + name: 'Microsoft Teams', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftTeamsOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Channel', + value: 'channel', + }, + { + name: 'Channel Message', + value: 'channelMessage', + }, + { + name: 'Chat Message', + value: 'chatMessage', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: 'channel', + }, + + ...channel.description, + ...channelMessage.description, + ...chatMessage.description, + ...task.description, + ], +}; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/common.description.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/common.description.ts new file mode 100644 index 0000000000000..6736c70917636 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/common.description.ts @@ -0,0 +1,22 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const groupSourceOptions: INodeProperties = { + displayName: 'Group Source', + name: 'groupSource', + required: true, + type: 'options', + default: 'all', + description: 'From where to select groups and teams', + options: [ + { + name: 'All Groups', + value: 'all', + description: 'From all groups', + }, + { + name: 'My Groups', + value: 'mine', + description: 'Only load groups that account is member of', + }, + ], +}; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/index.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/index.ts new file mode 100644 index 0000000000000..74ac575de09e1 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/index.ts @@ -0,0 +1,2 @@ +export * from './rlc.description'; +export * from './common.description'; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/rlc.description.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/rlc.description.ts new file mode 100644 index 0000000000000..37fcffc267cf8 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/descriptions/rlc.description.ts @@ -0,0 +1,269 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const teamRLC: INodeProperties = { + displayName: 'Team', + name: 'teamId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + description: + 'Select the team from the list, by URL, or by ID (the ID is the "groupId" parameter in the URL you get from "Get a link to the team")', + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'e.g. My Team', + typeOptions: { + searchListMethod: 'getTeams', + searchable: true, + }, + }, + { + displayName: 'From URL', + name: 'url', + type: 'string', + placeholder: 'e.g. https://teams.microsoft.com/l/team/19%3AP8l9gXd6oqlgq…', + extractValue: { + type: 'regex', + regex: 'groupId=([a-f0-9-]+)\\&', + }, + validation: [ + { + type: 'regex', + properties: { + regex: 'https:\\/\\/teams.microsoft.com\\/.*groupId=[a-f0-9-]+\\&.*', + errorMessage: 'Not a valid Microsoft Teams URL', + }, + }, + ], + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + placeholder: 'e.g. 61165b04-e4cc-4026-b43f-926b4e2a7182', + validation: [ + { + type: 'regex', + properties: { + regex: '^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})[ \t]*', + errorMessage: 'Not a valid Microsoft Teams Team ID', + }, + }, + ], + extractValue: { + type: 'regex', + regex: '^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})', + }, + }, + ], +}; + +export const channelRLC: INodeProperties = { + displayName: 'Channel', + name: 'channelId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + description: + 'Select the channel from the list, by URL, or by ID (the ID is the "threadId" in the URL)', + typeOptions: { + loadOptionsDependsOn: ['teamId.value'], + }, + 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: '19:-xlxyqXNSCxpI1SDzgQ_L9ZvzSR26pgphq1BJ9y7QJE1@thread.tacv2', + // validation missing because no documentation found how these unique ids look like. + }, + ], +}; + +export const chatRLC: INodeProperties = { + displayName: 'Chat', + name: 'chatId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + description: + 'Select the chat from the list, by URL, or by ID (find the chat ID after "conversations/" in the 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_0c5cfdbb-596f-4d39-b557-5d9516c94107@unq.gbl.spaces', + // validation missing because no documentation found how these unique chat ids look like. + url: '=https://teams.microsoft.com/l/chat/{{encodeURIComponent($value)}}/0', + }, + ], +}; + +export const groupRLC: INodeProperties = { + displayName: 'Team', + name: 'groupId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + typeOptions: { + loadOptionsDependsOn: ['groupSource'], + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a Team...', + typeOptions: { + searchListMethod: 'getGroups', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + placeholder: '12f0ca7d-b77f-4c4e-93d2-5cbdb4f464c6', + validation: [ + { + type: 'regex', + properties: { + regex: '^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})[ \t]*', + errorMessage: 'Not a valid Microsoft Teams Team ID', + }, + }, + ], + extractValue: { + type: 'regex', + regex: '^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})', + }, + }, + ], +}; + +export const planRLC: INodeProperties = { + displayName: 'Plan', + name: 'planId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + typeOptions: { + loadOptionsDependsOn: ['groupId.value'], + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a Plan...', + typeOptions: { + searchListMethod: 'getPlans', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + placeholder: 'rl1HYb0cUEiHPc7zgB_KWWUAA7Of', + // validation missing because no documentation found how these unique ids look like. + }, + ], + description: 'The plan for the task to belong to', +}; + +export const bucketRLC: INodeProperties = { + displayName: 'Bucket', + name: 'bucketId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + typeOptions: { + loadOptionsDependsOn: ['planId.value'], + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a Bucket...', + typeOptions: { + searchListMethod: 'getBuckets', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + placeholder: 'rl1HYb0cUEiHPc7zgB_KWWUAA7Of', + // validation missing because no documentation found how these unique ids look like. + }, + ], + description: 'The bucket for the task to belong to', +}; + +export const memberRLC: INodeProperties = { + displayName: 'Member', + name: 'memberId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + typeOptions: { + loadOptionsDependsOn: ['groupId.value'], + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + placeholder: 'Select a Member...', + typeOptions: { + searchListMethod: 'getMembers', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + placeholder: '7e2f1174-e8ee-4859-b8b1-a8d1cc63d276', + validation: [ + { + type: 'regex', + properties: { + regex: '^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})[ \t]*', + errorMessage: 'Not a valid Microsoft Teams Team ID', + }, + }, + ], + extractValue: { + type: 'regex', + regex: '^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})', + }, + }, + ], +}; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/helpers/utils.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/helpers/utils.ts new file mode 100644 index 0000000000000..6f0b9a9ab62c4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/helpers/utils.ts @@ -0,0 +1,44 @@ +import type { IExecuteFunctions, ILoadOptionsFunctions, INodeListSearchItems } from 'n8n-workflow'; + +export function prepareMessage( + this: IExecuteFunctions | ILoadOptionsFunctions, + message: string, + contentType: string, + includeLinkToWorkflow: boolean, + instanceId?: string, +) { + if (includeLinkToWorkflow) { + const { id } = this.getWorkflow(); + const link = `${this.getInstanceBaseUrl()}workflow/${id}?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=${encodeURIComponent( + 'n8n-nodes-base.microsoftTeams', + )}${instanceId ? '_' + instanceId : ''}`; + contentType = 'html'; + message = `${message}

Powered by this n8n workflow `; + } + + return { + body: { + contentType, + content: message, + }, + }; +} + +export function filterSortSearchListItems(items: INodeListSearchItems[], filter?: string) { + return items + .filter( + (item) => + !filter || + item.name.toLowerCase().includes(filter.toLowerCase()) || + item.value.toString().toLowerCase().includes(filter.toLowerCase()), + ) + .sort((a, b) => { + if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { + return -1; + } + if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { + return 1; + } + return 0; + }); +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/methods/index.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/methods/index.ts new file mode 100644 index 0000000000000..c7fb720e474f5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/methods/index.ts @@ -0,0 +1 @@ +export * as listSearch from './listSearch'; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/methods/listSearch.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/methods/listSearch.ts new file mode 100644 index 0000000000000..3b7c0ba125367 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/methods/listSearch.ts @@ -0,0 +1,279 @@ +import { + NodeOperationError, + type IDataObject, + type ILoadOptionsFunctions, + type INodeListSearchItems, + type INodeListSearchResult, + sleep, +} from 'n8n-workflow'; +import { microsoftApiRequest } from '../transport'; +import { filterSortSearchListItems } from '../helpers/utils'; + +export async function getChats( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const returnData: INodeListSearchItems[] = []; + const qs: IDataObject = { + $expand: 'members', + }; + + let value: IDataObject[] = []; + let attempts = 5; + do { + try { + value = ((await microsoftApiRequest.call(this, 'GET', '/v1.0/chats', {}, qs)) as IDataObject) + .value as IDataObject[]; + break; + } catch (error) { + if (attempts > 0) { + await sleep(1000); + attempts--; + } else { + throw new NodeOperationError(this.getNode(), error); + } + } + } while (attempts > 0); + + for (const chat of value) { + if (!chat.topic) { + chat.topic = (chat.members as IDataObject[]) + .filter((member: IDataObject) => member.displayName) + .map((member: IDataObject) => member.displayName) + .join(', '); + } + const chatName = `${chat.topic || '(no title) - ' + chat.id} (${chat.chatType})`; + const chatId = chat.id; + const url = chat.webUrl as string; + returnData.push({ + name: chatName, + value: chatId as string, + url, + }); + } + + const results = returnData + .filter( + (item) => + !filter || + item.name.toLowerCase().includes(filter.toLowerCase()) || + item.value.toString().toLowerCase().includes(filter.toLowerCase()), + ) + .sort((a, b) => { + if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { + return -1; + } + if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { + return 1; + } + return 0; + }); + + return { results }; +} + +export async function getTeams( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const returnData: INodeListSearchItems[] = []; + const { value } = await microsoftApiRequest.call(this, 'GET', '/v1.0/me/joinedTeams'); + + for (const team of value) { + const teamName = team.displayName; + const teamId = team.id; + // let channelId: string = ''; + + // try { + // const channels = await microsoftApiRequestAllItems.call( + // this, + // 'value', + // 'GET', + // `/v1.0/teams/${teamId}/channels`, + // {}, + // ); + + // if (channels.length > 0) { + // channelId = channels.find((channel: IDataObject) => channel.displayName === 'General').id; + // if (!channelId) { + // channelId = channels[0].id; + // } + // } + // } catch (error) {} + + returnData.push({ + name: teamName, + value: teamId, + // url: channelId + // ? `https://teams.microsoft.com/l/team/${channelId}/conversations?groupId=${teamId}&tenantId=${team.tenantId}` + // : undefined, + }); + } + const results = filterSortSearchListItems(returnData, filter); + + return { results }; +} + +export async function getChannels( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const returnData: INodeListSearchItems[] = []; + const teamId = this.getCurrentNodeParameter('teamId', { extractValue: true }) as string; + const operation = this.getNodeParameter('operation', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; + + const excludeGeneralChannel = ['deleteChannel']; + + if (resource === 'channel') excludeGeneralChannel.push('update'); + + const { value } = await microsoftApiRequest.call(this, 'GET', `/v1.0/teams/${teamId}/channels`); + + for (const channel of value) { + if (channel.displayName === 'General' && excludeGeneralChannel.includes(operation)) { + continue; + } + const channelName = channel.displayName; + const channelId = channel.id; + const url = channel.webUrl; + returnData.push({ + name: channelName, + value: channelId, + url, + }); + } + + const results = filterSortSearchListItems(returnData, filter); + return { results }; +} + +export async function getGroups( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const returnData: INodeListSearchItems[] = []; + // const groupSource = this.getCurrentNodeParameter('groupSource') as string; + const requestUrl = '/v1.0/groups' as string; + + // if (groupSource === 'mine') { + // requestUrl = '/v1.0/me/transitiveMemberOf'; + // } + + const { value } = await microsoftApiRequest.call(this, 'GET', requestUrl); + + for (const group of value) { + if (group.displayName === 'All Company') continue; + + const name = group.displayName || group.mail; + + if (name === undefined) continue; + + returnData.push({ + name, + value: group.id, + }); + } + + const results = filterSortSearchListItems(returnData, filter); + return { results }; +} + +export async function getPlans( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const returnData: INodeListSearchItems[] = []; + + let groupId = ''; + + try { + groupId = this.getCurrentNodeParameter('groupId', { extractValue: true }) as string; + } catch (error) {} + + const operation = this.getNodeParameter('operation', 0) as string; + + if (operation === 'update' && !groupId) { + groupId = this.getCurrentNodeParameter('updateFields.groupId', { + extractValue: true, + }) as string; + } + + const { value } = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/groups/${groupId}/planner/plans`, + ); + for (const plan of value) { + returnData.push({ + name: plan.title, + value: plan.id, + }); + } + const results = filterSortSearchListItems(returnData, filter); + return { results }; +} + +export async function getBuckets( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const returnData: INodeListSearchItems[] = []; + let planId = ''; + + try { + planId = this.getCurrentNodeParameter('planId', { extractValue: true }) as string; + } catch (error) {} + + const operation = this.getNodeParameter('operation', 0) as string; + + if (operation === 'update' && !planId) { + planId = this.getCurrentNodeParameter('updateFields.planId', { + extractValue: true, + }) as string; + } + + const { value } = await microsoftApiRequest.call( + this, + 'GET', + `/v1.0/planner/plans/${planId}/buckets`, + ); + for (const bucket of value) { + returnData.push({ + name: bucket.name, + value: bucket.id, + }); + } + const results = filterSortSearchListItems(returnData, filter); + return { results }; +} + +export async function getMembers( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const returnData: INodeListSearchItems[] = []; + let groupId = ''; + + try { + groupId = this.getCurrentNodeParameter('groupId', { extractValue: true }) as string; + } catch (error) {} + + const operation = this.getNodeParameter('operation', 0) as string; + + if (operation === 'update' && !groupId) { + groupId = this.getCurrentNodeParameter('updateFields.groupId', { + extractValue: true, + }) as string; + } + const { value } = await microsoftApiRequest.call(this, 'GET', `/v1.0/groups/${groupId}/members`); + + for (const member of value) { + returnData.push({ + name: member.displayName, + value: member.id, + }); + } + + const results = filterSortSearchListItems(returnData, filter); + return { results }; +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/transport/index.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/transport/index.ts new file mode 100644 index 0000000000000..5ae6a42566b65 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/transport/index.ts @@ -0,0 +1,103 @@ +import type { OptionsWithUri } from 'request'; + +import type { + IExecuteFunctions, + ILoadOptionsFunctions, + IDataObject, + JsonObject, +} from 'n8n-workflow'; +import { NodeApiError } from 'n8n-workflow'; +import { capitalize } from '../../../../../utils/utilities'; + +export async function microsoftApiRequest( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: any = {}, + qs: IDataObject = {}, + uri?: string, + headers: IDataObject = {}, +): Promise { + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://graph.microsoft.com${resource}`, + json: true, + }; + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'microsoftTeamsOAuth2Api', options); + } catch (error) { + const errorOptions: IDataObject = {}; + if (error.error && error.error.error) { + const httpCode = error.statusCode; + error = error.error.error; + error.statusCode = httpCode; + errorOptions.message = error.message; + + if (error.code === 'NotFound' && error.message === 'Resource not found') { + const nodeResource = capitalize(this.getNodeParameter('resource', 0) as string); + errorOptions.message = `${nodeResource} not found`; + } + } + throw new NodeApiError(this.getNode(), error as JsonObject, errorOptions); + } +} + +export async function microsoftApiRequestAllItems( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + + body: any = {}, + query: IDataObject = {}, +): Promise { + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData['@odata.nextLink']; + returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); + const limit = query.limit as number | undefined; + if (limit && limit <= returnData.length) { + return returnData; + } + } while (responseData['@odata.nextLink'] !== undefined); + + return returnData; +} + +export async function microsoftApiRequestAllItemsSkip( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + + body: any = {}, + query: IDataObject = {}, +): Promise { + const returnData: IDataObject[] = []; + + let responseData; + query.$top = 100; + query.$skip = 0; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query); + query.$skip += query.$top; + returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); + } while (responseData.value.length !== 0); + + return returnData; +}