diff --git a/libraries/botbuilder/etc/botbuilder.api.md b/libraries/botbuilder/etc/botbuilder.api.md index 1c9ce403bf..413900c235 100644 --- a/libraries/botbuilder/etc/botbuilder.api.md +++ b/libraries/botbuilder/etc/botbuilder.api.md @@ -11,12 +11,16 @@ import { AppBasedLinkQuery } from 'botbuilder-core'; import { AppCredentials } from 'botframework-connector'; import { AttachmentData } from 'botbuilder-core'; import { AuthenticationConfiguration } from 'botframework-connector'; +import { BatchFailedEntriesResponse } from 'botbuilder-core'; +import { BatchOperationResponse } from 'botbuilder-core'; +import { BatchOperationStateResponse } from 'botbuilder-core'; import { BotAdapter } from 'botbuilder-core'; import { BotConfigAuth } from 'botbuilder-core'; import { BotFrameworkAuthentication } from 'botframework-connector'; import { BotFrameworkClient } from 'botbuilder-core'; import { BotFrameworkSkill } from 'botbuilder-core'; import { BotState } from 'botbuilder-core'; +import { CancelOperationResponse } from 'botframework-connector'; import { ChannelAccount } from 'botbuilder-core'; import { ChannelInfo } from 'botbuilder-core'; import { ClaimsIdentity } from 'botframework-connector'; @@ -78,6 +82,7 @@ import { TeamInfo } from 'botbuilder-core'; import { TeamsChannelAccount } from 'botbuilder-core'; import { TeamsMeetingInfo } from 'botbuilder-core'; import { TeamsMeetingParticipant } from 'botbuilder-core'; +import { TeamsMember } from 'botbuilder-core'; import { TeamsPagedMembersResult } from 'botbuilder-core'; import { TenantInfo } from 'botbuilder-core'; import { TokenApiClient } from 'botframework-connector'; @@ -460,11 +465,14 @@ export function teamsGetTenant(activity: Activity): TenantInfo | null; // @public export class TeamsInfo { + static cancelOperation(context: TurnContext, operationId: string): Promise; + static getFailedEntries(context: TurnContext, operationId: string): Promise; static getMeetingInfo(context: TurnContext, meetingId?: string): Promise; static getMeetingParticipant(context: TurnContext, meetingId?: string, participantId?: string, tenantId?: string): Promise; static getMember(context: TurnContext, userId: string): Promise; // @deprecated static getMembers(context: TurnContext): Promise; + static getOperationState(context: TurnContext, operationId: string): Promise; static getPagedMembers(context: TurnContext, pageSize?: number, continuationToken?: string): Promise; static getPagedTeamMembers(context: TurnContext, teamId?: string, pageSize?: number, continuationToken?: string): Promise; static getTeamChannels(context: TurnContext, teamId?: string): Promise; @@ -473,6 +481,10 @@ export class TeamsInfo { // @deprecated static getTeamMembers(context: TurnContext, teamId?: string): Promise; static sendMeetingNotification(context: TurnContext, notification: MeetingNotification, meetingId?: string): Promise; + static sendMessageToAllUsersInTeam(context: TurnContext, activity: Activity, tenantId: string, teamId: string): Promise; + static sendMessageToAllUsersInTenant(context: TurnContext, activity: Activity, tenantId: string): Promise; + static sendMessageToListOfChannels(context: TurnContext, activity: Activity, tenantId: string, members: TeamsMember[]): Promise; + static sendMessageToListOfUsers(context: TurnContext, activity: Activity, tenantId: string, members: TeamsMember[]): Promise; static sendMessageToTeamsChannel(context: TurnContext, activity: Activity, teamsChannelId: string, botAppId?: string): Promise<[ConversationReference, string]>; } diff --git a/libraries/botbuilder/src/teamsInfo.ts b/libraries/botbuilder/src/teamsInfo.ts index eafe0d9f07..2cdf934c04 100644 --- a/libraries/botbuilder/src/teamsInfo.ts +++ b/libraries/botbuilder/src/teamsInfo.ts @@ -24,8 +24,17 @@ import { Channels, MeetingNotification, MeetingNotificationResponse, + TeamsMember, + BatchOperationResponse, + BatchOperationStateResponse, + BatchFailedEntriesResponse, } from 'botbuilder-core'; -import { ConnectorClient, TeamsConnectorClient, TeamsConnectorModels } from 'botframework-connector'; +import { + CancelOperationResponse, + ConnectorClient, + TeamsConnectorClient, + TeamsConnectorModels, +} from 'botframework-connector'; import { BotFrameworkAdapter } from './botFrameworkAdapter'; import { CloudAdapter } from './cloudAdapter'; @@ -364,6 +373,166 @@ export class TeamsInfo { return await this.getTeamsConnectorClient(context).teams.sendMeetingNotification(meetingId, notification); } + /** + * Sends a message to the provided users in the list of Teams members. + * + * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for this turn. + * @param activity The activity to send. + * @param tenantId The tenant ID. + * @param members The list of users recipients for the message. + * @returns Promise with operationId. + */ + static async sendMessageToListOfUsers( + context: TurnContext, + activity: Activity, + tenantId: string, + members: TeamsMember[] + ): Promise { + if (!activity) { + throw new Error('activity is required.'); + } + if (!tenantId) { + throw new Error('tenantId is required.'); + } + if (!members || members.length == 0) { + throw new Error('members list is required.'); + } + + return await this.getTeamsConnectorClient(context).teams.sendMessageToListOfUsers(activity, tenantId, members); + } + + /** + * Sends a message to all the users in a tenant. + * + * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for this turn. + * @param activity The activity to send. + * @param tenantId The tenant ID. + * @returns Promise with operationId. + */ + static async sendMessageToAllUsersInTenant( + context: TurnContext, + activity: Activity, + tenantId: string + ): Promise { + if (!activity) { + throw new Error('activity is required.'); + } + if (!tenantId) { + throw new Error('tenantId is required.'); + } + + return await this.getTeamsConnectorClient(context).teams.sendMessageToAllUsersInTenant(activity, tenantId); + } + + /** + * Sends a message to all the users in a team. + * + * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for this turn. + * @param activity The activity to send. + * @param tenantId The tenant ID. + * @param teamId The team ID. + * @returns Promise with operationId. + */ + static async sendMessageToAllUsersInTeam( + context: TurnContext, + activity: Activity, + tenantId: string, + teamId: string + ): Promise { + if (!activity) { + throw new Error('activity is required.'); + } + if (!tenantId) { + throw new Error('tenantId is required.'); + } + if (!teamId) { + throw new Error('teamId is required.'); + } + + return await this.getTeamsConnectorClient(context).teams.sendMessageToAllUsersInTeam( + activity, + tenantId, + teamId + ); + } + + /** + * Sends a message to the provided list of Teams channels. + * + * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for this turn. + * @param activity The activity to send. + * @param tenantId The tenant ID. + * @param members The list of channels recipients for the message. + * @returns Promise with operationId. + */ + static async sendMessageToListOfChannels( + context: TurnContext, + activity: Activity, + tenantId: string, + members: TeamsMember[] + ): Promise { + if (!activity) { + throw new Error('activity is required.'); + } + if (!tenantId) { + throw new Error('tenantId is required.'); + } + if (!members || members.length == 0) { + throw new Error('members list is required.'); + } + + return await this.getTeamsConnectorClient(context).teams.sendMessageToListOfChannels( + activity, + tenantId, + members + ); + } + + /** + * Gets the operation state. + * + * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for this turn. + * @param operationId The operationId to get the state of. + * @returns Promise with The state and responses of the operation. + */ + static async getOperationState(context: TurnContext, operationId: string): Promise { + if (!operationId) { + throw new Error('operationId is required.'); + } + + return await this.getTeamsConnectorClient(context).teams.getOperationState(operationId); + } + + /** + * Gets the failed entries of an executed operation. + * + * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for this turn. + * @param operationId The operationId to get the failed entries of. + * @returns Promise with the list of failed entries of the operation. + */ + static async getFailedEntries(context: TurnContext, operationId: string): Promise { + if (!operationId) { + throw new Error('operationId is required.'); + } + + return await this.getTeamsConnectorClient(context).teams.getOperationFailedEntries(operationId); + } + + /** + * Cancels a pending operation. + * + * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for this turn. + * @param operationId The id of the operation to cancel. + * @returns Promise representing the asynchronous operation. + */ + static async cancelOperation(context: TurnContext, operationId: string): Promise { + if (!operationId) { + throw new Error('operationId is required.'); + } + + return await this.getTeamsConnectorClient(context).teams.cancelOperation(operationId); + } + /** * @private */ diff --git a/libraries/botbuilder/tests/teamsInfo.test.js b/libraries/botbuilder/tests/teamsInfo.test.js index bf44091a55..96421354f1 100644 --- a/libraries/botbuilder/tests/teamsInfo.test.js +++ b/libraries/botbuilder/tests/teamsInfo.test.js @@ -1150,6 +1150,597 @@ describe('TeamsInfo', function () { }); }); + describe('sendMessageToListOfUsers()', function () { + const activity = MessageFactory.text('Message to users from batch'); + const tenantId = 'randomGUID'; + const members = [ + { id: '19:member-1' }, + { id: '19:member-2' }, + { id: '19:member-3' }, + { id: '19:member-4' }, + { id: '19:member-5' }, + ]; + + it('should correctly map message object as the request body of the POST request', async function () { + const content = { + activity: { + text: 'Message to users from batch', + type: 'message', + }, + members: [ + { id: '19:member-1' }, + { id: '19:member-2' }, + { id: '19:member-3' }, + { id: '19:member-4' }, + { id: '19:member-5' }, + ], + tenantId: 'randomGUID', + }; + + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const sendMessageToListOfUsersExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/users', content) + .matchHeader('Authorization', expectedAuthHeader) + .reply(201, {}); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + await TeamsInfo.sendMessageToListOfUsers(context, activity, tenantId, members); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToListOfUsersExpectation.isDone()); + }); + + it('should return operation id if a 201 status code was returned', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const sendMessageToListOfUsersExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/users') + .matchHeader('Authorization', expectedAuthHeader) + .reply(201, { operationId: '1' }); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + const operationId = await TeamsInfo.sendMessageToListOfUsers(context, activity, tenantId, members); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToListOfUsersExpectation.isDone()); + assert(operationId, { operationId: '1' }); + }); + + it('should return standard error response if a 4xx status code was returned', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; + + const sendMessageToListOfUsersExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/users') + .matchHeader('Authorization', expectedAuthHeader) + .reply(400, errorResponse); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + let isErrorThrown = false; + try { + await TeamsInfo.sendMessageToListOfUsers(context, activity, tenantId, members); + } catch (e) { + assert.deepEqual(errorResponse, e.errors[0].body); + isErrorThrown = true; + } + + assert(isErrorThrown); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToListOfUsersExpectation.isDone()); + }); + + it('should throw an error if an empty activity is provided', async function () { + await assert.rejects( + TeamsInfo.sendMessageToListOfUsers(context, null, tenantId, members), + Error('activity is required.') + ); + }); + + it('should throw an error if an empty member list is provided', async function () { + await assert.rejects( + TeamsInfo.sendMessageToListOfUsers(context, activity, tenantId, null), + Error('members list is required.') + ); + }); + + it('should throw an error if an empty tenant id is provided', async function () { + await assert.rejects( + TeamsInfo.sendMessageToListOfUsers(context, activity, null, members), + Error('tenantId is required.') + ); + }); + }); + + describe('sendMessageToAllUsersInTenant()', function () { + const activity = MessageFactory.text('Message to users from batch'); + const tenantId = 'randomGUID'; + + it('should correctly map message object as the request body of the POST request', async function () { + const content = { + activity: { + text: 'Message to users from batch', + type: 'message', + }, + tenantId: 'randomGUID', + }; + + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const sendMessageToAllUsersInTenantExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/tenant', content) + .matchHeader('Authorization', expectedAuthHeader) + .reply(201, {}); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + await TeamsInfo.sendMessageToAllUsersInTenant(context, activity, tenantId); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToAllUsersInTenantExpectation.isDone()); + }); + + it('should return operation id if a 201 status code was returned', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const sendMessageToAllUsersInTenantExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/tenant') + .matchHeader('Authorization', expectedAuthHeader) + .reply(201, { operationId: '1' }); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + const operationId = await TeamsInfo.sendMessageToAllUsersInTenant(context, activity, tenantId); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToAllUsersInTenantExpectation.isDone()); + assert(operationId, { operationId: '1' }); + }); + + it('should return standard error response if a 4xx status code was returned', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; + + const sendMessageToAllUsersInTenantExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/tenant') + .matchHeader('Authorization', expectedAuthHeader) + .reply(400, errorResponse); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + let isErrorThrown = false; + try { + await TeamsInfo.sendMessageToAllUsersInTenant(context, activity, tenantId); + } catch (e) { + assert.deepEqual(errorResponse, e.errors[0].body); + isErrorThrown = true; + } + + assert(isErrorThrown); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToAllUsersInTenantExpectation.isDone()); + }); + + it('should throw an error if an empty activity is provided', async function () { + await assert.rejects( + TeamsInfo.sendMessageToAllUsersInTenant(context, null, tenantId), + Error('activity is required.') + ); + }); + + it('should throw an error if an empty tenant id is provided', async function () { + await assert.rejects( + TeamsInfo.sendMessageToAllUsersInTenant(context, activity, null), + Error('tenantId is required.') + ); + }); + }); + + describe('sendMessageToAllUsersInTeam()', function () { + const activity = MessageFactory.text('Message to users from batch'); + const tenantId = 'randomGUID'; + const teamId = 'teamRandomGUID'; + + it('should correctly map message object as the request body of the POST request', async function () { + const content = { + activity: { + text: 'Message to users from batch', + type: 'message', + }, + tenantId: 'randomGUID', + teamId: 'teamRandomGUID', + }; + + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const sendMessageToAllUsersInTeamExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/team', content) + .matchHeader('Authorization', expectedAuthHeader) + .reply(201, {}); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + await TeamsInfo.sendMessageToAllUsersInTeam(context, activity, tenantId, teamId); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToAllUsersInTeamExpectation.isDone()); + }); + + it('should return operation id if a 201 status code was returned', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const sendMessageToAllUsersInTeamExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/team') + .matchHeader('Authorization', expectedAuthHeader) + .reply(201, { operationId: '1' }); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + const operationId = await TeamsInfo.sendMessageToAllUsersInTeam(context, activity, tenantId, teamId); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToAllUsersInTeamExpectation.isDone()); + assert(operationId, { operationId: '1' }); + }); + + it('should return standard error response if a 4xx status code was returned', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; + + const sendMessageToAllUsersInTeamExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/team') + .matchHeader('Authorization', expectedAuthHeader) + .reply(400, errorResponse); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + let isErrorThrown = false; + try { + await TeamsInfo.sendMessageToAllUsersInTeam(context, activity, tenantId, teamId); + } catch (e) { + assert.deepEqual(errorResponse, e.errors[0].body); + isErrorThrown = true; + } + + assert(isErrorThrown); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToAllUsersInTeamExpectation.isDone()); + }); + + it('should throw an error if an empty activity is provided', async function () { + await assert.rejects( + TeamsInfo.sendMessageToAllUsersInTeam(context, null, tenantId, teamId), + Error('activity is required.') + ); + }); + + it('should throw an error if an empty tenant id is provided', async function () { + await assert.rejects( + TeamsInfo.sendMessageToAllUsersInTeam(context, activity, null, teamId), + Error('tenantId is required.') + ); + }); + + it('should throw an error if an empty team id is provided', async function () { + await assert.rejects( + TeamsInfo.sendMessageToAllUsersInTeam(context, activity, teamId, null), + Error('teamId is required.') + ); + }); + }); + + describe('sendMessageToListOfChannels()', function () { + const activity = MessageFactory.text('Message to users from batch'); + const tenantId = 'randomGUID'; + const members = [ + { id: '19:member-1' }, + { id: '19:member-2' }, + { id: '19:member-3' }, + { id: '19:member-4' }, + { id: '19:member-5' }, + ]; + + it('should correctly map message object as the request body of the POST request', async function () { + const content = { + activity: { + text: 'Message to users from batch', + type: 'message', + }, + members: [ + { id: '19:member-1' }, + { id: '19:member-2' }, + { id: '19:member-3' }, + { id: '19:member-4' }, + { id: '19:member-5' }, + ], + tenantId: 'randomGUID', + }; + + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const sendMessageToListOfChannelsExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/channels', content) + .matchHeader('Authorization', expectedAuthHeader) + .reply(201, {}); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + await TeamsInfo.sendMessageToListOfChannels(context, activity, tenantId, members); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToListOfChannelsExpectation.isDone()); + }); + + it('should return operation id if a 201 status code was returned', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const sendMessageToListOfChannelsExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/channels') + .matchHeader('Authorization', expectedAuthHeader) + .reply(201, { operationId: '1' }); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + const operationId = await TeamsInfo.sendMessageToListOfChannels(context, activity, tenantId, members); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToListOfChannelsExpectation.isDone()); + assert(operationId, { operationId: '1' }); + }); + + it('should return standard error response if a 4xx status code was returned', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; + + const sendMessageToListOfChannelsExpectation = nock('https://smba.trafficmanager.net/amer') + .post('/v3/batch/conversation/channels') + .matchHeader('Authorization', expectedAuthHeader) + .reply(400, errorResponse); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + let isErrorThrown = false; + try { + await TeamsInfo.sendMessageToListOfChannels(context, activity, tenantId, members); + } catch (e) { + assert.deepEqual(errorResponse, e.errors[0].body); + isErrorThrown = true; + } + + assert(isErrorThrown); + + assert(fetchOauthToken.isDone()); + assert(sendMessageToListOfChannelsExpectation.isDone()); + }); + + it('should throw an error if an empty activity is provided', async function () { + await assert.rejects( + TeamsInfo.sendMessageToListOfChannels(context, null, tenantId, members), + Error('activity is required.') + ); + }); + + it('should throw an error if an empty member list is provided', async function () { + await assert.rejects( + TeamsInfo.sendMessageToListOfChannels(context, activity, tenantId, null), + Error('members list is required.') + ); + }); + + it('should throw an error if an empty tenant id is provided', async function () { + await assert.rejects( + TeamsInfo.sendMessageToListOfChannels(context, activity, null, members), + Error('tenantId is required.') + ); + }); + }); + + describe('getOperationState()', function () { + const operationId = 'amerOperationId'; + + it('should work with correct operationId in parameters', async function () { + const operationState = { + state: 'Completed', + statusMap: { + 201: 1, + 404: 4, + }, + totalEntriesCount: 5, + }; + + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const getOperationStateExpectation = nock('https://smba.trafficmanager.net/amer') + .get(`/v3/batch/conversation/${operationId}`) + .matchHeader('Authorization', expectedAuthHeader) + .reply(200, operationState); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + const operationStateDetails = await TeamsInfo.getOperationState(context, operationId); + + assert(fetchOauthToken.isDone()); + assert(getOperationStateExpectation.isDone()); + + assert.deepStrictEqual(operationStateDetails, operationState); + }); + + it('should return standard error response if a 4xx status code was returned', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; + + const getOperationStateExpectation = nock('https://smba.trafficmanager.net/amer') + .get(`/v3/batch/conversation/${operationId}`) + .matchHeader('Authorization', expectedAuthHeader) + .reply(400, errorResponse); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + let isErrorThrown = false; + try { + await TeamsInfo.getOperationState(context, operationId); + } catch (e) { + assert.deepEqual(errorResponse, e.errors[0].body); + isErrorThrown = true; + } + + assert(isErrorThrown); + + assert(fetchOauthToken.isDone()); + assert(getOperationStateExpectation.isDone()); + }); + + it('should throw error for missing operation Id', async function () { + await assert.rejects(TeamsInfo.getOperationState({ activity: {} }), Error('operationId is required.')); + }); + }); + + describe('getFailedEntries()', function () { + const operationId = 'amerOperationId'; + + it('should work with correct operationId in parameters', async function () { + const failedEntries = { + continuationToken: 'Token', + failedEntryResponses: [ + { + id: 'id-1', + error: 'error-1', + }, + { + id: 'id-2', + error: 'error-2', + }, + { + id: 'id-3', + error: 'error-3', + }, + ], + }; + + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const getFailedEntriesExpectation = nock('https://smba.trafficmanager.net/amer') + .get(`/v3/batch/conversation/failedentries/${operationId}`) + .matchHeader('Authorization', expectedAuthHeader) + .reply(200, failedEntries); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + const failedEntriesResponse = await TeamsInfo.getFailedEntries(context, operationId); + + assert(fetchOauthToken.isDone()); + assert(getFailedEntriesExpectation.isDone()); + + assert.deepStrictEqual(failedEntriesResponse, failedEntries); + }); + + it('should return standard error response if a 4xx status code was returned', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; + + const getFailedEntriesExpectation = nock('https://smba.trafficmanager.net/amer') + .get(`/v3/batch/conversation/failedentries/${operationId}`) + .matchHeader('Authorization', expectedAuthHeader) + .reply(400, errorResponse); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + let isErrorThrown = false; + try { + await TeamsInfo.getFailedEntries(context, operationId); + } catch (e) { + assert.deepEqual(errorResponse, e.errors[0].body); + isErrorThrown = true; + } + + assert(isErrorThrown); + + assert(fetchOauthToken.isDone()); + assert(getFailedEntriesExpectation.isDone()); + }); + + it('should throw error for missing operation Id', async function () { + await assert.rejects(TeamsInfo.getFailedEntries({ activity: {} }), Error('operationId is required.')); + }); + }); + + describe('cancelOperation()', function () { + const operationId = 'amerOperationId'; + + it('should finish operation with correct operationId in parameters', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const cancelOperationExpectation = nock('https://smba.trafficmanager.net/amer') + .delete(`/v3/batch/conversation/${operationId}`) + .matchHeader('Authorization', expectedAuthHeader) + .reply(200); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + await TeamsInfo.cancelOperation(context, operationId); + + assert(fetchOauthToken.isDone()); + assert(cancelOperationExpectation.isDone()); + }); + + it('should return standard error response if a 4xx status code was returned', async function () { + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; + + const cancelOperationExpectation = nock('https://smba.trafficmanager.net/amer') + .delete(`/v3/batch/conversation/${operationId}`) + .matchHeader('Authorization', expectedAuthHeader) + .reply(400, errorResponse); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + let isErrorThrown = false; + try { + await TeamsInfo.cancelOperation(context, operationId); + } catch (e) { + assert.deepEqual(errorResponse, e.errors[0].body); + isErrorThrown = true; + } + + assert(isErrorThrown); + + assert(fetchOauthToken.isDone()); + assert(cancelOperationExpectation.isDone()); + }); + + it('should throw error for missing operation Id', async function () { + await assert.rejects(TeamsInfo.getFailedEntries({ activity: {} }), Error('operationId is required.')); + }); + }); + describe('private methods', function () { describe('getConnectorClient()', function () { it("should error if the context doesn't have an adapter", function () { diff --git a/libraries/botframework-connector/src/teams/index.ts b/libraries/botframework-connector/src/teams/index.ts index 84f528962e..b6b040e378 100644 --- a/libraries/botframework-connector/src/teams/index.ts +++ b/libraries/botframework-connector/src/teams/index.ts @@ -25,3 +25,4 @@ export * from './teamsConnectorClient'; export * from './teamsConnectorClientContext'; export * from './models'; export * from './readReceiptInfo'; +export * from './retryAction'; diff --git a/libraries/botframework-connector/src/teams/models/index.ts b/libraries/botframework-connector/src/teams/models/index.ts index 6a82873db2..fefa1140df 100644 --- a/libraries/botframework-connector/src/teams/models/index.ts +++ b/libraries/botframework-connector/src/teams/models/index.ts @@ -10,6 +10,9 @@ import { TeamsMeetingInfo, MeetingNotificationResponse, TeamsMeetingParticipant, + BatchOperationResponse, + BatchOperationStateResponse, + BatchFailedEntriesResponse, } from 'botframework-schema'; /** @@ -143,3 +146,79 @@ export type TeamsMeetingNotificationResponse = MeetingNotificationResponse & { parsedBody: MeetingNotificationResponse | {}; }; }; + +/** + * Contains response data for the Teams batch operations. + */ +export type TeamsBatchOperationResponse = BatchOperationResponse & { + /** + * The underlying HTTP response. + */ + _response: HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + /** + * The response body as parsed JSON or XML + */ + parsedBody: BatchOperationResponse | {}; + }; +}; + +/** + * Contains response data for the Teams batch operation state. + */ +export type BatchBatchOperationStateResponse = BatchOperationStateResponse & { + /** + * The underlying HTTP response. + */ + _response: HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + /** + * The response body as parsed JSON or XML + */ + parsedBody: BatchOperationStateResponse | {}; + }; +}; + +/** + * Contains response data for the Teams batch failed entries. + */ +export type BatchBatchFailedEntriesResponse = BatchFailedEntriesResponse & { + /** + * The underlying HTTP response. + */ + _response: HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + /** + * The response body as parsed JSON or XML + */ + parsedBody: BatchFailedEntriesResponse | {}; + }; +}; + +/** + * Contains response data for the Teams batch cancel operation. + */ +export type CancelOperationResponse = { + /** + * The underlying HTTP response. + */ + _response: HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + /** + * The response body as parsed JSON or XML + */ + parsedBody: {}; + }; +}; diff --git a/libraries/botframework-connector/src/teams/models/mappers.ts b/libraries/botframework-connector/src/teams/models/mappers.ts index 3df94868a8..3111fb1076 100644 --- a/libraries/botframework-connector/src/teams/models/mappers.ts +++ b/libraries/botframework-connector/src/teams/models/mappers.ts @@ -1247,6 +1247,12 @@ export const Activity: msRest.CompositeMapper = { name: 'String', }, }, + text: { + serializedName: 'text', + type: { + name: 'String', + }, + }, }, }, }; @@ -2242,3 +2248,179 @@ export const AppBasedLinkQuery: msRest.CompositeMapper = { }, }, }; + +export const BatchOperationRequest: msRest.CompositeMapper = { + serializedName: 'BatchOperationRequest', + type: { + name: 'Composite', + className: 'BatchOperationRequest', + modelProperties: { + activity: { + serializedName: 'activity', + type: { + name: 'Composite', + className: 'Activity', + modelProperties: { + text: { + serializedName: 'text', + type: { + name: 'String', + }, + }, + type: { + serializedName: 'type', + type: { + name: 'String', + }, + }, + }, + }, + }, + tenantId: { + serializedName: 'tenantId', + type: { + name: 'String', + }, + }, + teamId: { + serializedName: 'teamId', + type: { + name: 'String', + }, + }, + members: { + serializedName: 'members', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'TeamsMember', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'String', + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; + +export const BatchOperationResponse: msRest.CompositeMapper = { + serializedName: 'BatchOperationResponse', + type: { + name: 'Composite', + className: 'BatchOperationResponse', + modelProperties: { + operationId: { + serializedName: 'operationId', + type: { + name: 'String', + }, + }, + }, + }, +}; + +export const GetTeamsOperationStateResponse: msRest.CompositeMapper = { + serializedName: 'GetTeamsOperationStateResponse', + type: { + name: 'Composite', + className: 'GetTeamsOperationStateResponse', + modelProperties: { + state: { + serializedName: 'state', + type: { + name: 'String', + }, + }, + stateMap: { + serializedName: 'stateMap', + type: { + name: 'Dictionary', + value: { + type: { + name: 'Number', + }, + }, + }, + }, + retryAfter: { + serializedName: 'retryAfter', + type: { + name: 'DateTime', + }, + }, + totalEntriesCount: { + serializedName: 'totalEntriesCount', + type: { + name: 'Number', + }, + }, + }, + }, +}; + +export const GetTeamsFailedEntriesResponse: msRest.CompositeMapper = { + serializedName: 'BatchFailedEntriesResponse', + type: { + name: 'Composite', + className: 'BatchFailedEntriesResponse', + modelProperties: { + continuationToken: { + serializedName: 'continuationToken', + type: { + name: 'String', + }, + }, + failedEntryResponses: { + serializedName: 'failedEntryResponses', + type: { + name: 'Sequence', + element: { + type: { + name: 'Composite', + className: 'BatchFailedEntry', + modelProperties: { + id: { + serializedName: 'Id', + type: { + name: 'String', + }, + }, + error: { + serializedName: 'error', + type: { + name: 'String', + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; + +export const TeamsMember: msRest.CompositeMapper = { + serializedName: 'TeamsMember', + type: { + name: 'Composite', + className: 'TeamsMember', + modelProperties: { + id: { + serializedName: 'id', + type: { + name: 'String', + }, + }, + }, + }, +}; diff --git a/libraries/botframework-connector/src/teams/models/parameters.ts b/libraries/botframework-connector/src/teams/models/parameters.ts index 4fb127d870..f9bbd68152 100644 --- a/libraries/botframework-connector/src/teams/models/parameters.ts +++ b/libraries/botframework-connector/src/teams/models/parameters.ts @@ -47,3 +47,13 @@ export const tenantId: msRest.OperationQueryParameter = { }, }, }; + +export const operationId: msRest.OperationURLParameter = { + parameterPath: 'operationId', + mapper: { + serializedName: 'operationId', + type: { + name: 'String', + }, + }, +}; diff --git a/libraries/botframework-connector/src/teams/models/teamsMappers.ts b/libraries/botframework-connector/src/teams/models/teamsMappers.ts index fe2a7a96ae..ced85cabaa 100644 --- a/libraries/botframework-connector/src/teams/models/teamsMappers.ts +++ b/libraries/botframework-connector/src/teams/models/teamsMappers.ts @@ -6,6 +6,7 @@ export { ErrorResponse, ErrorModel, InnerHttpError } from '../../connectorApi/models/mappers'; export { + Activity, ConversationList, ChannelInfo, MessageActionsPayloadConversation, @@ -23,4 +24,9 @@ export { TaskModuleContinueResponse, TaskModuleTaskInfo, Attachment, + BatchOperationRequest, + BatchOperationResponse, + GetTeamsFailedEntriesResponse, + GetTeamsOperationStateResponse, + TeamsMember, } from './mappers'; diff --git a/libraries/botframework-connector/src/teams/operations/teams.ts b/libraries/botframework-connector/src/teams/operations/teams.ts index 5a44bb7e37..4ffee4d8dd 100644 --- a/libraries/botframework-connector/src/teams/operations/teams.ts +++ b/libraries/botframework-connector/src/teams/operations/teams.ts @@ -8,12 +8,26 @@ import * as msRest from '@azure/ms-rest-js'; import * as Models from '../models'; import * as Mappers from '../models/teamsMappers'; import * as Parameters from '../models/parameters'; -import { TeamsConnectorClientContext } from '../'; -import { ConversationList, TeamDetails, TeamsMeetingInfo, TeamsMeetingParticipant, MeetingNotificationResponse, MeetingNotification } from 'botframework-schema'; +import { TeamsConnectorClientContext, retryAction } from '../'; +import { + Activity, + ConversationList, + TeamDetails, + TeamsMeetingInfo, + TeamsMeetingParticipant, + MeetingNotificationResponse, + MeetingNotification, + TeamsMember, + BatchOperationResponse, + BatchOperationStateResponse, + BatchFailedEntriesResponse +} from 'botframework-schema'; + /** Class representing a Teams. */ export class Teams { private readonly client: TeamsConnectorClientContext; + private readonly retryCount = 10; /** * Create a Teams. @@ -206,7 +220,7 @@ export class Teams { * @param meetingId Meeting Id, encoded as a BASE64 string. * @param callback The callback */ - fetchMeetingInfo( + fetchMeetingInfo( meetingId: string, callback: msRest.ServiceCallback ): void; @@ -215,7 +229,7 @@ export class Teams { * @param options The optional parameters * @param callback The callback */ - fetchMeetingInfo( + fetchMeetingInfo( meetingId: string, options: msRest.RequestOptionsBase | msRest.ServiceCallback, callback: msRest.ServiceCallback @@ -298,6 +312,198 @@ export class Teams { callback ) as Promise; } + + //Batch Operations + /** + * Send message to a list of users. + * + * @param activity The activity to send. + * @param tenantId The tenant Id. + * @param members The list of members. + * @param options The optional parameters. + * @param callback The callback. + * @returns Promise with TeamsBatchOperationResponse. + */ + sendMessageToListOfUsers( + activity: Activity, + tenantId: string, + members: TeamsMember[], + options?: msRest.RequestOptionsBase, + callback?: msRest.ServiceCallback + ): Promise { + const content = { + activity, + members, + tenantId + } + return retryAction(() => this.client.sendOperationRequest( + { + content, + options + }, + sendMessageToListOfUsersOperationSpec, + callback + ) as Promise, this.retryCount); + } + + /** + * Send message to all users belonging to a tenant. + * + * @param activity The activity to send. + * @param tenantId The id of the recipient Tenant. + * @param options The optional parameters. + * @param callback The callback. + * @returns Promise with TeamsBatchOperationResponse. + */ + sendMessageToAllUsersInTenant( + activity: Activity, + tenantId: string, + options?: msRest.RequestOptionsBase, + callback?: msRest.ServiceCallback + ): Promise { + const content = { + activity, + tenantId + } + return retryAction(() => this.client.sendOperationRequest( + { + content, + options + }, + sendMessageToAllUsersInTenantOperationSpec, + callback + ) as Promise, this.retryCount); + } + + /** + * Send message to all users belonging to a team. + * + * @param activity The activity to send. + * @param tenantId The tenant Id. + * @param teamId The id of the recipient Team. + * @param options The optional parameters. + * @param callback The callback. + * @returns Promise with TeamsBatchOperationResponse. + */ + sendMessageToAllUsersInTeam( + activity: Activity, + tenantId: string, + teamId: string, + options?: msRest.RequestOptionsBase, + callback?: msRest.ServiceCallback + ): Promise { + const content = { + activity, + tenantId, + teamId + } + return retryAction(() => this.client.sendOperationRequest( + { + content, + options + }, + sendMessageToAllUsersInTeamOperationSpec, + callback + ) as Promise, this.retryCount); + } + + /** + * Send message to a list of channels. + * + * @param activity The activity to send. + * @param tenantId The tenant Id. + * @param members The list of channels. + * @param options The optional parameters. + * @param callback The callback. + * @returns Promise with TeamsBatchOperationResponse. + */ + sendMessageToListOfChannels( + activity: Activity, + tenantId: string, + members: TeamsMember[], + options?: msRest.RequestOptionsBase, + callback?: msRest.ServiceCallback + ): Promise { + const content = { + activity, + tenantId, + members + } + return retryAction(() => this.client.sendOperationRequest( + { + content, + options + }, + sendMessageToListOfChannelsOperationSpec, + callback + ) as Promise, this.retryCount); + } + + /** + * Get the state of an operation. + * + * @param operationId The operationId to get the state of. + * @param options The optional parameters. + * @param callback The callback. + * @returns Promise with BatchOperationStateResponse. + */ + getOperationState( + operationId: string, + options?: msRest.RequestOptionsBase, + callback?: msRest.ServiceCallback + ): Promise { + return retryAction(() => this.client.sendOperationRequest( + { + operationId, + options + }, + getOperationStateSpec, + callback + ) as Promise, this.retryCount); + } + + /** + * Get the failed entries of an operation. + * + * @param operationId The operationId to get the failed entries of. + * @param options The optional parameters. + * @param callback The callback. + * @returns Promise with BatchFailedEntriesResponse. + */ + getOperationFailedEntries( + operationId: string, + options?: msRest.RequestOptionsBase, + callback?: msRest.ServiceCallback + ): Promise { + return retryAction(() => this.client.sendOperationRequest( + { + operationId, + options + }, + getPagedFailedEntriesSpec, + callback + ) as Promise, this.retryCount); + } + + /** + * Cancel an operation. + * + * @param operationId The id of the operation to cancel. + * @param options The optional parameters. + * @returns Promise with CancelOperationResponse. + */ + cancelOperation( + operationId: string, + options?: msRest.RequestOptionsBase, + ): Promise { + return retryAction(() => this.client.sendOperationRequest( + { + operationId, + options + }, + cancelOperationSpec + ) as Promise, this.retryCount); + } } // Operation Specifications @@ -378,4 +584,131 @@ const sendMeetingNotificationOperationSpec: msRest.OperationSpec = { } }, serializer -} \ No newline at end of file +} + +const sendMessageToListOfUsersOperationSpec: msRest.OperationSpec = { + httpMethod: 'POST', + path: 'v3/batch/conversation/users', + requestBody: { + parameterPath: 'content', + mapper: { + ...Mappers.BatchOperationRequest, + required: true + } + }, + responses: { + 201: { + bodyMapper: Mappers.BatchOperationResponse + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + serializer +} + +const sendMessageToAllUsersInTenantOperationSpec: msRest.OperationSpec = { + httpMethod: 'POST', + path: 'v3/batch/conversation/tenant', + requestBody: { + parameterPath: 'content', + mapper: { + ...Mappers.BatchOperationRequest, + required: true + } + }, + responses: { + 201: { + bodyMapper: Mappers.BatchOperationResponse + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + serializer +} + +const sendMessageToAllUsersInTeamOperationSpec: msRest.OperationSpec = { + httpMethod: 'POST', + path: 'v3/batch/conversation/team', + requestBody: { + parameterPath: 'content', + mapper: { + ...Mappers.BatchOperationRequest, + required: true + } + }, + responses: { + 201: { + bodyMapper: Mappers.BatchOperationResponse + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + serializer +} + +const sendMessageToListOfChannelsOperationSpec: msRest.OperationSpec = { + httpMethod: 'POST', + path: 'v3/batch/conversation/channels', + requestBody: { + parameterPath: 'content', + mapper: { + ...Mappers.BatchOperationRequest, + required: true + } + }, + responses: { + 201: { + bodyMapper: Mappers.BatchOperationResponse + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + serializer +} + +const getOperationStateSpec: msRest.OperationSpec = { + httpMethod: 'GET', + path: 'v3/batch/conversation/{operationId}', + urlParameters: [Parameters.operationId], + responses: { + 200: { + bodyMapper: Mappers.GetTeamsOperationStateResponse, + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + serializer, +}; + +const getPagedFailedEntriesSpec: msRest.OperationSpec = { + httpMethod: 'GET', + path: 'v3/batch/conversation/failedentries/{operationId}', + urlParameters: [Parameters.operationId], + responses: { + 200: { + bodyMapper: Mappers.GetTeamsFailedEntriesResponse, + }, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + serializer, +}; + +const cancelOperationSpec: msRest.OperationSpec = { + httpMethod: 'DELETE', + path: 'v3/batch/conversation/{operationId}', + urlParameters: [Parameters.operationId], + responses: { + 200: {}, + default: { + bodyMapper: Mappers.ErrorResponse + } + }, + serializer, +}; diff --git a/libraries/botframework-connector/src/teams/retryAction.ts b/libraries/botframework-connector/src/teams/retryAction.ts new file mode 100644 index 0000000000..ee04c59f13 --- /dev/null +++ b/libraries/botframework-connector/src/teams/retryAction.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Retry a given promise with gradually increasing delay when the HTTP status code received is 429(Too Many Requests). + * + * @param promise a function that returns a promise to retry. + * @param maxRetries the maximum number of times to retry. + * @param initialDelay the initial value to delay before retrying (in milliseconds). + * @returns a promise resolving to the result of the promise from the promise generating function, or undefined. + */ +export async function retryAction( + promise: (n: number) => Promise, + maxRetries: number, + initialDelay = 500 +): Promise { + let delay = initialDelay, + n = 1; + let errStatusCode: number; + const errorsArray = []; + + // Take care of negative or zero + maxRetries = Math.max(maxRetries, 1); + + do { + try { + // Note: return await intentional so we can catch errors + return await promise(n); + } catch (err: any) { + errorsArray.push(err); + errStatusCode = err.statusCode; + if (err.statusCode == 429) { + await new Promise((resolve) => setTimeout(resolve, delay)); + delay *= n; + n++; + } + } + } while (n <= maxRetries && errStatusCode === 429); + + throw { errors: errorsArray, message: 'Failed to perform the required operation.' }; +} diff --git a/libraries/botframework-schema/etc/botframework-schema.api.md b/libraries/botframework-schema/etc/botframework-schema.api.md index 4cff19dbea..2f9078df2c 100644 --- a/libraries/botframework-schema/etc/botframework-schema.api.md +++ b/libraries/botframework-schema/etc/botframework-schema.api.md @@ -367,6 +367,39 @@ export interface BasicCard { title: string; } +// @public +export interface BatchFailedEntriesResponse { + continuationToken: string; + failedEntryResponses: BatchFailedEntry[]; +} + +// @public +export interface BatchFailedEntry { + error: string; + id: string; +} + +// @public +export interface BatchOperationRequest { + activity: Activity; + members?: TeamsMember[]; + teamId?: string; + tenantId: string; +} + +// @public +export interface BatchOperationResponse { + operationId: string; +} + +// @public +export interface BatchOperationStateResponse { + retryAfter?: Date; + state: string; + statusMap: Record; + totalEntriesCount: number; +} + // @public export interface BotConfigAuth { suggestedActions?: SuggestedActions; @@ -1823,6 +1856,11 @@ export interface TeamsMeetingParticipant { user?: TeamsChannelAccount; } +// @public +export type TeamsMember = { + id: string; +}; + // @public (undocumented) export interface TeamsPagedMembersResult { continuationToken: string; diff --git a/libraries/botframework-schema/src/teams/index.ts b/libraries/botframework-schema/src/teams/index.ts index 3e1604f181..c459a19906 100644 --- a/libraries/botframework-schema/src/teams/index.ts +++ b/libraries/botframework-schema/src/teams/index.ts @@ -2070,3 +2070,101 @@ export interface UserMeetingDetails { */ role: string; } + +/** + * @type {TeamsMember} + * Defines the TeamsMember type. + */ +export type TeamsMember = { + /** + * @member {string} [id] The member id. + */ + id: string; +}; + +/** + * @interface + * Specifies the body of the teams batch operation request. + */ +export interface BatchOperationRequest { + /** + * @member {Activity} [activity] The activity of the request. + */ + activity: Activity; + /** + * @member {string} [tenantId] The id of the Teams tenant. + */ + tenantId: string; + /** + * @member {string} [teamId] The id of the team. + */ + teamId?: string; + /** + * @member {TeamsMember[]} [members] The list of members. + */ + members?: TeamsMember[]; +} + +/** + * @interface + * Specifies the body of the teams batch operation response. + */ +export interface BatchOperationResponse { + /** + * @member {string} [operationId] The id of the operation executed. + */ + operationId: string; +} + +/** + * @interface + * Specifies the body of the teams batch operation state response. + */ +export interface BatchOperationStateResponse { + /** + * @member {string} [state] The state of the operation. + */ + state: string; + /** + * @member {Record} [statusMap] The status map for processed operations. + */ + statusMap: Record; + /** + * @member {Date} [retryAfter] The datetime value to retry the operation. + */ + retryAfter?: Date; + /** + * @member {number} [totalEntriesCount] The number of entries. + */ + totalEntriesCount: number; +} + +/** + * @interface + * Specifies the failed entry with its id and error. + */ +export interface BatchFailedEntry { + /** + * @member {string} [id] The id of the failed entry. + */ + id: string; + /** + * @member {string} [error] The error of the failed entry. + */ + error: string; +} + +/** + * @interface + * Specifies the body of the batch failed entries response. + */ +export interface BatchFailedEntriesResponse { + /** + * @member {string} [continuationToken] The continuation token for paginated results. + */ + continuationToken: string; + /** + * @member {BatchFailedEntry[]} [failedEntryResponses] The list of failed entries result of a batch operation. + */ + failedEntryResponses: BatchFailedEntry[]; +}