Files
TentacleBot/Services/BotService.cs
2026-07-10 18:20:33 -04:00

206 lines
6.4 KiB
C#

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<BotService> _logger;
private readonly IConfiguration _configuration;
private readonly IServiceProvider _serviceProvider;
public BotService(
DiscordSocketClient client,
InteractionService interactionService,
LlmService llmService,
ILogger<BotService> 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 hasMention = message.MentionedUsers.Any(u => u.Id == botId);
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)
return;
var prompt = hasMention ? RemoveBotMentions(message.Content, botId) : 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.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 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");
}
}