forked from admin/VolcanicAgenticCore
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.
61 lines
3.5 KiB
C#
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><root>/<chat key>/token.json</c>). Unset (the default): each chat's token
|
|
/// lives in its workspace, <c><cwd>/.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) };
|
|
}
|