Public Access
Add meeting inactivity safeguards
PR and Push Build/Test / build-and-test (push) Failing after 14m53s
PR and Push Build/Test / build-and-test (push) Failing after 14m53s
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
using MeetingAssistant.MeetingNotes;
|
||||
|
||||
namespace MeetingAssistant.Recording;
|
||||
|
||||
internal static class MeetingArtifactContentDetector
|
||||
{
|
||||
private static readonly string[] DefaultAssistantContextLines =
|
||||
[
|
||||
"# Assistant Context",
|
||||
"## Live Context"
|
||||
];
|
||||
|
||||
public static async Task<bool> IsDefaultOnlyAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(artifacts.MeetingNotePath) ||
|
||||
!File.Exists(artifacts.TranscriptPath) ||
|
||||
!File.Exists(artifacts.AssistantContextPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var meetingNote = await File.ReadAllTextAsync(artifacts.MeetingNotePath, cancellationToken);
|
||||
var transcript = await File.ReadAllTextAsync(artifacts.TranscriptPath, cancellationToken);
|
||||
var assistantContext = await File.ReadAllTextAsync(artifacts.AssistantContextPath, cancellationToken);
|
||||
|
||||
return IsDefaultOnlyMeetingNote(meetingNote) &&
|
||||
IsDefaultOnlyTranscript(transcript) &&
|
||||
IsDefaultOnlyAssistantContext(assistantContext);
|
||||
}
|
||||
|
||||
private static bool IsDefaultOnlyMeetingNote(string content)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(MarkdownDocumentParser.SplitOptional(content).Body);
|
||||
}
|
||||
|
||||
private static bool IsDefaultOnlyTranscript(string content)
|
||||
{
|
||||
var body = MarkdownDocumentParser.SplitOptional(content).Body;
|
||||
return GetMeaningfulLines(body)
|
||||
.All(line => string.Equals(line, "# Meeting Transcript", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsDefaultOnlyAssistantContext(string content)
|
||||
{
|
||||
var body = MarkdownDocumentParser.SplitOptional(content).Body;
|
||||
return GetMeaningfulLines(body)
|
||||
.All(line => DefaultAssistantContextLines.Contains(line, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetMeaningfulLines(string body)
|
||||
{
|
||||
return body
|
||||
.Split(["\r\n", "\n"], StringSplitOptions.None)
|
||||
.Select(line => line.Trim())
|
||||
.Where(line => !string.IsNullOrWhiteSpace(line));
|
||||
}
|
||||
}
|
||||
@@ -502,6 +502,14 @@ public sealed class MeetingRecordingCoordinator
|
||||
run.TranscriptionCancellation);
|
||||
}
|
||||
|
||||
if (await MeetingArtifactContentDetector.IsDefaultOnlyAsync(artifacts, run.TranscriptionCancellation))
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Deleting stopped meeting artifacts instead of summarizing because note, transcript, and assistant context contain only generated defaults");
|
||||
await artifactCleaner.DeleteRunArtifactsAsync(artifacts, run.Options, run.TranscriptionCancellation);
|
||||
return;
|
||||
}
|
||||
|
||||
var summaryState = await RunSummaryAsync(run, run.TranscriptionCancellation);
|
||||
await CompletePendingFinalSpeakerIdentificationAsync(run, run.TranscriptionCancellation);
|
||||
await TransitionMeetingAsync(run, summaryState, CancellationToken.None);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace MeetingAssistant.Recording;
|
||||
|
||||
internal static class NotificationActivationArguments
|
||||
{
|
||||
public static IReadOnlyDictionary<string, string> Parse(string argumentText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(argumentText))
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
var arguments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var pair in argumentText.Split(['&', ';'], StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var separatorIndex = pair.IndexOf('=');
|
||||
if (separatorIndex < 0)
|
||||
{
|
||||
arguments[Uri.UnescapeDataString(pair)] = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = Uri.UnescapeDataString(pair[..separatorIndex]);
|
||||
var value = Uri.UnescapeDataString(pair[(separatorIndex + 1)..]);
|
||||
arguments[key] = value;
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using CommunityToolkit.WinUI.Notifications;
|
||||
|
||||
namespace MeetingAssistant.Recording;
|
||||
|
||||
public sealed class WindowsMeetingInactivityPromptService : IMeetingInactivityPromptService, IDisposable
|
||||
public sealed class WindowsMeetingInactivityPromptService : IMeetingInactivityPromptService, IHostedService, IDisposable
|
||||
{
|
||||
private const string NotificationSource = "meeting-inactivity-safeguard";
|
||||
private const string NotificationGroup = "meeting-inactivity";
|
||||
@@ -39,6 +39,10 @@ public sealed class WindowsMeetingInactivityPromptService : IMeetingInactivityPr
|
||||
EnsureRegistered();
|
||||
var promptId = Guid.NewGuid().ToString("N");
|
||||
pendingPrompts[promptId] = new PendingPrompt(handleResponseAsync);
|
||||
logger.LogInformation(
|
||||
"Registered native Windows inactivity notification {PromptId} response callback at threshold {Threshold}",
|
||||
promptId,
|
||||
request.Threshold);
|
||||
var notification = BuildNotification(promptId, request);
|
||||
notification.Show(toast =>
|
||||
{
|
||||
@@ -59,6 +63,23 @@ public sealed class WindowsMeetingInactivityPromptService : IMeetingInactivityPr
|
||||
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)
|
||||
@@ -69,11 +90,10 @@ public sealed class WindowsMeetingInactivityPromptService : IMeetingInactivityPr
|
||||
try
|
||||
{
|
||||
ToastNotificationManagerCompat.OnActivated -= OnNotificationInvoked;
|
||||
ToastNotificationManagerCompat.Uninstall();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Failed to unregister native Windows toast notifications");
|
||||
logger.LogWarning(exception, "Failed to detach native Windows toast notification activation handler");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,12 +146,31 @@ public sealed class WindowsMeetingInactivityPromptService : IMeetingInactivityPr
|
||||
|
||||
private void OnNotificationInvoked(ToastNotificationActivatedEventArgsCompat args)
|
||||
{
|
||||
var arguments = ParseArguments(args.Argument);
|
||||
logger.LogInformation(
|
||||
"Native Windows inactivity notification activation received with arguments {Arguments}",
|
||||
args.Argument);
|
||||
|
||||
var arguments = NotificationActivationArguments.Parse(args.Argument);
|
||||
if (!TryGetArgument(arguments, "source", out var source) ||
|
||||
!string.Equals(source, NotificationSource, StringComparison.OrdinalIgnoreCase) ||
|
||||
!TryGetArgument(arguments, "promptId", out var promptId) ||
|
||||
!pendingPrompts.TryRemove(promptId, out var pendingPrompt))
|
||||
!string.Equals(source, NotificationSource, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogDebug(
|
||||
"Ignoring native Windows notification activation for unrelated source {Source}",
|
||||
source);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGetArgument(arguments, "promptId", out var promptId))
|
||||
{
|
||||
logger.LogWarning("Ignoring native Windows inactivity notification activation without prompt id");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pendingPrompts.TryRemove(promptId, out var pendingPrompt))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Ignoring native Windows inactivity notification {PromptId} activation because no pending prompt callback exists",
|
||||
promptId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,31 +198,6 @@ public sealed class WindowsMeetingInactivityPromptService : IMeetingInactivityPr
|
||||
});
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> ParseArguments(string argumentText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(argumentText))
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
var arguments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var pair in argumentText.Split('&', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var separatorIndex = pair.IndexOf('=');
|
||||
if (separatorIndex < 0)
|
||||
{
|
||||
arguments[Uri.UnescapeDataString(pair)] = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = Uri.UnescapeDataString(pair[..separatorIndex]);
|
||||
var value = Uri.UnescapeDataString(pair[(separatorIndex + 1)..]);
|
||||
arguments[key] = value;
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
private static bool TryGetArgument(
|
||||
IReadOnlyDictionary<string, string> arguments,
|
||||
string key,
|
||||
|
||||
Reference in New Issue
Block a user