From 10b9fe09499cfa4c2632cd092f177f858dec1895 Mon Sep 17 00:00:00 2001 From: Cecilia Avila <44245136+ceciliaavila@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:57:23 -0300 Subject: [PATCH] port: [#4530] Add support for meeting participants added/removed events (#4538) * add meeting participants events * add unit tests * fix api signatures --------- Co-authored-by: JhontSouth --- libraries/botbuilder/etc/botbuilder.api.md | 5 + .../botbuilder/src/teamsActivityHandler.ts | 77 ++++++++++++ .../tests/teamsActivityHandler.test.js | 112 ++++++++++++++++++ .../etc/botframework-schema.api.md | 17 +++ .../botframework-schema/src/teams/index.ts | 41 +++++++ 5 files changed, 252 insertions(+) diff --git a/libraries/botbuilder/etc/botbuilder.api.md b/libraries/botbuilder/etc/botbuilder.api.md index 973aef33d8..1c9ce403bf 100644 --- a/libraries/botbuilder/etc/botbuilder.api.md +++ b/libraries/botbuilder/etc/botbuilder.api.md @@ -45,6 +45,7 @@ import { IStreamingTransportServer } from 'botframework-streaming'; import { MeetingEndEventDetails } from 'botbuilder-core'; import { MeetingNotification } from 'botbuilder-core'; import { MeetingNotificationResponse } from 'botbuilder-core'; +import { MeetingParticipantsEventDetails } from 'botbuilder-core'; import { MeetingStartEventDetails } from 'botbuilder-core'; import { MessagingExtensionAction } from 'botbuilder-core'; import { MessagingExtensionActionResponse } from 'botbuilder-core'; @@ -404,6 +405,10 @@ export class TeamsActivityHandler extends ActivityHandler { onTeamsChannelRestoredEvent(handler: (channelInfo: ChannelInfo, teamInfo: TeamInfo, context: TurnContext, next: () => Promise) => Promise): this; protected onTeamsMeetingEnd(context: TurnContext): Promise; onTeamsMeetingEndEvent(handler: (meeting: MeetingEndEventDetails, context: TurnContext, next: () => Promise) => Promise): this; + protected onTeamsMeetingParticipantsJoin(context: TurnContext): Promise; + onTeamsMeetingParticipantsJoinEvent(handler: (meeting: MeetingParticipantsEventDetails, context: TurnContext, next: () => Promise) => Promise): this; + protected onTeamsMeetingParticipantsLeave(context: TurnContext): Promise; + onTeamsMeetingParticipantsLeaveEvent(handler: (meeting: MeetingParticipantsEventDetails, context: TurnContext, next: () => Promise) => Promise): this; protected onTeamsMeetingStart(context: TurnContext): Promise; onTeamsMeetingStartEvent(handler: (meeting: MeetingStartEventDetails, context: TurnContext, next: () => Promise) => Promise): this; protected onTeamsMembersAdded(context: TurnContext): Promise; diff --git a/libraries/botbuilder/src/teamsActivityHandler.ts b/libraries/botbuilder/src/teamsActivityHandler.ts index 47da1d3b32..322096d0e8 100644 --- a/libraries/botbuilder/src/teamsActivityHandler.ts +++ b/libraries/botbuilder/src/teamsActivityHandler.ts @@ -35,6 +35,7 @@ import { tokenExchangeOperationName, TurnContext, verifyStateOperationName, + MeetingParticipantsEventDetails, } from 'botbuilder-core'; import { ReadReceiptInfo } from 'botframework-connector'; import { TeamsInfo } from './teamsInfo'; @@ -1193,6 +1194,10 @@ export class TeamsActivityHandler extends ActivityHandler { return this.onTeamsMeetingStart(context); case 'application/vnd.microsoft.meetingEnd': return this.onTeamsMeetingEnd(context); + case 'application/vnd.microsoft.meetingParticipantJoin': + return this.onTeamsMeetingParticipantsJoin(context); + case 'application/vnd.microsoft.meetingParticipantLeave': + return this.onTeamsMeetingParticipantsLeave(context); } } @@ -1232,6 +1237,28 @@ export class TeamsActivityHandler extends ActivityHandler { await this.handle(context, 'TeamsReadReceipt', this.defaultNextEvent(context)); } + /** + * Invoked when a Meeting Participant Join event activity is received from the connector. + * Override this in a derived class to provide logic for when a meeting participant is joined. + * + * @param context The context for this turn. + * @returns A promise that represents the work queued. + */ + protected async onTeamsMeetingParticipantsJoin(context: TurnContext): Promise { + await this.handle(context, 'TeamsMeetingParticipantsJoin', this.defaultNextEvent(context)); + } + + /** + * Invoked when a Meeting Participant Leave event activity is received from the connector. + * Override this in a derived class to provide logic for when a meeting participant is left. + * + * @param context The context for this turn. + * @returns A promise that represents the work queued. + */ + protected async onTeamsMeetingParticipantsLeave(context: TurnContext): Promise { + await this.handle(context, 'TeamsMeetingParticipantsLeave', this.defaultNextEvent(context)); + } + /** * Registers a handler for when a Teams meeting starts. * @@ -1296,4 +1323,54 @@ export class TeamsActivityHandler extends ActivityHandler { await handler(new ReadReceiptInfo(receiptInfo.lastReadMessageId), context, next); }); } + + /** + * Registers a handler for when a Teams meeting participant join. + * + * @param handler A callback that handles Meeting Participant Join events. + * @returns A promise that represents the work queued. + */ + onTeamsMeetingParticipantsJoinEvent( + handler: ( + meeting: MeetingParticipantsEventDetails, + context: TurnContext, + next: () => Promise + ) => Promise + ): this { + return this.on('TeamsMeetingParticipantsJoin', async (context, next) => { + const meeting = context.activity.value; + await handler( + { + members: meeting?.members, + }, + context, + next + ); + }); + } + + /** + * Registers a handler for when a Teams meeting participant leave. + * + * @param handler A callback that handles Meeting Participant Leave events. + * @returns A promise that represents the work queued. + */ + onTeamsMeetingParticipantsLeaveEvent( + handler: ( + meeting: MeetingParticipantsEventDetails, + context: TurnContext, + next: () => Promise + ) => Promise + ): this { + return this.on('TeamsMeetingParticipantsLeave', async (context, next) => { + const meeting = context.activity.value; + await handler( + { + members: meeting?.members, + }, + context, + next + ); + }); + } } diff --git a/libraries/botbuilder/tests/teamsActivityHandler.test.js b/libraries/botbuilder/tests/teamsActivityHandler.test.js index 5db46953d0..234a2e580e 100644 --- a/libraries/botbuilder/tests/teamsActivityHandler.test.js +++ b/libraries/botbuilder/tests/teamsActivityHandler.test.js @@ -2603,6 +2603,34 @@ describe('TeamsActivityHandler', function () { return activity; } + function createMeetingParticipantEventActivity(join = true) { + const value = { + members: [ + { + user: { + id: 'id', + name: 'name', + }, + meeting: { + role: 'role', + inMeeting: join, + }, + }, + ], + }; + + const activity = { + channelId: Channels.Msteams, + type: 'event', + value: value, + name: join + ? 'application/vnd.microsoft.meetingParticipantJoin' + : 'application/vnd.microsoft.meetingParticipantLeave', + }; + + return activity; + } + let onEventCalled; let onDialogCalled; this.beforeEach(function () { @@ -2797,5 +2825,89 @@ describe('TeamsActivityHandler', function () { }) .startTest(); }); + + it('onTeamsMeetingParticipantsJoin routed activity', async function () { + let onTeamsMeetingParticipantsJoinCalled = false; + const bot = new TeamsActivityHandler(); + const activity = createMeetingParticipantEventActivity(); + + bot.onEvent(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onEventCalled = true; + await next(); + }); + + bot.onTeamsMeetingParticipantsJoinEvent(async (meeting, context, next) => { + assert(meeting, 'meeting not found'); + assert(context, 'context not found'); + assert(next, 'next not found'); + assert.deepStrictEqual(meeting, activity.value); + onTeamsMeetingParticipantsJoinCalled = true; + await next(); + }); + + bot.onDialog(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onDialogCalled = true; + await next(); + }); + + const adapter = new TestAdapter(async (context) => { + await bot.run(context); + }); + + await adapter + .send(activity) + .then(() => { + assert(onTeamsMeetingParticipantsJoinCalled); + assert(onEventCalled, 'onConversationUpdate handler not called'); + assert(onDialogCalled, 'onDialog handler not called'); + }) + .startTest(); + }); + + it('onTeamsMeetingParticipantsLeave routed activity', async function () { + let onTeamsMeetingParticipantsLeaveCalled = false; + const bot = new TeamsActivityHandler(); + const activity = createMeetingParticipantEventActivity(false); + + bot.onEvent(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onEventCalled = true; + await next(); + }); + + bot.onTeamsMeetingParticipantsLeaveEvent(async (meeting, context, next) => { + assert(meeting, 'meeting not found'); + assert(context, 'context not found'); + assert(next, 'next not found'); + assert.deepStrictEqual(meeting, activity.value); + onTeamsMeetingParticipantsLeaveCalled = true; + await next(); + }); + + bot.onDialog(async (context, next) => { + assert(context, 'context not found'); + assert(next, 'next not found'); + onDialogCalled = true; + await next(); + }); + + const adapter = new TestAdapter(async (context) => { + await bot.run(context); + }); + + await adapter + .send(activity) + .then(() => { + assert(onTeamsMeetingParticipantsLeaveCalled); + assert(onEventCalled, 'onConversationUpdate handler not called'); + assert(onDialogCalled, 'onDialog handler not called'); + }) + .startTest(); + }); }); }); diff --git a/libraries/botframework-schema/etc/botframework-schema.api.md b/libraries/botframework-schema/etc/botframework-schema.api.md index dd4146b84c..4cff19dbea 100644 --- a/libraries/botframework-schema/etc/botframework-schema.api.md +++ b/libraries/botframework-schema/etc/botframework-schema.api.md @@ -1003,6 +1003,11 @@ export interface MeetingNotificationResponse { recipientsFailureInfo?: MeetingNotificationRecipientFailureInfo[]; } +// @public +export interface MeetingParticipantsEventDetails { + members: TeamsMeetingMember[]; +} + // @public export interface MeetingStageSurface { content: T; @@ -1805,6 +1810,12 @@ export interface TeamsMeetingInfo { id?: string; } +// @public +export interface TeamsMeetingMember { + meeting: UserMeetingDetails; + user: TeamsChannelAccount; +} + // @public export interface TeamsMeetingParticipant { conversation?: ConversationAccount; @@ -1949,6 +1960,12 @@ export type Type3 = MessagingExtensionResultType; // @public export type UserIdentityType = 'aadUser' | 'onPremiseAadUser' | 'anonymousGuest' | 'federatedUser'; +// @public +export interface UserMeetingDetails { + inMeeting: boolean; + role: string; +} + // @public export interface VideoCard { aspect: string; diff --git a/libraries/botframework-schema/src/teams/index.ts b/libraries/botframework-schema/src/teams/index.ts index 80b7cf305e..3e1604f181 100644 --- a/libraries/botframework-schema/src/teams/index.ts +++ b/libraries/botframework-schema/src/teams/index.ts @@ -2029,3 +2029,44 @@ export interface MeetingNotificationResponse { */ recipientsFailureInfo?: MeetingNotificationRecipientFailureInfo[]; } + +/** + * @interface + * Specific details about the meeting participants. + */ +export interface MeetingParticipantsEventDetails { + /** + * @member {TeamsMeetingMember[]} [members] The participants info. + */ + members: TeamsMeetingMember[]; +} + +/** + * @interface + * Specific details about the meeting participants. + */ +export interface TeamsMeetingMember { + /** + * @member {TeamsChannelAccount} [user] The participant account. + */ + user: TeamsChannelAccount; + /** + * @member {UserMeetingDetails} [meeting] The participants info. + */ + meeting: UserMeetingDetails; +} + +/** + * @interface + * Specific details of a user in a Teams meeting. + */ +export interface UserMeetingDetails { + /** + * @member {boolean} [inMeeting] The user in meeting indicator. + */ + inMeeting: boolean; + /** + * @member {string} [role] The user's role. + */ + role: string; +}