diff --git a/README.md b/README.md index 44595ae..f2108e1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TheSexy6BotWorker +# DSharpPlusBot A Discord bot built as a .NET 9.0 Worker Service that integrates multiple AI models (Google Gemini and X.AI Grok) with Microsoft Semantic Kernel for function calling capabilities. Features a modular bot architecture with engagement mode for autonomous conversation participation. @@ -112,19 +112,17 @@ cd DSharpPlusBot This project uses .NET User Secrets to store sensitive configuration. Set up your secrets with: ```bash -cd TheSexy6BotWorker - # Set Discord bot token -dotnet user-secrets set "DiscordToken" "your-discord-bot-token" +dotnet user-secrets --project src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj set "DiscordToken" "your-discord-bot-token" # Set Google AI Gemini API key -dotnet user-secrets set "GeminiKey" "your-gemini-api-key" +dotnet user-secrets --project src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj set "GeminiKey" "your-gemini-api-key" # Set X.AI Grok API key -dotnet user-secrets set "GrokKey" "your-grok-api-key" +dotnet user-secrets --project src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj set "GrokKey" "your-grok-api-key" # Set Perplexity API key -dotnet user-secrets set "PerplexityApiKey" "your-perplexity-api-key" +dotnet user-secrets --project src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj set "PerplexityApiKey" "your-perplexity-api-key" ``` **User Secrets ID**: `dotnet-TheSexy6BotWorker-d23e68fa-7622-4b43-ac67-735c9cf191f4` @@ -136,22 +134,23 @@ dotnet user-secrets set "PerplexityApiKey" "your-perplexity-api-key" List your configured secrets: ```bash -dotnet user-secrets list +dotnet user-secrets --project src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj list ``` ### 4. Restore Dependencies ```bash -cd .. -dotnet restore +dotnet restore TheSexy6BotWorker.slnx ``` ## Running Locally +For remote staging secret wiring and runbook steps, see `src/terraform/README.md`. + ### Standard Mode (Production Commands) ```bash -dotnet run --project TheSexy6BotWorker/TheSexy6BotWorker.csproj +dotnet run --project src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj ``` In Discord: @@ -169,14 +168,14 @@ In Discord: ### Local Development Mode (Test Commands) -Set the `LOCAL_DEV` environment variable to add "test-" prefix: +Set `DOTNET_ENVIRONMENT=Development` to add the `test-` command prefix: ```bash # PowerShell -$env:LOCAL_DEV="true"; dotnet run --project TheSexy6BotWorker/TheSexy6BotWorker.csproj +$env:DOTNET_ENVIRONMENT="Development"; dotnet run --project src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj # Bash/Linux -LOCAL_DEV=true dotnet run --project TheSexy6BotWorker/TheSexy6BotWorker.csproj +DOTNET_ENVIRONMENT=Development dotnet run --project src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj ``` Commands become: `test-gemini`, `test-grok`, `test-ping` @@ -230,41 +229,15 @@ docker buildx build --platform linux/amd64 \ ## Project Structure ``` -TheSexy6BotWorker/ -├── Commands/ # DSharpPlus command definitions -│ └── PingCommand.cs -├── Configuration/ # Bot configuration implementations -│ ├── BotConfigurationExtensions.cs -│ ├── GeminiBotConfiguration.cs -│ └── GrokBotConfiguration.cs -├── Contracts/ # Interfaces -│ ├── IBotConfiguration.cs -│ └── IConversationSessionManager.cs -├── DTOs/ # Data transfer objects -├── Handlers/ # Discord event handlers -│ └── MessageCreatedHandler.cs -├── Markdown/ # Fluent markdown generation library -│ ├── MarkdownBuilder.cs -│ ├── MarkdownGenerator.cs # ObjectMarkdownBuilder -│ └── *Attribute.cs # MarkdownProperty, MarkdownIgnore, etc. -├── Models/ # Domain models -│ ├── BotConfigurationModel.cs -│ ├── ConversationSession.cs -│ └── EngagementDecision.cs # Structured output for engagement mode -├── Services/ # Application services -│ ├── BotRegistry.cs # Bot lookup by prefix -│ ├── ConversationSessionManager.cs -│ ├── DynamicStatusService.cs -│ ├── PerplexitySearchService.cs -│ └── WeatherService.cs -├── Program.cs -└── DiscordWorker.cs - -TheSexy6BotWorker.Tests/ # xUnit test project -├── Configuration/ -├── Integration/ -├── Markdown/ -└── Services/ +DSharpPlusBot/ +├── src/ +│ ├── dotnet/ +│ │ ├── TheSexy6BotWorker/ # Main worker project +│ │ └── TheSexy6BotWorker.Tests/ # xUnit test project +│ └── terraform/ # Azure infra + secret contract +├── Dockerfile +├── TheSexy6BotWorker.slnx +└── README.md ``` ## Core Components @@ -308,7 +281,7 @@ var md = new ObjectMarkdownBuilder(config) | `GeminiKey` | Google AI Gemini API key | Yes | | `GrokKey` | X.AI Grok API key | Yes | | `PerplexityApiKey` | Perplexity API key | Yes | -| `LOCAL_DEV` | Enable test command prefixes | No | +| `DOTNET_ENVIRONMENT` | Set to `Development` to enable test command prefixes | No | ## Key Dependencies @@ -320,7 +293,7 @@ var md = new ObjectMarkdownBuilder(config) ## Troubleshooting ### Bot Not Responding -- Verify all user secrets are set: `dotnet user-secrets list` +- Verify all user secrets are set: `dotnet user-secrets --project src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj list` - Check Discord bot has Message Content intent enabled - Ensure bot has appropriate permissions in your Discord server @@ -344,4 +317,4 @@ var md = new ObjectMarkdownBuilder(config) ## Contributing -[Your Contributing Guidelines Here] \ No newline at end of file +[Your Contributing Guidelines Here] diff --git a/src/dotnet/TheSexy6BotWorker.Tests/Configuration/BotConfigurationExtensionsTests.cs b/src/dotnet/TheSexy6BotWorker.Tests/Configuration/BotConfigurationExtensionsTests.cs index 0fe32f5..cfe5d95 100644 --- a/src/dotnet/TheSexy6BotWorker.Tests/Configuration/BotConfigurationExtensionsTests.cs +++ b/src/dotnet/TheSexy6BotWorker.Tests/Configuration/BotConfigurationExtensionsTests.cs @@ -121,19 +121,16 @@ public void GenerateConfigurationDescription_HandlesComplexSettings() Assert.Contains("0.7", description); } - [Theory] - [InlineData("")] - [InlineData("test-")] - [InlineData("dev-")] - public void GenerateConfigurationDescription_IncludesEnvironmentPrefix(string prefix) + [Fact] + public void GenerateConfigurationDescription_IncludesCanonicalPrefix() { // Arrange - var config = new GeminiBotConfiguration(prefix); + var config = new GeminiBotConfiguration(); // Act var description = config.GenerateConfigurationDescription(); // Assert - Assert.Contains($"{prefix}gemini", description); + Assert.Contains("gemini", description); } } diff --git a/src/dotnet/TheSexy6BotWorker.Tests/Configuration/BotConfigurationIntegrationTests.cs b/src/dotnet/TheSexy6BotWorker.Tests/Configuration/BotConfigurationIntegrationTests.cs index cad19a9..9c2f079 100644 --- a/src/dotnet/TheSexy6BotWorker.Tests/Configuration/BotConfigurationIntegrationTests.cs +++ b/src/dotnet/TheSexy6BotWorker.Tests/Configuration/BotConfigurationIntegrationTests.cs @@ -31,15 +31,15 @@ public void Registry_WithMultipleBots_CanDistinguishByPrefix() } [Fact] - public void Registry_WithEnvironmentPrefixes_HandlesCollisions() + public void Registry_WithDuplicateCanonicalPrefixes_HandlesCollisions() { // Arrange var registry = new BotRegistry(); - registry.Register(new GeminiBotConfiguration("test-")); + registry.Register(new GeminiBotConfiguration()); // Act - trying to register same bot with same prefix should throw var exception = Assert.Throws(() => - registry.Register(new GeminiBotConfiguration("test-"))); + registry.Register(new GeminiBotConfiguration())); // Assert Assert.Contains("already registered", exception.Message); @@ -92,14 +92,12 @@ public void BotSettings_CanBeModifiedAtRuntime() [InlineData("gemini what is AI?", "gemini", "what is AI?")] [InlineData("grok tell me a joke", "grok", "tell me a joke")] [InlineData("GEMINI case test", "gemini", "case test")] - [InlineData("test-grok hello", "test-grok", "hello")] public void Registry_MessageParsing_HandlesVariousFormats(string message, string expectedPrefix, string expectedStripped) { // Arrange var registry = new BotRegistry(); registry.Register(new GeminiBotConfiguration()); registry.Register(new GrokBotConfiguration()); - registry.Register(new GrokBotConfiguration("test-")); // Act var found = registry.TryGetBot(message, out var bot, out var strippedMessage); @@ -110,6 +108,23 @@ public void Registry_MessageParsing_HandlesVariousFormats(string message, string Assert.Equal(expectedStripped, strippedMessage); } + [Fact] + public void Registry_MessageParsing_WithMessagePrefix_HandlesDevFormat() + { + // Arrange + var registry = new BotRegistry("test-"); + registry.Register(new GeminiBotConfiguration()); + registry.Register(new GrokBotConfiguration()); + + // Act + var found = registry.TryGetBot("test-grok hello", out var bot, out var strippedMessage); + + // Assert + Assert.True(found); + Assert.Equal("grok", bot!.Prefix); + Assert.Equal("hello", strippedMessage); + } + [Fact] public void Registry_GetAllBots_PreservesCapabilityInformation() { diff --git a/src/dotnet/TheSexy6BotWorker.Tests/Configuration/GeminiBotConfigurationTests.cs b/src/dotnet/TheSexy6BotWorker.Tests/Configuration/GeminiBotConfigurationTests.cs index 4af2389..ba17556 100644 --- a/src/dotnet/TheSexy6BotWorker.Tests/Configuration/GeminiBotConfigurationTests.cs +++ b/src/dotnet/TheSexy6BotWorker.Tests/Configuration/GeminiBotConfigurationTests.cs @@ -16,16 +16,6 @@ public void Constructor_WithoutEnvironmentPrefix_SetsDefaultPrefix() Assert.Equal("gemini", config.Prefix); } - [Fact] - public void Constructor_WithEnvironmentPrefix_SetsCustomPrefix() - { - // Arrange & Act - var config = new GeminiBotConfiguration("test-"); - - // Assert - Assert.Equal("test-gemini", config.Prefix); - } - [Fact] public void ServiceId_ReturnsCorrectValue() { @@ -119,20 +109,6 @@ public void SupportsImages_ReturnsFalse() Assert.False(config.SupportsImages); } - [Theory] - [InlineData("")] - [InlineData("test-")] - [InlineData("dev-")] - [InlineData("prod-")] - public void Constructor_WithVariousPrefixes_SetsCorrectPrefix(string environmentPrefix) - { - // Arrange & Act - var config = new GeminiBotConfiguration(environmentPrefix); - - // Assert - Assert.Equal($"{environmentPrefix}gemini", config.Prefix); - } - [Fact] public void GetConfigurationDescription_ReturnsNonEmptyString() { diff --git a/src/dotnet/TheSexy6BotWorker.Tests/Configuration/GrokBotConfigurationTests.cs b/src/dotnet/TheSexy6BotWorker.Tests/Configuration/GrokBotConfigurationTests.cs index 7928e7f..ea3ec9c 100644 --- a/src/dotnet/TheSexy6BotWorker.Tests/Configuration/GrokBotConfigurationTests.cs +++ b/src/dotnet/TheSexy6BotWorker.Tests/Configuration/GrokBotConfigurationTests.cs @@ -17,16 +17,6 @@ public void Constructor_WithoutEnvironmentPrefix_SetsDefaultPrefix() Assert.Equal("grok", config.Prefix); } - [Fact] - public void Constructor_WithEnvironmentPrefix_SetsCustomPrefix() - { - // Arrange & Act - var config = new GrokBotConfiguration("test-"); - - // Assert - Assert.Equal("test-grok", config.Prefix); - } - [Fact] public void ServiceId_ReturnsCorrectValue() { @@ -173,20 +163,6 @@ public void SupportsImages_ReturnsTrue() Assert.True(config.SupportsImages); } - [Theory] - [InlineData("")] - [InlineData("test-")] - [InlineData("dev-")] - [InlineData("prod-")] - public void Constructor_WithVariousPrefixes_SetsCorrectPrefix(string environmentPrefix) - { - // Arrange & Act - var config = new GrokBotConfiguration(environmentPrefix); - - // Assert - Assert.Equal($"{environmentPrefix}grok", config.Prefix); - } - [Fact] public void AllCapabilities_AreEnabled() { diff --git a/src/dotnet/TheSexy6BotWorker.Tests/Configuration/HostEnvironmentModeTests.cs b/src/dotnet/TheSexy6BotWorker.Tests/Configuration/HostEnvironmentModeTests.cs new file mode 100644 index 0000000..731e71c --- /dev/null +++ b/src/dotnet/TheSexy6BotWorker.Tests/Configuration/HostEnvironmentModeTests.cs @@ -0,0 +1,110 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using TheSexy6BotWorker.Configuration; + +namespace TheSexy6BotWorker.Tests.Configuration; + +public class HostEnvironmentModeTests +{ + [Theory] + [InlineData("Development")] + [InlineData("development")] + [InlineData("DEVELOPMENT")] + public void GetRuntimeMode_ReturnsDevelopment_ForDevelopment(string environmentName) + { + var environment = new TestHostEnvironment(environmentName); + + var mode = HostEnvironmentMode.GetRuntimeMode(environment); + + Assert.Equal(RuntimeMode.Development, mode); + } + + [Theory] + [InlineData("Staging")] + [InlineData("Production")] + [InlineData("qa")] + [InlineData("")] + public void GetRuntimeMode_ReturnsRemote_ForNonDevelopment(string environmentName) + { + var environment = new TestHostEnvironment(environmentName); + + var mode = HostEnvironmentMode.GetRuntimeMode(environment); + + Assert.Equal(RuntimeMode.Remote, mode); + } + + [Theory] + [InlineData("Development")] + [InlineData("development")] + [InlineData("DEVELOPMENT")] + public void ShouldLoadUserSecrets_ReturnsTrue_ForDevelopment(string environmentName) + { + var environment = new TestHostEnvironment(environmentName); + + var result = HostEnvironmentMode.ShouldLoadUserSecrets(environment); + + Assert.True(result); + } + + [Theory] + [InlineData("Staging")] + [InlineData("Production")] + [InlineData("qa")] + public void ShouldLoadUserSecrets_ReturnsFalse_ForNonDevelopment(string environmentName) + { + var environment = new TestHostEnvironment(environmentName); + + var result = HostEnvironmentMode.ShouldLoadUserSecrets(environment); + + Assert.False(result); + } + + [Theory] + [InlineData("Development")] + [InlineData("development")] + [InlineData("DEVELOPMENT")] + public void GetMessagePrefix_ReturnsTestPrefix_ForDevelopment(string environmentName) + { + var environment = new TestHostEnvironment(environmentName); + + var result = HostEnvironmentMode.GetMessagePrefix(environment); + + Assert.Equal(HostEnvironmentMode.DevelopmentCommandPrefix, result); + } + + [Theory] + [InlineData("Staging")] + [InlineData("Production")] + [InlineData("local")] + public void GetMessagePrefix_ReturnsEmpty_ForNonDevelopment(string environmentName) + { + var environment = new TestHostEnvironment(environmentName); + + var result = HostEnvironmentMode.GetMessagePrefix(environment); + + Assert.Equal(string.Empty, result); + } + + [Fact] + public void ShouldLoadUserSecrets_Throws_WhenEnvironmentIsNull() + { + Assert.Throws(() => HostEnvironmentMode.ShouldLoadUserSecrets(null!)); + } + + [Fact] + public void GetMessagePrefix_Throws_WhenEnvironmentIsNull() + { + Assert.Throws(() => HostEnvironmentMode.GetMessagePrefix(null!)); + } + + private sealed class TestHostEnvironment(string environmentName) : IHostEnvironment + { + public string EnvironmentName { get; set; } = environmentName; + + public string ApplicationName { get; set; } = "DSharpPlusBot.Tests"; + + public string ContentRootPath { get; set; } = Directory.GetCurrentDirectory(); + + public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider(); + } +} diff --git a/src/dotnet/TheSexy6BotWorker.Tests/Integration/MarkdownIntegrationTests.cs b/src/dotnet/TheSexy6BotWorker.Tests/Integration/MarkdownIntegrationTests.cs index a814cf6..2dad2d4 100644 --- a/src/dotnet/TheSexy6BotWorker.Tests/Integration/MarkdownIntegrationTests.cs +++ b/src/dotnet/TheSexy6BotWorker.Tests/Integration/MarkdownIntegrationTests.cs @@ -78,18 +78,18 @@ public void GrokBotConfiguration_GeneratesWellStructuredMarkdown() } [Fact] - public void BotConfiguration_WithEnvironmentPrefix_ReflectsInMarkdown() + public void BotConfiguration_UsesCanonicalPrefix_InMarkdown() { // Arrange - IBotConfiguration config = new GrokBotConfiguration("test-"); + IBotConfiguration config = new GrokBotConfiguration(); // Act var markdown = config.GetConfigurationDescription(); // Assert - Assert.Contains("test-grok", markdown); + Assert.Contains("grok", markdown); - _output.WriteLine("=== Test Environment Bot ==="); + _output.WriteLine("=== Canonical Prefix Bot ==="); _output.WriteLine(markdown); } diff --git a/src/dotnet/TheSexy6BotWorker.Tests/Services/BotRegistryTests.cs b/src/dotnet/TheSexy6BotWorker.Tests/Services/BotRegistryTests.cs index 1a625d3..78738d9 100644 --- a/src/dotnet/TheSexy6BotWorker.Tests/Services/BotRegistryTests.cs +++ b/src/dotnet/TheSexy6BotWorker.Tests/Services/BotRegistryTests.cs @@ -255,11 +255,11 @@ public void GetBot_CaseInsensitive_ReturnsBot() } [Fact] - public void TryGetBot_WithEnvironmentPrefix_MatchesCorrectly() + public void TryGetBot_WithMessagePrefixConfigured_MatchesCorrectly() { // Arrange - var registry = new BotRegistry(); - registry.Register(new GeminiBotConfiguration("test-")); + var registry = new BotRegistry("test-"); + registry.Register(new GeminiBotConfiguration()); var message = "test-gemini hello world"; // Act @@ -268,10 +268,27 @@ public void TryGetBot_WithEnvironmentPrefix_MatchesCorrectly() // Assert Assert.True(found); Assert.NotNull(result); - Assert.Equal("test-gemini", result.Prefix); + Assert.Equal("gemini", result.Prefix); Assert.Equal("hello world", strippedMessage); } + [Fact] + public void TryGetBot_WithMessagePrefixConfigured_WithoutPrefix_DoesNotMatch() + { + // Arrange + var registry = new BotRegistry("test-"); + registry.Register(new GeminiBotConfiguration()); + var message = "gemini hello world"; + + // Act + var found = registry.TryGetBot(message, out var result, out var strippedMessage); + + // Assert + Assert.False(found); + Assert.Null(result); + Assert.Equal(message, strippedMessage); + } + [Fact] public void TryGetBot_PartialPrefixMatch_DoesNotMatch() { diff --git a/src/dotnet/TheSexy6BotWorker/Configuration/GeminiBotConfiguration.cs b/src/dotnet/TheSexy6BotWorker/Configuration/GeminiBotConfiguration.cs index c0fd271..574020d 100644 --- a/src/dotnet/TheSexy6BotWorker/Configuration/GeminiBotConfiguration.cs +++ b/src/dotnet/TheSexy6BotWorker/Configuration/GeminiBotConfiguration.cs @@ -6,14 +6,7 @@ namespace TheSexy6BotWorker.Configuration; public class GeminiBotConfiguration : IBotConfiguration { - private readonly string _environmentPrefix; - - public GeminiBotConfiguration(string environmentPrefix = "") - { - _environmentPrefix = environmentPrefix; - } - - public string Prefix => $"{_environmentPrefix}gemini"; + public string Prefix => "gemini"; public string ServiceId => "gemini"; diff --git a/src/dotnet/TheSexy6BotWorker/Configuration/GrokBotConfiguration.cs b/src/dotnet/TheSexy6BotWorker/Configuration/GrokBotConfiguration.cs index d21e29d..70d0898 100644 --- a/src/dotnet/TheSexy6BotWorker/Configuration/GrokBotConfiguration.cs +++ b/src/dotnet/TheSexy6BotWorker/Configuration/GrokBotConfiguration.cs @@ -6,14 +6,7 @@ namespace TheSexy6BotWorker.Configuration; public class GrokBotConfiguration : IBotConfiguration { - private readonly string _environmentPrefix; - - public GrokBotConfiguration(string environmentPrefix = "") - { - _environmentPrefix = environmentPrefix; - } - - public string Prefix => $"{_environmentPrefix}grok"; + public string Prefix => "grok"; public string ServiceId => "grok"; diff --git a/src/dotnet/TheSexy6BotWorker/Configuration/HostEnvironmentMode.cs b/src/dotnet/TheSexy6BotWorker/Configuration/HostEnvironmentMode.cs new file mode 100644 index 0000000..3593a6e --- /dev/null +++ b/src/dotnet/TheSexy6BotWorker/Configuration/HostEnvironmentMode.cs @@ -0,0 +1,34 @@ +namespace TheSexy6BotWorker.Configuration; + +public enum RuntimeMode +{ + Development, + Remote +} + +public static class HostEnvironmentMode +{ + public const string DevelopmentCommandPrefix = "test-"; + + public static RuntimeMode GetRuntimeMode(IHostEnvironment hostEnvironment) + { + ArgumentNullException.ThrowIfNull(hostEnvironment); + return hostEnvironment.IsDevelopment() ? RuntimeMode.Development : RuntimeMode.Remote; + } + + public static bool ShouldLoadUserSecrets(IHostEnvironment hostEnvironment) + { + return GetRuntimeMode(hostEnvironment) == RuntimeMode.Development; + } + + public static string GetMessagePrefix(IHostEnvironment hostEnvironment) + { + return GetRuntimeMode(hostEnvironment) == RuntimeMode.Development ? DevelopmentCommandPrefix : string.Empty; + } + + [Obsolete("Use GetMessagePrefix instead.")] + public static string GetBotCommandPrefix(IHostEnvironment hostEnvironment) + { + return GetMessagePrefix(hostEnvironment); + } +} diff --git a/src/dotnet/TheSexy6BotWorker/Configuration/README.md b/src/dotnet/TheSexy6BotWorker/Configuration/README.md index 9e3d0b9..e4e0bb8 100644 --- a/src/dotnet/TheSexy6BotWorker/Configuration/README.md +++ b/src/dotnet/TheSexy6BotWorker/Configuration/README.md @@ -25,14 +25,7 @@ namespace TheSexy6BotWorker.Configuration; public class ClaudeBotConfiguration : IBotConfiguration { - private readonly string _environmentPrefix; - - public ClaudeBotConfiguration(string environmentPrefix = "") - { - _environmentPrefix = environmentPrefix; - } - - public string Prefix => $"{_environmentPrefix}claude"; + public string Prefix => "claude"; public string ServiceId => "claude"; @@ -71,10 +64,10 @@ In `DiscordWorker.cs`, add to the bot registry: ```csharp services.AddSingleton(sp => { - var registry = new BotRegistry(); - registry.Register(new GeminiBotConfiguration(environmentPrefix)); - registry.Register(new GrokBotConfiguration(environmentPrefix)); - registry.Register(new ClaudeBotConfiguration(environmentPrefix)); // ← Add here + var registry = new BotRegistry(messagePrefix); + registry.Register(new GeminiBotConfiguration()); + registry.Register(new GrokBotConfiguration()); + registry.Register(new ClaudeBotConfiguration()); // ← Add here return registry; }); ``` @@ -86,7 +79,8 @@ dotnet user-secrets set "ClaudeKey" "your-api-key-here" ``` That's it! The new bot will automatically: -- Respond to `claude ` (or `test-claude` in dev mode) +- Respond to `claude ` in normal mode +- Respond to `test-claude ` in Development mode - Use the specified settings and capabilities - Support all features enabled in the configuration diff --git a/src/dotnet/TheSexy6BotWorker/DiscordWorker.cs b/src/dotnet/TheSexy6BotWorker/DiscordWorker.cs index 54047a9..2fbd197 100644 --- a/src/dotnet/TheSexy6BotWorker/DiscordWorker.cs +++ b/src/dotnet/TheSexy6BotWorker/DiscordWorker.cs @@ -48,18 +48,18 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) builder.ConfigureServices(services => { // Determine environment prefix for bot commands - var environmentPrefix = _hostEnvironment.IsDevelopment() ? "test-" : ""; + var messagePrefix = HostEnvironmentMode.GetMessagePrefix(_hostEnvironment); // Register bot registry and configurations services.AddSingleton(sp => { - var registry = new BotRegistry(); + var registry = new BotRegistry(messagePrefix); // Register Gemini bot - registry.Register(new GeminiBotConfiguration(environmentPrefix)); + registry.Register(new GeminiBotConfiguration()); // Register Grok bot - registry.Register(new GrokBotConfiguration(environmentPrefix)); + registry.Register(new GrokBotConfiguration()); return registry; }); diff --git a/src/dotnet/TheSexy6BotWorker/Program.cs b/src/dotnet/TheSexy6BotWorker/Program.cs index 9ddb8ca..0e3ad5f 100644 --- a/src/dotnet/TheSexy6BotWorker/Program.cs +++ b/src/dotnet/TheSexy6BotWorker/Program.cs @@ -1,5 +1,4 @@ -using Microsoft.SemanticKernel; -using TheSexy6BotWorker.Handlers; +using TheSexy6BotWorker.Configuration; namespace TheSexy6BotWorker { @@ -15,7 +14,7 @@ public static int Main(string[] args) .ToArray(); var builder = Host.CreateApplicationBuilder(hostArgs); - if (builder.Environment.IsDevelopment()) + if (HostEnvironmentMode.ShouldLoadUserSecrets(builder.Environment)) { builder.Configuration.AddUserSecrets(); } @@ -32,6 +31,7 @@ public static int Main(string[] args) if (isSmokeTest) { Console.WriteLine("Container smoke test passed."); + Console.WriteLine($"DOTNET_ENVIRONMENT={builder.Environment.EnvironmentName}"); Console.WriteLine($"APP_VERSION={Environment.GetEnvironmentVariable("APP_VERSION") ?? "unset"}"); Console.WriteLine($"APP_COMMIT_MSG={Environment.GetEnvironmentVariable("APP_COMMIT_MSG") ?? "unset"}"); return 0; diff --git a/src/dotnet/TheSexy6BotWorker/Services/BotRegistry.cs b/src/dotnet/TheSexy6BotWorker/Services/BotRegistry.cs index 224b416..c8e296e 100644 --- a/src/dotnet/TheSexy6BotWorker/Services/BotRegistry.cs +++ b/src/dotnet/TheSexy6BotWorker/Services/BotRegistry.cs @@ -6,6 +6,12 @@ namespace TheSexy6BotWorker.Services; public class BotRegistry { private readonly Dictionary _bots = new(StringComparer.OrdinalIgnoreCase); + private readonly string _messagePrefix; + + public BotRegistry(string messagePrefix = "") + { + _messagePrefix = messagePrefix ?? throw new ArgumentNullException(nameof(messagePrefix)); + } /// /// Registers a bot configuration with the registry @@ -36,18 +42,30 @@ public bool TryGetBot(string messageContent, out IBotConfiguration? bot, out str bot = null; strippedMessage = messageContent; + var contentToMatch = messageContent; + + if (!string.IsNullOrEmpty(_messagePrefix)) + { + if (!messageContent.StartsWith(_messagePrefix, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + contentToMatch = messageContent[_messagePrefix.Length..].TrimStart(); + strippedMessage = contentToMatch; + } foreach (var kvp in _bots) { var prefix = kvp.Key; // Check if message starts with this bot's prefix (case-insensitive) - if (messageContent.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + if (contentToMatch.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { bot = kvp.Value; // Strip the prefix and any leading whitespace - strippedMessage = messageContent.Substring(prefix.Length).TrimStart(); + strippedMessage = contentToMatch[prefix.Length..].TrimStart(); return true; } diff --git a/src/terraform/README.md b/src/terraform/README.md index 8e37a93..4f8cb43 100644 --- a/src/terraform/README.md +++ b/src/terraform/README.md @@ -37,6 +37,12 @@ Use `scripts/upsert-required-secrets.sh` to set all required secrets in Key Vault. +Get the target vault name from Terraform output: + +```bash +terraform output -raw key_vault_name +``` + Example: ```bash @@ -75,4 +81,3 @@ for REVISION in $(az containerapp revision list \ done ``` - diff --git a/src/terraform/outputs.tf b/src/terraform/outputs.tf index c7ac54b..1ba0a41 100644 --- a/src/terraform/outputs.tf +++ b/src/terraform/outputs.tf @@ -21,3 +21,11 @@ output "container_app_id" { output "container_registry_id" { value = azurerm_container_registry.this.id } + +output "key_vault_name" { + value = azurerm_key_vault.this.name +} + +output "key_vault_uri" { + value = azurerm_key_vault.this.vault_uri +} diff --git a/src/terraform/scripts/upsert-required-secrets.sh b/src/terraform/scripts/upsert-required-secrets.sh index 9070b92..42844a4 100755 --- a/src/terraform/scripts/upsert-required-secrets.sh +++ b/src/terraform/scripts/upsert-required-secrets.sh @@ -18,6 +18,17 @@ Options: EOF } +require_option_value() { + local option_name="$1" + local option_value="${2:-}" + + if [[ -z "$option_value" || "$option_value" == --* ]]; then + echo "Missing value for $option_name." >&2 + usage >&2 + exit 1 + fi +} + require_command() { local command_name="$1" if ! command -v "$command_name" >/dev/null 2>&1; then @@ -26,6 +37,25 @@ require_command() { fi } +read_value_if_missing() { + local label="$1" + local current_value="$2" + + if [[ -n "$current_value" ]]; then + printf '%s' "$current_value" + return + fi + + if [[ "$NON_INTERACTIVE" == "true" ]]; then + printf '' + return + fi + + local entered_value + read -r -p "$label: " entered_value + printf '%s' "$entered_value" +} + read_secret_if_missing() { local label="$1" local current_value="$2" @@ -58,22 +88,27 @@ NON_INTERACTIVE="false" while [[ $# -gt 0 ]]; do case "$1" in --vault-name) + require_option_value "--vault-name" "${2:-}" KEY_VAULT_NAME="$2" shift 2 ;; --discord-token) + require_option_value "--discord-token" "${2:-}" DISCORD_TOKEN="$2" shift 2 ;; --gemini-key) + require_option_value "--gemini-key" "${2:-}" GEMINI_KEY="$2" shift 2 ;; --grok-key) + require_option_value "--grok-key" "${2:-}" GROK_KEY="$2" shift 2 ;; --perplexity-key) + require_option_value "--perplexity-key" "${2:-}" PERPLEXITY_API_KEY="$2" shift 2 ;; @@ -97,6 +132,8 @@ while [[ $# -gt 0 ]]; do esac done +KEY_VAULT_NAME="$(read_value_if_missing "Key Vault name" "$KEY_VAULT_NAME")" + if [[ -z "$KEY_VAULT_NAME" ]]; then echo "Missing Key Vault name. Set KEY_VAULT_NAME or pass --vault-name." >&2 exit 1