feat: wip on allow bot to respond to tagged messages

This commit is contained in:
Stevan Freeborn
2026-03-13 07:40:29 -05:00
parent e0716e8bdd
commit 29b96eaae3
16 changed files with 195 additions and 4 deletions
@@ -18,4 +18,12 @@ public sealed record DiscordMessage
[JsonPropertyName("author")]
public DiscordUser Author { get; init; } = new DiscordUser();
[JsonPropertyName("mentions")]
public IEnumerable<DiscordUser> Mentions { get; init; } = [];
public bool MentionsUser(string userId)
{
return Mentions.Any(u => u.Id.Equals(userId, StringComparison.OrdinalIgnoreCase));
}
}
@@ -18,8 +18,11 @@ public sealed class DiscordRestClient : IDiscordRestClient
HttpClient httpClient
)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
ArgumentNullException.ThrowIfNull(logger, nameof(logger));
ArgumentNullException.ThrowIfNull(httpClient, nameof(httpClient));
_logger = logger;
_httpClient = httpClient;
}
public async Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken = default)
@@ -65,4 +68,26 @@ public sealed class DiscordRestClient : IDiscordRestClient
return discordMessage;
}
public async Task<DiscordUser> GetMeAsync(CancellationToken cancellationToken)
{
var meEndpoint = new Uri($"users/@me", UriKind.Relative);
var response = await _httpClient.GetAsync(meEndpoint, cancellationToken);
if (response.IsSuccessStatusCode is false)
{
_logger.LogError("Failed to retrieve current user: {StatusCode}", response.StatusCode);
throw new DiscordRestClientException("Failed to create message.");
}
var discordUser = await response.Content.ReadFromJsonAsync<DiscordUser>(cancellationToken);
if (discordUser is null)
{
_logger.LogError("Failed to deserialize user response.");
throw new DiscordRestClientException("Failed to deserialize user response.");
}
return discordUser;
}
}
@@ -7,4 +7,5 @@ public interface IDiscordRestClient
{
Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken = default);
Task<DiscordMessage> CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken = default);
Task<DiscordUser> GetMeAsync(CancellationToken cancellationToken);
}
@@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection;
using StevesBot.Library.Discord.Common;
using StevesBot.Library.Discord.Rest;
using StevesBot.Library.Gemini;
using StevesBot.Library.Telemetry;
namespace StevesBot.Library.Discord;
@@ -28,4 +29,15 @@ public static class ServicesExtensions
return services;
}
public static IServiceCollection AddGeminiClient(this IServiceCollection services)
{
services
.AddHttpClient<IGeminiClient, GeminiClient>(static (sp, c) =>
{
throw new NotImplementedException();
})
.AddStandardResilienceHandler();
return services;
}
}
@@ -0,0 +1,6 @@
namespace StevesBot.Library.Gemini;
internal sealed record Candidate
{
public Content Content { get; init; } = new();
}
@@ -0,0 +1,6 @@
namespace StevesBot.Library.Gemini;
internal sealed record Content
{
public Part[] Parts { get; init; } = [];
}
@@ -0,0 +1,27 @@
using Microsoft.Extensions.Logging;
namespace StevesBot.Library.Gemini;
public sealed class GeminiClient : IGeminiClient
{
private readonly ILogger<GeminiClient> _logger;
private readonly HttpClient _httpClient;
public GeminiClient(
ILogger<GeminiClient> logger,
HttpClient httpClient
)
{
ArgumentNullException.ThrowIfNull(logger, nameof(logger));
ArgumentNullException.ThrowIfNull(httpClient, nameof(httpClient));
_logger = logger;
_httpClient = httpClient;
}
public Task<string> GenerateContentAsync(string input, CancellationToken ct)
{
throw new NotImplementedException();
}
}
@@ -0,0 +1,6 @@
namespace StevesBot.Library.Gemini;
internal sealed record GenerationConfig
{
public double? Temperature { get; init; }
}
@@ -0,0 +1,6 @@
namespace StevesBot.Library.Gemini;
public interface IGeminiClient
{
Task<string> GenerateContentAsync(string input, CancellationToken ct);
}
+6
View File
@@ -0,0 +1,6 @@
namespace StevesBot.Library.Gemini;
internal sealed record Part
{
public required string Text { get; init; }
}
@@ -0,0 +1,8 @@
namespace StevesBot.Library.Gemini;
internal sealed record GeminiRequest
{
public Content? SystemInstruction { get; init; }
public required Content[] Contents { get; init; }
public GenerationConfig? GenerationConfig { get; init; }
}
@@ -0,0 +1,23 @@
namespace StevesBot.Library.Gemini;
internal sealed record Response
{
public Candidate[] Candidates { get; init; } = [];
public string GetText()
{
if (Candidates.Length is 0)
{
return string.Empty;
}
var parts = Candidates.First().Content.Parts;
if (parts.Length is 0)
{
return string.Empty;
}
return parts.First().Text;
}
}
@@ -2,5 +2,6 @@ namespace StevesBot.Worker.Discord.Gateway.Events;
internal static class DiscordMessageTypes
{
public const int Default = 0;
public const int UserJoin = 7;
}
@@ -1,3 +1,4 @@
namespace StevesBot.Worker.Discord.Gateway.Events;
internal sealed record MessageCreateDiscordEvent : DispatchDiscordEvent
@@ -0,0 +1,53 @@
using StevesBot.Library.Gemini;
namespace StevesBot.Worker.Handlers;
internal static class TaggedMessageHandler
{
public static async Task HandleAsync(
DiscordEvent discordEvent,
IServiceProvider serviceProvider,
CancellationToken cancellationToken = default
)
{
var logger = serviceProvider.GetRequiredService<ILogger<IDiscordGatewayClient>>();
var discordRestClient = serviceProvider.GetRequiredService<IDiscordRestClient>();
var geminiClient = serviceProvider.GetRequiredService<IGeminiClient>();
if (
discordEvent is not MessageCreateDiscordEvent mcde ||
mcde.IsMessageType(DiscordMessageTypes.Default) is false
)
{
return;
}
var botUser = await discordRestClient.GetMeAsync(cancellationToken);
if (mcde.Data.MentionsUser(botUser.Id) is false)
{
return;
}
logger.LogInformation("Bot tagged in message");
// TODO: Get message content from discord
// event
var llmResponse = await geminiClient.GenerateContentAsync("Hello", cancellationToken);
var request = new CreateMessageRequest(
Content: llmResponse,
MessageReference: new(
Type: DiscordMessageReferenceTypes.Default,
MessageId: mcde.Data.Id,
ChannelId: mcde.Data.ChannelId,
GuildId: mcde.Data.GuildId,
FailIfNotExists: false
)
);
var message = await discordRestClient.CreateMessageAsync(mcde.Data.ChannelId, request, cancellationToken);
logger.LogInformation("Responded to tagged message with Id: {MessageId} for user: {UserId}", message.Id, mcde.Data.Author.Id);
}
}
+4 -2
View File
@@ -18,8 +18,10 @@ builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddDiscordRestClient();
builder.Services.AddDiscordGatewayClient(static (client) =>
client.On(DiscordEventTypes.MessageCreate, WelcomeMessageHandler.HandleAsync)
);
{
client.On(DiscordEventTypes.MessageCreate, WelcomeMessageHandler.HandleAsync);
client.On(DiscordEventTypes.MessageCreate, TaggedMessageHandler.HandleAsync);
});
builder.Services.AddHostedService<Worker>();