forked from admin/VolcanicAgenticCore
- 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_*
58 lines
3.1 KiB
C#
58 lines
3.1 KiB
C#
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();
|
||
}
|