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")]
|
[JsonPropertyName("author")]
|
||||||
public DiscordUser Author { get; init; } = new DiscordUser();
|
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
|
HttpClient httpClient
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
ArgumentNullException.ThrowIfNull(logger, nameof(logger));
|
||||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
ArgumentNullException.ThrowIfNull(httpClient, nameof(httpClient));
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
_httpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken = default)
|
public async Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken = default)
|
||||||
@@ -65,4 +68,26 @@ public sealed class DiscordRestClient : IDiscordRestClient
|
|||||||
|
|
||||||
return discordMessage;
|
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<string> GetGatewayUrlAsync(CancellationToken cancellationToken = default);
|
||||||
Task<DiscordMessage> CreateMessageAsync(string channelId, CreateMessageRequest request, 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.Common;
|
||||||
using StevesBot.Library.Discord.Rest;
|
using StevesBot.Library.Discord.Rest;
|
||||||
|
using StevesBot.Library.Gemini;
|
||||||
using StevesBot.Library.Telemetry;
|
using StevesBot.Library.Telemetry;
|
||||||
|
|
||||||
namespace StevesBot.Library.Discord;
|
namespace StevesBot.Library.Discord;
|
||||||
@@ -28,4 +29,15 @@ public static class ServicesExtensions
|
|||||||
return services;
|
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
|
internal static class DiscordMessageTypes
|
||||||
{
|
{
|
||||||
|
public const int Default = 0;
|
||||||
public const int UserJoin = 7;
|
public const int UserJoin = 7;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
namespace StevesBot.Worker.Discord.Gateway.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal sealed record MessageCreateDiscordEvent : DispatchDiscordEvent
|
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.AddDiscordRestClient();
|
||||||
|
|
||||||
builder.Services.AddDiscordGatewayClient(static (client) =>
|
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>();
|
builder.Services.AddHostedService<Worker>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user