Public Access
Add Outlook Teams recording prompts
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
using MeetingAssistant.Recording;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MeetingAssistant.Calendar;
|
||||
|
||||
public sealed class CalendarRecordingPromptScheduler : BackgroundService
|
||||
{
|
||||
private readonly IOptions<MeetingAssistantOptions> options;
|
||||
private readonly ICalendarMeetingProvider meetingProvider;
|
||||
private readonly IMeetingStartPromptService promptService;
|
||||
private readonly IMeetingPromptRecordingController recordingController;
|
||||
private readonly ICalendarPromptClock clock;
|
||||
private readonly ILogger<CalendarRecordingPromptScheduler> logger;
|
||||
private readonly HashSet<string> promptedMeetingKeys = new(StringComparer.OrdinalIgnoreCase);
|
||||
private IReadOnlyList<CalendarMeeting> cachedMeetings = [];
|
||||
private DateOnly promptedMeetingDay;
|
||||
private DateOnly cachedMeetingDay;
|
||||
private DateTimeOffset nextSyncAt;
|
||||
|
||||
public CalendarRecordingPromptScheduler(
|
||||
IOptions<MeetingAssistantOptions> options,
|
||||
ICalendarMeetingProvider meetingProvider,
|
||||
IMeetingStartPromptService promptService,
|
||||
IMeetingPromptRecordingController recordingController,
|
||||
ICalendarPromptClock clock,
|
||||
ILogger<CalendarRecordingPromptScheduler> logger)
|
||||
{
|
||||
this.options = options;
|
||||
this.meetingProvider = meetingProvider;
|
||||
this.promptService = promptService;
|
||||
this.recordingController = recordingController;
|
||||
this.clock = clock;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task SyncOnceAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var promptOptions = options.Value.CalendarRecordingPrompts;
|
||||
if (!promptOptions.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = clock.Now;
|
||||
var today = DateOnly.FromDateTime(now.LocalDateTime);
|
||||
ResetPromptedMeetingsIfDayChanged(today);
|
||||
|
||||
try
|
||||
{
|
||||
cachedMeetings = await meetingProvider.GetRecordingPromptCandidatesForDayAsync(today, cancellationToken);
|
||||
cachedMeetingDay = today;
|
||||
nextSyncAt = now.Add(GetSyncInterval(promptOptions));
|
||||
logger.LogInformation(
|
||||
"Synced {MeetingCount} Outlook Teams calendar meetings for {Day}; next sync at {NextSyncAt}",
|
||||
cachedMeetings.Count,
|
||||
today,
|
||||
nextSyncAt);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
logger.LogWarning(exception, "Could not read today's Outlook Teams meetings for recording prompts");
|
||||
nextSyncAt = now.Add(GetSyncInterval(promptOptions));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CheckDuePromptsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var promptOptions = options.Value.CalendarRecordingPrompts;
|
||||
if (!promptOptions.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = clock.Now;
|
||||
var today = DateOnly.FromDateTime(now.LocalDateTime);
|
||||
ResetPromptedMeetingsIfDayChanged(today);
|
||||
if (cachedMeetingDay != today)
|
||||
{
|
||||
cachedMeetings = [];
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var meeting in cachedMeetings.OrderBy(meeting => meeting.Start))
|
||||
{
|
||||
var promptKey = GetPromptKey(meeting);
|
||||
if (promptedMeetingKeys.Contains(promptKey) ||
|
||||
!IsInPromptWindow(meeting, now, promptOptions))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
promptedMeetingKeys.Add(promptKey);
|
||||
logger.LogInformation(
|
||||
"Prompting to record calendar meeting {Subject} scheduled at {Start}",
|
||||
meeting.Subject,
|
||||
meeting.Start);
|
||||
await promptService.ShowPromptAsync(
|
||||
new MeetingStartPromptRequest(meeting),
|
||||
HandlePromptResponseAsync,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var promptOptions = options.Value.CalendarRecordingPrompts;
|
||||
if (!promptOptions.Enabled)
|
||||
{
|
||||
await Task.Delay(GetSyncInterval(promptOptions), stoppingToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
var now = clock.Now;
|
||||
var today = DateOnly.FromDateTime(now.LocalDateTime);
|
||||
if (cachedMeetingDay != today || now >= nextSyncAt)
|
||||
{
|
||||
await SyncOnceAsync(stoppingToken);
|
||||
}
|
||||
|
||||
await CheckDuePromptsAsync(stoppingToken);
|
||||
|
||||
var delay = GetDelayUntilNextWork(clock.Now);
|
||||
if (delay <= TimeSpan.Zero)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await Task.Delay(delay, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandlePromptResponseAsync(
|
||||
MeetingStartPromptResponse response,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (response != MeetingStartPromptResponse.Record)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (recordingController.CurrentStatus.IsRecording)
|
||||
{
|
||||
await recordingController.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
await recordingController.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private void ResetPromptedMeetingsIfDayChanged(DateOnly today)
|
||||
{
|
||||
if (promptedMeetingDay == today)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
promptedMeetingDay = today;
|
||||
promptedMeetingKeys.Clear();
|
||||
}
|
||||
|
||||
private static bool IsInPromptWindow(
|
||||
CalendarMeeting meeting,
|
||||
DateTimeOffset now,
|
||||
CalendarRecordingPromptOptions options)
|
||||
{
|
||||
var promptWindow = options.PromptWindow > TimeSpan.Zero
|
||||
? options.PromptWindow
|
||||
: TimeSpan.FromMinutes(5);
|
||||
return now >= meeting.Start &&
|
||||
now <= meeting.Start.Add(promptWindow) &&
|
||||
now < meeting.End;
|
||||
}
|
||||
|
||||
private TimeSpan GetDelayUntilNextWork(DateTimeOffset now)
|
||||
{
|
||||
if (nextSyncAt <= DateTimeOffset.MinValue || now >= nextSyncAt)
|
||||
{
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
var syncDelay = nextSyncAt - now;
|
||||
|
||||
var nextMeetingStart = cachedMeetings
|
||||
.Where(meeting => !promptedMeetingKeys.Contains(GetPromptKey(meeting)))
|
||||
.Where(meeting => meeting.End > now)
|
||||
.Select(meeting => meeting.Start <= now ? now : meeting.Start)
|
||||
.Order()
|
||||
.FirstOrDefault();
|
||||
if (nextMeetingStart == default)
|
||||
{
|
||||
return syncDelay;
|
||||
}
|
||||
|
||||
var meetingDelay = nextMeetingStart - now;
|
||||
return meetingDelay <= TimeSpan.Zero
|
||||
? TimeSpan.Zero
|
||||
: meetingDelay < syncDelay ? meetingDelay : syncDelay;
|
||||
}
|
||||
|
||||
private static TimeSpan GetSyncInterval(CalendarRecordingPromptOptions options)
|
||||
{
|
||||
return options.SyncInterval > TimeSpan.Zero
|
||||
? options.SyncInterval
|
||||
: TimeSpan.FromMinutes(30);
|
||||
}
|
||||
|
||||
private static string GetPromptKey(CalendarMeeting meeting)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(meeting.Id))
|
||||
{
|
||||
return meeting.Id;
|
||||
}
|
||||
|
||||
return $"{meeting.Start:O}|{meeting.End:O}|{meeting.Subject}";
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICalendarMeetingProvider
|
||||
{
|
||||
Task<IReadOnlyList<CalendarMeeting>> GetRecordingPromptCandidatesForDayAsync(
|
||||
DateOnly day,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record CalendarMeeting(
|
||||
string Id,
|
||||
string Subject,
|
||||
DateTimeOffset Start,
|
||||
DateTimeOffset End);
|
||||
|
||||
public interface IMeetingStartPromptService
|
||||
{
|
||||
Task ShowPromptAsync(
|
||||
MeetingStartPromptRequest request,
|
||||
Func<MeetingStartPromptResponse, CancellationToken, Task> handleResponseAsync,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record MeetingStartPromptRequest(CalendarMeeting Meeting);
|
||||
|
||||
public enum MeetingStartPromptResponse
|
||||
{
|
||||
Record,
|
||||
Skip
|
||||
}
|
||||
|
||||
public interface IMeetingPromptRecordingController
|
||||
{
|
||||
RecordingStatus CurrentStatus { get; }
|
||||
|
||||
Task<RecordingStatus> StartAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task<RecordingStatus> StopAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class MeetingPromptRecordingController : IMeetingPromptRecordingController
|
||||
{
|
||||
private readonly MeetingRecordingCoordinator coordinator;
|
||||
|
||||
public MeetingPromptRecordingController(MeetingRecordingCoordinator coordinator)
|
||||
{
|
||||
this.coordinator = coordinator;
|
||||
}
|
||||
|
||||
public RecordingStatus CurrentStatus => coordinator.CurrentStatus;
|
||||
|
||||
public Task<RecordingStatus> StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return coordinator.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task<RecordingStatus> StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return coordinator.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICalendarPromptClock
|
||||
{
|
||||
DateTimeOffset Now { get; }
|
||||
}
|
||||
|
||||
public sealed class SystemCalendarPromptClock : ICalendarPromptClock
|
||||
{
|
||||
public DateTimeOffset Now => DateTimeOffset.Now;
|
||||
}
|
||||
|
||||
public sealed class NoopCalendarMeetingProvider : ICalendarMeetingProvider
|
||||
{
|
||||
public Task<IReadOnlyList<CalendarMeeting>> GetRecordingPromptCandidatesForDayAsync(
|
||||
DateOnly day,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<CalendarMeeting>>([]);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class NoopMeetingStartPromptService : IMeetingStartPromptService
|
||||
{
|
||||
public Task ShowPromptAsync(
|
||||
MeetingStartPromptRequest request,
|
||||
Func<MeetingStartPromptResponse, CancellationToken, Task> handleResponseAsync,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
#if WINDOWS
|
||||
using MeetingAssistant.MeetingNotes;
|
||||
|
||||
namespace MeetingAssistant.Calendar;
|
||||
|
||||
public sealed class OutlookClassicCalendarMeetingProvider : ICalendarMeetingProvider
|
||||
{
|
||||
private readonly ILogger<OutlookClassicCalendarMeetingProvider> logger;
|
||||
|
||||
public OutlookClassicCalendarMeetingProvider(ILogger<OutlookClassicCalendarMeetingProvider> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<CalendarMeeting>> GetRecordingPromptCandidatesForDayAsync(
|
||||
DateOnly day,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
return OutlookClassicCom.RunStaAsync(
|
||||
() => GetTeamsMeetingsForDay(day),
|
||||
cancellationToken,
|
||||
"Meeting Assistant Outlook Calendar Prompt Lookup");
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
logger.LogDebug(exception, "Outlook Classic calendar meetings were not available");
|
||||
return Task.FromResult<IReadOnlyList<CalendarMeeting>>([]);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<CalendarMeeting> GetTeamsMeetingsForDay(DateOnly day)
|
||||
{
|
||||
using var calendar = OutlookClassicCom.OpenDefaultCalendar(logger, "calendar prompt lookup");
|
||||
if (calendar is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var dayStart = day.ToDateTime(TimeOnly.MinValue);
|
||||
var dayEnd = dayStart.AddDays(1);
|
||||
var meetings = new List<CalendarMeeting>();
|
||||
foreach (var item in OutlookClassicCom.EnumerateItems(calendar.Items))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (item is null || !OutlookClassicCom.IsAppointment(item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var start = Convert.ToDateTime(OutlookClassicCom.GetProperty(item, "Start"));
|
||||
if (start >= dayEnd)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var end = Convert.ToDateTime(OutlookClassicCom.GetProperty(item, "End"));
|
||||
if (end <= dayStart ||
|
||||
start < dayStart ||
|
||||
!IsTeamsAppointment(item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rawSubject = Convert.ToString(OutlookClassicCom.GetProperty(item, "Subject"))?.Trim();
|
||||
var subject = string.IsNullOrWhiteSpace(rawSubject) ? "Teams meeting" : rawSubject;
|
||||
meetings.Add(new CalendarMeeting(
|
||||
GetAppointmentId(item, start, end, subject),
|
||||
subject,
|
||||
OutlookClassicCom.ToLocalOffset(start),
|
||||
OutlookClassicCom.ToLocalOffset(end)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
OutlookClassicCom.ReleaseComObject(item);
|
||||
}
|
||||
}
|
||||
|
||||
return meetings;
|
||||
}
|
||||
|
||||
private static string GetAppointmentId(
|
||||
object appointment,
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
string subject)
|
||||
{
|
||||
var id = Convert.ToString(OutlookClassicCom.TryGetProperty(appointment, "GlobalAppointmentID"));
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = Convert.ToString(OutlookClassicCom.TryGetProperty(appointment, "EntryID"));
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(id)
|
||||
? $"{start:O}|{end:O}|{subject}"
|
||||
: $"{id}|{start:O}";
|
||||
}
|
||||
|
||||
private static bool IsTeamsAppointment(object appointment)
|
||||
{
|
||||
var subject = Convert.ToString(OutlookClassicCom.GetProperty(appointment, "Subject")) ?? "";
|
||||
var location = Convert.ToString(OutlookClassicCom.GetProperty(appointment, "Location")) ?? "";
|
||||
var body = Convert.ToString(OutlookClassicCom.GetProperty(appointment, "Body")) ?? "";
|
||||
return TeamsMeetingMarkerDetector.IsTeamsAppointment(subject, location, body);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace MeetingAssistant.Calendar;
|
||||
|
||||
internal static class TeamsMeetingMarkerDetector
|
||||
{
|
||||
public static bool IsTeamsAppointment(
|
||||
string subject,
|
||||
string location,
|
||||
string body)
|
||||
{
|
||||
return ContainsTeamsMarker(subject) ||
|
||||
ContainsTeamsMarker(location) ||
|
||||
ContainsTeamsMarker(body);
|
||||
}
|
||||
|
||||
public static bool ContainsTeamsMarker(string value)
|
||||
{
|
||||
return value.Contains("teams.microsoft.com", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Contains("Microsoft Teams", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Contains("Join the meeting now", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Contains("Join Microsoft Teams Meeting", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
#if WINDOWS
|
||||
using System.Collections.Concurrent;
|
||||
using CommunityToolkit.WinUI.Notifications;
|
||||
using MeetingAssistant.Recording;
|
||||
|
||||
namespace MeetingAssistant.Calendar;
|
||||
|
||||
public sealed class WindowsMeetingStartPromptService : IMeetingStartPromptService, IHostedService, IDisposable
|
||||
{
|
||||
private const string NotificationSource = "meeting-start-prompt";
|
||||
private const string NotificationGroup = "meeting-start";
|
||||
private readonly ConcurrentDictionary<string, Func<MeetingStartPromptResponse, CancellationToken, Task>> pendingPrompts = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ILogger<WindowsMeetingStartPromptService> logger;
|
||||
private readonly object registrationGate = new();
|
||||
private bool registered;
|
||||
|
||||
public WindowsMeetingStartPromptService(ILogger<WindowsMeetingStartPromptService> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public Task ShowPromptAsync(
|
||||
MeetingStartPromptRequest request,
|
||||
Func<MeetingStartPromptResponse, CancellationToken, Task> handleResponseAsync,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17763))
|
||||
{
|
||||
logger.LogWarning("Windows app notifications require Windows 10 version 1809 or later");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled(cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EnsureRegistered();
|
||||
var promptId = Guid.NewGuid().ToString("N");
|
||||
pendingPrompts[promptId] = handleResponseAsync;
|
||||
BuildNotification(promptId, request).Show(toast =>
|
||||
{
|
||||
toast.Group = NotificationGroup;
|
||||
toast.Tag = promptId;
|
||||
toast.ExpirationTime = request.Meeting.End;
|
||||
});
|
||||
logger.LogInformation(
|
||||
"Displayed native Windows meeting-start notification {PromptId} for {Subject}",
|
||||
promptId,
|
||||
request.Meeting.Subject);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to display native Windows meeting-start notification");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17763))
|
||||
{
|
||||
logger.LogWarning("Windows app notifications require Windows 10 version 1809 or later");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
EnsureRegistered();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!registered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ToastNotificationManagerCompat.OnActivated -= OnNotificationInvoked;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Failed to detach native Windows meeting-start notification activation handler");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureRegistered()
|
||||
{
|
||||
if (registered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (registrationGate)
|
||||
{
|
||||
if (registered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ToastNotificationManagerCompat.OnActivated += OnNotificationInvoked;
|
||||
registered = true;
|
||||
logger.LogInformation("Registered native Windows meeting-start notification activation handler");
|
||||
}
|
||||
}
|
||||
|
||||
private static ToastContentBuilder BuildNotification(
|
||||
string promptId,
|
||||
MeetingStartPromptRequest request)
|
||||
{
|
||||
var yesButton = new ToastButton()
|
||||
.SetContent("Yes")
|
||||
.AddArgument("source", NotificationSource)
|
||||
.AddArgument("promptId", promptId)
|
||||
.AddArgument("response", "record")
|
||||
.SetBackgroundActivation();
|
||||
|
||||
var noButton = new ToastButton()
|
||||
.SetContent("No")
|
||||
.AddArgument("source", NotificationSource)
|
||||
.AddArgument("promptId", promptId)
|
||||
.AddArgument("response", "skip")
|
||||
.SetBackgroundActivation();
|
||||
|
||||
return new ToastContentBuilder()
|
||||
.AddArgument("source", NotificationSource)
|
||||
.AddArgument("promptId", promptId)
|
||||
.AddText("Record meeting?")
|
||||
.AddText($"{request.Meeting.Subject} starts at {request.Meeting.Start.LocalDateTime:t}.")
|
||||
.AddButton(yesButton)
|
||||
.AddButton(noButton);
|
||||
}
|
||||
|
||||
private void OnNotificationInvoked(ToastNotificationActivatedEventArgsCompat args)
|
||||
{
|
||||
var arguments = NotificationActivationArguments.Parse(args.Argument);
|
||||
if (!TryGetArgument(arguments, "source", out var source) ||
|
||||
!string.Equals(source, NotificationSource, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGetArgument(arguments, "promptId", out var promptId))
|
||||
{
|
||||
logger.LogWarning("Ignoring native Windows meeting-start notification activation without prompt id");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pendingPrompts.TryRemove(promptId, out var handleResponseAsync))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Ignoring native Windows meeting-start notification {PromptId} activation because no pending callback exists",
|
||||
promptId);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = TryGetArgument(arguments, "response", out var responseValue) &&
|
||||
string.Equals(responseValue, "record", StringComparison.OrdinalIgnoreCase)
|
||||
? MeetingStartPromptResponse.Record
|
||||
: MeetingStartPromptResponse.Skip;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Native Windows meeting-start notification {PromptId} activated with response {Response}",
|
||||
promptId,
|
||||
response);
|
||||
await handleResponseAsync(response, CancellationToken.None);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Failed to handle native Windows meeting-start notification response {Response}",
|
||||
response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static bool TryGetArgument(
|
||||
IReadOnlyDictionary<string, string> arguments,
|
||||
string key,
|
||||
out string value)
|
||||
{
|
||||
if (arguments.TryGetValue(key, out value!))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
value = "";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user