Skip to content
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
Expand Up @@ -25,14 +25,18 @@
"bots": [
{
"botId": "${{AAD_APP_CLIENT_ID}}",
"scopes": [ "groupChat" ],
"scopes": [
"groupChat"
],
"isNotificationOnly": true
}
],
"validDomains": [
"${{BOT_DOMAIN}}"
],
"permissions": [ "messageTeamMembers" ],
"permissions": [
"messageTeamMembers"
],
"webApplicationInfo": {
"id": "${{AAD_APP_CLIENT_ID}}",
"resource": "https://RscPermission"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ provision:
with:
name: meetings-transcription-aad # Note: when you run aadApp/update, the AAD app name will be updated based on the definition in manifest. If you don't want to change the name, make sure the name in AAD manifest is the same with the name defined here.
generateClientSecret: true # If the value is false, the action will not generate client secret for you
signInAudience: "AzureADMultipleOrgs" # Multitenant
signInAudience: "AzureADMyOrg" # SingleTenant
writeToEnvironmentFile:
# Write the information of created resources into environment file for the specified environment variable(s).
clientId: AAD_APP_CLIENT_ID
Expand Down Expand Up @@ -58,16 +58,16 @@ provision:
MicrosoftAppTenantId: ${{MICROSOFT_APP_TENANT_ID}}
AppBaseUrl: ${{BOT_ENDPOINT}}

- uses: aadApp/update # Apply the AAD manifest to an existing AAD app. Will use the object id in manifest file to determine which AAD app to update.
with:
manifestPath: ./aad.manifest.json # Relative path to teamsfx folder. Environment variables in manifest will be replaced before apply to AAD app
outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json
# - uses: aadApp/update # Apply the AAD manifest to an existing AAD app. Will use the object id in manifest file to determine which AAD app to update.
# with:
# manifestPath: ./aad.manifest.json # Relative path to teamsfx folder. Environment variables in manifest will be replaced before apply to AAD app
# outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json

# Validate using manifest schema
- uses: teamsApp/validateManifest
with:
# Path to manifest template
manifestPath: ./appPackage/manifest.json
# # Validate using manifest schema
# - uses: teamsApp/validateManifest
# with:
# # Path to manifest template
# manifestPath: ./appPackage/manifest.json

# Build Teams app package with latest env value
- uses: teamsApp/zipAppPackage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ namespace MeetingTranscription
{
public class AdapterWithErrorHandler : CloudAdapter
{
public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger<IBotFrameworkHttpAdapter> logger)
: base(auth, logger)
public AdapterWithErrorHandler(BotFrameworkAuthentication botFrameworkAuthentication, ILogger<CloudAdapter> logger)
: base(botFrameworkAuthentication, logger)
{
OnTurnError = async (turnContext, exception) =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -77,20 +78,45 @@ protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivi
/// <returns>A task that represents the work queued to execute.</returns>
protected override async Task OnTeamsMeetingEndAsync(MeetingEndEventDetails meeting, ITurnContext<IEventActivity> turnContext, CancellationToken cancellationToken)
{
var meetingInfo = await TeamsInfo.GetMeetingInfoAsync(turnContext);

var result = await graphHelper.GetMeetingTranscriptionsAsync(meetingInfo.Details.MsGraphResourceId);
if (!string.IsNullOrEmpty(result))
try
{
transcriptsDictionary.AddOrUpdate(meetingInfo.Details.MsGraphResourceId, result, (key, newValue) => result);
var meetingInfo = await TeamsInfo.GetMeetingInfoAsync(turnContext);
Console.WriteLine($"Meeting Ended: {meetingInfo.Details.MsGraphResourceId}");

// NEW: Get meeting organizer information when meeting ends
var organizerId = await graphHelper.GetMeetingOrganizerFromTeamsContextAsync(turnContext);
if (!string.IsNullOrEmpty(organizerId))
{
Console.WriteLine($"Meeting organizer identified: {organizerId}");
}

var attachment = this.cardFactory.CreateAdaptiveCardAttachement(new { MeetingId = meetingInfo.Details.MsGraphResourceId });
await turnContext.SendActivityAsync(MessageFactory.Attachment(attachment), cancellationToken);
// NEW: Use Teams context to find organizer and get transcripts
var result = await graphHelper.GetMeetingTranscriptionsAsync(meetingInfo.Details.MsGraphResourceId, organizerId);

if (!string.IsNullOrEmpty(result))
{
transcriptsDictionary.AddOrUpdate(meetingInfo.Details.MsGraphResourceId, result, (key, newValue) => result);

var attachment = this.cardFactory.CreateAdaptiveCardAttachement(new { MeetingId = meetingInfo.Details.MsGraphResourceId });
await turnContext.SendActivityAsync(MessageFactory.Attachment(attachment), cancellationToken);

Console.WriteLine($"Successfully retrieved and cached meeting transcript for {meetingInfo.Details.MsGraphResourceId}");
}
else
{
var attachment = this.cardFactory.CreateNotFoundCardAttachement();
await turnContext.SendActivityAsync(MessageFactory.Attachment(attachment), cancellationToken);

Console.WriteLine($"No transcript found for meeting {meetingInfo.Details.MsGraphResourceId}");
}
}
else
catch (Exception ex)
{
var attachment = this.cardFactory.CreateNotFoundCardAttachement();
await turnContext.SendActivityAsync(MessageFactory.Attachment(attachment), cancellationToken);
Console.WriteLine($"Error in OnTeamsMeetingEndAsync: {ex.Message}");

// Send error card to user
var errorAttachment = this.cardFactory.CreateNotFoundCardAttachement();
await turnContext.SendActivityAsync(MessageFactory.Attachment(errorAttachment), cancellationToken);
}
}

Expand All @@ -106,22 +132,7 @@ protected override async Task<TaskModuleResponse> OnTeamsTaskModuleFetchAsync(IT
{
try
{
// Validate taskModuleRequest and its Data property
if (taskModuleRequest?.Data == null)
{
Console.WriteLine("TaskModuleRequest or Data is null");
return CreateFallbackTaskModuleResponse();
}

var dataObject = JObject.FromObject(taskModuleRequest.Data);
var meetingId = dataObject["meetingId"];

// Validate meetingId exists and is not null
if (meetingId == null)
{
Console.WriteLine("MeetingId not found in task module request data");
return CreateFallbackTaskModuleResponse();
}
var meetingId = JObject.FromObject(taskModuleRequest.Data)["meetingId"];

return new TaskModuleResponse
{
Expand All @@ -141,32 +152,22 @@ protected override async Task<TaskModuleResponse> OnTeamsTaskModuleFetchAsync(IT
catch (Exception ex)
{
Console.WriteLine($"Error in OnTeamsTaskModuleFetchAsync: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");

return CreateFallbackTaskModuleResponse();
}
}

/// <summary>
/// Creates a fallback task module response when errors occur.
/// </summary>
/// <returns>A fallback TaskModuleResponse.</returns>
private TaskModuleResponse CreateFallbackTaskModuleResponse()
{
return new TaskModuleResponse
{
Task = new TaskModuleContinueResponse
return new TaskModuleResponse
{
Type = "continue",
Value = new TaskModuleTaskInfo()
Task = new TaskModuleContinueResponse
{
Url = this.azureSettings.Value.AppBaseUrl + "/home",
Height = 350,
Width = 350,
Title = "Meeting Transcript",
},
}
};
Type = "continue",
Value = new TaskModuleTaskInfo()
{
Url = this.azureSettings.Value.AppBaseUrl + "/home",
Height = 350,
Width = 350,
Title = "Meeting Transcript",
},
}
};
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,18 @@

namespace MeetingTranscription.Controllers
{
// This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot
// implementation at runtime. Multiple different IBot implementations running at different endpoints can be
// achieved by specifying a more specific type for the bot constructor argument.
// BotController handles incoming HTTP POST requests and delegates them to the CloudAdapter.
// It acts as an interface between incoming requests and the bot's processing logic for SingleTenant setup.
[Route("api/messages")]
[ApiController]
public class BotController : ControllerBase
{
private readonly IBotFrameworkHttpAdapter _adapter;
private readonly CloudAdapter adapter; // Updated to CloudAdapter for SingleTenant authentication
private readonly IBot _bot;

public BotController(IBotFrameworkHttpAdapter adapter, IBot bot)
public BotController(CloudAdapter adapter, IBot bot)
{
_adapter = adapter;
this.adapter = adapter;
_bot = bot;
}

Expand All @@ -31,7 +30,7 @@ public async Task PostAsync()
{
// Delegate the processing of the HTTP POST to the adapter.
// The adapter will invoke the bot.
await _adapter.ProcessAsync(Request, Response, _bot);
await adapter.ProcessAsync(Request, Response, _bot);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ public async Task<IActionResult> Index([FromQuery] string meetingId)
}
}
}

return View();
}
}
}
}
Loading