Main#4
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a Telegram bot integration to DualMind.API (long-polling hosted service, command handlers, auth/session persistence), expands AI provider support via Cloudflare AI Gateway + Cloudflare Workers AI, and updates CI/deployment configuration to target the main branch.
Changes:
- Add Telegram bot background service, transport abstraction, command/update handling, session storage, and supporting unit tests.
- Add Cloudflare AI Gateway settings + Cloudflare Workers AI provider; adjust model selection/provider factory behavior accordingly.
- Add/adjust Supabase single-row select behavior and introduce new DB migration scripts for Workers AI models + leaderboard seeding.
Reviewed changes
Copilot reviewed 68 out of 69 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/DualMind.API.Tests/UnitTest1.cs | Removes placeholder test. |
| tests/DualMind.API.Tests/TelegramUpdateHandlerTests.cs | Adds bot update handler coverage (start/help/sign-in flow). |
| tests/DualMind.API.Tests/TelegramStateCacheTests.cs | Adds state/cooldown/vote tracking tests. |
| tests/DualMind.API.Tests/TelegramSessionStoreTests.cs | Adds encryption round-trip test for Telegram sessions. |
| tests/DualMind.API.Tests/TelegramBotServiceCollectionExtensionsTests.cs | Adds DI/config gating tests for bot registration. |
| tests/DualMind.API.Tests/TelegramAuthServiceTests.cs | Adds session persistence/refresh/legacy-session behavior tests. |
| tests/DualMind.API.Tests/SupabaseServiceTests.cs | Adds coverage for PostgREST “0 rows” 406 behavior handling. |
| tests/DualMind.API.Tests/StatsCommandHandlerTests.cs | Adds leaderboard rendering test for stats command. |
| tests/DualMind.API.Tests/ModelSelectorTests.cs | Adds filtering/exclusion tests for provider/model selection. |
| tests/DualMind.API.Tests/CloudflareAiGatewaySettingsTests.cs | Adds tests for gateway URL/model mapping and validation. |
| tests/DualMind.API.Tests/BotTestDoubles.cs | Adds fakes/stubs used by bot tests (transport, auth, supabase, time). |
| tests/DualMind.API.Tests/BattleCommandHandlerTests.cs | Adds battle/vote/retry/cooldown/timeout tests. |
| src/DualMind.API/appsettings.json | Adds Telegram config defaults; updates Supabase settings. |
| src/DualMind.API/appsettings.Development.json | Adds Telegram config defaults for development. |
| src/DualMind.API/Program.cs | Registers Telegram bot services + adds Cloudflare Workers AI typed client. |
| src/DualMind.API/Infrastructure/Data/SupabaseService.cs | Treats specific PostgREST 406 “0 rows” as default/null for SelectSingle. |
| src/DualMind.API/Infrastructure/Configuration/EnvConfig.cs | Adds env vars for Cloudflare AI Gateway + Workers AI. |
| src/DualMind.API/Infrastructure/Configuration/CloudflareAiGatewaySettings.cs | Introduces Cloudflare gateway config/validation + model mapping helpers. |
| src/DualMind.API/DualMind.API.csproj | Adds Telegram.Bot dependency. |
| src/DualMind.API/Core/Services/ModelSelector.cs | Filters unsupported providers; excludes manual-only providers from auto-selection. |
| src/DualMind.API/Core/Services/LeaderboardModelSelector.cs | Ensures leaderboard selection only uses models present in current catalog. |
| src/DualMind.API/Bot/Transport/TelegramBotTransport.cs | Adds Telegram.Bot-backed transport implementation. |
| src/DualMind.API/Bot/Transport/ITelegramBotTransport.cs | Adds transport abstraction for bot messaging/updates. |
| src/DualMind.API/Bot/TelegramUpdateHandler.cs | Adds command parsing + callback/message routing + sign-in flow. |
| src/DualMind.API/Bot/TelegramStateCache.cs | Adds per-chat state/session caching + cooldown/vote tracking. |
| src/DualMind.API/Bot/TelegramSessionStore.cs | Adds Supabase-backed encrypted Telegram session persistence. |
| src/DualMind.API/Bot/TelegramMessageFormatter.cs | Adds bot message formatting + keyboards + markdown escaping helper. |
| src/DualMind.API/Bot/TelegramBotServiceCollectionExtensions.cs | Adds opt-in Telegram bot DI registration based on config presence. |
| src/DualMind.API/Bot/TelegramBotService.cs | Adds long-polling hosted service + bot command registration. |
| src/DualMind.API/Bot/TelegramBotOptions.cs | Adds Telegram bot configuration options and enablement check. |
| src/DualMind.API/Bot/TelegramBotExceptions.cs | Adds bot/auth/API exception types. |
| src/DualMind.API/Bot/TelegramAuthService.cs | Adds session validation/refresh/sign-in with per-chat async locking. |
| src/DualMind.API/Bot/SupabaseTelegramAuthClient.cs | Adds Supabase password/refresh token client for bot auth. |
| src/DualMind.API/Bot/Models/UserState.cs | Adds user state model + modes. |
| src/DualMind.API/Bot/Models/TelegramBotCommand.cs | Adds bot command model. |
| src/DualMind.API/Bot/Models/BattleSession.cs | Adds in-memory battle session + vote-state handling. |
| src/DualMind.API/Bot/Models/ApiResponseModels.cs | Adds API DTOs for bot and transport update/message models. |
| src/DualMind.API/Bot/ITelegramSessionStore.cs | Adds session store interface. |
| src/DualMind.API/Bot/ITelegramAuthService.cs | Adds auth service interface. |
| src/DualMind.API/Bot/ISupabaseTelegramAuthClient.cs | Adds Supabase auth client interface. |
| src/DualMind.API/Bot/IDualMindBotApiClient.cs | Adds bot API client interface for arena endpoints. |
| src/DualMind.API/Bot/DualMindBotApiClient.cs | Adds HTTP client for battle/vote/stats endpoints. |
| src/DualMind.API/Bot/Commands/StatsCommandHandler.cs | Adds stats command handler. |
| src/DualMind.API/Bot/Commands/StartCommandHandler.cs | Adds start command handler. |
| src/DualMind.API/Bot/Commands/HelpCommandHandler.cs | Adds help command handler. |
| src/DualMind.API/Bot/Commands/CancelCommandHandler.cs | Adds cancel command handler. |
| src/DualMind.API/Bot/Commands/BattleCommandHandler.cs | Adds battle prompt flow + vote submission + auth retry/cooldown handling. |
| src/DualMind.API/AI/Providers/GroqService.cs | Routes chat/streaming via Cloudflare AI Gateway; keeps speech direct. |
| src/DualMind.API/AI/Providers/GoogleService.cs | Routes chat/streaming via Cloudflare AI Gateway. |
| src/DualMind.API/AI/Providers/CloudflareWorkersAiService.cs | Adds native Workers AI provider implementation. |
| src/DualMind.API/AI/Gateway/ChatProviderFactory.cs | Adds provider routing for Cloudflare/workers-ai and google-ai-studio alias. |
| postman/cloudflare-workers-ai-models-stable.json | Adds reference model list (JSON) for Workers AI. |
| postman/cloudflare-workers-ai-models-stable.csv | Adds reference model list (CSV) for Workers AI. |
| database/migrations/20260319_seed_cloudflare_leaderboard.sql | Adds Cloudflare leaderboard seeding script (auto). |
| database/migrations/20260319_manual_elo_setup_and_seed.sql | Adds ELO infra setup + manual model seeding script. |
| database/migrations/20260319_manual_cloudflare_leaderboard_seed.sql | Adds manual Cloudflare leaderboard seeding script. |
| database/migrations/20260319_fixed_cloudflare_leaderboard_seed.sql | Adds revised seeding script with view/round fixes. |
| database/migrations/20260319_final_cloudflare_leaderboard_seed.sql | Adds final seeding script with explicit view drop/recreate. |
| database/migrations/20260319_elo_rating_setup.sql | Adds ELO infra + view + Cloudflare leaderboard init script. |
| database/migrations/20260319_add_cloudflare_workers_ai_models.sql | Seeds providers + Workers AI model catalog into Supabase tables. |
| database/migrations/20260319_deactivate_unreachable_cloudflare_workers_ai_models.sql | Deactivates specific Workers AI models failing smoke tests. |
| database/migrations/20260315_add_telegram_session_refresh_tokens.sql | Adds refresh token/expiry fields for telegram session persistence. |
| ENV_SETUP.md | Updates environment setup docs for gateway + Telegram config. |
| .gitignore | Ignores .codex/. |
| .github/workflows/verify.yml | Updates workflow triggers from master to main. |
| .github/workflows/dotnet.yml | Removes legacy build/deploy workflow. |
| .github/workflows/deploy-dualmind-arena.yml | Updates deploy workflow to main and artifact packaging paths. |
| .env.example | Adds example env file including Telegram configuration keys. |
| .env | Removes committed local env secrets file. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| session = await _authService.ForceRefreshSessionAsync(chatId, cancellationToken); | ||
| if (session == null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| return await _apiClient.StartBattleAsync(session.AccessToken, prompt, cancellationToken); |
There was a problem hiding this comment.
StartBattleWithRetryAsync assigns the nullable result of ForceRefreshSessionAsync into the non-nullable TelegramAuthSession session parameter, which triggers nullable warnings and makes it easier to accidentally dereference null in future edits. Prefer storing the refresh result in a separate TelegramAuthSession? refreshed variable (or change the local to nullable) and only continue once it’s proven non-null.
| session = await _authService.ForceRefreshSessionAsync(chatId, cancellationToken); | |
| if (session == null) | |
| { | |
| return null; | |
| } | |
| return await _apiClient.StartBattleAsync(session.AccessToken, prompt, cancellationToken); | |
| var refreshed = await _authService.ForceRefreshSessionAsync(chatId, cancellationToken); | |
| if (refreshed == null) | |
| { | |
| return null; | |
| } | |
| return await _apiClient.StartBattleAsync(refreshed.AccessToken, prompt, cancellationToken); |
| private readonly ConcurrentDictionary<long, SemaphoreSlim> _locks = new(); | ||
|
|
There was a problem hiding this comment.
The per-chat lock dictionary (_locks) will grow monotonically because entries are never evicted/removed. In a long-running bot this can lead to unbounded memory growth. Consider using an expiring cache for locks, removing locks when sessions are cleared, or associating the async lock with the per-chat state so it can be collected when the state is removed.
| -- ============================================================================= | ||
| -- FINAL REVISED MANUAL CLOUDFLARE MODEL LEADERBOARD SEEDING | ||
| -- ============================================================================= | ||
| -- Fixes: ERROR 42P16 (cannot drop columns from view) | ||
| -- Provides: Manual ELO scores and explicit DROP VIEW for clean replacement. |
There was a problem hiding this comment.
Multiple Cloudflare leaderboard seeding scripts are being added for the same day (seed/manual/fixed/final variants). If all files in database/migrations are applied automatically, these will run sequentially and can re-create the same view/table repeatedly. Suggest keeping only the intended final migration(s) and deleting superseded drafts to prevent accidental double-application.
| @@ -0,0 +1,99 @@ | |||
| -- ============================================================================= | |||
| -- DUALMIND ELO SYSTEM & CLOUDFARE LEADERBOARD INITIALIZATION | |||
There was a problem hiding this comment.
Typo in header comment: “CLOUDFARE” → “CLOUDFLARE”.
| -- DUALMIND ELO SYSTEM & CLOUDFARE LEADERBOARD INITIALIZATION | |
| -- DUALMIND ELO SYSTEM & CLOUDFLARE LEADERBOARD INITIALIZATION |
| "Url": "https://calqfzajyidkdzbaswjp.supabase.co", | ||
| "Key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNhbHFmemFqeWlka2R6YmFzd2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQyNzMwODMsImV4cCI6MjA3OTg0OTA4M30.ptXyUNCcAhGi9u2kVDHOxSBvQv0W72S5HHqkIFXQS08", | ||
| "ServiceKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNhbHFmemFqeWlka2R6YmFzd2pwIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2NDI3MzA4MywiZXhwIjoyMDc5ODQ5MDgzfQ.bt3MjR2dItU1FT3yRTlNkNhNPRFO5_NBO1lMCqQy1d8" | ||
| }, | ||
| "Telegram": { | ||
| "SignupUrl": "https://dualmind.arena/signup", | ||
| "BattleCooldownSeconds": 15, | ||
| "SoftTimeoutSeconds": 30, | ||
| "ApiTimeoutSeconds": 75 |
There was a problem hiding this comment.
appsettings.json includes what appear to be real Supabase API keys/service keys. These are secrets and should not be committed to the repo; rotate them and move configuration to environment variables / secret manager (keep placeholders only in appsettings*.json).
| public async Task<TelegramSentMessage> SendTextMessageAsync(long chatId, string text, InlineKeyboardMarkup? replyMarkup, CancellationToken cancellationToken) | ||
| { | ||
| var message = await _client.SendMessage( | ||
| chatId: chatId, | ||
| text: text, | ||
| parseMode: ParseMode.MarkdownV2, | ||
| replyMarkup: replyMarkup, | ||
| cancellationToken: cancellationToken); | ||
|
|
||
| return new TelegramSentMessage | ||
| { | ||
| ChatId = chatId, | ||
| MessageId = message.MessageId, | ||
| Text = message.Text | ||
| }; | ||
| } | ||
|
|
||
| public async Task EditMessageTextAsync(long chatId, int messageId, string text, InlineKeyboardMarkup? replyMarkup, CancellationToken cancellationToken) | ||
| { | ||
| await _client.EditMessageText( | ||
| chatId: chatId, | ||
| messageId: messageId, | ||
| text: text, | ||
| parseMode: ParseMode.MarkdownV2, | ||
| replyMarkup: replyMarkup, | ||
| cancellationToken: cancellationToken); |
There was a problem hiding this comment.
TelegramBotTransport forces ParseMode.MarkdownV2 for all messages/edits. Many call sites pass plain text that isn't escaped for MarkdownV2 (e.g., strings with '.' / '-' / '!' etc.), which can cause Telegram to reject the request with “can't parse entities”. Consider either (a) defaulting to no parse mode (plain text) and only enabling MarkdownV2 for messages you explicitly format, or (b) adding a transport API that accepts a parse mode and ensuring every message body is properly escaped before sending.
| var top = stats | ||
| .OrderBy(s => s.EloRank == 0 ? int.MaxValue : s.EloRank) | ||
| .ThenByDescending(s => s.EloScore) | ||
| .Take(10) | ||
| .Select((stat, index) => | ||
| { | ||
| var rank = stat.EloRank > 0 ? stat.EloRank : index + 1; | ||
| var name = string.IsNullOrWhiteSpace(stat.DisplayName) ? stat.ModelName : stat.DisplayName; | ||
| return $"{rank}) {EscapeMarkdown(name)} | {EscapeMarkdown(stat.ProviderName)} | Elo {stat.EloScore:F0} | Win {stat.WinRate:F1}%"; | ||
| }); | ||
|
|
||
| return "Top Models\n\n" + string.Join("\n", top); |
There was a problem hiding this comment.
FormatStats builds MarkdownV2 text but interpolates numeric values directly (e.g., {stat.WinRate:F1} yields 64.8) and includes | / ) formatting. If the transport uses MarkdownV2, the . (and other reserved chars) in the interpolated portions can break parsing. Consider escaping the full rendered line (or at least the formatted numeric segments) consistently, or switch stats rendering to plain text parse mode.
| public static string EscapeMarkdown(string text) | ||
| { | ||
| if (string.IsNullOrEmpty(text)) | ||
| { | ||
| return string.Empty; | ||
| } | ||
|
|
||
| return text | ||
| .Replace("_", "\\_") | ||
| .Replace("*", "\\*") | ||
| .Replace("[", "\\[") | ||
| .Replace("]", "\\]") | ||
| .Replace("(", "\\(") | ||
| .Replace(")", "\\)") | ||
| .Replace("~", "\\~") | ||
| .Replace("`", "\\`") | ||
| .Replace(">", "\\>") | ||
| .Replace("#", "\\#") | ||
| .Replace("+", "\\+") | ||
| .Replace("-", "\\-") | ||
| .Replace("=", "\\=") | ||
| .Replace("|", "\\|") | ||
| .Replace("{", "\\{") | ||
| .Replace("}", "\\}") | ||
| .Replace("!", "\\!"); |
There was a problem hiding this comment.
EscapeMarkdown is intended for Telegram MarkdownV2 escaping but doesn't escape some characters that are treated as special in MarkdownV2 (notably . and the backslash itself). If you keep using MarkdownV2 globally, expand this helper to cover the full reserved set (and ensure escaping order handles \ first) to avoid intermittent “can't parse entities” failures.
No description provided.