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 GetParticipant #4770

Merged
merged 13 commits into from
Oct 17, 2020
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Licensed under the MIT License.
// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using AdaptiveExpressions.Properties;
using Microsoft.Bot.Builder.Teams;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Schema.Teams;
using Newtonsoft.Json;

namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Actions
{
/// <summary>
/// Calls TeamsInfo.GetMeetingParticipantAsync and sets the result to a memory property.
/// </summary>
public class GetMeetingParticipant : Dialog
{
/// <summary>
/// Class identifier.
/// </summary>
[JsonProperty("$kind")]
public const string Kind = "Teams.GetMeetingParticipant";

/// <summary>
/// Initializes a new instance of the <see cref="GetMeetingParticipant"/> class.
/// </summary>
/// <param name="callerPath">Optional, source file full path.</param>
/// <param name="callerLine">Optional, line number in source file.</param>
[JsonConstructor]
public GetMeetingParticipant([CallerFilePath] string callerPath = "", [CallerLineNumber] int callerLine = 0)
: base()
{
this.RegisterSourceLocation(callerPath, callerLine);
}

/// <summary>
/// Gets or sets an optional expression which if is true will disable this action.
/// </summary>
/// <example>
/// "user.age > 18".
/// </example>
/// <value>
/// A boolean expression.
/// </value>
[JsonProperty("disabled")]
public BoolExpression Disabled { get; set; }

/// <summary>
/// Gets or sets property path to put the value in.
/// </summary>
/// <value>
/// Property path to put the value in.
/// </value>
[JsonProperty("property")]
public StringExpression Property { get; set; }

/// <summary>
/// Gets or sets the expression to get the value to use for meeting id.
/// </summary>
/// <value>
/// The expression to get the value to use for meeting id. Default value is turn.activity.channelData.meeting.id.
/// </value>
[JsonProperty("meetingId")]
public StringExpression MeetingId { get; set; } = "=turn.activity.channelData.meeting.id";

/// <summary>
/// Gets or sets the expression to get the value to use for participant id.
/// </summary>
/// <value>
/// The expression to get the value to use for participant id. Default value is turn.activity.from.aadObjectId.
/// </value>
[JsonProperty("participantId")]
public StringExpression ParticipantId { get; set; } = "=turn.activity.from.aadObjectId";

/// <summary>
/// Gets or sets the expression to get the value to use for tenant id.
/// </summary>
/// <value>
/// The expression to get the value to use for tenant id. Default value is turn.activity.channelData.meetingInfo.Id.
/// </value>
[JsonProperty("tenantId")]
public StringExpression TenantId { get; set; } = "=turn.activity.channelData.tenant.id";

/// <summary>
/// Called when the dialog is started and pushed onto the dialog stack.
/// </summary>
/// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
/// <param name="options">Optional, initial information to pass to the dialog.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (options is CancellationToken)
{
throw new ArgumentException($"{nameof(options)} cannot be a cancellation token");
}

if (this.Disabled != null && this.Disabled.GetValue(dc.State) == true)
{
return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}

if (dc.Context.Activity.ChannelId != Channels.Msteams)
{
throw new Exception("TeamsInfo.GetMeetingParticipantAsync() works only on the Teams channel.");
}

string meetingId = GetValueOrNull(dc, this.MeetingId);
string participantId = GetValueOrNull(dc, this.ParticipantId);
string tenantId = GetValueOrNull(dc, this.TenantId);

if (participantId == null)
{
// TeamsInfo.GetMeetingParticipantAsync will default to retrieving the current meeting's participant
// if none is provided. This could lead to unexpected results. Therefore, GetMeetingParticipant action
// throws an exception if the expression provided somehow maps to an invalid result.
throw new InvalidOperationException($"GetMeetingParticipant could determine the participant id by expression value provided. {nameof(participantId)} is required.");
}

var result = await TeamsInfo.GetMeetingParticipantAsync(dc.Context, meetingId, participantId, tenantId, cancellationToken: cancellationToken).ConfigureAwait(false);

dc.State.SetValue(this.Property.GetValue(dc.State), result);

return await dc.EndDialogAsync(result, cancellationToken: cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Builds the compute Id for the dialog.
/// </summary>
/// <returns>A string representing the compute Id.</returns>
protected override string OnComputeId()
{
return $"{this.GetType().Name}[{this.MeetingId?.ToString() ?? string.Empty},{this.ParticipantId?.ToString() ?? string.Empty},{this.TenantId?.ToString() ?? string.Empty},{this.Property?.ToString() ?? string.Empty}]";
}

private string GetValueOrNull(DialogContext dc, StringExpression stringExpression)
{
if (stringExpression != null)
{
var (value, valueError) = stringExpression.TryGetValue(dc.State);
if (valueError != null)
{
throw new Exception($"Expression evaluation resulted in an error. Expression: {stringExpression.ExpressionText}. Error: {valueError}");
}

return value as string;
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
</ItemGroup>

<ItemGroup>
<None Remove="Schemas\Actions\Teams.GetMeetingParticipant.schema" />
<None Remove="Schemas\TriggerConditions\Microsoft.OnInvokeActivity.schema" />
<None Remove="Schemas\TriggerConditions\Teams.OnChannelRestored.schema" />
<None Remove="Schemas\TriggerConditions\Teams.OnTeamArchived.schema" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema",
"$role": "implements(Microsoft.IDialog)",
"title": "Get meeting participant",
"description": "Get teams meeting partipant information.",
"type": "object",
"properties": {
"id": {
"type": "string",
"title": "Id",
"description": "Optional id for the dialog"
},
"property": {
"$ref": "schema:#/definitions/stringExpression",
"title": "Property",
"description": "Property (named location to store information).",
"examples": [
"user.participantInfo"
]
},
"meetingId": {
"$ref": "schema:#/definitions/stringExpression",
"title": "Meeting Id",
"description": "Meeting Id or expression to a meetingId to use to get the participant information. Default value is the current turn.activity.channelData.meeting.id.",
"examples": [
"=turn.activity.channelData.meeting.id"
]
},
"participantId": {
"$ref": "schema:#/definitions/stringExpression",
"title": "Participant Id",
"description": "Participant Id or expression to a participantId to use to get the participant information. Default value is the current turn.activity.from.aadObjectId.",
"examples": [
"=turn.activity.from.aadObjectId"
]
},
"tenantId": {
"$ref": "schema:#/definitions/stringExpression",
"title": "Tenant Id",
"description": "Tenant Id or expression to a tenantId to use to get the participant information. Default value is the current $turn.activity.channelData.tenant.id.",
"examples": [
"=turn.activity.channelData.tenant.id"
]
},
"disabled": {
"$ref": "schema:#/definitions/booleanExpression",
"title": "Disabled",
"description": "Optional condition which if true will disable this action.",
"examples": [
"=user.age > 3"
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.Collections.Generic;
using Microsoft.Bot.Builder.Dialogs.Adaptive.Actions;
using Microsoft.Bot.Builder.Dialogs.Debugging;
using Microsoft.Bot.Builder.Dialogs.Declarative;
using Microsoft.Bot.Builder.Dialogs.Declarative.Resources;
Expand Down Expand Up @@ -37,6 +38,9 @@ public virtual IEnumerable<DeclarativeType> GetDeclarativeTypes(ResourceExplorer
yield return new DeclarativeType<OnTeamsTeamRenamed>(OnTeamsTeamRenamed.Kind);
yield return new DeclarativeType<OnTeamsTeamRestored>(OnTeamsTeamRestored.Kind);
yield return new DeclarativeType<OnTeamsTeamUnarchived>(OnTeamsTeamUnarchived.Kind);

// Actions
yield return new DeclarativeType<GetMeetingParticipant>(GetMeetingParticipant.Kind);
}

public virtual IEnumerable<JsonConverter> GetConverters(ResourceExplorer resourceExplorer, SourceContext sourceContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ public async override Task ExecuteAsync(TestAdapter adapter, BotCallbackHandler
activity.From.Id = User;
activity.From.Name = User;
}
else if (Activity.From != null)
{
activity.From = ObjectPath.Clone(Activity.From);
}

Stopwatch sw = new Stopwatch();
sw.Start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ namespace Microsoft.Bot.Builder.Teams
/// </summary>
public static class TeamsActivityExtensions
{
/// <summary>
/// Gets the TeamsMeetingInfo object from the current activity.
/// </summary>
/// <param name="activity">This activity.</param>
/// <returns>The current activity's team's meeting, or null.</returns>
public static TeamsMeetingInfo TeamsGetMeetingInfo(this IActivity activity)
{
var channelData = activity.GetChannelData<TeamsChannelData>();
return channelData?.Meeting;
}

/// <summary>
/// Gets the Team's channel id from the current activity.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions libraries/Microsoft.Bot.Builder/Teams/TeamsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ namespace Microsoft.Bot.Builder.Teams
/// </summary>
public static class TeamsInfo
{
/// <summary>
/// Gets the details for the given meeting participant. This only works in teams meeting scoped conversations.
/// </summary>
/// <param name="turnContext">Turn context.</param>
/// <param name="meetingId">The id of the Teams meeting. TeamsChannelData.Meeting.Id will be used if none provided.</param>
/// <param name="participantId">The id of the Teams meeting participant. From.AadObjectId will be used if none provided.</param>
/// <param name="tenantId">The id of the Teams meeting Tenant. TeamsChannelData.Tenant.Id will be used if none provided.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <remarks>InvalidOperationException will be thrown if meetingId, participantId or tenantId have not been
/// provided, and also cannot be retrieved from turnContext.Activity.</remarks>
/// <returns>Team participant channel account.</returns>
public static async Task<TeamsParticipantChannelAccount> GetMeetingParticipantAsync(ITurnContext turnContext, string meetingId = null, string participantId = null, string tenantId = null, CancellationToken cancellationToken = default)
{
meetingId ??= turnContext.Activity.TeamsGetMeetingInfo()?.Id ?? throw new InvalidOperationException("This method is only valid within the scope of a MS Teams Meeting.");
participantId ??= turnContext.Activity.From.AadObjectId ?? throw new InvalidOperationException($"{nameof(participantId)} is required.");
tenantId ??= turnContext.Activity.GetChannelData<TeamsChannelData>()?.Tenant?.Id ?? throw new InvalidOperationException($"{nameof(tenantId)} is required.");

#pragma warning disable CA2000 // Dispose objects before losing scope (we need to review this, disposing the connectorClient may have unintended consequences)
return await GetTeamsConnectorClient(turnContext).Teams.FetchParticipantAsync(meetingId, participantId, tenantId, cancellationToken).ConfigureAwait(false);
#pragma warning restore CA2000 // Dispose objects before losing scope
}

/// <summary>
/// Gets the details for the given team id. This only works in teams scoped conversations.
/// </summary>
Expand Down
13 changes: 4 additions & 9 deletions libraries/Microsoft.Bot.Connector/Teams/ITeamsOperations.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <auto-generated>
// <auto-generated>
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for
// license information.
Expand All @@ -23,11 +23,8 @@ namespace Microsoft.Bot.Connector.Teams
public partial interface ITeamsOperations
{
/// <summary>
/// Fetches channel list for a given team
/// Fetches channel list for a given team.
/// </summary>
/// <remarks>
/// Fetch the channel list.
/// </remarks>
/// <param name='teamId'>
/// Team Id
/// </param>
Expand All @@ -47,12 +44,10 @@ public partial interface ITeamsOperations
/// Thrown when a required parameter is null
/// </exception>
Task<HttpOperationResponse<ConversationList>> FetchChannelListWithHttpMessagesAsync(string teamId, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));

/// <summary>
/// Fetches details related to a team
/// Fetches details related to a team.
/// </summary>
/// <remarks>
/// Fetch details for a team
/// </remarks>
/// <param name='teamId'>
/// Team Id
/// </param>
Expand Down
Loading