From 42dcc709f78ae8bf048fddefe8a658a312e44fb7 Mon Sep 17 00:00:00 2001 From: Yen Su <49794855+yens1@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:24:43 +0000 Subject: [PATCH] feat(Microsoft Teams Node): Add the chat member get many operation (#37745) --- .../__schema__/v2.0.0/chatMember/getAll.json | 33 ++++++ .../chatMember/getAll.returnAll.workflow.json | 107 ++++++++++++++++++ .../test/v2/node/chatMember/getAll.test.ts | 57 ++++++++++ .../v2/node/chatMember/getAll.workflow.json | 94 +++++++++++++++ .../test/v2/node/servicePrincipal.test.ts | 20 ++++ .../v2/servicePrincipalDisplayOptions.test.ts | 100 ++++++---------- .../v2/actions/chatMember/getAll.operation.ts | 38 +++++++ .../Teams/v2/actions/chatMember/index.ts | 47 ++++++++ .../v2/actions/chatMember/sharedGuard.ts | 25 ++++ .../Microsoft/Teams/v2/actions/node.type.ts | 1 + .../Microsoft/Teams/v2/actions/router.ts | 4 + .../Teams/v2/actions/versionDescription.ts | 6 + .../nodes-base/utils/microsoft/transport.ts | 2 +- 13 files changed, 465 insertions(+), 69 deletions(-) create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/__schema__/v2.0.0/chatMember/getAll.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.returnAll.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.workflow.json create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/sharedGuard.ts diff --git a/packages/nodes-base/nodes/Microsoft/Teams/__schema__/v2.0.0/chatMember/getAll.json b/packages/nodes-base/nodes/Microsoft/Teams/__schema__/v2.0.0/chatMember/getAll.json new file mode 100644 index 00000000000..5025f4b548a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/__schema__/v2.0.0/chatMember/getAll.json @@ -0,0 +1,33 @@ +{ + "type": "object", + "properties": { + "@odata.type": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "tenantId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "visibleHistoryStartDateTime": { + "type": "string" + } + }, + "version": 1 +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.returnAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.returnAll.workflow.json new file mode 100644 index 00000000000..75b2fe9e04a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.returnAll.workflow.json @@ -0,0 +1,107 @@ +{ + "name": "Teams chatMember getAll returnAll", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [880, 380] + }, + { + "parameters": { + "resource": "chatMember", + "operation": "getAll", + "chatId": { + "__rl": true, + "value": "19:ff1e2d3c4b5a69788796a5b4c3d2e1f0@thread.v2", + "mode": "list", + "cachedResultName": "Project team (group)" + }, + "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": "MCMjMCMjMjM3ODZjYTYtN2ZmMi00NjcyLTg3ZDAtNWM2NDllZTBhMzM3IyMxOTplYmVkOWFkNDJjOTA0ZDZjODNhZGYwZGIzNjAwNTNlY0B0aHJlYWQudjIjI2U3NmY0NTZmLTVjM2YtNGYxZS05ZDVlLTRkOGYwZjZhYjExMQ==", + "@odata.type": "#microsoft.graph.aadUserConversationMember", + "roles": ["owner"], + "displayName": "Ann Smith", + "userId": "e76f456f-5c3f-4f1e-9d5e-4d8f0f6ab111", + "email": "ann@contoso.com", + "tenantId": "23786ca6-7ff2-4672-87d0-5c649ee0a337", + "visibleHistoryStartDateTime": "0001-01-01T00:00:00Z" + } + }, + { + "json": { + "id": "MCMjMCMjMjM3ODZjYTYtN2ZmMi00NjcyLTg3ZDAtNWM2NDllZTBhMzM3IyMxOTplYmVkOWFkNDJjOTA0ZDZjODNhZGYwZGIzNjAwNTNlY0B0aHJlYWQudjIjI2FhMTFiYjIyLTVjM2YtNGYxZS05ZDVlLTRkOGYwZjZhYjIyMg==", + "@odata.type": "#microsoft.graph.aadUserConversationMember", + "roles": ["owner"], + "displayName": "Bob Jones", + "userId": "aa11bb22-5c3f-4f1e-9d5e-4d8f0f6ab222", + "email": "bob@contoso.com", + "tenantId": "23786ca6-7ff2-4672-87d0-5c649ee0a337", + "visibleHistoryStartDateTime": null + } + } + ] + }, + "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": "8a1b2c3d-0004-4000-8000-000000000004", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.test.ts new file mode 100644 index 00000000000..999de2a04ad --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.test.ts @@ -0,0 +1,57 @@ +import { NodeTestHarness } from '@nodes-testing/node-test-harness'; +import nock from 'nock'; + +import { credentials } from '../../../credentials'; + +// Membership ids are opaque base64 of `0##0######`, +// derived here from the tenant, chat and user ids below so the fixture is self-consistent. +const member1 = { + id: 'MCMjMCMjMjM3ODZjYTYtN2ZmMi00NjcyLTg3ZDAtNWM2NDllZTBhMzM3IyMxOTplYmVkOWFkNDJjOTA0ZDZjODNhZGYwZGIzNjAwNTNlY0B0aHJlYWQudjIjI2U3NmY0NTZmLTVjM2YtNGYxZS05ZDVlLTRkOGYwZjZhYjExMQ==', + '@odata.type': '#microsoft.graph.aadUserConversationMember', + roles: ['owner'], + displayName: 'Ann Smith', + userId: 'e76f456f-5c3f-4f1e-9d5e-4d8f0f6ab111', + email: 'ann@contoso.com', + tenantId: '23786ca6-7ff2-4672-87d0-5c649ee0a337', + visibleHistoryStartDateTime: '0001-01-01T00:00:00Z', +}; +const member2 = { + id: 'MCMjMCMjMjM3ODZjYTYtN2ZmMi00NjcyLTg3ZDAtNWM2NDllZTBhMzM3IyMxOTplYmVkOWFkNDJjOTA0ZDZjODNhZGYwZGIzNjAwNTNlY0B0aHJlYWQudjIjI2FhMTFiYjIyLTVjM2YtNGYxZS05ZDVlLTRkOGYwZjZhYjIyMg==', + '@odata.type': '#microsoft.graph.aadUserConversationMember', + roles: ['owner'], + displayName: 'Bob Jones', + userId: 'aa11bb22-5c3f-4f1e-9d5e-4d8f0f6ab222', + email: 'bob@contoso.com', + tenantId: '23786ca6-7ff2-4672-87d0-5c649ee0a337', + visibleHistoryStartDateTime: null, +}; + +describe('Test MicrosoftTeamsV2, chatMember => getAll', () => { + // Registered WITHOUT `.query(...)`: this endpoint supports no OData parameters, so + // the test fails if `$top` is ever added. + // The workflow passes this chat id By ID and percent-encoded, the way the RLC hint + // tells users to copy it out of a Teams URL. The decoded form below is what + // `validateMicrosoftGraphId` must interpolate into the path. + nock('https://graph.microsoft.com') + .get('/v1.0/chats/19:ebed9ad42c904d6c83adf0db360053ec@thread.v2/members') + .reply(200, { value: [member1, member2] }); + + // Two pages for the returnAll workflow, on its own chat id so the interceptors + // cannot be matched in the wrong order. The nextLink must stay same-origin or the + // transport refuses it. + nock('https://graph.microsoft.com') + .get('/v1.0/chats/19:ff1e2d3c4b5a69788796a5b4c3d2e1f0@thread.v2/members') + .reply(200, { + value: [member1], + '@odata.nextLink': + 'https://graph.microsoft.com/v1.0/chats/19:ff1e2d3c4b5a69788796a5b4c3d2e1f0@thread.v2/members?$skiptoken=page2', + }) + .get('/v1.0/chats/19:ff1e2d3c4b5a69788796a5b4c3d2e1f0@thread.v2/members') + .query({ $skiptoken: 'page2' }) + .reply(200, { value: [member2] }); + + new NodeTestHarness().setupTests({ + credentials, + workflowFiles: ['getAll.workflow.json', 'getAll.returnAll.workflow.json'], + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.workflow.json new file mode 100644 index 00000000000..d09339caf69 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/chatMember/getAll.workflow.json @@ -0,0 +1,94 @@ +{ + "name": "Teams chatMember getAll", + "nodes": [ + { + "parameters": {}, + "id": "6666-9999-77777", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [880, 380] + }, + { + "parameters": { + "resource": "chatMember", + "operation": "getAll", + "chatId": { + "__rl": true, + "value": "19%3Aebed9ad42c904d6c83adf0db360053ec%40thread.v2", + "mode": "id" + }, + "limit": 1 + }, + "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": "MCMjMCMjMjM3ODZjYTYtN2ZmMi00NjcyLTg3ZDAtNWM2NDllZTBhMzM3IyMxOTplYmVkOWFkNDJjOTA0ZDZjODNhZGYwZGIzNjAwNTNlY0B0aHJlYWQudjIjI2U3NmY0NTZmLTVjM2YtNGYxZS05ZDVlLTRkOGYwZjZhYjExMQ==", + "@odata.type": "#microsoft.graph.aadUserConversationMember", + "roles": ["owner"], + "displayName": "Ann Smith", + "userId": "e76f456f-5c3f-4f1e-9d5e-4d8f0f6ab111", + "email": "ann@contoso.com", + "tenantId": "23786ca6-7ff2-4672-87d0-5c649ee0a337", + "visibleHistoryStartDateTime": "0001-01-01T00:00:00Z" + } + } + ] + }, + "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": "8a1b2c3d-0003-4000-8000-000000000003", + "id": "i3NYGF0LXV4qDFV9", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/servicePrincipal.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/servicePrincipal.test.ts index 5ea9a2c7f3d..526972170f9 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/servicePrincipal.test.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/node/servicePrincipal.test.ts @@ -72,6 +72,26 @@ describe('Microsoft Teams V2 — Service Principal runtime guards', () => { expect(transport.microsoftApiRequestAllItems).not.toHaveBeenCalled(); }); + it.each(['getAll'])( + 'chatMember:%s throws a static error and issues no request under SP', + async (operation) => { + selectSp({ + resource: 'chatMember', + operation, + chatId: 'chatID', + returnAll: true, + }); + + await expect(node.execute.call(ctx)).rejects.toThrow( + 'Chat members are not available with the Service Principal credential', + ); + expect(transport.microsoftApiRequest).not.toHaveBeenCalled(); + expect(transport.microsoftApiRequestAllItems).not.toHaveBeenCalled(); + // the guard reads only the `authentication` parameter, never the credential itself + expect(ctx.getCredentials).not.toHaveBeenCalled(); + }, + ); + it.each([ ['create', { subject: 'Sync', startDateTime: '2026-09-01T10:00:00Z' }], ['get', { meetingId: { __rl: true, mode: 'id', value: 'meeting-id' } }], diff --git a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/servicePrincipalDisplayOptions.test.ts b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/servicePrincipalDisplayOptions.test.ts index faf3e7592a8..35fa9e99ad1 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/test/v2/servicePrincipalDisplayOptions.test.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/test/v2/servicePrincipalDisplayOptions.test.ts @@ -32,76 +32,40 @@ describe('Microsoft Teams Service Principal displayOptions contract', () => { }); }); - describe('chatMessage — hidden under SP via the slash-prefixed field-level key', () => { - it('operation selector carries hide["/authentication"] = [SP]', () => { - const op = actionProps.find( - (p) => p.name === 'operation' && p.displayOptions?.show?.resource?.includes('chatMessage'), - ); - expect(isSpHidden(op)).toBe(true); - // distinct from the un-prefixed credential gate - expect(op?.displayOptions?.hide?.authentication).toBeUndefined(); - }); + describe.each(['chatMessage', 'chatMember', 'onlineMeeting'])( + '%s - hidden under SP via the slash-prefixed field-level key', + (resource) => { + it('operation selector carries hide["/authentication"] = [SP]', () => { + const op = actionProps.find( + (p) => p.name === 'operation' && p.displayOptions?.show?.resource?.includes(resource), + ); + expect(isSpHidden(op)).toBe(true); + // distinct from the un-prefixed credential gate + expect(op?.displayOptions?.hide?.authentication).toBeUndefined(); + }); - it('every chatMessage field copy is hidden under SP', () => { - const chatFields = actionProps.filter((p) => - p.displayOptions?.show?.resource?.includes('chatMessage'), - ); - // the chatId RLC, message, contentType, options, messageId, send-and-wait props… - const gatedFields = chatFields.filter((p) => p.type !== 'notice' && p.name !== 'operation'); - expect(gatedFields.length).toBeGreaterThan(0); - for (const field of gatedFields) { - expect(isSpHidden(field)).toBe(true); - } - }); + it('every field copy is hidden under SP', () => { + const fields = actionProps.filter((p) => + p.displayOptions?.show?.resource?.includes(resource), + ); + const gatedFields = fields.filter((p) => p.type !== 'notice' && p.name !== 'operation'); + expect(gatedFields.length).toBeGreaterThan(0); + for (const field of gatedFields) { + expect(isSpHidden(field)).toBe(true); + } + }); - it('shows an SP notice for the chatMessage resource', () => { - const notice = actionProps.find( - (p) => - p.type === 'notice' && - p.displayOptions?.show?.resource?.includes('chatMessage') && - p.displayOptions?.show?.authentication?.includes(SERVICE_PRINCIPAL_AUTH), - ); - expect(notice).toBeDefined(); - expect(notice?.displayOptions?.show?.authentication).toEqual([SERVICE_PRINCIPAL_AUTH]); - }); - }); - - describe('onlineMeeting — hidden under SP via the slash-prefixed field-level key', () => { - it('operation selector carries hide["/authentication"] = [SP]', () => { - const op = actionProps.find( - (p) => - p.name === 'operation' && p.displayOptions?.show?.resource?.includes('onlineMeeting'), - ); - expect(isSpHidden(op)).toBe(true); - // distinct from the un-prefixed credential gate - expect(op?.displayOptions?.hide?.authentication).toBeUndefined(); - }); - - it('every onlineMeeting field copy is hidden under SP', () => { - const meetingFields = actionProps.filter((p) => - p.displayOptions?.show?.resource?.includes('onlineMeeting'), - ); - // subject, start/end times, options, meetingId… - const gatedFields = meetingFields.filter( - (p) => p.type !== 'notice' && p.name !== 'operation', - ); - expect(gatedFields.length).toBeGreaterThan(0); - for (const field of gatedFields) { - expect(isSpHidden(field)).toBe(true); - } - }); - - it('shows an SP notice for the onlineMeeting resource', () => { - const notice = actionProps.find( - (p) => - p.type === 'notice' && - p.displayOptions?.show?.resource?.includes('onlineMeeting') && - p.displayOptions?.show?.authentication?.includes(SERVICE_PRINCIPAL_AUTH), - ); - expect(notice).toBeDefined(); - expect(notice?.displayOptions?.show?.authentication).toEqual([SERVICE_PRINCIPAL_AUTH]); - }); - }); + it('shows an SP notice for the resource', () => { + const notice = actionProps.find( + (p) => + p.type === 'notice' && + p.displayOptions?.show?.resource?.includes(resource) && + p.displayOptions?.show?.authentication?.includes(SERVICE_PRINCIPAL_AUTH), + ); + expect(notice?.displayOptions?.show?.authentication).toEqual([SERVICE_PRINCIPAL_AUTH]); + }); + }, + ); describe('channelMessage — only the sending operations are hidden under SP', () => { const fieldsFor = (operation: string) => diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/getAll.operation.ts new file mode 100644 index 00000000000..8571acbd9e9 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/getAll.operation.ts @@ -0,0 +1,38 @@ +import type { INodeProperties, IExecuteFunctions } from 'n8n-workflow'; + +import { returnAllOrLimit } from '@utils/descriptions'; +import { updateDisplayOptions } from '@utils/utilities'; + +import { chatRLC } from '../../descriptions'; +import { buildTeamsPath, microsoftApiRequestAllItems, SP_HIDE } from '../../transport'; +import { throwIfChatMemberUnsupported } from './sharedGuard'; + +const properties: INodeProperties[] = [chatRLC, ...returnAllOrLimit]; + +const displayOptions = { + show: { + resource: ['chatMember'], + operation: ['getAll'], + }, + hide: { + ...SP_HIDE, + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, i: number) { + // https://learn.microsoft.com/en-us/graph/api/chat-list-members?view=graph-rest-1.0 + + // The chat picker cannot list chats app-only; fail before any request. + throwIfChatMemberUnsupported.call(this); + + const chatId = this.getNodeParameter('chatId', i, '', { extractValue: true }) as string; + const endpoint = buildTeamsPath.call(this, ['/v1.0/chats/', { id: chatId }, '/members']); + + // No `$top`: this endpoint supports no OData query parameters, so the limit is + // applied client-side while paging through @odata.nextLink. + const returnAll = this.getNodeParameter('returnAll', i); + const limit = returnAll ? undefined : this.getNodeParameter('limit', i); + return await microsoftApiRequestAllItems.call(this, 'value', 'GET', endpoint, {}, {}, limit); +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/index.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/index.ts new file mode 100644 index 00000000000..9ff32d947fa --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/index.ts @@ -0,0 +1,47 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as getAll from './getAll.operation'; +import { SERVICE_PRINCIPAL_AUTH, SP_HIDE } from '../../transport'; + +export { getAll }; + +export const description: INodeProperties[] = [ + { + displayName: + 'Chat members are not available with the Service Principal credential. The chat picker cannot list chats app-only; use an OAuth2 credential for chat actions.', + name: 'chatMemberServicePrincipalNotice', + type: 'notice', + default: '', + displayOptions: { + show: { + resource: ['chatMember'], + authentication: [SERVICE_PRINCIPAL_AUTH], + }, + }, + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['chatMember'], + }, + hide: { + ...SP_HIDE, + }, + }, + options: [ + { + name: 'Get Many', + value: 'getAll', + description: 'Get many members of a chat', + action: 'Get many chat members', + }, + ], + default: 'getAll', + }, + + ...getAll.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/sharedGuard.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/sharedGuard.ts new file mode 100644 index 00000000000..149222d5fd3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/chatMember/sharedGuard.ts @@ -0,0 +1,25 @@ +import type { IExecuteFunctions } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; + +import { getTeamsCredentialType, SERVICE_PRINCIPAL_AUTH } from '../../transport'; + +/** + * Throws a static `NodeOperationError` when the node is configured with the + * app-only (Service Principal) credential. The endpoints themselves have + * application permissions, but the chat picker does not: `GET /chats` is + * delegated-only (app-only must go through `GET /users/{id}/chats`), so every + * chatMember operation guards on this BEFORE any request - covering + * hand-edited workflows that bypass the hidden UI. + */ +export function throwIfChatMemberUnsupported(this: IExecuteFunctions): void { + if (getTeamsCredentialType.call(this) === SERVICE_PRINCIPAL_AUTH) { + throw new NodeOperationError( + this.getNode(), + 'Chat members are not available with the Service Principal credential', + { + description: + 'The chat picker cannot list chats app-only. Use an OAuth2 credential for chat actions.', + }, + ); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/node.type.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/node.type.ts index 3002f1dd99f..8694b8db0e3 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/node.type.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/node.type.ts @@ -3,6 +3,7 @@ import type { AllEntities } from 'n8n-workflow'; type NodeMap = { channel: 'create' | 'deleteChannel' | 'get' | 'getAll' | 'update'; channelMessage: 'create' | 'get' | 'getAll' | 'getAllReplies' | 'reply'; + chatMember: 'getAll'; chatMessage: 'create' | 'get' | 'getAll' | 'sendAndWait'; onlineMeeting: 'create' | 'get'; task: 'create' | 'deleteTask' | 'get' | 'getAll' | 'update'; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts index 6aa7af1d23b..ff3d6005a2b 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/v2/actions/router.ts @@ -9,6 +9,7 @@ import { import * as channel from './channel'; import * as channelMessage from './channelMessage'; +import * as chatMember from './chatMember'; import * as chatMessage from './chatMessage'; import type { MicrosoftTeamsType } from './node.type'; import * as onlineMeeting from './onlineMeeting'; @@ -64,6 +65,9 @@ export async function router(this: IExecuteFunctions): Promise