Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Teams] Add support for meeting participants added/removed events #6677

Merged
merged 5 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions libraries/Microsoft.Bot.Builder/Teams/TeamsActivityHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,10 @@ protected override Task OnEventActivityAsync(ITurnContext<IEventActivity> turnCo
return OnTeamsMeetingStartAsync(JObject.FromObject(turnContext.Activity.Value).ToObject<MeetingStartEventDetails>(), turnContext, cancellationToken);
case "application/vnd.microsoft.meetingEnd":
return OnTeamsMeetingEndAsync(JObject.FromObject(turnContext.Activity.Value).ToObject<MeetingEndEventDetails>(), turnContext, cancellationToken);
case "application/vnd.microsoft.meetingParticipantJoin":
return OnTeamsMeetingParticipantsJoinAsync(JObject.FromObject(turnContext.Activity.Value).ToObject<MeetingParticipantsEventDetails>(), turnContext, cancellationToken);
case "application/vnd.microsoft.meetingParticipantLeave":
return OnTeamsMeetingParticipantsLeaveAsync(JObject.FromObject(turnContext.Activity.Value).ToObject<MeetingParticipantsEventDetails>(), turnContext, cancellationToken);
}
}

Expand Down Expand Up @@ -857,6 +861,34 @@ protected virtual Task OnTeamsReadReceiptAsync(ReadReceiptInfo readReceiptInfo,
return Task.CompletedTask;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="meeting">The details of the meeting.</param>
/// <param name="turnContext">A strongly-typed context object for this turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
protected virtual Task OnTeamsMeetingParticipantsJoinAsync(MeetingParticipantsEventDetails meeting, ITurnContext<IEventActivity> turnContext, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="meeting">The details of the meeting.</param>
/// <param name="turnContext">A strongly-typed context object for this turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
protected virtual Task OnTeamsMeetingParticipantsLeaveAsync(MeetingParticipantsEventDetails meeting, ITurnContext<IEventActivity> turnContext, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

/// <summary>
/// Invoked when an message update activity is received.
/// <see cref="ActivityTypes.MessageUpdate"/> activities, such as the conversational logic.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Data about the meeting participants.
/// </summary>
public partial class MeetingParticipantsEventDetails
{
/// <summary>
/// Initializes a new instance of the <see cref="MeetingParticipantsEventDetails"/> class.
/// </summary>
public MeetingParticipantsEventDetails()
{
CustomInit();
}

/// <summary>
/// Initializes a new instance of the <see cref="MeetingParticipantsEventDetails"/> class.
/// </summary>
/// <param name="members">The members involved in the meeting event.</param>
public MeetingParticipantsEventDetails(
IList<TeamsMeetingMember> members = default)
{
Members = members;
CustomInit();
}

/// <summary>
/// Gets the meeting participants info.
/// </summary>
/// <value>
/// The participant accounts info.
/// </value>
[JsonProperty(PropertyName = "Members")]
public IList<TeamsMeetingMember> Members { get; private set; } = new List<TeamsMeetingMember>();

partial void CustomInit();
}
}
42 changes: 42 additions & 0 deletions libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingMember.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Newtonsoft.Json;

namespace Microsoft.Bot.Schema.Teams
{
/// <summary>
/// Data about the meeting participants.
/// </summary>
public partial class TeamsMeetingMember
{
/// <summary>
/// Initializes a new instance of the <see cref="TeamsMeetingMember"/> class.
/// </summary>
/// <param name="user">The channel user data.</param>
/// <param name="meeting">The user meeting details.</param>
public TeamsMeetingMember(TeamsChannelAccount user, UserMeetingDetails meeting)
{
User = user;
Meeting = meeting;
}

/// <summary>
/// Gets or sets the meeting participant.
/// </summary>
/// <value>
/// The joined participant account.
/// </value>
[JsonProperty(PropertyName = "user")]
public TeamsChannelAccount User { get; set; }

/// <summary>
/// Gets or sets the user meeting details.
/// </summary>
/// <value>
/// The users meeting details.
/// </value>
[JsonProperty(PropertyName = "Meeting")]
public UserMeetingDetails Meeting { get; set; }
}
}
31 changes: 31 additions & 0 deletions libraries/Microsoft.Bot.Schema/Teams/UserMeetingDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Newtonsoft.Json;

namespace Microsoft.Bot.Schema.Teams
{
/// <summary>
/// Specific details of a user in a Teams meeting.
/// </summary>
public partial class UserMeetingDetails
{
/// <summary>
/// Gets or sets a value indicating whether the user is in the meeting.
/// </summary>
/// <value>
/// The user in meeting indicator.
/// </value>
[JsonProperty(PropertyName = "InMeeting")]
public bool InMeeting { get; set; }

/// <summary>
/// Gets or sets the value of the user's role.
/// </summary>
/// <value>
/// The user's role.
/// </value>
[JsonProperty(PropertyName = "Role")]
public string Role { get; set; }
}
}
124 changes: 117 additions & 7 deletions tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -333,7 +333,7 @@ public async Task TestConversationUpdateTeamsChannelRestored()
[Fact]
public async Task TestConversationUpdateTeamsTeamArchived()
{
// Arrange
// Arrange
var activity = new Activity
{
Type = ActivityTypes.ConversationUpdate,
Expand All @@ -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()
{
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
Record.Add(MethodBase.GetCurrentMethod().Name);
Expand All @@ -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<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -1536,6 +1632,20 @@ protected override Task OnTeamsReadReceiptAsync(ReadReceiptInfo readReceiptInfo,
return Task.CompletedTask;
}

protected override Task OnTeamsMeetingParticipantsJoinAsync(MeetingParticipantsEventDetails meeting, ITurnContext<IEventActivity> 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<IEventActivity> 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<InvokeResponse> OnInvokeActivityAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -1745,7 +1855,7 @@ private class RosterHttpMessageHandler : HttpMessageHandler
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);

// GetMembers (Team)
if (request.RequestUri.PathAndQuery.EndsWith("team-id/members"))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TeamsMeetingMember>() { member };

// Act
var meeting = new MeetingParticipantsEventDetails(eventMembers);

// Assert
Assert.NotNull(meeting);
Assert.IsType<MeetingParticipantsEventDetails>(meeting);

Assert.StrictEqual(eventMembers, meeting.Members);
}

[Fact]
public void MeetingParticipantsEventDetailsInitsWithNoArgs()
{
// Act
var meeting = new MeetingParticipantsEventDetails();

// Assert
Assert.NotNull(meeting);
Assert.IsType<MeetingParticipantsEventDetails>(meeting);
}
}
}
Loading