Document commute provider seam for personal report
This commit is contained in:
@@ -103,6 +103,33 @@ The default country code is `US`, but you can override it with:
|
||||
If you later add custom holiday authoring, disabled records can be used to suppress a holiday for a
|
||||
loop without removing the underlying system holiday source.
|
||||
|
||||
## Calendar Wiring
|
||||
|
||||
Calendar report output is now driven by a loop-scoped in-process provider.
|
||||
|
||||
The provider currently:
|
||||
|
||||
- reads persisted loop calendar events
|
||||
- folds in birthday and holiday dates that already live in the loop-scoped holiday list
|
||||
- returns a safe empty calendar view when nothing is scheduled
|
||||
|
||||
This keeps the personal report moving toward Pegasus-style household-aware output without forcing a
|
||||
full external calendar integration yet.
|
||||
|
||||
## Commute Wiring
|
||||
|
||||
Commute report output is now driven by a loop-scoped commute profile plus a provider seam.
|
||||
|
||||
The provider currently:
|
||||
|
||||
- reads persisted loop commute profiles
|
||||
- returns a setup view when commute is missing or incomplete
|
||||
- computes commute timing from the loop profile and the current clock
|
||||
- keeps the personal report flow aligned with the stock `Commute_*` shape
|
||||
|
||||
The provider is intentionally conservative for now. It preserves the old report shape and gives us
|
||||
room to add a richer travel-time source later without changing the behavior layer again.
|
||||
|
||||
## Recovery Strategy
|
||||
|
||||
The first supported device path is:
|
||||
|
||||
@@ -44,5 +44,9 @@ public interface ICloudStateStore
|
||||
IReadOnlyList<KeyRequestRecord> GetBinaryRequests();
|
||||
IReadOnlyList<HolidayRecord> GetHolidays(string? loopId = null);
|
||||
HolidayRecord UpsertHoliday(HolidayRecord holiday);
|
||||
IReadOnlyList<CommuteProfileRecord> GetCommuteProfiles(string? loopId = null);
|
||||
CommuteProfileRecord UpsertCommuteProfile(CommuteProfileRecord commuteProfile);
|
||||
IReadOnlyList<CalendarEventRecord> GetCalendarEvents(string? loopId = null);
|
||||
CalendarEventRecord UpsertCalendarEvent(CalendarEventRecord calendarEvent);
|
||||
void UpdateRobot(DeviceRegistration registration);
|
||||
}
|
||||
|
||||
@@ -12,4 +12,7 @@ public sealed record CommuteReportSnapshot(
|
||||
string Summary,
|
||||
int DurationMinutes,
|
||||
string? Mode = null,
|
||||
bool EventIsEarly = false);
|
||||
bool EventIsEarly = false,
|
||||
int MinutesUntilWork = 0,
|
||||
int ExtraMinutes = 0,
|
||||
bool RequiresSetup = false);
|
||||
|
||||
@@ -44,7 +44,20 @@ public sealed class JiboConditionedReply
|
||||
public IReadOnlyList<string> CalendarNothingReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CalendarServiceDownReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CalendarOutroReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteAppSetupReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteConfirmSpeakerReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteNowReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteMinutesLeftReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDepartTimeNormalReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDepartTimeNotNormalReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDriveNormalReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDriveLateReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDriveHurryReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDrivePoorReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteDriveTerribleReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteTransportNormalReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteTransportLateReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteTransportHurryReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> CommuteServiceDownReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> NewsIntroReplies { get; init; } = [];
|
||||
public IReadOnlyList<string> NewsCategoryIntroReplies { get; init; } = [];
|
||||
|
||||
@@ -355,12 +355,47 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
|
||||
|
||||
private ProtocolDispatchResult HandlePerson(string operation, ProtocolEnvelope envelope)
|
||||
{
|
||||
if (!operation.Equals("ListHolidays", StringComparison.OrdinalIgnoreCase))
|
||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||
|
||||
var body = envelope.TryParseBody();
|
||||
var loopId = ReadString(body, "loopId");
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetHolidays(loopId).Select(MapHoliday));
|
||||
|
||||
if (operation.Equals("ListHolidays", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var loopId = ReadString(body, "loopId");
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetHolidays(loopId).Select(MapHoliday));
|
||||
}
|
||||
|
||||
if (operation.Equals("ListCommute", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var loopId = ReadString(body, "loopId");
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetCommuteProfiles(loopId).Select(MapCommute));
|
||||
}
|
||||
|
||||
if (operation.Equals("UpsertCommute", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var hasIsEnabled = body is { } enabledBody && enabledBody.TryGetProperty("isEnabled", out _);
|
||||
var hasIsComplete = body is { } completeBody && completeBody.TryGetProperty("isComplete", out _);
|
||||
var workHour = ReadLong(body, "workHour");
|
||||
var workMinute = ReadLong(body, "workMinute");
|
||||
var typicalDurationMinutes = ReadLong(body, "typicalDurationMinutes");
|
||||
var commute = new CommuteProfileRecord
|
||||
{
|
||||
Id = ReadString(body, "id") ?? string.Empty,
|
||||
LoopId = ReadString(body, "loopId") ?? string.Empty,
|
||||
MemberId = ReadString(body, "memberId"),
|
||||
IsEnabled = hasIsEnabled ? ReadBool(body, "isEnabled") : true,
|
||||
IsComplete = hasIsComplete ? ReadBool(body, "isComplete") : true,
|
||||
Mode = ReadString(body, "mode") ?? "driving",
|
||||
WorkHour = workHour is > 0 and < 24 ? (int)workHour.Value : 8,
|
||||
WorkMinute = workMinute is >= 0 and < 60 ? (int)workMinute.Value : 30,
|
||||
OriginName = ReadString(body, "originName"),
|
||||
DestinationName = ReadString(body, "destinationName"),
|
||||
TypicalDurationMinutes = typicalDurationMinutes is > 0
|
||||
? (int)typicalDurationMinutes.Value
|
||||
: 25
|
||||
};
|
||||
return ProtocolDispatchResult.Ok(MapCommute(stateStore.UpsertCommuteProfile(commute)));
|
||||
}
|
||||
|
||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleBackup(string operation, ProtocolEnvelope envelope)
|
||||
@@ -572,6 +607,26 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
|
||||
};
|
||||
}
|
||||
|
||||
private static object MapCommute(CommuteProfileRecord commute)
|
||||
{
|
||||
return new
|
||||
{
|
||||
id = commute.Id,
|
||||
loopId = commute.LoopId,
|
||||
memberId = commute.MemberId,
|
||||
isEnabled = commute.IsEnabled,
|
||||
isComplete = commute.IsComplete,
|
||||
mode = commute.Mode,
|
||||
workHour = commute.WorkHour,
|
||||
workMinute = commute.WorkMinute,
|
||||
originName = commute.OriginName,
|
||||
destinationName = commute.DestinationName,
|
||||
typicalDurationMinutes = commute.TypicalDurationMinutes,
|
||||
created = commute.Created,
|
||||
updated = commute.Updated
|
||||
};
|
||||
}
|
||||
|
||||
private static object MapMedia(MediaRecord item)
|
||||
{
|
||||
return new
|
||||
|
||||
@@ -1626,6 +1626,11 @@ public sealed class JiboInteractionService(
|
||||
"commute",
|
||||
ChooseCommuteServiceDownReply(catalog));
|
||||
|
||||
if (snapshot.RequiresSetup)
|
||||
return new JiboInteractionDecision(
|
||||
"commute_setup",
|
||||
ChooseCommuteAppSetupReply(catalog));
|
||||
|
||||
return new JiboInteractionDecision(
|
||||
"commute",
|
||||
BuildCommuteSpokenReply(snapshot, catalog));
|
||||
@@ -1754,47 +1759,122 @@ public sealed class JiboInteractionService(
|
||||
{
|
||||
var duration = snapshot.DurationMinutes;
|
||||
var durationText = duration <= 1 ? "1 minute" : $"{duration} minutes";
|
||||
var minutesLeft = snapshot.MinutesUntilWork;
|
||||
var minutesLeftText = minutesLeft <= 1 ? "1 minute" : $"{Math.Abs(minutesLeft)} minutes";
|
||||
var mode = string.IsNullOrWhiteSpace(snapshot.Mode) ? "driving" : snapshot.Mode.Trim();
|
||||
var template = ChooseCommuteTemplate(snapshot, catalog, mode);
|
||||
var reply = RenderCommuteTemplate(template, durationText, minutesLeftText);
|
||||
|
||||
var template = ChooseCommuteTemplate(catalog.CommuteNowReplies, mode,
|
||||
"For your commute, it should take about ${skill.commute.durationMins} minutes.");
|
||||
if (minutesLeft > 0 && minutesLeft < 30)
|
||||
{
|
||||
var minutesTemplate = ChooseShortestTemplate(catalog.CommuteMinutesLeftReplies)
|
||||
?? "That's in about ${skill.commute.minsLeft} minutes.";
|
||||
reply = $"{reply} {RenderCommuteTemplate(minutesTemplate, durationText, minutesLeftText)}";
|
||||
}
|
||||
|
||||
if (minutesLeft > 0 && minutesLeft < 120)
|
||||
{
|
||||
var departTemplate = ChooseCommuteDepartTimeTemplate(snapshot, catalog, mode);
|
||||
if (!string.IsNullOrWhiteSpace(departTemplate))
|
||||
reply = $"{reply} {RenderCommuteTemplate(departTemplate, durationText, minutesLeftText)}";
|
||||
}
|
||||
|
||||
return reply.Replace(" ", " ", StringComparison.Ordinal).Trim();
|
||||
}
|
||||
|
||||
private string ChooseCommuteAppSetupReply(JiboExperienceCatalog catalog)
|
||||
{
|
||||
return SelectLegacyReply(
|
||||
catalog.CommuteAppSetupReplies,
|
||||
[
|
||||
"I need your commute settings before I can give you a commute report."
|
||||
]);
|
||||
}
|
||||
|
||||
private static string ChooseCommuteTemplate(
|
||||
CommuteReportSnapshot snapshot,
|
||||
JiboExperienceCatalog catalog,
|
||||
string mode)
|
||||
{
|
||||
var minutesUntilWork = snapshot.MinutesUntilWork;
|
||||
var extraMinutes = Math.Max(0, snapshot.ExtraMinutes);
|
||||
var isLate = minutesUntilWork <= 0;
|
||||
var isHurry = minutesUntilWork > 0 && minutesUntilWork <= 10;
|
||||
var isNormal = !isLate && !isHurry;
|
||||
var isFarAway = minutesUntilWork > 120 || minutesUntilWork < -30;
|
||||
var hasTrafficSeverity = minutesUntilWork > 0;
|
||||
var isTerrible = hasTrafficSeverity && extraMinutes >= 15;
|
||||
var isPoor = hasTrafficSeverity && extraMinutes >= 5;
|
||||
|
||||
var loweredMode = mode.Trim().ToLowerInvariant();
|
||||
IReadOnlyList<string> candidates = loweredMode switch
|
||||
{
|
||||
"walking" when isHurry => catalog.CommuteTransportHurryReplies,
|
||||
"walking" when isLate => catalog.CommuteTransportLateReplies,
|
||||
"walking" => catalog.CommuteTransportNormalReplies,
|
||||
"transit" when isHurry => catalog.CommuteTransportHurryReplies,
|
||||
"transit" when isLate => catalog.CommuteTransportLateReplies,
|
||||
"transit" => catalog.CommuteTransportNormalReplies,
|
||||
"bicycling" when isHurry => catalog.CommuteDriveHurryReplies,
|
||||
"bicycling" when isLate => catalog.CommuteDriveLateReplies,
|
||||
"bicycling" => catalog.CommuteDriveNormalReplies,
|
||||
_ when isFarAway => catalog.CommuteNowReplies,
|
||||
_ when isTerrible => catalog.CommuteDriveTerribleReplies,
|
||||
_ when isPoor => catalog.CommuteDrivePoorReplies,
|
||||
_ when isHurry => catalog.CommuteDriveHurryReplies,
|
||||
_ when isLate => catalog.CommuteDriveLateReplies,
|
||||
_ when isNormal => catalog.CommuteDriveNormalReplies,
|
||||
_ => catalog.CommuteNowReplies
|
||||
};
|
||||
|
||||
if (candidates.Count == 0)
|
||||
return "For your commute, it should take about ${skill.commute.durationMins} minutes.";
|
||||
|
||||
var selected = ChooseShortestTemplate(candidates);
|
||||
return string.IsNullOrWhiteSpace(selected)
|
||||
? "For your commute, it should take about ${skill.commute.durationMins} minutes."
|
||||
: selected!;
|
||||
}
|
||||
|
||||
private static string ChooseCommuteDepartTimeTemplate(
|
||||
CommuteReportSnapshot snapshot,
|
||||
JiboExperienceCatalog catalog,
|
||||
string mode)
|
||||
{
|
||||
var loweredMode = mode.Trim().ToLowerInvariant();
|
||||
var templates = snapshot.MinutesUntilWork <= 0
|
||||
? catalog.CommuteDepartTimeNotNormalReplies
|
||||
: catalog.CommuteDepartTimeNormalReplies;
|
||||
|
||||
if (templates.Count == 0) return string.Empty;
|
||||
|
||||
var selected = ChooseShortestTemplate(templates);
|
||||
if (!string.IsNullOrWhiteSpace(selected)) return selected!;
|
||||
|
||||
return loweredMode switch
|
||||
{
|
||||
"walking" => "If you leave at the usual time, that should work out fine.",
|
||||
"transit" => "If you leave at the usual time, that should work out fine.",
|
||||
_ => "If you leave at the usual time, that should work out fine."
|
||||
};
|
||||
}
|
||||
|
||||
private static string RenderCommuteTemplate(string template, string durationText, string minutesLeftText)
|
||||
{
|
||||
return template
|
||||
.Replace("${skill.commute.durationMins}", durationText, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("${skill.commute.minsLeft}", minutesLeftText, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("${speaker}", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(" ", " ", StringComparison.Ordinal)
|
||||
.Trim();
|
||||
}
|
||||
|
||||
private static string ChooseCommuteTemplate(
|
||||
IReadOnlyList<string> templates,
|
||||
string mode,
|
||||
string fallback)
|
||||
private static string? ChooseShortestTemplate(IEnumerable<string> templates)
|
||||
{
|
||||
if (templates.Count == 0) return fallback;
|
||||
|
||||
var loweredMode = mode.Trim().ToLowerInvariant();
|
||||
var filtered = templates.Where(template =>
|
||||
{
|
||||
var lowered = template.ToLowerInvariant();
|
||||
return loweredMode switch
|
||||
{
|
||||
"walking" => lowered.Contains("walk", StringComparison.OrdinalIgnoreCase),
|
||||
"transit" => lowered.Contains("public transportation", StringComparison.OrdinalIgnoreCase) ||
|
||||
lowered.Contains("transit", StringComparison.OrdinalIgnoreCase) ||
|
||||
lowered.Contains("transportation", StringComparison.OrdinalIgnoreCase),
|
||||
"bicycling" => lowered.Contains("bike", StringComparison.OrdinalIgnoreCase) ||
|
||||
lowered.Contains("ride", StringComparison.OrdinalIgnoreCase),
|
||||
_ => lowered.Contains("drive", StringComparison.OrdinalIgnoreCase) ||
|
||||
lowered.Contains("commute", StringComparison.OrdinalIgnoreCase)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
var selected = filtered.Count > 0
|
||||
? filtered.OrderBy(static template => template.Length).First()
|
||||
: templates.OrderBy(static template => template.Length).FirstOrDefault();
|
||||
|
||||
return string.IsNullOrWhiteSpace(selected) ? fallback : selected!;
|
||||
return templates
|
||||
.Where(static template => !string.IsNullOrWhiteSpace(template))
|
||||
.OrderBy(static template => template.Length)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static string BuildWeeklyForecastSpokenReply(
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class CalendarEventRecord
|
||||
{
|
||||
public string Id { get; init; } = $"calendar-{Guid.NewGuid():N}";
|
||||
public string LoopId { get; init; } = "openjibo-default-loop";
|
||||
public string Summary { get; init; } = "Calendar event";
|
||||
public string? TimeLabel { get; init; }
|
||||
public DateOnly Date { get; init; } = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
public DateOnly? EndDate { get; init; }
|
||||
public bool IsAllDay { get; init; }
|
||||
public bool IsEnabled { get; init; } = true;
|
||||
public string Source { get; init; } = "manual";
|
||||
public string? MemberId { get; init; }
|
||||
public DateTimeOffset Created { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class CommuteProfileRecord
|
||||
{
|
||||
public string Id { get; init; } = $"commute-{Guid.NewGuid():N}";
|
||||
public string LoopId { get; init; } = "openjibo-default-loop";
|
||||
public string? MemberId { get; init; }
|
||||
public bool IsEnabled { get; init; } = true;
|
||||
public bool IsComplete { get; init; } = true;
|
||||
public string Mode { get; init; } = "driving";
|
||||
public int WorkHour { get; init; } = 8;
|
||||
public int WorkMinute { get; init; } = 30;
|
||||
public string? OriginName { get; init; } = "home";
|
||||
public string? DestinationName { get; init; } = "work";
|
||||
public int TypicalDurationMinutes { get; init; } = 25;
|
||||
public DateTimeOffset Created { get; init; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset Updated { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
using Jibo.Cloud.Infrastructure.Persistence;
|
||||
using Jibo.Runtime.Abstractions;
|
||||
|
||||
namespace Jibo.Cloud.Infrastructure.Calendar;
|
||||
|
||||
public sealed class CloudStateCalendarReportProvider(ICloudStateStore cloudStateStore) : ICalendarReportProvider
|
||||
{
|
||||
public Task<CalendarReportSnapshot?> GetReportAsync(
|
||||
TurnContext turn,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var loopId = ResolveLoopId(turn);
|
||||
var today = DateOnly.FromDateTime(DateTimeOffset.UtcNow.Date);
|
||||
var tomorrow = today.AddDays(1);
|
||||
|
||||
var calendarEvents = cloudStateStore.GetCalendarEvents(loopId)
|
||||
.Where(static calendarEvent => calendarEvent.IsEnabled)
|
||||
.Where(calendarEvent => calendarEvent.Date != default)
|
||||
.ToArray();
|
||||
|
||||
var holidays = cloudStateStore.GetHolidays(loopId)
|
||||
.Where(static holiday => holiday.IsEnabled)
|
||||
.Where(holiday => holiday.Date != default)
|
||||
.ToArray();
|
||||
|
||||
var todaySummaries = new List<string>();
|
||||
var todayTimes = new List<string>();
|
||||
var tomorrowSummaries = new List<string>();
|
||||
|
||||
foreach (var entry in calendarEvents
|
||||
.Select(calendarEvent => (
|
||||
Summary: calendarEvent.Summary,
|
||||
TimeLabel: calendarEvent.TimeLabel ?? "all day",
|
||||
Date: calendarEvent.Date))
|
||||
.Concat(ToCalendarEntries(holidays)))
|
||||
{
|
||||
if (entry.Date == today)
|
||||
{
|
||||
todaySummaries.Add(entry.Summary);
|
||||
todayTimes.Add(entry.TimeLabel);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.Date == tomorrow)
|
||||
tomorrowSummaries.Add(entry.Summary);
|
||||
}
|
||||
|
||||
return Task.FromResult<CalendarReportSnapshot?>(
|
||||
new CalendarReportSnapshot(todaySummaries, todayTimes, tomorrowSummaries));
|
||||
}
|
||||
|
||||
private static string ResolveLoopId(TurnContext turn)
|
||||
{
|
||||
if (turn.Attributes.TryGetValue("loopId", out var loopValue) &&
|
||||
loopValue is not null &&
|
||||
!string.IsNullOrWhiteSpace(loopValue.ToString()))
|
||||
return loopValue.ToString()!.Trim();
|
||||
|
||||
return "openjibo-default-loop";
|
||||
}
|
||||
|
||||
private static IEnumerable<(string Summary, string TimeLabel, DateOnly Date)> ToCalendarEntries(
|
||||
IEnumerable<HolidayRecord> holidays)
|
||||
{
|
||||
foreach (var holiday in holidays)
|
||||
{
|
||||
yield return (
|
||||
holiday.Name,
|
||||
"all day",
|
||||
holiday.Date);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Jibo.Cloud.Application.Abstractions;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
using Jibo.Runtime.Abstractions;
|
||||
|
||||
namespace Jibo.Cloud.Infrastructure.Commute;
|
||||
|
||||
public sealed class CloudStateCommuteReportProvider(ICloudStateStore cloudStateStore) : ICommuteReportProvider
|
||||
{
|
||||
private static readonly Regex TimeLabelRegex = new(
|
||||
@"(?<hour>\d{1,2})(?::(?<minute>\d{2}))?\s*(?<period>a\.?m\.?|p\.?m\.?)",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public Task<CommuteReportSnapshot?> GetReportAsync(
|
||||
TurnContext turn,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var loopId = ResolveLoopId(turn);
|
||||
var memberId = ResolveMemberId(turn);
|
||||
var commuteProfiles = cloudStateStore.GetCommuteProfiles(loopId);
|
||||
var commute = !string.IsNullOrWhiteSpace(memberId)
|
||||
? commuteProfiles.FirstOrDefault(profile =>
|
||||
profile.IsEnabled &&
|
||||
!string.IsNullOrWhiteSpace(profile.MemberId) &&
|
||||
string.Equals(profile.MemberId, memberId, StringComparison.OrdinalIgnoreCase))
|
||||
: null;
|
||||
|
||||
commute ??= commuteProfiles.FirstOrDefault(profile => profile.IsEnabled);
|
||||
|
||||
if (commute is null || !commute.IsComplete)
|
||||
{
|
||||
return Task.FromResult<CommuteReportSnapshot?>(
|
||||
new CommuteReportSnapshot(string.Empty, string.Empty, 0, RequiresSetup: true));
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.Now;
|
||||
var workTarget = ResolveWorkTarget(now, commute);
|
||||
var earlyTarget = ResolveEarlyCalendarTarget(loopId, now, workTarget);
|
||||
var arrivalTarget = earlyTarget ?? workTarget;
|
||||
var minutesUntilWork = (int)Math.Round((arrivalTarget - now).TotalMinutes);
|
||||
var durationMinutes = commute.TypicalDurationMinutes > 0 ? commute.TypicalDurationMinutes : 25;
|
||||
var extraMinutes = Math.Max(0, durationMinutes - Math.Max(0, minutesUntilWork));
|
||||
|
||||
var summary = commute.Mode.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"walking" => "your walk to work",
|
||||
"transit" => "your trip to work by public transportation",
|
||||
"bicycling" => "your bike ride to work",
|
||||
_ => "your drive to work"
|
||||
};
|
||||
|
||||
return Task.FromResult<CommuteReportSnapshot?>(
|
||||
new CommuteReportSnapshot(
|
||||
string.IsNullOrWhiteSpace(commute.DestinationName) ? "work" : commute.DestinationName.Trim(),
|
||||
summary,
|
||||
durationMinutes,
|
||||
commute.Mode,
|
||||
earlyTarget is not null,
|
||||
minutesUntilWork,
|
||||
extraMinutes));
|
||||
}
|
||||
|
||||
private static DateTimeOffset ResolveWorkTarget(DateTimeOffset now, CommuteProfileRecord commute)
|
||||
{
|
||||
var localDate = now.Date;
|
||||
var workTime = new DateTimeOffset(
|
||||
localDate.Year,
|
||||
localDate.Month,
|
||||
localDate.Day,
|
||||
Math.Clamp(commute.WorkHour, 0, 23),
|
||||
Math.Clamp(commute.WorkMinute, 0, 59),
|
||||
0,
|
||||
now.Offset);
|
||||
|
||||
return workTime;
|
||||
}
|
||||
|
||||
private DateTimeOffset? ResolveEarlyCalendarTarget(
|
||||
string loopId,
|
||||
DateTimeOffset now,
|
||||
DateTimeOffset workTarget)
|
||||
{
|
||||
var today = DateOnly.FromDateTime(now.DateTime);
|
||||
DateTimeOffset? earliest = null;
|
||||
|
||||
foreach (var calendarEvent in cloudStateStore.GetCalendarEvents(loopId)
|
||||
.Where(static calendarEvent => calendarEvent.IsEnabled)
|
||||
.Where(calendarEvent => calendarEvent.Date == today))
|
||||
{
|
||||
if (!TryParseTimeLabel(calendarEvent.TimeLabel, now, out var eventTime)) continue;
|
||||
if (eventTime >= workTarget) continue;
|
||||
if (earliest is null || eventTime < earliest)
|
||||
earliest = eventTime;
|
||||
}
|
||||
|
||||
return earliest;
|
||||
}
|
||||
|
||||
private static bool TryParseTimeLabel(string? timeLabel, DateTimeOffset now, out DateTimeOffset parsed)
|
||||
{
|
||||
parsed = default;
|
||||
if (string.IsNullOrWhiteSpace(timeLabel)) return false;
|
||||
|
||||
var match = TimeLabelRegex.Match(timeLabel);
|
||||
if (!match.Success) return false;
|
||||
|
||||
if (!int.TryParse(match.Groups["hour"].Value, out var hour)) return false;
|
||||
var minute = match.Groups["minute"].Success && int.TryParse(match.Groups["minute"].Value, out var parsedMinute)
|
||||
? parsedMinute
|
||||
: 0;
|
||||
var period = match.Groups["period"].Value.ToLowerInvariant();
|
||||
|
||||
hour %= 12;
|
||||
if (period.StartsWith("p", StringComparison.Ordinal) && hour < 12) hour += 12;
|
||||
if (period.StartsWith("a", StringComparison.Ordinal) && hour == 12) hour = 0;
|
||||
|
||||
parsed = new DateTimeOffset(
|
||||
now.Year,
|
||||
now.Month,
|
||||
now.Day,
|
||||
hour,
|
||||
minute,
|
||||
0,
|
||||
now.Offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? ResolveLoopId(TurnContext turn)
|
||||
{
|
||||
if (turn.Attributes.TryGetValue("loopId", out var loopValue) &&
|
||||
loopValue is not null &&
|
||||
!string.IsNullOrWhiteSpace(loopValue.ToString()))
|
||||
return loopValue.ToString()!.Trim();
|
||||
|
||||
return "openjibo-default-loop";
|
||||
}
|
||||
|
||||
private static string? ResolveMemberId(TurnContext turn)
|
||||
{
|
||||
if (turn.Attributes.TryGetValue("personId", out var personValue) &&
|
||||
personValue is not null &&
|
||||
!string.IsNullOrWhiteSpace(personValue.ToString()))
|
||||
return personValue.ToString()!.Trim();
|
||||
|
||||
if (turn.Attributes.TryGetValue("speakerId", out var speakerValue) &&
|
||||
speakerValue is not null &&
|
||||
!string.IsNullOrWhiteSpace(speakerValue.ToString()))
|
||||
return speakerValue.ToString()!.Trim();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -180,11 +180,77 @@ public sealed class InMemoryJiboExperienceContentRepository : IJiboExperienceCon
|
||||
"I heard your calendar request. The cloud knows the phrase, but the real calendar integration is still ahead.",
|
||||
"Calendar is recognized. We still need to connect the actual service path."
|
||||
],
|
||||
CommuteAppSetupReplies =
|
||||
[
|
||||
"I need your commute settings before I can give you a commute report."
|
||||
],
|
||||
CommuteConfirmSpeakerReplies =
|
||||
[
|
||||
"Let me make sure I have the right speaker for your commute."
|
||||
],
|
||||
CommuteReplies =
|
||||
[
|
||||
"I heard your commute request. That one is recognized, but not fully implemented yet.",
|
||||
"Commute is on the discovery list now. The real travel answer still needs a provider."
|
||||
],
|
||||
CommuteNowReplies =
|
||||
[
|
||||
"For your commute, it should take about {duration}.",
|
||||
"If you head out now, it should take about {duration}."
|
||||
],
|
||||
CommuteMinutesLeftReplies =
|
||||
[
|
||||
"That's in about {minutes} minutes.",
|
||||
"That's about {minutes} minutes from now."
|
||||
],
|
||||
CommuteDepartTimeNormalReplies =
|
||||
[
|
||||
"If you leave at the usual time, that should work out fine."
|
||||
],
|
||||
CommuteDepartTimeNotNormalReplies =
|
||||
[
|
||||
"Your leave-time looks a little off today."
|
||||
],
|
||||
CommuteDriveNormalReplies =
|
||||
[
|
||||
"Traffic looks about normal today.",
|
||||
"Your drive today looks pretty normal."
|
||||
],
|
||||
CommuteDriveLateReplies =
|
||||
[
|
||||
"Looking at traffic, if you left now, it'd be a little late for work.",
|
||||
"For your drive, you look a little late today."
|
||||
],
|
||||
CommuteDriveHurryReplies =
|
||||
[
|
||||
"You should've left a few minutes ago!",
|
||||
"You'd better get moving."
|
||||
],
|
||||
CommuteDrivePoorReplies =
|
||||
[
|
||||
"Traffic looks a little rough today.",
|
||||
"Your drive looks pretty slow right now."
|
||||
],
|
||||
CommuteDriveTerribleReplies =
|
||||
[
|
||||
"Traffic looks terrible today.",
|
||||
"Your drive is going to be rough."
|
||||
],
|
||||
CommuteTransportNormalReplies =
|
||||
[
|
||||
"Your public transportation commute looks pretty normal.",
|
||||
"Transit looks about normal today."
|
||||
],
|
||||
CommuteTransportLateReplies =
|
||||
[
|
||||
"Your transit commute looks like it may be a little late today.",
|
||||
"You might be late if you leave now and take transit."
|
||||
],
|
||||
CommuteTransportHurryReplies =
|
||||
[
|
||||
"You should've left a few minutes ago if you want transit to work.",
|
||||
"You're running a little late for transit."
|
||||
],
|
||||
NewsReplies =
|
||||
[
|
||||
"I heard your news request. That path is still a future cloud integration.",
|
||||
|
||||
@@ -170,8 +170,47 @@ public static class LegacyMimCatalogImporter
|
||||
if (fileName.StartsWith("CalendarOutro", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CalendarOutro;
|
||||
|
||||
if (fileName.StartsWith("CommuteAppSetup", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteAppSetup;
|
||||
|
||||
if (fileName.StartsWith("CommuteConfirmSpeaker", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteConfirmSpeaker;
|
||||
|
||||
if (fileName.StartsWith("CommuteNow", StringComparison.OrdinalIgnoreCase)) return LegacyMimBucket.CommuteNow;
|
||||
|
||||
if (fileName.StartsWith("CommuteMinutesLeft", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteMinutesLeft;
|
||||
|
||||
if (fileName.StartsWith("CommuteDepartTimeNormal", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDepartTimeNormal;
|
||||
|
||||
if (fileName.StartsWith("CommuteDepartTimeNotNormal", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDepartTimeNotNormal;
|
||||
|
||||
if (fileName.StartsWith("CommuteDriveNormal", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDriveNormal;
|
||||
|
||||
if (fileName.StartsWith("CommuteDriveLate", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDriveLate;
|
||||
|
||||
if (fileName.StartsWith("CommuteDriveHurry", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDriveHurry;
|
||||
|
||||
if (fileName.StartsWith("CommuteDrivePoor", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDrivePoor;
|
||||
|
||||
if (fileName.StartsWith("CommuteDriveTerrible", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteDriveTerrible;
|
||||
|
||||
if (fileName.StartsWith("CommuteTransportNormal", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteTransportNormal;
|
||||
|
||||
if (fileName.StartsWith("CommuteTransportLate", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteTransportLate;
|
||||
|
||||
if (fileName.StartsWith("CommuteTransportHurry", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteTransportHurry;
|
||||
|
||||
if (fileName.StartsWith("CommuteServiceDown", StringComparison.OrdinalIgnoreCase))
|
||||
return LegacyMimBucket.CommuteServiceDown;
|
||||
|
||||
@@ -295,7 +334,26 @@ public static class LegacyMimCatalogImporter
|
||||
CalendarServiceDownReplies = Merge(baseCatalog.CalendarServiceDownReplies,
|
||||
importedCatalog.CalendarServiceDownReplies),
|
||||
CalendarOutroReplies = Merge(baseCatalog.CalendarOutroReplies, importedCatalog.CalendarOutroReplies),
|
||||
CommuteAppSetupReplies = Merge(baseCatalog.CommuteAppSetupReplies, importedCatalog.CommuteAppSetupReplies),
|
||||
CommuteConfirmSpeakerReplies = Merge(baseCatalog.CommuteConfirmSpeakerReplies,
|
||||
importedCatalog.CommuteConfirmSpeakerReplies),
|
||||
CommuteNowReplies = Merge(baseCatalog.CommuteNowReplies, importedCatalog.CommuteNowReplies),
|
||||
CommuteMinutesLeftReplies = Merge(baseCatalog.CommuteMinutesLeftReplies, importedCatalog.CommuteMinutesLeftReplies),
|
||||
CommuteDepartTimeNormalReplies = Merge(baseCatalog.CommuteDepartTimeNormalReplies,
|
||||
importedCatalog.CommuteDepartTimeNormalReplies),
|
||||
CommuteDepartTimeNotNormalReplies = Merge(baseCatalog.CommuteDepartTimeNotNormalReplies,
|
||||
importedCatalog.CommuteDepartTimeNotNormalReplies),
|
||||
CommuteDriveNormalReplies = Merge(baseCatalog.CommuteDriveNormalReplies, importedCatalog.CommuteDriveNormalReplies),
|
||||
CommuteDriveLateReplies = Merge(baseCatalog.CommuteDriveLateReplies, importedCatalog.CommuteDriveLateReplies),
|
||||
CommuteDriveHurryReplies = Merge(baseCatalog.CommuteDriveHurryReplies, importedCatalog.CommuteDriveHurryReplies),
|
||||
CommuteDrivePoorReplies = Merge(baseCatalog.CommuteDrivePoorReplies, importedCatalog.CommuteDrivePoorReplies),
|
||||
CommuteDriveTerribleReplies = Merge(baseCatalog.CommuteDriveTerribleReplies, importedCatalog.CommuteDriveTerribleReplies),
|
||||
CommuteTransportNormalReplies = Merge(baseCatalog.CommuteTransportNormalReplies,
|
||||
importedCatalog.CommuteTransportNormalReplies),
|
||||
CommuteTransportLateReplies = Merge(baseCatalog.CommuteTransportLateReplies,
|
||||
importedCatalog.CommuteTransportLateReplies),
|
||||
CommuteTransportHurryReplies = Merge(baseCatalog.CommuteTransportHurryReplies,
|
||||
importedCatalog.CommuteTransportHurryReplies),
|
||||
CommuteServiceDownReplies = Merge(baseCatalog.CommuteServiceDownReplies,
|
||||
importedCatalog.CommuteServiceDownReplies),
|
||||
NewsIntroReplies = Merge(baseCatalog.NewsIntroReplies, importedCatalog.NewsIntroReplies),
|
||||
@@ -449,6 +507,19 @@ public static class LegacyMimCatalogImporter
|
||||
CalendarServiceDown,
|
||||
CalendarOutro,
|
||||
CommuteNow,
|
||||
CommuteMinutesLeft,
|
||||
CommuteDepartTimeNormal,
|
||||
CommuteDepartTimeNotNormal,
|
||||
CommuteAppSetup,
|
||||
CommuteConfirmSpeaker,
|
||||
CommuteDriveNormal,
|
||||
CommuteDriveLate,
|
||||
CommuteDriveHurry,
|
||||
CommuteDrivePoor,
|
||||
CommuteDriveTerrible,
|
||||
CommuteTransportNormal,
|
||||
CommuteTransportLate,
|
||||
CommuteTransportHurry,
|
||||
CommuteServiceDown,
|
||||
NewsIntro,
|
||||
NewsCategoryIntro,
|
||||
@@ -462,7 +533,20 @@ public static class LegacyMimCatalogImporter
|
||||
private readonly List<string> _calendarNothingTodayReplies = [];
|
||||
private readonly List<string> _calendarOutroReplies = [];
|
||||
private readonly List<string> _calendarServiceDownReplies = [];
|
||||
private readonly List<string> _commuteAppSetupReplies = [];
|
||||
private readonly List<string> _commuteConfirmSpeakerReplies = [];
|
||||
private readonly List<string> _commuteDepartTimeNormalReplies = [];
|
||||
private readonly List<string> _commuteDepartTimeNotNormalReplies = [];
|
||||
private readonly List<string> _commuteNowReplies = [];
|
||||
private readonly List<string> _commuteMinutesLeftReplies = [];
|
||||
private readonly List<string> _commuteDriveNormalReplies = [];
|
||||
private readonly List<string> _commuteDriveLateReplies = [];
|
||||
private readonly List<string> _commuteDriveHurryReplies = [];
|
||||
private readonly List<string> _commuteDrivePoorReplies = [];
|
||||
private readonly List<string> _commuteDriveTerribleReplies = [];
|
||||
private readonly List<string> _commuteTransportNormalReplies = [];
|
||||
private readonly List<string> _commuteTransportLateReplies = [];
|
||||
private readonly List<string> _commuteTransportHurryReplies = [];
|
||||
private readonly List<string> _commuteServiceDownReplies = [];
|
||||
private readonly List<string> _birthdayCelebrationReplies = [];
|
||||
private readonly List<JiboConditionedReply> _emotionReplies = [];
|
||||
@@ -615,9 +699,48 @@ public static class LegacyMimCatalogImporter
|
||||
case LegacyMimBucket.CalendarOutro:
|
||||
AddDistinct(_calendarOutroReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteAppSetup:
|
||||
AddDistinct(_commuteAppSetupReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteConfirmSpeaker:
|
||||
AddDistinct(_commuteConfirmSpeakerReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteNow:
|
||||
AddDistinct(_commuteNowReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteMinutesLeft:
|
||||
AddDistinct(_commuteMinutesLeftReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDepartTimeNormal:
|
||||
AddDistinct(_commuteDepartTimeNormalReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDepartTimeNotNormal:
|
||||
AddDistinct(_commuteDepartTimeNotNormalReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDriveNormal:
|
||||
AddDistinct(_commuteDriveNormalReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDriveLate:
|
||||
AddDistinct(_commuteDriveLateReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDriveHurry:
|
||||
AddDistinct(_commuteDriveHurryReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDrivePoor:
|
||||
AddDistinct(_commuteDrivePoorReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteDriveTerrible:
|
||||
AddDistinct(_commuteDriveTerribleReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteTransportNormal:
|
||||
AddDistinct(_commuteTransportNormalReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteTransportLate:
|
||||
AddDistinct(_commuteTransportLateReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteTransportHurry:
|
||||
AddDistinct(_commuteTransportHurryReplies, text);
|
||||
return;
|
||||
case LegacyMimBucket.CommuteServiceDown:
|
||||
AddDistinct(_commuteServiceDownReplies, text);
|
||||
return;
|
||||
@@ -670,7 +793,20 @@ public static class LegacyMimCatalogImporter
|
||||
CalendarNothingReplies = [.. _calendarNothingReplies],
|
||||
CalendarServiceDownReplies = [.. _calendarServiceDownReplies],
|
||||
CalendarOutroReplies = [.. _calendarOutroReplies],
|
||||
CommuteAppSetupReplies = [.. _commuteAppSetupReplies],
|
||||
CommuteConfirmSpeakerReplies = [.. _commuteConfirmSpeakerReplies],
|
||||
CommuteNowReplies = [.. _commuteNowReplies],
|
||||
CommuteMinutesLeftReplies = [.. _commuteMinutesLeftReplies],
|
||||
CommuteDepartTimeNormalReplies = [.. _commuteDepartTimeNormalReplies],
|
||||
CommuteDepartTimeNotNormalReplies = [.. _commuteDepartTimeNotNormalReplies],
|
||||
CommuteDriveNormalReplies = [.. _commuteDriveNormalReplies],
|
||||
CommuteDriveLateReplies = [.. _commuteDriveLateReplies],
|
||||
CommuteDriveHurryReplies = [.. _commuteDriveHurryReplies],
|
||||
CommuteDrivePoorReplies = [.. _commuteDrivePoorReplies],
|
||||
CommuteDriveTerribleReplies = [.. _commuteDriveTerribleReplies],
|
||||
CommuteTransportNormalReplies = [.. _commuteTransportNormalReplies],
|
||||
CommuteTransportLateReplies = [.. _commuteTransportLateReplies],
|
||||
CommuteTransportHurryReplies = [.. _commuteTransportHurryReplies],
|
||||
CommuteServiceDownReplies = [.. _commuteServiceDownReplies],
|
||||
NewsIntroReplies = [.. _newsIntroReplies],
|
||||
NewsCategoryIntroReplies = [.. _newsCategoryIntroReplies],
|
||||
|
||||
@@ -54,8 +54,10 @@ public static class ServiceCollectionExtensions
|
||||
services.AddHttpClient<INewsBriefingProvider, NewsApiBriefingProvider>();
|
||||
services.AddSingleton<IHolidayCalendarProvider>(provider =>
|
||||
new NagerDateHolidayCalendarProvider(provider.GetRequiredService<HolidayCalendarOptions>()));
|
||||
services.AddSingleton<ICalendarReportProvider, UnavailableCalendarReportProvider>();
|
||||
services.AddSingleton<ICommuteReportProvider, UnavailableCommuteReportProvider>();
|
||||
services.AddSingleton<ICalendarReportProvider>(provider =>
|
||||
new CloudStateCalendarReportProvider(provider.GetRequiredService<ICloudStateStore>()));
|
||||
services.AddSingleton<ICommuteReportProvider>(provider =>
|
||||
new CloudStateCommuteReportProvider(provider.GetRequiredService<ICloudStateStore>()));
|
||||
var statePersistencePath = configuration?["OpenJibo:State:PersistencePath"]
|
||||
?? Path.Combine(AppContext.BaseDirectory, "App_Data", "cloud-state.json");
|
||||
var personalMemoryPersistencePath = configuration?["OpenJibo:PersonalMemory:PersistencePath"]
|
||||
|
||||
@@ -18,6 +18,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
};
|
||||
|
||||
private readonly List<BackupRecord> _backups = [];
|
||||
private readonly List<CommuteProfileRecord> _commuteProfiles = [];
|
||||
private readonly List<CalendarEventRecord> _calendarEvents = [];
|
||||
private readonly ConcurrentDictionary<string, DeviceRegistration> _devices = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly ConcurrentDictionary<string, KeyRequestRecord>
|
||||
@@ -161,6 +163,12 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
_backups.Clear();
|
||||
_backups.AddRange(snapshot.Backups ?? []);
|
||||
|
||||
_commuteProfiles.Clear();
|
||||
_commuteProfiles.AddRange(snapshot.CommuteProfiles ?? []);
|
||||
|
||||
_calendarEvents.Clear();
|
||||
_calendarEvents.AddRange(snapshot.CalendarEvents ?? []);
|
||||
|
||||
_loops.Clear();
|
||||
_loops.AddRange(snapshot.Loops ?? []);
|
||||
|
||||
@@ -214,6 +222,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
Updates = _updates.ToArray(),
|
||||
Media = _media.ToArray(),
|
||||
Backups = _backups.ToArray(),
|
||||
CommuteProfiles = _commuteProfiles.ToArray(),
|
||||
CalendarEvents = _calendarEvents.ToArray(),
|
||||
Loops = _loops.ToArray(),
|
||||
Holidays = _holidayOverrides.ToArray(),
|
||||
People = _people.ToArray()
|
||||
@@ -479,6 +489,55 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
return backup;
|
||||
}
|
||||
|
||||
public IReadOnlyList<CalendarEventRecord> GetCalendarEvents(string? loopId = null)
|
||||
{
|
||||
var resolvedLoopId = string.IsNullOrWhiteSpace(loopId) ? ResolveDefaultLoopId() : loopId.Trim();
|
||||
return _calendarEvents
|
||||
.Where(calendarEvent => calendarEvent.IsEnabled)
|
||||
.Where(calendarEvent =>
|
||||
string.Equals(calendarEvent.LoopId, resolvedLoopId, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(calendarEvent => calendarEvent.Date)
|
||||
.ThenBy(calendarEvent => calendarEvent.TimeLabel ?? string.Empty, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(calendarEvent => calendarEvent.Summary, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public CalendarEventRecord UpsertCalendarEvent(CalendarEventRecord calendarEvent)
|
||||
{
|
||||
var resolvedLoopId = string.IsNullOrWhiteSpace(calendarEvent.LoopId)
|
||||
? ResolveDefaultLoopId()
|
||||
: calendarEvent.LoopId.Trim();
|
||||
var normalizedId = string.IsNullOrWhiteSpace(calendarEvent.Id)
|
||||
? $"calendar-{resolvedLoopId}-{Slugify(calendarEvent.Summary)}"
|
||||
: calendarEvent.Id.Trim();
|
||||
|
||||
var resolvedCalendarEvent = new CalendarEventRecord
|
||||
{
|
||||
Id = normalizedId,
|
||||
LoopId = resolvedLoopId,
|
||||
Summary = string.IsNullOrWhiteSpace(calendarEvent.Summary) ? "Calendar event" : calendarEvent.Summary.Trim(),
|
||||
TimeLabel = string.IsNullOrWhiteSpace(calendarEvent.TimeLabel) ? null : calendarEvent.TimeLabel.Trim(),
|
||||
Date = calendarEvent.Date,
|
||||
EndDate = calendarEvent.EndDate,
|
||||
IsAllDay = calendarEvent.IsAllDay,
|
||||
IsEnabled = calendarEvent.IsEnabled,
|
||||
Source = string.IsNullOrWhiteSpace(calendarEvent.Source) ? "manual" : calendarEvent.Source.Trim(),
|
||||
MemberId = calendarEvent.MemberId,
|
||||
Created = calendarEvent.Created
|
||||
};
|
||||
|
||||
var existingIndex = _calendarEvents.FindIndex(existing =>
|
||||
string.Equals(existing.Id, normalizedId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existingIndex >= 0)
|
||||
_calendarEvents[existingIndex] = resolvedCalendarEvent;
|
||||
else
|
||||
_calendarEvents.Add(resolvedCalendarEvent);
|
||||
|
||||
TouchState();
|
||||
return resolvedCalendarEvent;
|
||||
}
|
||||
|
||||
public bool ShouldCreateSymmetricKey(string loopId)
|
||||
{
|
||||
return !_symmetricKeys.ContainsKey(loopId);
|
||||
@@ -580,6 +639,59 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public IReadOnlyList<CommuteProfileRecord> GetCommuteProfiles(string? loopId = null)
|
||||
{
|
||||
var resolvedLoopId = string.IsNullOrWhiteSpace(loopId) ? ResolveDefaultLoopId() : loopId.Trim();
|
||||
return _commuteProfiles
|
||||
.Where(commute => commute.IsEnabled)
|
||||
.Where(commute => string.Equals(commute.LoopId, resolvedLoopId, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(static commute => commute.IsComplete)
|
||||
.ThenByDescending(static commute => commute.Updated)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public CommuteProfileRecord UpsertCommuteProfile(CommuteProfileRecord commuteProfile)
|
||||
{
|
||||
var resolvedLoopId = string.IsNullOrWhiteSpace(commuteProfile.LoopId)
|
||||
? ResolveDefaultLoopId()
|
||||
: commuteProfile.LoopId.Trim();
|
||||
var normalizedId = string.IsNullOrWhiteSpace(commuteProfile.Id)
|
||||
? $"commute-{resolvedLoopId}"
|
||||
: commuteProfile.Id.Trim();
|
||||
|
||||
var resolvedProfile = new CommuteProfileRecord
|
||||
{
|
||||
Id = normalizedId,
|
||||
LoopId = resolvedLoopId,
|
||||
MemberId = string.IsNullOrWhiteSpace(commuteProfile.MemberId) ? null : commuteProfile.MemberId.Trim(),
|
||||
IsEnabled = commuteProfile.IsEnabled,
|
||||
IsComplete = commuteProfile.IsComplete,
|
||||
Mode = string.IsNullOrWhiteSpace(commuteProfile.Mode) ? "driving" : commuteProfile.Mode.Trim(),
|
||||
WorkHour = commuteProfile.WorkHour,
|
||||
WorkMinute = commuteProfile.WorkMinute,
|
||||
OriginName = string.IsNullOrWhiteSpace(commuteProfile.OriginName) ? null : commuteProfile.OriginName.Trim(),
|
||||
DestinationName = string.IsNullOrWhiteSpace(commuteProfile.DestinationName)
|
||||
? null
|
||||
: commuteProfile.DestinationName.Trim(),
|
||||
TypicalDurationMinutes = commuteProfile.TypicalDurationMinutes > 0
|
||||
? commuteProfile.TypicalDurationMinutes
|
||||
: 25,
|
||||
Created = commuteProfile.Created == default ? DateTimeOffset.UtcNow : commuteProfile.Created,
|
||||
Updated = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var existingIndex = _commuteProfiles.FindIndex(existing =>
|
||||
string.Equals(existing.Id, normalizedId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existingIndex >= 0)
|
||||
_commuteProfiles[existingIndex] = resolvedProfile;
|
||||
else
|
||||
_commuteProfiles.Add(resolvedProfile);
|
||||
|
||||
TouchState();
|
||||
return resolvedProfile;
|
||||
}
|
||||
|
||||
public HolidayRecord UpsertHoliday(HolidayRecord holiday)
|
||||
{
|
||||
var resolvedLoopId = string.IsNullOrWhiteSpace(holiday.LoopId) ? ResolveDefaultLoopId() : holiday.LoopId.Trim();
|
||||
@@ -656,6 +768,7 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
|
||||
if (_people.Count != 0)
|
||||
{
|
||||
EnsureDefaultCommuteProfile();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -680,6 +793,29 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
Alias = "Household Member",
|
||||
IsPrimary = false
|
||||
});
|
||||
|
||||
EnsureDefaultCommuteProfile();
|
||||
}
|
||||
|
||||
private void EnsureDefaultCommuteProfile()
|
||||
{
|
||||
if (_commuteProfiles.Any(commute =>
|
||||
string.Equals(commute.LoopId, ResolveDefaultLoopId(), StringComparison.OrdinalIgnoreCase)))
|
||||
return;
|
||||
|
||||
_commuteProfiles.Add(new CommuteProfileRecord
|
||||
{
|
||||
Id = $"commute-{ResolveDefaultLoopId()}",
|
||||
LoopId = ResolveDefaultLoopId(),
|
||||
IsEnabled = true,
|
||||
IsComplete = true,
|
||||
Mode = "driving",
|
||||
WorkHour = 8,
|
||||
WorkMinute = 30,
|
||||
OriginName = "home",
|
||||
DestinationName = "work",
|
||||
TypicalDurationMinutes = 25
|
||||
});
|
||||
}
|
||||
|
||||
private static string Slugify(string value)
|
||||
@@ -767,6 +903,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
public UpdateManifest[]? Updates { get; init; }
|
||||
public MediaRecord[]? Media { get; init; }
|
||||
public BackupRecord[]? Backups { get; init; }
|
||||
public CommuteProfileRecord[]? CommuteProfiles { get; init; }
|
||||
public CalendarEventRecord[]? CalendarEvents { get; init; }
|
||||
public LoopRecord[]? Loops { get; init; }
|
||||
public HolidayRecord[]? Holidays { get; init; }
|
||||
public PersonRecord[]? People { get; init; }
|
||||
|
||||
Reference in New Issue
Block a user