Files
Playa/hosts/Volcanic.AgenticCore.Telegram/GoogleCalendarSetup.cs
EugeneTes 881e7f0be9 fix(google-calendar): address review findings
- log an unknown stored zone once even when the tool description reads it first
- serialise the flow's token write with /calendar_disconnect under the flow gate
- a cancelled or expired pending flow is cleared, not re-sent or shown
- chat keys anchor with \z so a trailing newline is hashed
- a timeMax date includes that whole day (spec updated)
- dispose the flow's CancellationTokenSource, drop ConnectFlow.Token, Begin is a plain write
- GoogleCalendarOptions.IsConfigured replaces the host's copy of the rule; StoredToken is internal
- test BotSetupService's menu through DI, tokens absent from tool results
- Telegram.Tests csproj reference placed as in telegram-message-context; ChatHost passes calendar by name
- docs: per-event timeZone, both config sources for /calendar_*
2026-09-26 20:30:56 +02:00

58 lines
3.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Telegram.Bot.Types;
using Volcanic.AgenticCore.Abstractions.Extensions;
using Volcanic.AgenticCore.Extensions.GoogleCalendar;
using Volcanic.AgenticCore.Features.Extensions;
namespace Volcanic.AgenticCore.Telegram;
/// <summary>The <c>GoogleCalendar</c> configuration section. <c>GOOGLE_CLIENT_ID</c>/<c>GOOGLE_CLIENT_SECRET</c> fill empty values.</summary>
public sealed class GoogleCalendarSection
{
public const string SectionName = "GoogleCalendar";
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
}
/// <summary>Everything the Telegram host does for Google Calendar, kept out of the shared host files.</summary>
public static class GoogleCalendarSetup
{
public static readonly IReadOnlyList<BotCommand> Menu =
[
new() { Command = "calendar_connect", Description = "Connect your Google Calendar" },
new() { Command = "calendar_status", Description = "Google Calendar connection" },
new() { Command = "calendar_disconnect", Description = "Disconnect Google Calendar" },
];
/// <summary>Null when neither the client id nor the secret is set; empty values count as unset.</summary>
public static GoogleCalendarOptions? Options(GoogleCalendarSection section, Func<string, string?> getEnv)
{
ArgumentNullException.ThrowIfNull(section);
ArgumentNullException.ThrowIfNull(getEnv);
var id = NonEmpty(section.ClientId) ?? NonEmpty(getEnv("GOOGLE_CLIENT_ID"));
var secret = NonEmpty(section.ClientSecret) ?? NonEmpty(getEnv("GOOGLE_CLIENT_SECRET"));
return id is null && secret is null ? null : new GoogleCalendarOptions { ClientId = id, ClientSecret = secret, GetEnv = getEnv };
}
/// <summary>True when both the client id and the secret are set, i.e. the extension will register its tool and commands.</summary>
public static bool IsActive(GoogleCalendarOptions? options) => options?.IsConfigured == true;
/// <summary>The bot's command menu, with the calendar commands appended when the calendar is active.</summary>
public static IReadOnlyList<BotCommand> MenuFor(IReadOnlyList<BotCommand> menu, GoogleCalendarOptions? options) =>
IsActive(options) ? [.. menu, .. Menu] : menu;
/// <summary>Extra <c>/help</c> lines when the session has the calendar commands.</summary>
public static string HelpLines(IReadOnlyList<CommandInfo> sessionCommands)
{
ArgumentNullException.ThrowIfNull(sessionCommands);
return sessionCommands.Any(c => c.Name == "calendar_connect")
? "\n/calendar_connect – connect this chat's Google Calendar\n/calendar_status, /calendar_disconnect – check or remove the connection"
: "";
}
/// <summary>A new extension instance per session over the shared connections; the session scope picks the chat's account.</summary>
public static ExtensionRegistration Registration(GoogleCalendarOptions options) =>
new(GoogleCalendarExtension.ExtensionName, _ => new GoogleCalendarExtension(options), "telegram");
private static string? NonEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}