Public Access
145 lines
4.8 KiB
C#
145 lines
4.8 KiB
C#
using MeetingAssistant.MeetingNotes;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace MeetingAssistant.Summary;
|
|
|
|
public interface IMeetingSummaryArtifactResolver
|
|
{
|
|
Task<MeetingSessionArtifacts?> ResolveBySummaryPathAsync(
|
|
string summaryPath,
|
|
CancellationToken cancellationToken);
|
|
|
|
Task<MeetingSessionArtifacts?> ResolveBySummaryPathAsync(
|
|
string summaryPath,
|
|
MeetingAssistantOptions options,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return ResolveBySummaryPathAsync(summaryPath, cancellationToken);
|
|
}
|
|
}
|
|
|
|
public sealed class MeetingSummaryArtifactResolver : IMeetingSummaryArtifactResolver
|
|
{
|
|
private readonly MeetingAssistantOptions options;
|
|
private readonly IMeetingNoteStore meetingNoteStore;
|
|
|
|
public MeetingSummaryArtifactResolver(
|
|
IOptions<MeetingAssistantOptions> options,
|
|
IMeetingNoteStore meetingNoteStore)
|
|
{
|
|
this.options = options.Value;
|
|
this.meetingNoteStore = meetingNoteStore;
|
|
}
|
|
|
|
public async Task<MeetingSessionArtifacts?> ResolveBySummaryPathAsync(
|
|
string summaryPath,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return await ResolveBySummaryPathAsync(summaryPath, options, cancellationToken);
|
|
}
|
|
|
|
public async Task<MeetingSessionArtifacts?> ResolveBySummaryPathAsync(
|
|
string summaryPath,
|
|
MeetingAssistantOptions options,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var requestedSummaryPath = ResolveRequestedSummaryPath(summaryPath, options);
|
|
if (requestedSummaryPath is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var meetingNotesFolder = VaultPath.Resolve(options.Vault, options.Vault.MeetingNotesFolder);
|
|
if (!Directory.Exists(meetingNotesFolder))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (var meetingNotePath in Directory.EnumerateFiles(meetingNotesFolder, "*.md"))
|
|
{
|
|
var note = await meetingNoteStore.ReadAsync(meetingNotePath, cancellationToken);
|
|
var noteSummaryPath = ResolveNoteLink(note.Frontmatter.Summary, note.Path);
|
|
if (!PathsEqual(requestedSummaryPath, noteSummaryPath))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return new MeetingSessionArtifacts(
|
|
note.Path,
|
|
ResolveNoteLink(note.Frontmatter.Transcript, note.Path),
|
|
ResolveNoteLink(note.Frontmatter.AssistantContext, note.Path),
|
|
requestedSummaryPath);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string? ResolveRequestedSummaryPath(
|
|
string summaryPath,
|
|
MeetingAssistantOptions options)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(summaryPath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var summariesFolder = VaultPath.Resolve(options.Vault, options.Vault.SummariesFolder);
|
|
var requested = Environment.ExpandEnvironmentVariables(summaryPath);
|
|
var fullPath = Path.IsPathRooted(requested)
|
|
? Path.GetFullPath(requested)
|
|
: Path.GetFullPath(Path.Combine(summariesFolder, requested));
|
|
|
|
return IsWithinDirectory(summariesFolder, fullPath) ? fullPath : null;
|
|
}
|
|
|
|
private static string ResolveNoteLink(string pathOrLink, string sourceNotePath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(pathOrLink))
|
|
{
|
|
return "";
|
|
}
|
|
|
|
var sourceFolder = Path.GetDirectoryName(sourceNotePath) ?? "";
|
|
var target = ExtractWikiLinkTarget(pathOrLink) ?? pathOrLink;
|
|
target = target.Replace('/', Path.DirectorySeparatorChar);
|
|
|
|
if (!Path.HasExtension(target))
|
|
{
|
|
target += ".md";
|
|
}
|
|
|
|
return Path.GetFullPath(Path.IsPathRooted(target)
|
|
? target
|
|
: Path.Combine(sourceFolder, target));
|
|
}
|
|
|
|
private static string? ExtractWikiLinkTarget(string value)
|
|
{
|
|
if (!value.StartsWith("[[", StringComparison.Ordinal) ||
|
|
!value.EndsWith("]]", StringComparison.Ordinal))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var inner = value[2..^2];
|
|
var labelSeparator = inner.IndexOf('|', StringComparison.Ordinal);
|
|
return labelSeparator < 0 ? inner : inner[..labelSeparator];
|
|
}
|
|
|
|
private static bool PathsEqual(string left, string right)
|
|
{
|
|
return string.Equals(
|
|
Path.GetFullPath(left),
|
|
Path.GetFullPath(right),
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static bool IsWithinDirectory(string directory, string path)
|
|
{
|
|
var normalizedDirectory = Path.GetFullPath(directory)
|
|
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
|
var normalizedPath = Path.GetFullPath(path);
|
|
return normalizedPath.StartsWith(normalizedDirectory, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|