feat: wip on allow bot to respond to tagged messages
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user