feat(google-calendar): /calendar_status and /calendar_disconnect, per-chat isolation tests

This commit is contained in:
EugeneTes
2026-09-26 20:15:16 +02:00
parent 59d404ed08
commit 149b932ca1
5 changed files with 260 additions and 2 deletions

View File

@@ -32,3 +32,50 @@ internal static class ConnectCommand
if (flow is not null) await flow.RunAsync(ctx.Ui, ctx.Logger).ConfigureAwait(false);
});
}
/// <summary><c>/calendar_status</c>.</summary>
internal static class StatusCommand
{
public const string Name = "calendar_status";
public const string Description = "Show this chat's Google Calendar connection";
public static CommandDefinition Create(IExtensionApi api, GoogleCalendarExtension extension) => new(Description, (_, ctx, _) =>
{
var connection = extension.ConnectionFor(ctx.SessionManager.Scope);
CommandReply.Send(api, ctx, Name, Describe(connection));
return Task.CompletedTask;
});
internal static string Describe(GoogleCalendarConnection connection)
{
if (connection.Pending is { } flow)
{
var d = flow.Device;
return $"Waiting for approval: open {d.VerificationUrl} and enter {d.UserCode} ({ConnectFlow.MinutesLeft(d, connection.Settings.Time)} min left).";
}
if (connection.Store.Read() is not { } token) return "Not connected. Run /calendar_connect.";
var since = token.ConnectedAt?.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture) ?? "an unknown date";
return $"Connected as {token.Account ?? "an unknown account"} since {since}, time zone {token.TimeZone ?? "unknown"}.";
}
}
/// <summary><c>/calendar_disconnect</c>: cancel a pending flow, revoke (best effort), delete the file, drop the cache.</summary>
internal static class DisconnectCommand
{
public const string Name = "calendar_disconnect";
public const string Description = "Disconnect this chat's Google Calendar";
public static CommandDefinition Create(IExtensionApi api, GoogleCalendarExtension extension) => new(Description, async (_, ctx, ct) =>
{
var connection = extension.ConnectionFor(ctx.SessionManager.Scope);
connection.CancelPending();
var token = connection.Store.Read();
var revokeError = token is null ? null : await connection.OAuth.RevokeAsync(token.RefreshToken, ct).ConfigureAwait(false);
connection.Store.Delete();
connection.Invalidate();
var text = "Calendar disconnected.";
if (revokeError is not null)
text += $" Revoking at Google failed: {revokeError}; remove the app at {GoogleCalendarMessages.PermissionsUrl}.";
CommandReply.Send(api, ctx, Name, text);
});
}

View File

@@ -43,6 +43,8 @@ public sealed class GoogleCalendarExtension(GoogleCalendarOptions? options = nul
_settings = _options.Resolve()!;
api.RegisterTool(new CalendarTool(_settings, Connections, CurrentZoneName));
api.RegisterCommand(ConnectCommand.Name, ConnectCommand.Create(api, this));
api.RegisterCommand(StatusCommand.Name, StatusCommand.Create(api, this));
api.RegisterCommand(DisconnectCommand.Name, DisconnectCommand.Create(api, this));
api.On<SessionStartEvent>((_, ctx, _) => Track(ctx));
api.On<BeforeAgentStartEvent>((_, ctx, _) => Track(ctx));
return ValueTask.CompletedTask;