diff --git a/libraries/Microsoft.Bot.Builder/Teams/TeamsActivityHandler.cs b/libraries/Microsoft.Bot.Builder/Teams/TeamsActivityHandler.cs index 06b57931db..1701b7c793 100644 --- a/libraries/Microsoft.Bot.Builder/Teams/TeamsActivityHandler.cs +++ b/libraries/Microsoft.Bot.Builder/Teams/TeamsActivityHandler.cs @@ -809,6 +809,10 @@ protected override Task OnEventActivityAsync(ITurnContext turnCo return OnTeamsMeetingStartAsync(JObject.FromObject(turnContext.Activity.Value).ToObject(), turnContext, cancellationToken); case "application/vnd.microsoft.meetingEnd": return OnTeamsMeetingEndAsync(JObject.FromObject(turnContext.Activity.Value).ToObject(), turnContext, cancellationToken); + case "application/vnd.microsoft.meetingParticipantJoin": + return OnTeamsMeetingParticipantsJoinAsync(JObject.FromObject(turnContext.Activity.Value).ToObject(), turnContext, cancellationToken); + case "application/vnd.microsoft.meetingParticipantLeave": + return OnTeamsMeetingParticipantsLeaveAsync(JObject.FromObject(turnContext.Activity.Value).ToObject(), turnContext, cancellationToken); } } @@ -857,6 +861,34 @@ protected virtual Task OnTeamsReadReceiptAsync(ReadReceiptInfo readReceiptInfo, return Task.CompletedTask; } + /// + /// Invoked when a Teams Participants Join event activity is received from the connector. + /// Override this in a derived class to provide logic for when meeting participants are added. + /// + /// The details of the meeting. + /// A strongly-typed context object for this turn. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + protected virtual Task OnTeamsMeetingParticipantsJoinAsync(MeetingParticipantsEventDetails meeting, ITurnContext turnContext, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// + /// Invoked when a Teams Participants Leave event activity is received from the connector. + /// Override this in a derived class to provide logic for when meeting participants are removed. + /// + /// The details of the meeting. + /// A strongly-typed context object for this turn. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + protected virtual Task OnTeamsMeetingParticipantsLeaveAsync(MeetingParticipantsEventDetails meeting, ITurnContext turnContext, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + /// /// Invoked when an message update activity is received. /// activities, such as the conversational logic. diff --git a/libraries/Microsoft.Bot.Schema/Teams/MeetingParticipantsEventDetails.cs b/libraries/Microsoft.Bot.Schema/Teams/MeetingParticipantsEventDetails.cs new file mode 100644 index 0000000000..cd0e143956 --- /dev/null +++ b/libraries/Microsoft.Bot.Schema/Teams/MeetingParticipantsEventDetails.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Schema.Teams +{ + /// + /// Data about the meeting participants. + /// + public partial class MeetingParticipantsEventDetails + { + /// + /// Initializes a new instance of the class. + /// + public MeetingParticipantsEventDetails() + { + CustomInit(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The members involved in the meeting event. + public MeetingParticipantsEventDetails( + IList members = default) + { + Members = members; + CustomInit(); + } + + /// + /// Gets the meeting participants info. + /// + /// + /// The participant accounts info. + /// + [JsonProperty(PropertyName = "Members")] + public IList Members { get; private set; } = new List(); + + partial void CustomInit(); + } +} diff --git a/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingMember.cs b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingMember.cs new file mode 100644 index 0000000000..eddca61cc6 --- /dev/null +++ b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingMember.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; + +namespace Microsoft.Bot.Schema.Teams +{ + /// + /// Data about the meeting participants. + /// + public partial class TeamsMeetingMember + { + /// + /// Initializes a new instance of the class. + /// + /// The channel user data. + /// The user meeting details. + public TeamsMeetingMember(TeamsChannelAccount user, UserMeetingDetails meeting) + { + User = user; + Meeting = meeting; + } + + /// + /// Gets or sets the meeting participant. + /// + /// + /// The joined participant account. + /// + [JsonProperty(PropertyName = "user")] + public TeamsChannelAccount User { get; set; } + + /// + /// Gets or sets the user meeting details. + /// + /// + /// The users meeting details. + /// + [JsonProperty(PropertyName = "Meeting")] + public UserMeetingDetails Meeting { get; set; } + } +} diff --git a/libraries/Microsoft.Bot.Schema/Teams/UserMeetingDetails.cs b/libraries/Microsoft.Bot.Schema/Teams/UserMeetingDetails.cs new file mode 100644 index 0000000000..c3f30f06d5 --- /dev/null +++ b/libraries/Microsoft.Bot.Schema/Teams/UserMeetingDetails.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; + +namespace Microsoft.Bot.Schema.Teams +{ + /// + /// Specific details of a user in a Teams meeting. + /// + public partial class UserMeetingDetails + { + /// + /// Gets or sets a value indicating whether the user is in the meeting. + /// + /// + /// The user in meeting indicator. + /// + [JsonProperty(PropertyName = "InMeeting")] + public bool InMeeting { get; set; } + + /// + /// Gets or sets the value of the user's role. + /// + /// + /// The user's role. + /// + [JsonProperty(PropertyName = "Role")] + public string Role { get; set; } + } +} diff --git a/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerTests.cs b/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerTests.cs index f13d0326d0..4cc3263efb 100644 --- a/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerTests.cs +++ b/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerTests.cs @@ -314,7 +314,7 @@ public async Task TestConversationUpdateTeamsChannelRestored() // Arrange var activity = new Activity { - Type = ActivityTypes.ConversationUpdate, + Type = ActivityTypes.ConversationUpdate, ChannelData = new TeamsChannelData { EventType = "channelRestored" }, ChannelId = Channels.Msteams, }; @@ -333,7 +333,7 @@ public async Task TestConversationUpdateTeamsChannelRestored() [Fact] public async Task TestConversationUpdateTeamsTeamArchived() { - // Arrange + // Arrange var activity = new Activity { Type = ActivityTypes.ConversationUpdate, @@ -351,7 +351,7 @@ public async Task TestConversationUpdateTeamsTeamArchived() Assert.Equal("OnConversationUpdateActivityAsync", bot.Record[0]); Assert.Equal("OnTeamsTeamArchivedAsync", bot.Record[1]); } - + [Fact] public async Task TestConversationUpdateTeamsTeamDeleted() { @@ -1139,7 +1139,7 @@ void CaptureSend(Activity[] arg) // Act var bot = new TestActivityHandler(); await ((IBot)bot).OnTurnAsync(turnContext); - + // Assert Assert.Equal(2, bot.Record.Count); Assert.Equal("OnInvokeActivityAsync", bot.Record[0]); @@ -1270,6 +1270,102 @@ void CaptureSend(Activity[] arg) Assert.Equal("10101010", activitiesToSend[0].Text); } + [Fact] + public async Task TestMeetingParticipantsJoinEvent() + { + // Arrange + var activity = new Activity + { + ChannelId = Channels.Msteams, + Type = ActivityTypes.Event, + Name = "application/vnd.microsoft.meetingParticipantJoin", + Value = JObject.Parse(@"{ + Members: [ + { + User: + { + Id: 'id', + Name: 'name' + }, + Meeting: + { + Role: 'role', + InMeeting: true + } + } + ] + }"), + }; + + Activity[] activitiesToSend = null; + void CaptureSend(Activity[] arg) + { + activitiesToSend = arg; + } + + var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); + + // Act + var bot = new TestActivityHandler(); + await ((IBot)bot).OnTurnAsync(turnContext); + + // Assert + Assert.Equal(2, bot.Record.Count); + Assert.Equal("OnEventActivityAsync", bot.Record[0]); + Assert.Equal("OnTeamsMeetingParticipantsJoinAsync", bot.Record[1]); + Assert.NotNull(activitiesToSend); + Assert.Single(activitiesToSend); + Assert.Equal("id", activitiesToSend[0].Text); + } + + [Fact] + public async Task TestMeetingParticipantsLeaveEvent() + { + // Arrange + var activity = new Activity + { + ChannelId = Channels.Msteams, + Type = ActivityTypes.Event, + Name = "application/vnd.microsoft.meetingParticipantLeave", + Value = JObject.Parse(@"{ + Members: [ + { + User: + { + Id: 'id', + Name: 'name' + }, + Meeting: + { + Role: 'role', + InMeeting: true + } + } + ] + }"), + }; + + Activity[] activitiesToSend = null; + void CaptureSend(Activity[] arg) + { + activitiesToSend = arg; + } + + var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); + + // Act + var bot = new TestActivityHandler(); + await ((IBot)bot).OnTurnAsync(turnContext); + + // Assert + Assert.Equal(2, bot.Record.Count); + Assert.Equal("OnEventActivityAsync", bot.Record[0]); + Assert.Equal("OnTeamsMeetingParticipantsLeaveAsync", bot.Record[1]); + Assert.NotNull(activitiesToSend); + Assert.Single(activitiesToSend); + Assert.Equal("id", activitiesToSend[0].Text); + } + [Fact] public async Task TestMessageUpdateActivityTeamsMessageEdit() { @@ -1462,7 +1558,7 @@ protected override Task OnTeamsChannelRenamedAsync(ChannelInfo channelInfo, Team Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnTeamsChannelRenamedAsync(channelInfo, teamInfo, turnContext, cancellationToken); } - + protected override Task OnTeamsChannelRestoredAsync(ChannelInfo channelInfo, TeamInfo teamInfo, ITurnContext turnContext, CancellationToken cancellationToken) { Record.Add(MethodBase.GetCurrentMethod().Name); @@ -1485,7 +1581,7 @@ protected override Task OnTeamsTeamHardDeletedAsync(TeamInfo teamInfo, ITurnCont { Record.Add(MethodBase.GetCurrentMethod().Name); return base.OnTeamsTeamHardDeletedAsync(teamInfo, turnContext, cancellationToken); - } + } protected override Task OnTeamsTeamRenamedAsync(TeamInfo teamInfo, ITurnContext turnContext, CancellationToken cancellationToken) { @@ -1536,6 +1632,20 @@ protected override Task OnTeamsReadReceiptAsync(ReadReceiptInfo readReceiptInfo, return Task.CompletedTask; } + protected override Task OnTeamsMeetingParticipantsJoinAsync(MeetingParticipantsEventDetails meeting, ITurnContext turnContext, CancellationToken cancellationToken) + { + Record.Add(MethodBase.GetCurrentMethod().Name); + turnContext.SendActivityAsync(meeting.Members[0].User.Id); + return base.OnTeamsMeetingParticipantsJoinAsync(meeting, turnContext, cancellationToken); + } + + protected override Task OnTeamsMeetingParticipantsLeaveAsync(MeetingParticipantsEventDetails meeting, ITurnContext turnContext, CancellationToken cancellationToken) + { + Record.Add(MethodBase.GetCurrentMethod().Name); + turnContext.SendActivityAsync(meeting.Members[0].User.Id); + return base.OnTeamsMeetingParticipantsLeaveAsync(meeting, turnContext, cancellationToken); + } + // Invoke protected override Task OnInvokeActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) { @@ -1745,7 +1855,7 @@ private class RosterHttpMessageHandler : HttpMessageHandler protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var response = new HttpResponseMessage(HttpStatusCode.OK); - + // GetMembers (Team) if (request.RequestUri.PathAndQuery.EndsWith("team-id/members")) { diff --git a/tests/Microsoft.Bot.Schema.Tests/Teams/MeetingParticipantsEventDetailsTests.cs b/tests/Microsoft.Bot.Schema.Tests/Teams/MeetingParticipantsEventDetailsTests.cs new file mode 100644 index 0000000000..5c9c6a0076 --- /dev/null +++ b/tests/Microsoft.Bot.Schema.Tests/Teams/MeetingParticipantsEventDetailsTests.cs @@ -0,0 +1,41 @@ +// Copyright(c) Microsoft Corporation.All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Bot.Schema.Teams; +using Xunit; + +namespace Microsoft.Bot.Schema.Tests.Teams +{ + public class MeetingParticipantsEventDetailsTests + { + [Fact] + public void MeetingParticipantsEventDetailsInits() + { + // Arrange + var user = new TeamsChannelAccount("id", "name", "givenName", "surname", "email", "userPrincipalName"); + var member = new TeamsMeetingMember(user, new UserMeetingDetails { InMeeting = true, Role = "user" }); + var eventMembers = new List() { member }; + + // Act + var meeting = new MeetingParticipantsEventDetails(eventMembers); + + // Assert + Assert.NotNull(meeting); + Assert.IsType(meeting); + + Assert.StrictEqual(eventMembers, meeting.Members); + } + + [Fact] + public void MeetingParticipantsEventDetailsInitsWithNoArgs() + { + // Act + var meeting = new MeetingParticipantsEventDetails(); + + // Assert + Assert.NotNull(meeting); + Assert.IsType(meeting); + } + } +}