using Discord; using Discord.WebSocket; using Discord.Interactions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Configuration; namespace TentacleBot.Services; public class BotService { private readonly DiscordSocketClient _client; private readonly InteractionService _interactionService; private readonly LlmService _llmService; private readonly ILogger _logger; private readonly IConfiguration _configuration; private readonly IServiceProvider _serviceProvider; public BotService( DiscordSocketClient client, InteractionService interactionService, LlmService llmService, ILogger logger, IConfiguration configuration, IServiceProvider serviceProvider) { _client = client; _interactionService = interactionService; _llmService = llmService; _logger = logger; _configuration = configuration; _serviceProvider = serviceProvider; } public async Task InitializeAsync() { _client.Log += LogAsync; _interactionService.Log += LogAsync; _client.Ready += ClientReadyAsync; _client.InteractionCreated += InteractionCreatedAsync; _client.MessageReceived += MessageReceivedAsync; var token = Environment.GetEnvironmentVariable("DISCORD_BOT_TOKEN"); if (string.IsNullOrWhiteSpace(token)) { token = _configuration["Discord:BotToken"]; } if (string.IsNullOrEmpty(token)) { throw new InvalidOperationException("Discord bot token not found. Set DISCORD_BOT_TOKEN or Discord:BotToken in appsettings.json"); } await _client.LoginAsync(TokenType.Bot, token); await _client.StartAsync(); _logger.LogInformation("Bot service initialized"); } private async Task ClientReadyAsync() { _logger.LogInformation("Bot connected as {BotName}#{Discriminator}", _client.CurrentUser?.Username, _client.CurrentUser?.Discriminator); // Register slash commands await _interactionService.RegisterCommandsGloballyAsync(); _logger.LogInformation("Slash commands registered"); } private async Task InteractionCreatedAsync(SocketInteraction interaction) { try { var ctx = new SocketInteractionContext(_client, interaction); await _interactionService.ExecuteCommandAsync(ctx, _serviceProvider); } catch (Exception ex) { _logger.LogError(ex, "Error handling interaction"); if (interaction.Type == InteractionType.ApplicationCommand) { await interaction.GetOriginalResponseAsync().ContinueWith(async msg => { if (msg.IsCompletedSuccessfully) { await interaction.FollowupAsync("An error occurred while executing the command."); } }); } } } private async Task MessageReceivedAsync(SocketMessage socketMessage) { if (socketMessage is not SocketUserMessage message) return; if (message.Source != MessageSource.User || message.Author.IsBot || message.Author.IsWebhook) return; if (_client.CurrentUser is null) return; var botId = _client.CurrentUser.Id; var botName = _client.CurrentUser.Username; var hasMention = message.MentionedUsers.Any(u => u.Id == botId); var hasTextMention = message.Content.TrimStart() .StartsWith($"@{botName}", StringComparison.OrdinalIgnoreCase); var isReplyToBot = false; if (message.Reference?.MessageId.IsSpecified == true) { try { var referenced = message.ReferencedMessage ?? await message.Channel.GetMessageAsync(message.Reference.MessageId.Value); isReplyToBot = referenced?.Author.Id == botId; } catch (Exception ex) { _logger.LogDebug(ex, "Unable to resolve referenced message for reply detection."); } } if (!hasMention && !isReplyToBot && !hasTextMention) return; string prompt; if (hasMention) prompt = RemoveBotMentions(message.Content, botId); else if (hasTextMention) prompt = RemoveBotName(message.Content, botName); else prompt = message.Content.Trim(); if (string.IsNullOrWhiteSpace(prompt)) { await message.ReplyAsync("Please include a prompt after mentioning me."); return; } try { var accumulated = string.Empty; await foreach (var text in _llmService.SendMessageAsync(message.Channel.Id, message.Author.Id, message.Author.Username, prompt)) { accumulated = text; } if (string.IsNullOrWhiteSpace(accumulated)) { await message.ReplyAsync("The model returned an empty response."); return; } await SendChunkedReplyAsync(message, accumulated); } catch (Exception ex) { _logger.LogError(ex, "Error handling mention/reply LLM chat in channel {ChannelId}", message.Channel.Id); await message.ReplyAsync("I hit an error while generating a response."); } } private static async Task SendChunkedReplyAsync(SocketUserMessage message, string response) { const int maxChunkLength = 2000; for (var start = 0; start < response.Length; start += maxChunkLength) { var length = Math.Min(maxChunkLength, response.Length - start); var chunk = response.Substring(start, length); if (start == 0) { await message.ReplyAsync(chunk); } else { await message.Channel.SendMessageAsync(chunk); } } } private static string RemoveBotMentions(string content, ulong botId) { if (string.IsNullOrWhiteSpace(content)) return string.Empty; var rawMention = $"<@{botId}>"; var nickMention = $"<@!{botId}>"; return content .Replace(rawMention, string.Empty, StringComparison.Ordinal) .Replace(nickMention, string.Empty, StringComparison.Ordinal) .Trim(); } private static string RemoveBotName(string content, string botName) { if (string.IsNullOrWhiteSpace(content)) return string.Empty; return System.Text.RegularExpressions.Regex.Replace( content, $@"^\s*@{System.Text.RegularExpressions.Regex.Escape(botName)}\s*", "", System.Text.RegularExpressions.RegexOptions.IgnoreCase).Trim(); } private static Task LogAsync(LogMessage message) { Console.WriteLine($"[{message.Severity}] {message.Source}: {message.Message}"); return Task.CompletedTask; } public async Task StopAsync() { await _client.StopAsync(); _client.Dispose(); _logger.LogInformation("Bot service stopped"); } }