Files
Playa/extensions/Volcanic.AgenticCore.Extensions.GoogleCalendar/GoogleCalendarOptions.cs
EugeneTes c3a0cf234c feat(google-calendar): store each chat's token in its workspace
Tokens now live in <cwd>/.volcanic/google-calendar/token.json, as in the
VolcanicSecretary reference, instead of <agentDir>/google-calendar/<chat
key>/. GoogleCalendarOptions.TokenDirectory, when set, keeps the old
per-chat layout under that root. Spec, docs and CLAUDE.md updated.
2026-09-28 20:42:58 +02:00

61 lines
3.5 KiB
C#

namespace Volcanic.AgenticCore.Extensions.GoogleCalendar;
/// <summary>
/// Configures the Google Calendar extension. The OAuth client is process-wide (one Google Cloud app for the bot);
/// accounts are per chat. Empty strings count as unset.
/// </summary>
public sealed record GoogleCalendarOptions
{
/// <summary>OAuth client id (type "TVs and Limited Input devices"). Else <c>GOOGLE_CLIENT_ID</c>.</summary>
public string? ClientId { get; init; }
/// <summary>OAuth client secret. Else <c>GOOGLE_CLIENT_SECRET</c>.</summary>
public string? ClientSecret { get; init; }
/// <summary>
/// Root of per-chat token directories (<c>&lt;root&gt;/&lt;chat key&gt;/token.json</c>). Unset (the default): each chat's token
/// lives in its workspace, <c>&lt;cwd&gt;/.volcanic/google-calendar/token.json</c>.
/// </summary>
public string? TokenDirectory { get; init; }
/// <summary>Environment lookup; defaults to <see cref="Environment.GetEnvironmentVariable(string)"/>.</summary>
public Func<string, string?>? GetEnv { get; init; }
/// <summary>HTTP client for Google; tests pass one over a fake handler.</summary>
public HttpClient? Http { get; init; }
/// <summary>Clock for polling and token expiry; tests pass a fake.</summary>
public TimeProvider? Time { get; init; }
/// <summary>Per-chat connection state. Defaults to <see cref="GoogleCalendarConnections.Shared"/>.</summary>
public GoogleCalendarConnections? Connections { get; init; }
private Func<string, string?> Env => GetEnv ?? Environment.GetEnvironmentVariable;
internal string? EffectiveClientId => NonEmpty(ClientId) ?? NonEmpty(Env("GOOGLE_CLIENT_ID"));
internal string? EffectiveClientSecret => NonEmpty(ClientSecret) ?? NonEmpty(Env("GOOGLE_CLIENT_SECRET"));
/// <summary>True when both the client id and the secret are set (here or in the environment): the extension registers its tool and commands.</summary>
public bool IsConfigured => EffectiveClientId is not null && EffectiveClientSecret is not null;
/// <summary>The configured token root, or null to keep each chat's token in its workspace.</summary>
internal string? EffectiveTokenDirectory => NonEmpty(TokenDirectory);
/// <summary>The settings a connection needs, or null when the client id or secret is missing.</summary>
internal GoogleCalendarSettings? Resolve() =>
EffectiveClientId is { } id && EffectiveClientSecret is { } secret
? new GoogleCalendarSettings(id, secret, EffectiveTokenDirectory, Http ?? GoogleHttp.Default, Time ?? TimeProvider.System)
: null;
public override string ToString() =>
$"GoogleCalendarOptions {{ ClientId = {ClientId}, ClientSecret = {(string.IsNullOrEmpty(ClientSecret) ? "" : "***")}, TokenDirectory = {TokenDirectory} }}";
internal static string? NonEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
/// <summary>Resolved, validated configuration shared by every chat's connection.</summary>
internal sealed record GoogleCalendarSettings(string ClientId, string ClientSecret, string? TokenRoot, HttpClient Http, TimeProvider Time)
{
public override string ToString() => $"GoogleCalendarSettings {{ ClientId = {ClientId}, TokenRoot = {TokenRoot} }}";
}
internal static class GoogleHttp
{
/// <summary>One client for the process: connections of all chats share it.</summary>
public static readonly HttpClient Default = new() { Timeout = TimeSpan.FromSeconds(60) };
}