forked from admin/VolcanicAgenticCore
feat(telegram): Google Calendar per chat (config, secret value, menu, help)
Spec §8: extension added when either value is set, menu and /help only when both are; host code in GoogleCalendarSetup.cs.
This commit is contained in:
@@ -281,15 +281,21 @@ Behaviour follows the reference's recurring-events spec:
|
||||
|
||||
- Configuration: a `GoogleCalendar` section (`ClientId`, `ClientSecret`) with `GOOGLE_CLIENT_ID` and
|
||||
`GOOGLE_CLIENT_SECRET` as environment shortcuts, bound into a `GoogleCalendarOptions` singleton. Empty values count
|
||||
as unset (the Docker compose file passes both through, empty by default). The host adds the client secret to the
|
||||
process environment hook's `SecretValues` (`2026-09-26-process-environment-hook-design.md`), and the default
|
||||
deny-list already removes `GOOGLE_CLIENT_SECRET` from `bash`, scripts and MCP servers.
|
||||
as unset (the Docker compose file passes both through, empty by default). The singleton is registered when
|
||||
**either** value is set, so a half configuration still reaches the extension, which logs the warning naming the
|
||||
missing value; with neither set nothing is registered. The host adds the client secret to the process environment
|
||||
hook's `SecretValues` (`2026-09-26-process-environment-hook-design.md`), and the default deny-list already removes
|
||||
`GOOGLE_CLIENT_SECRET` from `bash`, scripts and MCP servers.
|
||||
- `ChatRegistry` and `BotSetupService` take the options as an optional constructor parameter (DI passes the default,
|
||||
null, when the singleton isn't registered). New host code (section, options, menu, help lines, registration) lives
|
||||
in `hosts/Volcanic.AgenticCore.Telegram/GoogleCalendarSetup.cs`, so the shared host files get one-line edits.
|
||||
- `ChatRegistry.CreateAsync` adds the extension to `AgentSessionOptions.ExtraExtensions` for **every** chat when the
|
||||
options are configured. It uses a factory registration (`ExtensionRegistration`) so each session gets its own
|
||||
options are present. It uses a factory registration (`ExtensionRegistration`) so each session gets its own
|
||||
instance over the shared `GoogleCalendarConnections`; the scope `telegram-<chatId>` picks the chat's account.
|
||||
- `BotSetupService` adds `calendar_connect`, `calendar_status` and `calendar_disconnect` to the global command menu when
|
||||
the calendar is configured. Every chat can use them.
|
||||
- `/help` mentions the three commands when the extension is active.
|
||||
- `BotSetupService` adds `calendar_connect`, `calendar_status` and `calendar_disconnect` to the global command menu only
|
||||
when **both** values are set. Every chat can use them.
|
||||
- `/help` mentions the three commands when the extension is active, detected from the session's registered commands
|
||||
(`calendar_connect` present).
|
||||
- Nothing else changes. The flow uses `Notify` only; the user's replies are never needed, so the pending-input
|
||||
routing is untouched.
|
||||
|
||||
@@ -339,9 +345,11 @@ add to this test project) so polling and expiry run instantly.
|
||||
store written and their own `Notify`; `/calendar_connect` twice in A reuses A's code while B gets a fresh one;
|
||||
`/calendar_disconnect` in A leaves B's token and cache alone. Resolved zone: the calendar's stored zone, else
|
||||
`ctx.TimeZone`; the two-zone header when they differ.
|
||||
- **Telegram:** with `ChatHost`, every allowed chat lists the `calendar` tool.
|
||||
`/calendar_connect` in a chat, with a fake handler that approves on the second poll, ends with the
|
||||
"connected" message in that chat.
|
||||
- **Telegram** (`tests/Volcanic.AgenticCore.Telegram.Tests/GoogleCalendarHostTests.cs`): with `ChatHost`, every
|
||||
allowed chat lists the `calendar` tool, and none does without the options. `/calendar_connect` in a chat, with a
|
||||
fake handler that approves on the second poll, ends with the "connected" message in that chat. Options come from
|
||||
the section, then the environment, empty counting as unset; the client secret lands in `SecretValues`; the menu and
|
||||
`/help` list the commands only when the extension is active.
|
||||
|
||||
## Docs
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Telegram.Bot;
|
||||
using Volcanic.AgenticCore.Extensions.GoogleCalendar;
|
||||
using Volcanic.AgenticCore.Telegram.Chats;
|
||||
|
||||
namespace Volcanic.AgenticCore.Telegram;
|
||||
@@ -9,7 +10,8 @@ namespace Volcanic.AgenticCore.Telegram;
|
||||
/// Best-effort start-up calls: log who the bot is and publish the command menu. Retried in the background until the
|
||||
/// first success, so a host started without network still comes up.
|
||||
/// </summary>
|
||||
public sealed class BotSetupService(ITelegramBotClient bot, TelegramOptions options, ILogger<BotSetupService> logger) : BackgroundService
|
||||
public sealed class BotSetupService(ITelegramBotClient bot, TelegramOptions options, ILogger<BotSetupService> logger,
|
||||
GoogleCalendarOptions? calendar = null) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
@@ -19,7 +21,7 @@ public sealed class BotSetupService(ITelegramBotClient bot, TelegramOptions opti
|
||||
try
|
||||
{
|
||||
var me = await bot.GetMe(stoppingToken).ConfigureAwait(false);
|
||||
await bot.SetMyCommands(ChatCommands.Menu, cancellationToken: stoppingToken).ConfigureAwait(false);
|
||||
await bot.SetMyCommands(GoogleCalendarSetup.MenuFor(ChatCommands.Menu, calendar), cancellationToken: stoppingToken).ConfigureAwait(false);
|
||||
logger.LogInformation("Bot @{Username} ({Id}) is ready", me.Username, me.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ public sealed class ChatCommands(TelegramSender sender)
|
||||
/sessions, /resume <id> – earlier conversations
|
||||
/compact – summarise the conversation to free context
|
||||
/thinking [level] – show or set the thinking level
|
||||
""";
|
||||
""" + GoogleCalendarSetup.HelpLines(chat.Session.Commands);
|
||||
|
||||
private static string Status(ChatContext chat)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using Volcanic.AgenticCore.Abstractions.Events;
|
||||
using Volcanic.AgenticCore.Abstractions.Extensions.Events;
|
||||
using Volcanic.AgenticCore.Abstractions.Messages;
|
||||
using Volcanic.AgenticCore.Abstractions.Ui;
|
||||
using Volcanic.AgenticCore.Extensions.GoogleCalendar;
|
||||
using Volcanic.AgenticCore.Features.Agent;
|
||||
using Volcanic.AgenticCore.Hosting;
|
||||
using Volcanic.AgenticCore.Telegram.Rendering;
|
||||
@@ -54,7 +55,8 @@ public sealed class ChatContext(long chatId, AgentSession session, TelegramUi ui
|
||||
/// One <see cref="ChatContext"/> per chat, created on first use. Each chat works in <c><WorkspaceRoot>/<chatId></c>,
|
||||
/// stores its sessions under scope <c>telegram-<chatId></c> and resumes the most recent one after a restart.
|
||||
/// </summary>
|
||||
public sealed class ChatRegistry(IAgentSessionFactory factory, TelegramSender sender, TelegramOptions options, ILoggerFactory loggerFactory)
|
||||
public sealed class ChatRegistry(IAgentSessionFactory factory, TelegramSender sender, TelegramOptions options, ILoggerFactory loggerFactory,
|
||||
GoogleCalendarOptions? calendar = null)
|
||||
: IAsyncDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<long, Lazy<Task<ChatContext>>> _chats = new();
|
||||
@@ -102,6 +104,7 @@ public sealed class ChatRegistry(IAgentSessionFactory factory, TelegramSender se
|
||||
SessionId = last?.SessionId,
|
||||
Model = options.Model,
|
||||
Ui = ui,
|
||||
ExtraExtensions = calendar is null ? null : [GoogleCalendarSetup.Registration(calendar)],
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
|
||||
58
hosts/Volcanic.AgenticCore.Telegram/GoogleCalendarSetup.cs
Normal file
58
hosts/Volcanic.AgenticCore.Telegram/GoogleCalendarSetup.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
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 is not null && NonEmpty(options.ClientId) is not null && NonEmpty(options.ClientSecret) is not null;
|
||||
|
||||
/// <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();
|
||||
}
|
||||
@@ -20,8 +20,11 @@ var builder = Host.CreateApplicationBuilder(args);
|
||||
var options = builder.Configuration.GetSection(TelegramOptions.SectionName).Get<TelegramOptions>() ?? new TelegramOptions();
|
||||
options.ApplyEnvironment(Environment.GetEnvironmentVariable);
|
||||
var processSection = builder.Configuration.GetSection(ProcessEnvironmentSection.SectionName).Get<ProcessEnvironmentSection>() ?? new ProcessEnvironmentSection();
|
||||
var calendar = GoogleCalendarSetup.Options(
|
||||
builder.Configuration.GetSection(GoogleCalendarSection.SectionName).Get<GoogleCalendarSection>() ?? new GoogleCalendarSection(),
|
||||
Environment.GetEnvironmentVariable);
|
||||
var processEnvironment = new ProcessEnvironmentOptions();
|
||||
TelegramProcessEnvironment.Configure(processEnvironment, options, processSection, Environment.GetEnvironmentVariable);
|
||||
TelegramProcessEnvironment.Configure(processEnvironment, options, processSection, Environment.GetEnvironmentVariable, calendar);
|
||||
var errors = options.Validate().Concat(processEnvironment.Validate()).ToList();
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
@@ -30,6 +33,7 @@ if (errors.Count > 0)
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton(options);
|
||||
if (calendar is not null) builder.Services.AddSingleton(calendar); // ChatRegistry and BotSetupService take it optionally
|
||||
builder.Services.AddSingleton<OwnerLoginChannel>();
|
||||
// The extension is constructed per session through ActivatorUtilities, which injects this. LoginUi resolves
|
||||
// OwnerLoginChannel lazily (at call time, not here) to avoid a construction cycle: ChatRegistry depends on the
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Volcanic.AgenticCore.Extensions.GoogleCalendar;
|
||||
using Volcanic.AgenticCore.Features.Processes;
|
||||
|
||||
namespace Volcanic.AgenticCore.Telegram;
|
||||
@@ -7,8 +8,9 @@ public static class TelegramProcessEnvironment
|
||||
{
|
||||
public const string AllowVariable = "PROCESS_ENV_ALLOW";
|
||||
|
||||
/// <summary>Adds the section's lists, <c>PROCESS_ENV_ALLOW</c> and the bot token to <paramref name="target"/>.</summary>
|
||||
public static void Configure(ProcessEnvironmentOptions target, TelegramOptions telegram, ProcessEnvironmentSection section, Func<string, string?> getEnv)
|
||||
/// <summary>Adds the section's lists, <c>PROCESS_ENV_ALLOW</c>, the bot token and the Google client secret to <paramref name="target"/>.</summary>
|
||||
public static void Configure(ProcessEnvironmentOptions target, TelegramOptions telegram, ProcessEnvironmentSection section, Func<string, string?> getEnv,
|
||||
GoogleCalendarOptions? calendar = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
ArgumentNullException.ThrowIfNull(telegram);
|
||||
@@ -20,6 +22,7 @@ public static class TelegramProcessEnvironment
|
||||
target.Allow.AddRange(allow.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
|
||||
target.HardenHostProcess = section.HardenHostProcess;
|
||||
if (!string.IsNullOrWhiteSpace(telegram.BotToken)) target.SecretValues.Add(telegram.BotToken);
|
||||
if (!string.IsNullOrWhiteSpace(calendar?.ClientSecret)) target.SecretValues.Add(calendar.ClientSecret);
|
||||
}
|
||||
|
||||
/// <summary>The startup log line: names only, never values.</summary>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/Volcanic.AgenticCore/Volcanic.AgenticCore.csproj" />
|
||||
<ProjectReference Include="../../extensions/Volcanic.AgenticCore.Extensions.ClaudeCode/Volcanic.AgenticCore.Extensions.ClaudeCode.csproj" />
|
||||
<ProjectReference Include="../../extensions/Volcanic.AgenticCore.Extensions.GoogleCalendar/Volcanic.AgenticCore.Extensions.GoogleCalendar.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageReference Include="Telegram.Bot" Version="22.10.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Volcanic.AgenticCore.Extensions.GoogleCalendar;
|
||||
using Volcanic.AgenticCore.Features.Processes;
|
||||
using Volcanic.AgenticCore.Telegram.Chats;
|
||||
using Volcanic.AgenticCore.Telegram.Tests.Support;
|
||||
|
||||
namespace Volcanic.AgenticCore.Telegram.Tests;
|
||||
|
||||
public class GoogleCalendarHostTests : IDisposable
|
||||
{
|
||||
private readonly string _tokens = Directory.CreateTempSubdirectory("gcal-telegram-").FullName;
|
||||
private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 9, 26, 8, 0, 0, TimeSpan.Zero));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_tokens, recursive: true); } catch (IOException) { }
|
||||
}
|
||||
|
||||
/// <summary>Google that approves on the second poll.</summary>
|
||||
private sealed class ScriptedGoogle : HttpMessageHandler
|
||||
{
|
||||
private int _polls;
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
|
||||
Task.FromResult(request.RequestUri!.AbsolutePath switch
|
||||
{
|
||||
"/device/code" => Json(HttpStatusCode.OK, """{"device_code":"dev-1","user_code":"ABCD-EFGH","verification_url":"https://www.google.com/device","expires_in":1800,"interval":5}"""),
|
||||
"/token" => Interlocked.Increment(ref _polls) < 2
|
||||
? Json(HttpStatusCode.PreconditionRequired, """{"error":"authorization_pending"}""")
|
||||
: Json(HttpStatusCode.OK, """{"access_token":"at-1","expires_in":3599,"refresh_token":"rt-1","scope":"https://www.googleapis.com/auth/calendar"}"""),
|
||||
"/calendar/v3/calendars/primary" => Json(HttpStatusCode.OK, """{"id":"owner@example.com","timeZone":"Europe/Berlin"}"""),
|
||||
_ => Json(HttpStatusCode.NotImplemented, "{}"),
|
||||
});
|
||||
|
||||
private static HttpResponseMessage Json(HttpStatusCode status, string body) =>
|
||||
new(status) { Content = new StringContent(body, Encoding.UTF8, "application/json") };
|
||||
}
|
||||
|
||||
private GoogleCalendarOptions Calendar() => new()
|
||||
{
|
||||
ClientId = "cid.apps.googleusercontent.com", ClientSecret = "GOCSPX-test-secret", TokenDirectory = _tokens,
|
||||
Http = new HttpClient(new ScriptedGoogle()), Time = _time, Connections = new GoogleCalendarConnections(), GetEnv = _ => null,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Options_come_from_the_section_then_the_environment_and_treat_empty_as_unset()
|
||||
{
|
||||
var env = new Dictionary<string, string> { ["GOOGLE_CLIENT_ID"] = "env-id", ["GOOGLE_CLIENT_SECRET"] = "env-secret" };
|
||||
var fromEnv = GoogleCalendarSetup.Options(new GoogleCalendarSection { ClientId = "", ClientSecret = " " }, n => env.GetValueOrDefault(n));
|
||||
Assert.Equal(("env-id", "env-secret"), (fromEnv!.ClientId, fromEnv.ClientSecret));
|
||||
var fromSection = GoogleCalendarSetup.Options(new GoogleCalendarSection { ClientId = "s-id", ClientSecret = "s-secret" }, n => env.GetValueOrDefault(n));
|
||||
Assert.Equal(("s-id", "s-secret"), (fromSection!.ClientId, fromSection.ClientSecret));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Options_are_null_when_neither_value_is_set()
|
||||
{
|
||||
Assert.Null(GoogleCalendarSetup.Options(new GoogleCalendarSection(), _ => ""));
|
||||
var half = GoogleCalendarSetup.Options(new GoogleCalendarSection { ClientId = "id" }, _ => null);
|
||||
Assert.NotNull(half); // the extension logs the warning
|
||||
Assert.False(GoogleCalendarSetup.IsActive(half));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_adds_the_client_secret_to_the_secret_values()
|
||||
{
|
||||
var target = new ProcessEnvironmentOptions { HardenHostProcess = false };
|
||||
TelegramProcessEnvironment.Configure(target, new TelegramOptions { BotToken = "123456:bot-token-value" }, new ProcessEnvironmentSection(), _ => null, Calendar());
|
||||
Assert.Contains("GOCSPX-test-secret", target.SecretValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_menu_gets_the_calendar_commands_only_when_both_values_are_set()
|
||||
{
|
||||
Assert.Same(ChatCommands.Menu, GoogleCalendarSetup.MenuFor(ChatCommands.Menu, null));
|
||||
Assert.Same(ChatCommands.Menu, GoogleCalendarSetup.MenuFor(ChatCommands.Menu, new GoogleCalendarOptions { ClientId = "id", GetEnv = _ => null }));
|
||||
var menu = GoogleCalendarSetup.MenuFor(ChatCommands.Menu, Calendar());
|
||||
Assert.Equal(["calendar_connect", "calendar_status", "calendar_disconnect"], menu.Skip(ChatCommands.Menu.Count).Select(c => c.Command).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Every_allowed_chat_lists_the_calendar_tool()
|
||||
{
|
||||
await using var host = new ChatHost(calendar: Calendar());
|
||||
host.Options.AllowedUserIds.Add(222222222);
|
||||
Assert.Contains("calendar", (await host.ChatAsync()).Session.ActiveTools);
|
||||
Assert.Contains("calendar", (await host.ChatAsync(222222222)).Session.ActiveTools);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Without_calendar_options_no_chat_has_the_tool()
|
||||
{
|
||||
await using var host = new ChatHost();
|
||||
Assert.DoesNotContain("calendar", (await host.ChatAsync()).Session.ActiveTools);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_mentions_the_calendar_commands_when_the_extension_is_active()
|
||||
{
|
||||
await using var host = new ChatHost(calendar: Calendar());
|
||||
await host.SendAsync("/help");
|
||||
await Wait.UntilAsync(() => host.Texts.Any(t => t.Contains("/calendar_connect")), "the help text");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_leaves_out_the_calendar_commands_without_the_extension()
|
||||
{
|
||||
await using var host = new ChatHost();
|
||||
await host.SendAsync("/help");
|
||||
await Wait.UntilAsync(() => host.Texts.Any(t => t.Contains("/compact")), "the help text");
|
||||
Assert.DoesNotContain(host.Texts, t => t.Contains("/calendar_connect"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Calendar_connect_in_a_chat_ends_with_the_connected_message()
|
||||
{
|
||||
await using var host = new ChatHost(calendar: Calendar());
|
||||
await host.SendAsync("/calendar_connect");
|
||||
await Wait.UntilAsync(() => host.Texts.Any(t => t.Contains("ABCD-EFGH")), "the link and code");
|
||||
await Wait.UntilAsync(() =>
|
||||
{
|
||||
_time.Advance(TimeSpan.FromSeconds(5));
|
||||
return host.Texts.Any(t => t.Contains("Google Calendar connected as owner@example.com"));
|
||||
}, "the connected message");
|
||||
Assert.True(File.Exists(Path.Combine(_tokens, ChatRegistry.ScopeFor(Updates.AllowedUser), "token.json")));
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,8 @@ public sealed class ChatHost : IAsyncDisposable
|
||||
private readonly ServiceProvider _services;
|
||||
private readonly string _root;
|
||||
|
||||
public ChatHost(Action<ChatHost, IServiceCollection>? configure = null, Action<AgenticCoreOptions>? core = null)
|
||||
public ChatHost(Action<ChatHost, IServiceCollection>? configure = null, Action<AgenticCoreOptions>? core = null,
|
||||
Volcanic.AgenticCore.Extensions.GoogleCalendar.GoogleCalendarOptions? calendar = null)
|
||||
{
|
||||
_root = Path.Combine(Path.GetTempPath(), "volcanic-telegram", Guid.NewGuid().ToString("N"));
|
||||
Options = new TelegramOptions
|
||||
@@ -54,7 +55,7 @@ public sealed class ChatHost : IAsyncDisposable
|
||||
_services = services.BuildServiceProvider();
|
||||
ILoggerFactory loggers = NullLoggerFactory.Instance;
|
||||
var sender = new TelegramSender(Bot, NullLogger<TelegramSender>.Instance);
|
||||
Chats = new ChatRegistry(_services.GetRequiredService<IAgentSessionFactory>(), sender, Options, loggers);
|
||||
Chats = new ChatRegistry(_services.GetRequiredService<IAgentSessionFactory>(), sender, Options, loggers, calendar);
|
||||
Router = new UpdateRouter(Options, Chats, new ChatCommands(sender), sender, NullLogger<UpdateRouter>.Instance);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1" />
|
||||
|
||||
Reference in New Issue
Block a user