From 74a718833e7ebf9bbcb0d32de3a375864bc89efb Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Wed, 7 Dec 2022 13:13:00 -0800 Subject: [PATCH 1/8] initial working version --- libraries/botbuilder/src/teamsInfo.ts | 16 ++ .../src/teams/models/mappers.ts | 149 ++++++++++++++++++ .../src/teams/models/teamsMappers.ts | 8 + .../src/teams/operations/teams.ts | 35 ++++ .../botframework-schema/src/teams/index.ts | 38 +++++ 5 files changed, 246 insertions(+) diff --git a/libraries/botbuilder/src/teamsInfo.ts b/libraries/botbuilder/src/teamsInfo.ts index 06626f932a..88853ff47b 100644 --- a/libraries/botbuilder/src/teamsInfo.ts +++ b/libraries/botbuilder/src/teamsInfo.ts @@ -333,6 +333,22 @@ export class TeamsInfo { return await this.getMemberInternal(this.getConnectorClient(context), t, userId); } + // TODO: Update types + static async sendMeetingNotification(context: TurnContext, notification: any, meetingId?: string): Promise { + const activity = context.activity; + + if (meetingId == null) { + const meeting = teamsGetTeamMeetingInfo(activity); + meetingId = meeting?.id; + } + + if (!meetingId) { + throw new Error('meetingId is required.'); + } + + return await this.getTeamsConnectorClient(context).teams.sendMeetingNotification(meetingId, notification); + } + /** * @private */ diff --git a/libraries/botframework-connector/src/teams/models/mappers.ts b/libraries/botframework-connector/src/teams/models/mappers.ts index fa19ce8353..33fb5b48ec 100644 --- a/libraries/botframework-connector/src/teams/models/mappers.ts +++ b/libraries/botframework-connector/src/teams/models/mappers.ts @@ -434,6 +434,155 @@ export const TeamsMeetingDetails: msRest.CompositeMapper = { }, }; +export const TeamsMeetingNotificationInfo: msRest.CompositeMapper = { + serializedName: "TeamsMeetingNotificationInfo", + type: { + name: "Composite", + className: "TeamsMeetingNotificationInfo", + modelProperties: { + recipients: { + serializedName: "recipients", + type: { + name: "Sequence", + element: { + type: { + name: "String", + } + } + } + }, + surfaces: { + serializedName: "surfaces", + type: { + name: "Sequence", + element: { + type: { + name: "Composite", + className: "TeamsMeetingNotificationSurface" + } + } + } + } + } + } +} + +export const TeamsMeetingNotification: msRest.CompositeMapper = { + serializedName: "TeamsMeetingNotification", + type: { + name: "Composite", + className: "TeamsMeetingNotification", + modelProperties: { + type: { + serializedName: "type", + type: { + name: "String" + } + }, + value: { + serializedName: "value", + type: { + name: "Composite", + className: "TeamsMeetingNotificationInfo" + } + }, + channelData: { + serializedName: "channelData", + type: { + name: "Composite", + className: "TeamsMeetingNotificationChannelData" + } + } + + } + } +} + +export const TeamsMeetingNotificationSurface: msRest.CompositeMapper = { + serializedName: "TeamsMeetingNotificationSurface", + type: { + name: "Composite", + className: "TeamsMeetingNotificationSurface", + modelProperties: { + surface: { + serializedName: "surface", + type: { + name: "String" + } + }, + contentType: { + serializedName: "contentType", + type: { + name: "String", + } + }, + content: { + serializedName: "content", + type: { + name: "Composite", + className: "TaskModuleContinueResponse" + } + } + } + } +} + +export const TeamsMeetingNotificationChannelData = { + serializedName: "TeamsMeetingNotificationChannelData", + type: { + name: "Composite", + className: "TeamsMeetingNotificationChannelData", + modelProperties: { + onBehalfOf: { + serializedName: "onBehalfOf", + type: { + name: "Sequence", + element: { + type: { + name: "Composite", + className: "TeamsMeetingOnBehalfOf" + } + } + } + } + } + } +} + +export const TeamsMeetingOnBehalfOf: msRest.CompositeMapper = { + serializedName: "TeamsMeetingOnBehalfOf", + type: { + name: "Composite", + className: "TeamsMeetingOnBehalfOf", + modelProperties: { + itemid: { + serializedName: "itemid", + type: { + name: "Number" + } + }, + mentionType: { + serializedName: "mentionType", + type: { + name: "String" + } + }, + mri: { + serializedName: "mri", + type: { + name: "String" + } + }, + displayName: { + serializedName: "displayName", + type: { + name: "String" + } + } + } + } +} + export const CardAction: msRest.CompositeMapper = { serializedName: 'CardAction', type: { diff --git a/libraries/botframework-connector/src/teams/models/teamsMappers.ts b/libraries/botframework-connector/src/teams/models/teamsMappers.ts index 9de38dce7c..41ce4b0e60 100644 --- a/libraries/botframework-connector/src/teams/models/teamsMappers.ts +++ b/libraries/botframework-connector/src/teams/models/teamsMappers.ts @@ -13,4 +13,12 @@ export { TeamsMeetingDetails, TeamsMeetingInfo, TeamsMeetingParticipant, + TeamsMeetingNotification, + TeamsMeetingNotificationInfo, + TeamsMeetingNotificationSurface, + TeamsMeetingNotificationChannelData, + TeamsMeetingOnBehalfOf, + TaskModuleContinueResponse, + TaskModuleTaskInfo, + Attachment, } from './mappers'; diff --git a/libraries/botframework-connector/src/teams/operations/teams.ts b/libraries/botframework-connector/src/teams/operations/teams.ts index 19bc854794..7e495d9b27 100644 --- a/libraries/botframework-connector/src/teams/operations/teams.ts +++ b/libraries/botframework-connector/src/teams/operations/teams.ts @@ -240,6 +240,24 @@ export class Teams { callback ) as Promise; } + + // TODO: Add multiple method definitions + sendMeetingNotification( + meetingId: string, + notification: any, + options?: msRest.RequestOptionsBase, + callback?: msRest.ServiceCallback + ): Promise { + return this.client.sendOperationRequest( + { + meetingId, + notification, + options + }, + sendMeetingNotificationOperationSpec, + callback + ) as Promise; + } } // Operation Specifications @@ -296,3 +314,20 @@ const fetchMeetingInfoOperationSpec: msRest.OperationSpec = { }, serializer, }; + +const sendMeetingNotificationOperationSpec: msRest.OperationSpec = { + httpMethod: 'POST', + path: 'v1/meetings/{meetingId}/notification', + urlParameters: [Parameters.meetingId], + requestBody: { + parameterPath: "notification", + mapper: { + ...Mappers.TeamsMeetingNotification, + required: true + } + }, + responses: { + default: {} // TODO: ADD response mappers + }, + serializer +} \ No newline at end of file diff --git a/libraries/botframework-schema/src/teams/index.ts b/libraries/botframework-schema/src/teams/index.ts index b328967737..f2376a6dc3 100644 --- a/libraries/botframework-schema/src/teams/index.ts +++ b/libraries/botframework-schema/src/teams/index.ts @@ -1817,3 +1817,41 @@ export interface MeetingEndEventDetails extends MeetingEventDetails { */ endTime: Date; } + +export interface TeamsMeetingNotification { + type: string; + value: TeamsMeetingNotificationInfo; + channelData: TeamsMeetingNotificationChannelData; +} + +export interface TeamsMeetingNotificationInfo { + recipients: string[]; + surfaces: TeamsMeetingNotificationSurface[]; +} + +export interface TeamsMeetingNotificationSurface { + surface: string; + contentType: string; + content: TaskModuleContinueResponse; +} + +export interface TeamsMeetingNotificationChannelData { + onBehalfOf: TeamsMeetingOnBehalfOf; +} + +export interface TeamsMeetingOnBehalfOf { + itemid: number; // Supposed to be an integer BUT Typescript does not have an integer type. Should we be explicit here? i.e itemid: 0 | 1 | ..etc + mentionType: string; + mri: string; + displayName: string; +} + +export interface TeamsMeetingNotificationRecipientFailureInfo { + recipientMri: string; + failureReason: string; + errorCode: string; +} + +export interface TeamsMeetingNotificationRecipientFailureInfos { + recipientsFailureInfo: TeamsMeetingNotificationRecipientFailureInfo[]; +} \ No newline at end of file From 1ca64a713c59c013195310e4f391a687c653bcf4 Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Wed, 7 Dec 2022 20:12:05 -0800 Subject: [PATCH 2/8] manual tested implementation w/o unit tests, --- libraries/botbuilder/src/teamsInfo.ts | 5 +- .../src/teams/models/index.ts | 21 ++++++- .../src/teams/models/mappers.ts | 50 +++++++++++++++ .../src/teams/models/teamsMappers.ts | 10 ++- .../src/teams/operations/teams.ts | 63 ++++++++++++++++--- .../botframework-schema/src/teams/index.ts | 28 +++++++++ 6 files changed, 166 insertions(+), 11 deletions(-) diff --git a/libraries/botbuilder/src/teamsInfo.ts b/libraries/botbuilder/src/teamsInfo.ts index 88853ff47b..150c09d21d 100644 --- a/libraries/botbuilder/src/teamsInfo.ts +++ b/libraries/botbuilder/src/teamsInfo.ts @@ -22,6 +22,8 @@ import { TeamsMeetingParticipant, TeamsMeetingInfo, Channels, + TeamsMeetingNotification, + TeamsMeetingNotificationRecipientFailureInfos, } from 'botbuilder-core'; import { ConnectorClient, TeamsConnectorClient, TeamsConnectorModels } from 'botframework-connector'; @@ -333,8 +335,7 @@ export class TeamsInfo { return await this.getMemberInternal(this.getConnectorClient(context), t, userId); } - // TODO: Update types - static async sendMeetingNotification(context: TurnContext, notification: any, meetingId?: string): Promise { + static async sendMeetingNotification(context: TurnContext, notification: TeamsMeetingNotification, meetingId?: string): Promise { const activity = context.activity; if (meetingId == null) { diff --git a/libraries/botframework-connector/src/teams/models/index.ts b/libraries/botframework-connector/src/teams/models/index.ts index 4b7982e12f..6a6d9d8561 100644 --- a/libraries/botframework-connector/src/teams/models/index.ts +++ b/libraries/botframework-connector/src/teams/models/index.ts @@ -4,7 +4,7 @@ */ import { HttpResponse, ServiceClientOptions, RequestOptionsBase } from '@azure/ms-rest-js'; -import { ConversationList, TeamDetails, TeamsMeetingInfo, TeamsMeetingParticipant } from 'botframework-schema'; +import { ConversationList, TeamDetails, TeamsMeetingInfo, TeamsMeetingNotificationRecipientFailureInfos, TeamsMeetingParticipant } from 'botframework-schema'; /** * @interface @@ -118,3 +118,22 @@ export type TeamsMeetingInfoResponse = TeamsMeetingInfo & { parsedBody: TeamsMeetingParticipant; }; }; + +/** + * Contains response data for the sendMeetingNotification operation. + */ + export type TeamsSendMeetingNotificationResponse = TeamsMeetingNotificationRecipientFailureInfos & { + /** + * The underlying HTTP response. + */ + _response: HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + /** + * The response body as parsed JSON or XML + */ + parsedBody: TeamsMeetingNotificationRecipientFailureInfos | {}; + }; +}; \ No newline at end of file diff --git a/libraries/botframework-connector/src/teams/models/mappers.ts b/libraries/botframework-connector/src/teams/models/mappers.ts index 33fb5b48ec..dfb4d790dd 100644 --- a/libraries/botframework-connector/src/teams/models/mappers.ts +++ b/libraries/botframework-connector/src/teams/models/mappers.ts @@ -583,6 +583,56 @@ export const TeamsMeetingOnBehalfOf: msRest.CompositeMapper = { } } +export const TeamsMeetingNotificationRecipientFailureInfo: msRest.CompositeMapper = { + serializedName: "TeamsMeetingNotificationRecipientFailureInfo", + type: { + name: "Composite", + className: "TeamsMeetingNotificationRecipientFailureInfo", + modelProperties: { + recipientMri: { + serializedName: "recipientMri", + type: { + name: "String" + } + }, + failureReason: { + serializedName: "failureReason", + type: { + name: "String" + } + }, + errorCode: { + serializedName: "errorCode", + type: { + name: "String" + } + }, + } + } +} + +export const TeamsMeetingNotificationRecipientFailureInfos: msRest.CompositeMapper = { + serializedName: "TeamsMeetingNotificationRecipientFailureInfos", + type: { + name: "Composite", + className: "TeamsMeetingNotificationRecipientFailureInfos", + modelProperties: { + recipientsFailureInfo: { + serializedName: "recipientsFailureInfo", + type: { + name: "Sequence", + element: { + type: { + name: "Composite", + className: "TeamsMeetingNotificationRecipientFailureInfo" + } + } + } + }, + } + } +} + export const CardAction: msRest.CompositeMapper = { serializedName: 'CardAction', type: { diff --git a/libraries/botframework-connector/src/teams/models/teamsMappers.ts b/libraries/botframework-connector/src/teams/models/teamsMappers.ts index 41ce4b0e60..78f58e68d3 100644 --- a/libraries/botframework-connector/src/teams/models/teamsMappers.ts +++ b/libraries/botframework-connector/src/teams/models/teamsMappers.ts @@ -3,6 +3,12 @@ * Licensed under the MIT License. */ +export { + ErrorResponse, + ErrorModel, + InnerHttpError +} from '../../connectorApi/models/mappers'; + export { ConversationList, ChannelInfo, @@ -18,7 +24,9 @@ export { TeamsMeetingNotificationSurface, TeamsMeetingNotificationChannelData, TeamsMeetingOnBehalfOf, + TeamsMeetingNotificationRecipientFailureInfo, + TeamsMeetingNotificationRecipientFailureInfos, TaskModuleContinueResponse, TaskModuleTaskInfo, Attachment, -} from './mappers'; +} from './mappers'; \ No newline at end of file diff --git a/libraries/botframework-connector/src/teams/operations/teams.ts b/libraries/botframework-connector/src/teams/operations/teams.ts index 7e495d9b27..cbec06134a 100644 --- a/libraries/botframework-connector/src/teams/operations/teams.ts +++ b/libraries/botframework-connector/src/teams/operations/teams.ts @@ -9,7 +9,7 @@ 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 } from 'botframework-schema'; +import { ConversationList, TeamDetails, TeamsMeetingInfo, TeamsMeetingParticipant, TeamsMeetingNotificationRecipientFailureInfos, TeamsMeetingNotification } from 'botframework-schema'; /** Class representing a Teams. */ export class Teams { @@ -241,13 +241,54 @@ export class Teams { ) as Promise; } - // TODO: Add multiple method definitions + /** + * Send meeting notification. + * + * @param meetingId Meeting Id. + * @param notification The content and configuration for the notification to send. + * @param options The optional parameters. + * @param options + */ sendMeetingNotification( meetingId: string, - notification: any, + notification: TeamsMeetingNotification, options?: msRest.RequestOptionsBase, - callback?: msRest.ServiceCallback - ): Promise { + ): Promise + /** + * @param meetingId Meeting Id. + * @param notification The content and configuration for the notification to send. + * @param callback The callback. + */ + sendMeetingNotification( + meetingId: string, + notification: TeamsMeetingNotification, + callback: msRest.ServiceCallback + ): void; + /** + * @param meetingId Meeting Id. + * @param notification The content and configuration for the notification to send. + * @param options The optional parameters. + * @param callback The callback. + */ + sendMeetingNotification( + meetingId: string, + notification: TeamsMeetingNotification, + options: msRest.RequestOptionsBase, + callback: msRest.ServiceCallback + ): void; + /** + * @param meetingId Meeting Id. + * @param notification The content and configuration for the notification to send. + * @param options The optional parameters. + * @param callback The callback. + * @returns Promise with either TeamsMeetingNotificationRecipientFailureInfos or an empty object. + */ + sendMeetingNotification( + meetingId: string, + notification: TeamsMeetingNotification, + options?: msRest.RequestOptionsBase, + callback?: msRest.ServiceCallback + ): Promise { return this.client.sendOperationRequest( { meetingId, @@ -256,7 +297,7 @@ export class Teams { }, sendMeetingNotificationOperationSpec, callback - ) as Promise; + ) as Promise; } } @@ -327,7 +368,15 @@ const sendMeetingNotificationOperationSpec: msRest.OperationSpec = { } }, responses: { - default: {} // TODO: ADD response mappers + 202: { + bodyMapper: Mappers.TeamsMeetingNotificationRecipientFailureInfos + }, + 207: { + bodyMapper: Mappers.TeamsMeetingNotificationRecipientFailureInfos + }, + default: { + bodyMapper: Mappers.ErrorResponse + } }, serializer } \ No newline at end of file diff --git a/libraries/botframework-schema/src/teams/index.ts b/libraries/botframework-schema/src/teams/index.ts index f2376a6dc3..210bb89f87 100644 --- a/libraries/botframework-schema/src/teams/index.ts +++ b/libraries/botframework-schema/src/teams/index.ts @@ -1818,27 +1818,47 @@ export interface MeetingEndEventDetails extends MeetingEventDetails { endTime: Date; } +/** + * @interface + * Specifies details of a Teams meeting send notification payload. + */ export interface TeamsMeetingNotification { type: string; value: TeamsMeetingNotificationInfo; channelData: TeamsMeetingNotificationChannelData; } +/** + * @interface + * Specifies recipients and surfaces to target in a Teams meeting notification. + */ export interface TeamsMeetingNotificationInfo { recipients: string[]; surfaces: TeamsMeetingNotificationSurface[]; } +/** + * @interface + * Specifies the surface and content for a Teams meeting notification. + */ export interface TeamsMeetingNotificationSurface { surface: string; contentType: string; content: TaskModuleContinueResponse; } +/** + * @interface + * Specifies channel data associated with the Teams meeting notification. + */ export interface TeamsMeetingNotificationChannelData { onBehalfOf: TeamsMeetingOnBehalfOf; } +/** + * @interface + * Specifies the Teams user that triggered the Teams meeting notification. + */ export interface TeamsMeetingOnBehalfOf { itemid: number; // Supposed to be an integer BUT Typescript does not have an integer type. Should we be explicit here? i.e itemid: 0 | 1 | ..etc mentionType: string; @@ -1846,12 +1866,20 @@ export interface TeamsMeetingOnBehalfOf { displayName: string; } +/** + * @interface + * Specifies the recipients for which the Teams meeting notification was not sent. + */ export interface TeamsMeetingNotificationRecipientFailureInfo { recipientMri: string; failureReason: string; errorCode: string; } +/** + * @interface + * Specifies the list of recipients for which the Teams meeting notification was not sent. + */ export interface TeamsMeetingNotificationRecipientFailureInfos { recipientsFailureInfo: TeamsMeetingNotificationRecipientFailureInfo[]; } \ No newline at end of file From b5ea5075842ffe31c0d8c5e439090da8cb2b7e88 Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Thu, 8 Dec 2022 00:20:54 -0800 Subject: [PATCH 3/8] added unit tests --- libraries/botbuilder/tests/teamsInfo.test.js | 141 +++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/libraries/botbuilder/tests/teamsInfo.test.js b/libraries/botbuilder/tests/teamsInfo.test.js index 0e18915eff..1a8b8ec24d 100644 --- a/libraries/botbuilder/tests/teamsInfo.test.js +++ b/libraries/botbuilder/tests/teamsInfo.test.js @@ -953,6 +953,147 @@ describe('TeamsInfo', function () { }); }); + describe('sendTeamsMeetingNotification()', function () { + it("should correctly map notification object as the request body of the POST request", async () => { + const notification = { + type: "targetedMeetingNotification", + value: { + recipients: [ + "8:orgid:3c493705-8971-4e22-9830-967ad65cc74a", + ], + surfaces: [ + { + surface: "meetingStage", + contentType: "task", + content: { + value: { + height: "3", + width: "4", + title: "this is Yunny's test", + url: "https://www.bing.com" + } + } + } + ] + }, + channelData: { + onBehalfOf: [ // support for user attributions + { + itemid: 0, + mentionType: "person", + mri: "8:orgid:cf41f188-30ee-4698-9a65-8af95b9eb9c3", + displayName: "MOD Administrator" + } + ] + } + }; + const meetingId = "randomGUID"; + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') + .post(`/v1/meetings/${meetingId}/notification`, notification) + .matchHeader('Authorization', expectedAuthHeader) + .reply(202, {}); + + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + // if notification object wasn't passed as request body, test would fail + const sendTeamsMeetingNotification = await TeamsInfo.sendMeetingNotification(context, notification, meetingId); + + assert(fetchOauthToken.isDone()); + assert(sendTeamsMeetingNotificationExpectation.isDone()); + }) + + it("should return an empty object if a 202 status code was returned", async function () { + const notification = {}; + const meetingId = "randomGUID"; + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') + .post(`/v1/meetings/${meetingId}/notification`, notification) + .matchHeader('Authorization', expectedAuthHeader) + .reply(202, {}); + + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + const sendTeamsMeetingNotification = await TeamsInfo.sendMeetingNotification(context, notification, meetingId); + + assert(fetchOauthToken.isDone()); + assert(sendTeamsMeetingNotificationExpectation.isDone()); + + const isEmptyObject = obj => Object.keys(obj).length == 0; + assert(isEmptyObject(sendTeamsMeetingNotification)); + }) + + it("should return a TeamsMeetingNotificationRecipientFailureInfos if a 207 status code was returned", async function () { + const notification = {}; + const meetingId = "randomGUID"; + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const recipientsFailureInfo = { + recipientsFailureInfo: [ + { + recipientMri: '8:orgid:4e8a10c0-4687-4f0a-9ed6-95f28d67c102', + failureReason: 'Invalid recipient. Recipient not in roster', + errorCode: 'MemberNotFoundInConversation' + }, + { + recipientMri: '8:orgid:4e8a10c0-4687-4f0a-9ed6-95f28d67c103', + failureReason: 'Invalid recipient. Recipient not in roster', + errorCode: 'MemberNotFoundInConversation' + } + ] + } + + const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') + .post(`/v1/meetings/${meetingId}/notification`, notification) + .matchHeader('Authorization', expectedAuthHeader) + .reply(207, recipientsFailureInfo); + + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + const sendTeamsMeetingNotification = await TeamsInfo.sendMeetingNotification(context, notification, meetingId); + + assert(fetchOauthToken.isDone()); + assert(sendTeamsMeetingNotificationExpectation.isDone()); + + assert.deepEqual(sendTeamsMeetingNotification, recipientsFailureInfo); + }) + + it("should return standard error response if a 4xx status code was returned", async function () { + const notification = {}; + const meetingId = "randomGUID"; + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; + + const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') + .post(`/v1/meetings/${meetingId}/notification`, notification) + .matchHeader('Authorization', expectedAuthHeader) + .reply(400, errorResponse) + + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + let isErrorThrown = false; + try { + await TeamsInfo.sendMeetingNotification(context, notification, meetingId); + } catch (e) { + assert.deepEqual(errorResponse, e.body); + isErrorThrown = true; + } + + assert(isErrorThrown); + + assert(fetchOauthToken.isDone()); + assert(sendTeamsMeetingNotificationExpectation.isDone()); + }) + }) + describe('private methods', function () { describe('getConnectorClient()', function () { it("should error if the context doesn't have an adapter", function () { From ea2be51e82e1d6ac378e509c36fc5da0d341bc14 Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Thu, 8 Dec 2022 00:35:20 -0800 Subject: [PATCH 4/8] run api extractor tool --- libraries/botbuilder/etc/botbuilder.api.md | 4 ++ .../etc/botframework-schema.api.md | 62 +++++++++++++++++++ .../botframework-schema/src/teams/index.ts | 2 +- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/libraries/botbuilder/etc/botbuilder.api.md b/libraries/botbuilder/etc/botbuilder.api.md index 3775c6c5c5..784e8365cc 100644 --- a/libraries/botbuilder/etc/botbuilder.api.md +++ b/libraries/botbuilder/etc/botbuilder.api.md @@ -70,6 +70,8 @@ import { TeamDetails } from 'botbuilder-core'; import { TeamInfo } from 'botbuilder-core'; import { TeamsChannelAccount } from 'botbuilder-core'; import { TeamsMeetingInfo } from 'botbuilder-core'; +import { TeamsMeetingNotification } from 'botbuilder-core'; +import { TeamsMeetingNotificationRecipientFailureInfos } from 'botbuilder-core'; import { TeamsMeetingParticipant } from 'botbuilder-core'; import { TeamsPagedMembersResult } from 'botbuilder-core'; import { TenantInfo } from 'botbuilder-core'; @@ -448,6 +450,8 @@ export class TeamsInfo { static getTeamMember(context: TurnContext, teamId?: string, userId?: string): Promise; // @deprecated static getTeamMembers(context: TurnContext, teamId?: string): Promise; + // (undocumented) + static sendMeetingNotification(context: TurnContext, notification: TeamsMeetingNotification, meetingId?: string): Promise; static sendMessageToTeamsChannel(context: TurnContext, activity: Activity, teamsChannelId: string, botAppId?: string): Promise<[ConversationReference, string]>; } diff --git a/libraries/botframework-schema/etc/botframework-schema.api.md b/libraries/botframework-schema/etc/botframework-schema.api.md index e90a9c18ec..e560385b08 100644 --- a/libraries/botframework-schema/etc/botframework-schema.api.md +++ b/libraries/botframework-schema/etc/botframework-schema.api.md @@ -1716,6 +1716,68 @@ export interface TeamsMeetingInfo { id?: string; } +// @public +export interface TeamsMeetingNotification { + // (undocumented) + channelData: TeamsMeetingNotificationChannelData; + // (undocumented) + type: string; + // (undocumented) + value: TeamsMeetingNotificationInfo; +} + +// @public +export interface TeamsMeetingNotificationChannelData { + // (undocumented) + onBehalfOf: TeamsMeetingOnBehalfOf; +} + +// @public +export interface TeamsMeetingNotificationInfo { + // (undocumented) + recipients: string[]; + // (undocumented) + surfaces: TeamsMeetingNotificationSurface[]; +} + +// @public +export interface TeamsMeetingNotificationRecipientFailureInfo { + // (undocumented) + errorCode: string; + // (undocumented) + failureReason: string; + // (undocumented) + recipientMri: string; +} + +// @public +export interface TeamsMeetingNotificationRecipientFailureInfos { + // (undocumented) + recipientsFailureInfo: TeamsMeetingNotificationRecipientFailureInfo[]; +} + +// @public +export interface TeamsMeetingNotificationSurface { + // (undocumented) + content: TaskModuleContinueResponse; + // (undocumented) + contentType: string; + // (undocumented) + surface: string; +} + +// @public +export interface TeamsMeetingOnBehalfOf { + // (undocumented) + displayName: string; + // (undocumented) + itemid: number; + // (undocumented) + mentionType: string; + // (undocumented) + mri: string; +} + // @public export interface TeamsMeetingParticipant { conversation?: ConversationAccount; diff --git a/libraries/botframework-schema/src/teams/index.ts b/libraries/botframework-schema/src/teams/index.ts index 210bb89f87..a9ec0e36da 100644 --- a/libraries/botframework-schema/src/teams/index.ts +++ b/libraries/botframework-schema/src/teams/index.ts @@ -1860,7 +1860,7 @@ export interface TeamsMeetingNotificationChannelData { * Specifies the Teams user that triggered the Teams meeting notification. */ export interface TeamsMeetingOnBehalfOf { - itemid: number; // Supposed to be an integer BUT Typescript does not have an integer type. Should we be explicit here? i.e itemid: 0 | 1 | ..etc + itemid: number; mentionType: string; mri: string; displayName: string; From c92a671b43e295d3d6569451a89fb694f75f8d46 Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Thu, 8 Dec 2022 01:13:30 -0800 Subject: [PATCH 5/8] processed lint fixes, api generations & added missing documentation --- libraries/botbuilder/etc/botbuilder.api.md | 1 - libraries/botbuilder/src/teamsInfo.ts | 16 +- libraries/botbuilder/tests/teamsInfo.test.js | 139 +++++----- .../src/teams/models/index.ts | 12 +- .../src/teams/models/mappers.ts | 237 +++++++++--------- .../src/teams/models/teamsMappers.ts | 8 +- .../src/teams/operations/teams.ts | 3 +- .../etc/botframework-schema.api.md | 28 +-- .../botframework-schema/src/teams/index.ts | 62 ++++- 9 files changed, 275 insertions(+), 231 deletions(-) diff --git a/libraries/botbuilder/etc/botbuilder.api.md b/libraries/botbuilder/etc/botbuilder.api.md index 784e8365cc..324d807b40 100644 --- a/libraries/botbuilder/etc/botbuilder.api.md +++ b/libraries/botbuilder/etc/botbuilder.api.md @@ -450,7 +450,6 @@ export class TeamsInfo { static getTeamMember(context: TurnContext, teamId?: string, userId?: string): Promise; // @deprecated static getTeamMembers(context: TurnContext, teamId?: string): Promise; - // (undocumented) static sendMeetingNotification(context: TurnContext, notification: TeamsMeetingNotification, meetingId?: string): 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 150c09d21d..d72c780a4c 100644 --- a/libraries/botbuilder/src/teamsInfo.ts +++ b/libraries/botbuilder/src/teamsInfo.ts @@ -335,7 +335,21 @@ export class TeamsInfo { return await this.getMemberInternal(this.getConnectorClient(context), t, userId); } - static async sendMeetingNotification(context: TurnContext, notification: TeamsMeetingNotification, meetingId?: string): Promise { + /** + * Sends a meeting notification to specific users in a Teams meeting. + * + * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for this turn. + * @param notification The meeting notification payload. + * @param meetingId Id of the Teams meeting. + * @returns Promise with either an empty object if notifications were successfully sent to all recipients or + * [TeamsMeetingNotificationRecipientFailureInfos](xref:botframework-schema.TeamsMeetingNotificationRecipientFailureInfos) if notifications + * were sent to some but not all recipients. + */ + static async sendMeetingNotification( + context: TurnContext, + notification: TeamsMeetingNotification, + meetingId?: string + ): Promise { const activity = context.activity; if (meetingId == null) { diff --git a/libraries/botbuilder/tests/teamsInfo.test.js b/libraries/botbuilder/tests/teamsInfo.test.js index 1a8b8ec24d..0676ed57b6 100644 --- a/libraries/botbuilder/tests/teamsInfo.test.js +++ b/libraries/botbuilder/tests/teamsInfo.test.js @@ -954,128 +954,131 @@ describe('TeamsInfo', function () { }); describe('sendTeamsMeetingNotification()', function () { - it("should correctly map notification object as the request body of the POST request", async () => { + it('should correctly map notification object as the request body of the POST request', async function () { const notification = { - type: "targetedMeetingNotification", + type: 'targetedMeetingNotification', value: { - recipients: [ - "8:orgid:3c493705-8971-4e22-9830-967ad65cc74a", - ], + recipients: ['random recipient id'], surfaces: [ { - surface: "meetingStage", - contentType: "task", - content: { - value: { - height: "3", - width: "4", - title: "this is Yunny's test", - url: "https://www.bing.com" - } - } - } - ] + surface: 'meetingStage', + contentType: 'task', + content: { + value: { + height: '3', + width: '4', + title: "this is Johnny's test", + url: 'https://www.bing.com', + }, + }, + }, + ], }, channelData: { - onBehalfOf: [ // support for user attributions - { - itemid: 0, - mentionType: "person", - mri: "8:orgid:cf41f188-30ee-4698-9a65-8af95b9eb9c3", - displayName: "MOD Administrator" - } - ] - } - }; - const meetingId = "randomGUID"; + onBehalfOf: [ + // support for user attributions + { + itemid: 0, + mentionType: 'person', + mri: 'random admin', + displayName: 'admin', + }, + ], + }, + }; + const meetingId = 'randomGUID'; const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') - .post(`/v1/meetings/${meetingId}/notification`, notification) - .matchHeader('Authorization', expectedAuthHeader) - .reply(202, {}); + .post(`/v1/meetings/${meetingId}/notification`, notification) + .matchHeader('Authorization', expectedAuthHeader) + .reply(202, {}); - const context = new TestContext(teamActivity); context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); // if notification object wasn't passed as request body, test would fail - const sendTeamsMeetingNotification = await TeamsInfo.sendMeetingNotification(context, notification, meetingId); + await TeamsInfo.sendMeetingNotification(context, notification, meetingId); assert(fetchOauthToken.isDone()); assert(sendTeamsMeetingNotificationExpectation.isDone()); - }) + }); - it("should return an empty object if a 202 status code was returned", async function () { + it('should return an empty object if a 202 status code was returned', async function () { const notification = {}; - const meetingId = "randomGUID"; + const meetingId = 'randomGUID'; const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') - .post(`/v1/meetings/${meetingId}/notification`, notification) - .matchHeader('Authorization', expectedAuthHeader) - .reply(202, {}); + .post(`/v1/meetings/${meetingId}/notification`, notification) + .matchHeader('Authorization', expectedAuthHeader) + .reply(202, {}); - const context = new TestContext(teamActivity); context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); - const sendTeamsMeetingNotification = await TeamsInfo.sendMeetingNotification(context, notification, meetingId); + const sendTeamsMeetingNotification = await TeamsInfo.sendMeetingNotification( + context, + notification, + meetingId + ); assert(fetchOauthToken.isDone()); assert(sendTeamsMeetingNotificationExpectation.isDone()); - const isEmptyObject = obj => Object.keys(obj).length == 0; + const isEmptyObject = (obj) => Object.keys(obj).length == 0; assert(isEmptyObject(sendTeamsMeetingNotification)); - }) + }); - it("should return a TeamsMeetingNotificationRecipientFailureInfos if a 207 status code was returned", async function () { + it('should return a TeamsMeetingNotificationRecipientFailureInfos if a 207 status code was returned', async function () { const notification = {}; - const meetingId = "randomGUID"; + const meetingId = 'randomGUID'; const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); const recipientsFailureInfo = { recipientsFailureInfo: [ { - recipientMri: '8:orgid:4e8a10c0-4687-4f0a-9ed6-95f28d67c102', - failureReason: 'Invalid recipient. Recipient not in roster', - errorCode: 'MemberNotFoundInConversation' + recipientMri: '8:orgid:4e8a10c0-4687-4f0a-9ed6-95f28d67c102', + failureReason: 'Invalid recipient. Recipient not in roster', + errorCode: 'MemberNotFoundInConversation', }, { - recipientMri: '8:orgid:4e8a10c0-4687-4f0a-9ed6-95f28d67c103', - failureReason: 'Invalid recipient. Recipient not in roster', - errorCode: 'MemberNotFoundInConversation' - } - ] - } + recipientMri: '8:orgid:4e8a10c0-4687-4f0a-9ed6-95f28d67c103', + failureReason: 'Invalid recipient. Recipient not in roster', + errorCode: 'MemberNotFoundInConversation', + }, + ], + }; const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') - .post(`/v1/meetings/${meetingId}/notification`, notification) - .matchHeader('Authorization', expectedAuthHeader) - .reply(207, recipientsFailureInfo); + .post(`/v1/meetings/${meetingId}/notification`, notification) + .matchHeader('Authorization', expectedAuthHeader) + .reply(207, recipientsFailureInfo); - const context = new TestContext(teamActivity); context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); - const sendTeamsMeetingNotification = await TeamsInfo.sendMeetingNotification(context, notification, meetingId); + const sendTeamsMeetingNotification = await TeamsInfo.sendMeetingNotification( + context, + notification, + meetingId + ); assert(fetchOauthToken.isDone()); assert(sendTeamsMeetingNotificationExpectation.isDone()); assert.deepEqual(sendTeamsMeetingNotification, recipientsFailureInfo); - }) + }); - it("should return standard error response if a 4xx status code was returned", async function () { + it('should return standard error response if a 4xx status code was returned', async function () { const notification = {}; - const meetingId = "randomGUID"; + const meetingId = 'randomGUID'; const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); const errorResponse = { error: { code: 'BadSyntax', message: 'Payload is incorrect' } }; const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') - .post(`/v1/meetings/${meetingId}/notification`, notification) - .matchHeader('Authorization', expectedAuthHeader) - .reply(400, errorResponse) + .post(`/v1/meetings/${meetingId}/notification`, notification) + .matchHeader('Authorization', expectedAuthHeader) + .reply(400, errorResponse); - const context = new TestContext(teamActivity); context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); @@ -1091,8 +1094,8 @@ describe('TeamsInfo', function () { assert(fetchOauthToken.isDone()); assert(sendTeamsMeetingNotificationExpectation.isDone()); - }) - }) + }); + }); describe('private methods', function () { describe('getConnectorClient()', function () { diff --git a/libraries/botframework-connector/src/teams/models/index.ts b/libraries/botframework-connector/src/teams/models/index.ts index 6a6d9d8561..74777f9ba7 100644 --- a/libraries/botframework-connector/src/teams/models/index.ts +++ b/libraries/botframework-connector/src/teams/models/index.ts @@ -4,7 +4,13 @@ */ import { HttpResponse, ServiceClientOptions, RequestOptionsBase } from '@azure/ms-rest-js'; -import { ConversationList, TeamDetails, TeamsMeetingInfo, TeamsMeetingNotificationRecipientFailureInfos, TeamsMeetingParticipant } from 'botframework-schema'; +import { + ConversationList, + TeamDetails, + TeamsMeetingInfo, + TeamsMeetingNotificationRecipientFailureInfos, + TeamsMeetingParticipant, +} from 'botframework-schema'; /** * @interface @@ -122,7 +128,7 @@ export type TeamsMeetingInfoResponse = TeamsMeetingInfo & { /** * Contains response data for the sendMeetingNotification operation. */ - export type TeamsSendMeetingNotificationResponse = TeamsMeetingNotificationRecipientFailureInfos & { +export type TeamsSendMeetingNotificationResponse = TeamsMeetingNotificationRecipientFailureInfos & { /** * The underlying HTTP response. */ @@ -136,4 +142,4 @@ export type TeamsMeetingInfoResponse = TeamsMeetingInfo & { */ parsedBody: TeamsMeetingNotificationRecipientFailureInfos | {}; }; -}; \ No newline at end of file +}; diff --git a/libraries/botframework-connector/src/teams/models/mappers.ts b/libraries/botframework-connector/src/teams/models/mappers.ts index dfb4d790dd..7bb9f126f8 100644 --- a/libraries/botframework-connector/src/teams/models/mappers.ts +++ b/libraries/botframework-connector/src/teams/models/mappers.ts @@ -435,203 +435,202 @@ export const TeamsMeetingDetails: msRest.CompositeMapper = { }; export const TeamsMeetingNotificationInfo: msRest.CompositeMapper = { - serializedName: "TeamsMeetingNotificationInfo", + serializedName: 'TeamsMeetingNotificationInfo', type: { - name: "Composite", - className: "TeamsMeetingNotificationInfo", + name: 'Composite', + className: 'TeamsMeetingNotificationInfo', modelProperties: { recipients: { - serializedName: "recipients", + serializedName: 'recipients', type: { - name: "Sequence", + name: 'Sequence', element: { type: { - name: "String", - } - } - } + name: 'String', + }, + }, + }, }, surfaces: { - serializedName: "surfaces", + serializedName: 'surfaces', type: { - name: "Sequence", + name: 'Sequence', element: { type: { - name: "Composite", - className: "TeamsMeetingNotificationSurface" - } - } - } - } - } - } -} + name: 'Composite', + className: 'TeamsMeetingNotificationSurface', + }, + }, + }, + }, + }, + }, +}; export const TeamsMeetingNotification: msRest.CompositeMapper = { - serializedName: "TeamsMeetingNotification", + serializedName: 'TeamsMeetingNotification', type: { - name: "Composite", - className: "TeamsMeetingNotification", + name: 'Composite', + className: 'TeamsMeetingNotification', modelProperties: { type: { - serializedName: "type", + serializedName: 'type', type: { - name: "String" - } + name: 'String', + }, }, value: { - serializedName: "value", + serializedName: 'value', type: { - name: "Composite", - className: "TeamsMeetingNotificationInfo" - } + name: 'Composite', + className: 'TeamsMeetingNotificationInfo', + }, }, channelData: { - serializedName: "channelData", + serializedName: 'channelData', type: { - name: "Composite", - className: "TeamsMeetingNotificationChannelData" - } - } - - } - } -} + name: 'Composite', + className: 'TeamsMeetingNotificationChannelData', + }, + }, + }, + }, +}; export const TeamsMeetingNotificationSurface: msRest.CompositeMapper = { - serializedName: "TeamsMeetingNotificationSurface", + serializedName: 'TeamsMeetingNotificationSurface', type: { - name: "Composite", - className: "TeamsMeetingNotificationSurface", + name: 'Composite', + className: 'TeamsMeetingNotificationSurface', modelProperties: { surface: { - serializedName: "surface", + serializedName: 'surface', type: { - name: "String" - } + name: 'String', + }, }, contentType: { - serializedName: "contentType", + serializedName: 'contentType', type: { - name: "String", - } + name: 'String', + }, }, content: { - serializedName: "content", + serializedName: 'content', type: { - name: "Composite", - className: "TaskModuleContinueResponse" - } - } - } - } -} + name: 'Composite', + className: 'TaskModuleContinueResponse', + }, + }, + }, + }, +}; export const TeamsMeetingNotificationChannelData = { - serializedName: "TeamsMeetingNotificationChannelData", + serializedName: 'TeamsMeetingNotificationChannelData', type: { - name: "Composite", - className: "TeamsMeetingNotificationChannelData", + name: 'Composite', + className: 'TeamsMeetingNotificationChannelData', modelProperties: { onBehalfOf: { - serializedName: "onBehalfOf", + serializedName: 'onBehalfOf', type: { - name: "Sequence", + name: 'Sequence', element: { - type: { - name: "Composite", - className: "TeamsMeetingOnBehalfOf" - } - } - } - } - } - } -} + type: { + name: 'Composite', + className: 'TeamsMeetingOnBehalfOf', + }, + }, + }, + }, + }, + }, +}; export const TeamsMeetingOnBehalfOf: msRest.CompositeMapper = { - serializedName: "TeamsMeetingOnBehalfOf", + serializedName: 'TeamsMeetingOnBehalfOf', type: { - name: "Composite", - className: "TeamsMeetingOnBehalfOf", + name: 'Composite', + className: 'TeamsMeetingOnBehalfOf', modelProperties: { itemid: { - serializedName: "itemid", + serializedName: 'itemid', type: { - name: "Number" - } + name: 'Number', + }, }, mentionType: { - serializedName: "mentionType", + serializedName: 'mentionType', type: { - name: "String" - } + name: 'String', + }, }, mri: { - serializedName: "mri", + serializedName: 'mri', type: { - name: "String" - } + name: 'String', + }, }, displayName: { - serializedName: "displayName", + serializedName: 'displayName', type: { - name: "String" - } - } - } - } -} + name: 'String', + }, + }, + }, + }, +}; export const TeamsMeetingNotificationRecipientFailureInfo: msRest.CompositeMapper = { - serializedName: "TeamsMeetingNotificationRecipientFailureInfo", + serializedName: 'TeamsMeetingNotificationRecipientFailureInfo', type: { - name: "Composite", - className: "TeamsMeetingNotificationRecipientFailureInfo", + name: 'Composite', + className: 'TeamsMeetingNotificationRecipientFailureInfo', modelProperties: { recipientMri: { - serializedName: "recipientMri", + serializedName: 'recipientMri', type: { - name: "String" - } + name: 'String', + }, }, failureReason: { - serializedName: "failureReason", + serializedName: 'failureReason', type: { - name: "String" - } + name: 'String', + }, }, errorCode: { - serializedName: "errorCode", + serializedName: 'errorCode', type: { - name: "String" - } + name: 'String', + }, }, - } - } -} + }, + }, +}; export const TeamsMeetingNotificationRecipientFailureInfos: msRest.CompositeMapper = { - serializedName: "TeamsMeetingNotificationRecipientFailureInfos", + serializedName: 'TeamsMeetingNotificationRecipientFailureInfos', type: { - name: "Composite", - className: "TeamsMeetingNotificationRecipientFailureInfos", + name: 'Composite', + className: 'TeamsMeetingNotificationRecipientFailureInfos', modelProperties: { recipientsFailureInfo: { - serializedName: "recipientsFailureInfo", + serializedName: 'recipientsFailureInfo', type: { - name: "Sequence", + name: 'Sequence', element: { type: { - name: "Composite", - className: "TeamsMeetingNotificationRecipientFailureInfo" - } - } - } - }, - } - } -} + name: 'Composite', + className: 'TeamsMeetingNotificationRecipientFailureInfo', + }, + }, + }, + }, + }, + }, +}; export const CardAction: msRest.CompositeMapper = { serializedName: 'CardAction', diff --git a/libraries/botframework-connector/src/teams/models/teamsMappers.ts b/libraries/botframework-connector/src/teams/models/teamsMappers.ts index 78f58e68d3..55686ad3aa 100644 --- a/libraries/botframework-connector/src/teams/models/teamsMappers.ts +++ b/libraries/botframework-connector/src/teams/models/teamsMappers.ts @@ -3,11 +3,7 @@ * Licensed under the MIT License. */ -export { - ErrorResponse, - ErrorModel, - InnerHttpError -} from '../../connectorApi/models/mappers'; +export { ErrorResponse, ErrorModel, InnerHttpError } from '../../connectorApi/models/mappers'; export { ConversationList, @@ -29,4 +25,4 @@ export { TaskModuleContinueResponse, TaskModuleTaskInfo, Attachment, -} from './mappers'; \ No newline at end of file +} from './mappers'; diff --git a/libraries/botframework-connector/src/teams/operations/teams.ts b/libraries/botframework-connector/src/teams/operations/teams.ts index cbec06134a..db04269a4b 100644 --- a/libraries/botframework-connector/src/teams/operations/teams.ts +++ b/libraries/botframework-connector/src/teams/operations/teams.ts @@ -247,7 +247,6 @@ export class Teams { * @param meetingId Meeting Id. * @param notification The content and configuration for the notification to send. * @param options The optional parameters. - * @param options */ sendMeetingNotification( meetingId: string, @@ -361,7 +360,7 @@ const sendMeetingNotificationOperationSpec: msRest.OperationSpec = { path: 'v1/meetings/{meetingId}/notification', urlParameters: [Parameters.meetingId], requestBody: { - parameterPath: "notification", + parameterPath: 'notification', mapper: { ...Mappers.TeamsMeetingNotification, required: true diff --git a/libraries/botframework-schema/etc/botframework-schema.api.md b/libraries/botframework-schema/etc/botframework-schema.api.md index e560385b08..ddfaa0616e 100644 --- a/libraries/botframework-schema/etc/botframework-schema.api.md +++ b/libraries/botframework-schema/etc/botframework-schema.api.md @@ -1721,60 +1721,46 @@ export interface TeamsMeetingNotification { // (undocumented) channelData: TeamsMeetingNotificationChannelData; // (undocumented) - type: string; + type: 'targetedMeetingNotification' | string; // (undocumented) value: TeamsMeetingNotificationInfo; } // @public export interface TeamsMeetingNotificationChannelData { - // (undocumented) - onBehalfOf: TeamsMeetingOnBehalfOf; + onBehalfOf?: TeamsMeetingOnBehalfOf; } // @public export interface TeamsMeetingNotificationInfo { - // (undocumented) recipients: string[]; - // (undocumented) surfaces: TeamsMeetingNotificationSurface[]; } // @public export interface TeamsMeetingNotificationRecipientFailureInfo { - // (undocumented) errorCode: string; - // (undocumented) failureReason: string; - // (undocumented) recipientMri: string; } // @public export interface TeamsMeetingNotificationRecipientFailureInfos { - // (undocumented) recipientsFailureInfo: TeamsMeetingNotificationRecipientFailureInfo[]; } // @public export interface TeamsMeetingNotificationSurface { - // (undocumented) content: TaskModuleContinueResponse; - // (undocumented) - contentType: string; - // (undocumented) - surface: string; + contentType: 'task' | string; + surface: 'meetingStage' | string; } // @public export interface TeamsMeetingOnBehalfOf { - // (undocumented) - displayName: string; - // (undocumented) - itemid: number; - // (undocumented) - mentionType: string; - // (undocumented) + displayName?: string; + itemid: 0 | number; + mentionType: 'person' | string; mri: string; } diff --git a/libraries/botframework-schema/src/teams/index.ts b/libraries/botframework-schema/src/teams/index.ts index a9ec0e36da..fac0cdcc0f 100644 --- a/libraries/botframework-schema/src/teams/index.ts +++ b/libraries/botframework-schema/src/teams/index.ts @@ -1823,7 +1823,7 @@ export interface MeetingEndEventDetails extends MeetingEventDetails { * Specifies details of a Teams meeting send notification payload. */ export interface TeamsMeetingNotification { - type: string; + type: 'targetedMeetingNotification' | string; value: TeamsMeetingNotificationInfo; channelData: TeamsMeetingNotificationChannelData; } @@ -1833,17 +1833,32 @@ export interface TeamsMeetingNotification { * Specifies recipients and surfaces to target in a Teams meeting notification. */ export interface TeamsMeetingNotificationInfo { + /** + * @member {string[]} [recipients] The list of recipient meeting MRIs. + */ recipients: string[]; + /** + * @member {TeamsMeetingNotificationSurface[]} [surfaces] The List of notification surface configuration. + */ surfaces: TeamsMeetingNotificationSurface[]; } /** * @interface - * Specifies the surface and content for a Teams meeting notification. + * Specifies the surface and content for a Teams meeting notification. */ export interface TeamsMeetingNotificationSurface { - surface: string; - contentType: string; + /** + * @member {string} [surface] The surface type. + */ + surface: 'meetingStage' | string; + /** + * @member {string} [contentType] The content type. + */ + contentType: 'task' | string; + /** + * @member {TaskModuleContinueResponse} [content] The content to display in the meeting notification. + */ content: TaskModuleContinueResponse; } @@ -1852,7 +1867,10 @@ export interface TeamsMeetingNotificationSurface { * Specifies channel data associated with the Teams meeting notification. */ export interface TeamsMeetingNotificationChannelData { - onBehalfOf: TeamsMeetingOnBehalfOf; + /** + * @member {TeamsMeetingOnBehalfOf} [onBehalfOf] The user that the bot is sending the notification on behalfOf, if any. + */ + onBehalfOf?: TeamsMeetingOnBehalfOf; } /** @@ -1860,19 +1878,40 @@ export interface TeamsMeetingNotificationChannelData { * Specifies the Teams user that triggered the Teams meeting notification. */ export interface TeamsMeetingOnBehalfOf { - itemid: number; - mentionType: string; + /** + * @member {number} [itemid] The id of the item. + */ + itemid: 0 | number; + /** + * @member {string} [mentionType] The mention type of a "person". + */ + mentionType: 'person' | string; + /** + * @member {string} [mri] Ther user MRI of the sender. + */ mri: string; - displayName: string; + /** + * @member {string} [displayName] The of the person. + */ + displayName?: string; } /** * @interface - * Specifies the recipients for which the Teams meeting notification was not sent. + * Specifies the recipients for which the Teams meeting notification was not sent. */ export interface TeamsMeetingNotificationRecipientFailureInfo { + /** + * @member {string} [recipientMri] The targeted recipient's MRI. + */ recipientMri: string; + /** + * @member {string} [failureReason] The reason for which the meetings notification could not be sent to the recipient. + */ failureReason: string; + /** + * @member {string} [errorCode] The error code. + */ errorCode: string; } @@ -1881,5 +1920,8 @@ export interface TeamsMeetingNotificationRecipientFailureInfo { * Specifies the list of recipients for which the Teams meeting notification was not sent. */ export interface TeamsMeetingNotificationRecipientFailureInfos { + /** + * @member {string} [errorCode] The list of recipients that failed to recieve the sent meetings notification. + */ recipientsFailureInfo: TeamsMeetingNotificationRecipientFailureInfo[]; -} \ No newline at end of file +} From f559f241d9e11dcb3360726a405663e026f335e1 Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Thu, 8 Dec 2022 11:17:52 -0800 Subject: [PATCH 6/8] added missing docs --- .../botframework-schema/etc/botframework-schema.api.md | 3 --- libraries/botframework-schema/src/teams/index.ts | 9 +++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/libraries/botframework-schema/etc/botframework-schema.api.md b/libraries/botframework-schema/etc/botframework-schema.api.md index ddfaa0616e..0fcbc9ef76 100644 --- a/libraries/botframework-schema/etc/botframework-schema.api.md +++ b/libraries/botframework-schema/etc/botframework-schema.api.md @@ -1718,11 +1718,8 @@ export interface TeamsMeetingInfo { // @public export interface TeamsMeetingNotification { - // (undocumented) channelData: TeamsMeetingNotificationChannelData; - // (undocumented) type: 'targetedMeetingNotification' | string; - // (undocumented) value: TeamsMeetingNotificationInfo; } diff --git a/libraries/botframework-schema/src/teams/index.ts b/libraries/botframework-schema/src/teams/index.ts index fac0cdcc0f..b34338df25 100644 --- a/libraries/botframework-schema/src/teams/index.ts +++ b/libraries/botframework-schema/src/teams/index.ts @@ -1823,8 +1823,17 @@ export interface MeetingEndEventDetails extends MeetingEventDetails { * Specifies details of a Teams meeting send notification payload. */ export interface TeamsMeetingNotification { + /** + * @member {string} [type] The type of meeting notification. + */ type: 'targetedMeetingNotification' | string; + /** + * @member {TeamsMeetingNotificationInfo} [type] The specific details of the meeting notification. + */ value: TeamsMeetingNotificationInfo; + /** + * @member {TeamsMeetingNotificationChannelData} [type] The channel data of the meeting notification. + */ channelData: TeamsMeetingNotificationChannelData; } From a370172de0ef6b0dbbf7e36577553b366e28c0ae Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Thu, 8 Dec 2022 12:29:03 -0800 Subject: [PATCH 7/8] added more unit tests for coverage --- libraries/botbuilder/tests/teamsInfo.test.js | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/libraries/botbuilder/tests/teamsInfo.test.js b/libraries/botbuilder/tests/teamsInfo.test.js index 0676ed57b6..508ae3994f 100644 --- a/libraries/botbuilder/tests/teamsInfo.test.js +++ b/libraries/botbuilder/tests/teamsInfo.test.js @@ -1095,6 +1095,52 @@ describe('TeamsInfo', function () { assert(fetchOauthToken.isDone()); assert(sendTeamsMeetingNotificationExpectation.isDone()); }); + + it('should throw an error if an empty meeting id is provided', async function () { + const notification = {}; + const emptyMeetingId = ""; + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') + .post(`/v1/meetings/${emptyMeetingId}/notification`, notification) + .matchHeader('Authorization', expectedAuthHeader) + .reply(202, {}); + + const context = new TestContext(teamActivity); + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + let isErrorThrown = false; + try { + await TeamsInfo.sendMeetingNotification(context, notification, emptyMeetingId); + } catch (e) { + assert(typeof e, 'Error'); + assert(e.message, 'meetingId is required.'); + isErrorThrown = true; + } + + assert(isErrorThrown); + assert(fetchOauthToken.isDone() === false); + assert(sendTeamsMeetingNotificationExpectation.isDone() === false); + }); + + it('should get the meeting id from the context object if no meeting id is provided', async function () { + const notification = {}; + const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); + + const context = new TestContext(teamActivity); + + const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') + .post(`/v1/meetings/${encodeURIComponent(teamActivity.channelData.meeting.id)}/notification`, notification) + .matchHeader('Authorization', expectedAuthHeader) + .reply(202, {}); + + context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); + + await TeamsInfo.sendMeetingNotification(context, notification); + + assert(fetchOauthToken.isDone()); + assert(sendTeamsMeetingNotificationExpectation.isDone()); + }); }); describe('private methods', function () { From f9ef086379b35ca8e04da01293db4a25e6baba88 Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Thu, 8 Dec 2022 12:34:21 -0800 Subject: [PATCH 8/8] lint fix --- libraries/botbuilder/tests/teamsInfo.test.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libraries/botbuilder/tests/teamsInfo.test.js b/libraries/botbuilder/tests/teamsInfo.test.js index 508ae3994f..bf3c77bbe6 100644 --- a/libraries/botbuilder/tests/teamsInfo.test.js +++ b/libraries/botbuilder/tests/teamsInfo.test.js @@ -1098,7 +1098,7 @@ describe('TeamsInfo', function () { it('should throw an error if an empty meeting id is provided', async function () { const notification = {}; - const emptyMeetingId = ""; + const emptyMeetingId = ''; const { expectedAuthHeader, expectation: fetchOauthToken } = nockOauth(); const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') @@ -1117,7 +1117,7 @@ describe('TeamsInfo', function () { assert(e.message, 'meetingId is required.'); isErrorThrown = true; } - + assert(isErrorThrown); assert(fetchOauthToken.isDone() === false); assert(sendTeamsMeetingNotificationExpectation.isDone() === false); @@ -1130,14 +1130,17 @@ describe('TeamsInfo', function () { const context = new TestContext(teamActivity); const sendTeamsMeetingNotificationExpectation = nock('https://smba.trafficmanager.net/amer') - .post(`/v1/meetings/${encodeURIComponent(teamActivity.channelData.meeting.id)}/notification`, notification) + .post( + `/v1/meetings/${encodeURIComponent(teamActivity.channelData.meeting.id)}/notification`, + notification + ) .matchHeader('Authorization', expectedAuthHeader) .reply(202, {}); context.turnState.set(context.adapter.ConnectorClientKey, connectorClient); await TeamsInfo.sendMeetingNotification(context, notification); - + assert(fetchOauthToken.isDone()); assert(sendTeamsMeetingNotificationExpectation.isDone()); });