Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add precondition attributes for lyrics analyzer commands #136

Merged
merged 3 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using MuzakBot.App.Extensions;
using MuzakBot.App.Handlers;
using MuzakBot.App.Models.Responses;
using MuzakBot.App.Preconditions.LyricsAnalyzer;
using MuzakBot.App.Services;
using MuzakBot.Lib;
using MuzakBot.Lib.Models.AppleMusic;
Expand All @@ -35,6 +36,8 @@ public partial class LyricsAnalyzerCommandModule
/// <exception cref="NullReferenceException"></exception>
[CommandContextType(InteractionContextType.BotDm, InteractionContextType.PrivateChannel, InteractionContextType.Guild)]
[IntegrationType(ApplicationIntegrationType.UserInstall, ApplicationIntegrationType.GuildInstall)]
[RequireUserRateLimit]
[RequireLyricsAnalyzerEnabledForServer]
[SlashCommand(
name: "lyricsanalyzer",
description: "Get an analysis of the lyrics of a song"
Expand Down Expand Up @@ -86,74 +89,6 @@ await FollowupAsync(
return;
}

// Check if the command is enabled for the current server.
if (!Context.Interaction.IsDMInteraction && lyricsAnalyzerConfig.CommandIsEnabledToSpecificGuilds && lyricsAnalyzerConfig.CommandEnabledGuildIds is not null && !lyricsAnalyzerConfig.CommandEnabledGuildIds.Contains(Context.Guild.Id))
{
await FollowupAsync(
embed: GenerateErrorEmbed("This command is not enabled on this server. 😥").Build(),
components: GenerateRemoveComponent().Build(),
ephemeral: isPrivateResponse
);

return;
}

// Check if the command is disabled for the current server.
if (!Context.Interaction.IsDMInteraction && !lyricsAnalyzerConfig.CommandIsEnabledToSpecificGuilds && lyricsAnalyzerConfig.CommandDisabledGuildIds is not null && lyricsAnalyzerConfig.CommandDisabledGuildIds.Contains(Context.Guild.Id))
{
await FollowupAsync(
embed: GenerateErrorEmbed("This command is not enabled on this server. 😥").Build(),
components: GenerateRemoveComponent().Build(),
ephemeral: isPrivateResponse
);

return;
}

// Check if the current user is on the rate limit ignore list.
bool userIsOnRateLimitIgnoreList = lyricsAnalyzerConfig.RateLimitIgnoredUserIds is not null && lyricsAnalyzerConfig.RateLimitIgnoredUserIds.Contains(Context.User.Id.ToString());

// Get the current rate limit for the user.
LyricsAnalyzerUserRateLimit? lyricsAnalyzerUserRateLimit = null;
if (lyricsAnalyzerConfig.RateLimitEnabled && !userIsOnRateLimitIgnoreList)
{
_logger.LogInformation("Getting current rate limit for user '{UserId}' from database.", Context.User.Id);
lyricsAnalyzerUserRateLimit = await dbContext.LyricsAnalyzerUserRateLimits
.WithPartitionKey("user-item")
.FirstOrDefaultAsync(item => item.UserId == Context.User.Id.ToString());

if (lyricsAnalyzerUserRateLimit is null)
{
lyricsAnalyzerUserRateLimit = new(Context.User.Id.ToString());

dbContext.LyricsAnalyzerUserRateLimits.Add(lyricsAnalyzerUserRateLimit);

await dbContext.SaveChangesAsync();
}

_logger.LogInformation("Current rate limit for user '{UserId}' is {CurrentRequestCount}/{MaxRequests}.", Context.User.Id, lyricsAnalyzerUserRateLimit.CurrentRequestCount, lyricsAnalyzerConfig.RateLimitMaxRequests);

// If the user has reached the rate limit, send a message and return.
if (!lyricsAnalyzerUserRateLimit.EvaluateRequest(lyricsAnalyzerConfig.RateLimitMaxRequests))
{
_logger.LogInformation("User '{UserId}' has reached the rate limit.", Context.User.Id);

StringBuilder rateLimitMessageBuilder = new($"You have reached the rate limit ({lyricsAnalyzerConfig.RateLimitMaxRequests} requests) for this command. 😥\n\n");

DateTimeOffset resetTime = lyricsAnalyzerUserRateLimit.LastRequestTime.AddDays(1);

rateLimitMessageBuilder.AppendLine($"Rate limit will reset <t:{resetTime.ToUnixTimeSeconds()}:R>.");

await FollowupAsync(
embed: GenerateErrorEmbed(rateLimitMessageBuilder.ToString()).Build(),
components: GenerateRemoveComponent().Build(),
ephemeral: isPrivateResponse
);

return;
}
}

// Get the prompt style from the database.
LyricsAnalyzerPromptStyle promptStyle;
try
Expand Down Expand Up @@ -355,6 +290,20 @@ await FollowupAsync(
ephemeral: isPrivateResponse
);

bool userIsOnRateLimitIgnoreList = lyricsAnalyzerConfig.RateLimitIgnoredUserIds is not null && lyricsAnalyzerConfig.RateLimitIgnoredUserIds.Contains(Context.User.Id.ToString());
LyricsAnalyzerUserRateLimit? lyricsAnalyzerUserRateLimit = await dbContext.LyricsAnalyzerUserRateLimits
.WithPartitionKey("user-item")
.FirstOrDefaultAsync(item => item.UserId == Context.User.Id.ToString());

if (lyricsAnalyzerUserRateLimit is null)
{
lyricsAnalyzerUserRateLimit = new(Context.User.Id.ToString());

dbContext.LyricsAnalyzerUserRateLimits.Add(lyricsAnalyzerUserRateLimit);

await dbContext.SaveChangesAsync();
}

if (lyricsAnalyzerConfig.RateLimitEnabled && !userIsOnRateLimitIgnoreList)
{
_logger.LogInformation("Updating rate limit for user '{UserId}' in database.", Context.User.Id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.Extensions.Logging;

using MuzakBot.App.Models.Responses;
using MuzakBot.App.Preconditions.LyricsAnalyzer;
using MuzakBot.Lib;
using MuzakBot.Lib.Models.Database.LyricsAnalyzer;
using MuzakBot.Lib.Models.OpenAi;
Expand All @@ -23,6 +24,8 @@ public partial class LyricsAnalyzerCommandModule
/// <param name="responseId">The ID of the response to regenerate.</param>
/// <returns></returns>
/// <exception cref="NullReferenceException">Thrown when the lyrics analyzer item is not found in the database.</exception>
[RequireUserRateLimit]
[RequireLyricsAnalyzerEnabledForServer]
[ComponentInteraction("lyrics-analyzer-regenerate-*")]
private async Task HandleLyricsAnalyzerRegenerateAsync(string responseId)
{
Expand Down Expand Up @@ -71,50 +74,6 @@ await componentInteraction.FollowupAsync(
return;
}

// Check if the current user is on the rate limit ignore list.
bool userIsOnRateLimitIgnoreList = lyricsAnalyzerConfig.RateLimitIgnoredUserIds is not null && lyricsAnalyzerConfig.RateLimitIgnoredUserIds.Contains(Context.User.Id.ToString());

// Get the current rate limit for the user.
LyricsAnalyzerUserRateLimit? lyricsAnalyzerUserRateLimit = null;
if (lyricsAnalyzerConfig.RateLimitEnabled && !userIsOnRateLimitIgnoreList)
{
_logger.LogInformation("Getting current rate limit for user '{UserId}' from database.", Context.User.Id);
lyricsAnalyzerUserRateLimit = await dbContext.LyricsAnalyzerUserRateLimits
.WithPartitionKey("user-item")
.FirstOrDefaultAsync(item => item.UserId == Context.User.Id.ToString());

if (lyricsAnalyzerUserRateLimit is null)
{
lyricsAnalyzerUserRateLimit = new(Context.User.Id.ToString());

dbContext.LyricsAnalyzerUserRateLimits.Add(lyricsAnalyzerUserRateLimit);

await dbContext.SaveChangesAsync();
}

_logger.LogInformation("Current rate limit for user '{UserId}' is {CurrentRequestCount}/{MaxRequests}.", Context.User.Id, lyricsAnalyzerUserRateLimit.CurrentRequestCount, lyricsAnalyzerConfig.RateLimitMaxRequests);

// If the user has reached the rate limit, send a message and return.
if (!lyricsAnalyzerUserRateLimit.EvaluateRequest(lyricsAnalyzerConfig.RateLimitMaxRequests))
{
_logger.LogInformation("User '{UserId}' has reached the rate limit.", Context.User.Id);

StringBuilder rateLimitMessageBuilder = new($"You have reached the rate limit ({lyricsAnalyzerConfig.RateLimitMaxRequests} requests) for this command. 😥\n\n");

DateTimeOffset resetTime = lyricsAnalyzerUserRateLimit.LastRequestTime.AddDays(1);

rateLimitMessageBuilder.AppendLine($"Rate limit will reset <t:{resetTime.ToUnixTimeSeconds()}:R>.");

await componentInteraction.FollowupAsync(
embed: GenerateErrorEmbed(rateLimitMessageBuilder.ToString()).Build(),
components: GenerateRemoveComponent().Build(),
ephemeral: isEphemeral
);

return;
}
}

// Get the lyrics analyzer item from the database.
_logger.LogInformation("Getting lyrics analyzer item from database.");
LyricsAnalyzerItem? lyricsAnalyzerItem;
Expand Down Expand Up @@ -282,6 +241,20 @@ await componentInteraction.FollowupAsync(
ephemeral: isEphemeral
);

bool userIsOnRateLimitIgnoreList = lyricsAnalyzerConfig.RateLimitIgnoredUserIds is not null && lyricsAnalyzerConfig.RateLimitIgnoredUserIds.Contains(Context.User.Id.ToString());
LyricsAnalyzerUserRateLimit? lyricsAnalyzerUserRateLimit = await dbContext.LyricsAnalyzerUserRateLimits
.WithPartitionKey("user-item")
.FirstOrDefaultAsync(item => item.UserId == Context.User.Id.ToString());

if (lyricsAnalyzerUserRateLimit is null)
{
lyricsAnalyzerUserRateLimit = new(Context.User.Id.ToString());

dbContext.LyricsAnalyzerUserRateLimits.Add(lyricsAnalyzerUserRateLimit);

await dbContext.SaveChangesAsync();
}

if (lyricsAnalyzerConfig.RateLimitEnabled && !userIsOnRateLimitIgnoreList)
{
_logger.LogInformation("Updating rate limit for user '{UserId}' in database.", Context.User.Id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Discord;
using Discord.Interactions;

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

using MuzakBot.Database;
using MuzakBot.Lib.Models.Database.LyricsAnalyzer;

namespace MuzakBot.App.Preconditions.LyricsAnalyzer;

/// <summary>
/// Precondition attribute to check if the '/lyricsanalyzer' command is enabled for the current server.
/// </summary>
public class RequireLyricsAnalyzerEnabledForServerAttribute : PreconditionAttribute
{
/// <inheritdoc />
public override async Task<PreconditionResult> CheckRequirementsAsync(IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services)
{
var lyricsAnalyzerDbContextFactory = services.GetRequiredService<IDbContextFactory<LyricsAnalyzerDbContext>>();
var logger = services.GetRequiredService<ILogger<RequireLyricsAnalyzerEnabledForServerAttribute>>();

using var dbContext = lyricsAnalyzerDbContextFactory.CreateDbContext();

LyricsAnalyzerConfig lyricsAnalyzerConfig;
try
{
lyricsAnalyzerConfig = await dbContext.LyricsAnalyzerConfigs
.WithPartitionKey("lyricsanalyzer-config")
.FirstAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "Error getting lyrics analyzer config from database.");

EmbedBuilder rateLimitExceededEmbed = new EmbedBuilder()
.WithTitle("💥 An error occurred")
.WithDescription("An error occurred while checking your rate limit. Please try again later. 😥")
.WithColor(Color.Red);

return PreconditionResult.FromError(ex);
}

// Check if the command is enabled for the current server.
if (!context.Interaction.IsDMInteraction && lyricsAnalyzerConfig.CommandIsEnabledToSpecificGuilds && lyricsAnalyzerConfig.CommandEnabledGuildIds is not null && !lyricsAnalyzerConfig.CommandEnabledGuildIds.Contains(context.Guild.Id))
{
EmbedBuilder notEnabledEmbed = new EmbedBuilder()
.WithTitle("💥 An error occurred")
.WithDescription("This command is not enabled on this server. 😥")
.WithColor(Color.Red);

await context.Interaction.RespondAsync(
embed: notEnabledEmbed.Build(),
ephemeral: true
);

return PreconditionResult.FromError("The '/lyricsanalyzer' command is not enabled for this server.");
}

// Check if the command is disabled for the current server.
if (!context.Interaction.IsDMInteraction && !lyricsAnalyzerConfig.CommandIsEnabledToSpecificGuilds && lyricsAnalyzerConfig.CommandDisabledGuildIds is not null && lyricsAnalyzerConfig.CommandDisabledGuildIds.Contains(context.Guild.Id))
{
EmbedBuilder notEnabledEmbed = new EmbedBuilder()
.WithTitle("💥 An error occurred")
.WithDescription("This command is not enabled on this server. 😥")
.WithColor(Color.Red);

await context.Interaction.RespondAsync(
embed: notEnabledEmbed.Build(),
ephemeral: true
);

return PreconditionResult.FromError("The '/lyricsanalyzer' command is not enabled for this server.");
}

return PreconditionResult.FromSuccess();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Text;

using Discord;
using Discord.Interactions;

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

using MuzakBot.Database;
using MuzakBot.Lib.Models.Database.LyricsAnalyzer;

namespace MuzakBot.App.Preconditions.LyricsAnalyzer;

/// <summary>
/// Precondition attribute to check if the user has reached the rate limit for the '/lyricsanalyzer' command.
/// </summary>
public class RequireUserRateLimitAttribute : PreconditionAttribute
{
/// <inheritdoc />
public override async Task<PreconditionResult> CheckRequirementsAsync(IInteractionContext context, ICommandInfo commandInfo, IServiceProvider services)
{
var lyricsAnalyzerDbContextFactory = services.GetRequiredService<IDbContextFactory<LyricsAnalyzerDbContext>>();
var logger = services.GetRequiredService<ILogger<RequireUserRateLimitAttribute>>();

using var dbContext = lyricsAnalyzerDbContextFactory.CreateDbContext();

LyricsAnalyzerConfig lyricsAnalyzerConfig;
try
{
lyricsAnalyzerConfig = await dbContext.LyricsAnalyzerConfigs
.WithPartitionKey("lyricsanalyzer-config")
.FirstAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "Error getting lyrics analyzer config from database.");

EmbedBuilder rateLimitExceededEmbed = new EmbedBuilder()
.WithTitle("💥 An error occurred")
.WithDescription("An error occurred while checking your rate limit. Please try again later. 😥")
.WithColor(Color.Red);

return PreconditionResult.FromError(ex);
}

// Check if the current user is on the rate limit ignore list.
bool userIsOnRateLimitIgnoreList = lyricsAnalyzerConfig.RateLimitIgnoredUserIds is not null && lyricsAnalyzerConfig.RateLimitIgnoredUserIds.Contains(context.User.Id.ToString());

// Get the current rate limit for the user.
LyricsAnalyzerUserRateLimit? lyricsAnalyzerUserRateLimit = null;
if (lyricsAnalyzerConfig.RateLimitEnabled && !userIsOnRateLimitIgnoreList)
{
logger.LogInformation("Getting current rate limit for user '{UserId}' from database.", context.User.Id);
lyricsAnalyzerUserRateLimit = await dbContext.LyricsAnalyzerUserRateLimits
.WithPartitionKey("user-item")
.FirstOrDefaultAsync(item => item.UserId == context.User.Id.ToString());

if (lyricsAnalyzerUserRateLimit is null)
{
lyricsAnalyzerUserRateLimit = new(context.User.Id.ToString());

dbContext.LyricsAnalyzerUserRateLimits.Add(lyricsAnalyzerUserRateLimit);

await dbContext.SaveChangesAsync();
}

logger.LogInformation("Current rate limit for user '{UserId}' is {CurrentRequestCount}/{MaxRequests}.", context.User.Id, lyricsAnalyzerUserRateLimit.CurrentRequestCount, lyricsAnalyzerConfig.RateLimitMaxRequests);

// If the user has reached the rate limit, send a message and return.
if (!lyricsAnalyzerUserRateLimit.EvaluateRequest(lyricsAnalyzerConfig.RateLimitMaxRequests))
{
logger.LogInformation("User '{UserId}' has reached the rate limit.", context.User.Id);

StringBuilder rateLimitMessageBuilder = new($"You have reached the rate limit ({lyricsAnalyzerConfig.RateLimitMaxRequests} requests) for this command. 😥\n\n");

DateTimeOffset resetTime = lyricsAnalyzerUserRateLimit.LastRequestTime.AddDays(1);

rateLimitMessageBuilder.AppendLine($"Rate limit will reset <t:{resetTime.ToUnixTimeSeconds()}:R>.");

EmbedBuilder rateLimitExceededEmbed = new EmbedBuilder()
.WithTitle("💥 An error occurred")
.WithDescription(rateLimitMessageBuilder.ToString())
.WithColor(Color.Red);

await context.Interaction.RespondAsync(
embed: rateLimitExceededEmbed.Build(),
ephemeral: true
);

return PreconditionResult.FromError("User has reached the rate limit for this command.");
}
}

return PreconditionResult.FromSuccess();
}
}