Skip to content

Commit

Permalink
Migrate promotions to slash commands/components (#991)
Browse files Browse the repository at this point in the history
  • Loading branch information
Scott-Caldwell authored Jul 26, 2023
1 parent ffa5445 commit cf8ec3d
Show file tree
Hide file tree
Showing 19 changed files with 1,978 additions and 416 deletions.
11 changes: 11 additions & 0 deletions Modix.Bot/Attributes/EphemeralErrorsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

namespace Modix.Bot.Attributes;

/// <summary>
/// Indicates that error responses generated by this interaction should be ephemeral.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class EphemeralErrorsAttribute : Attribute
{
}
74 changes: 53 additions & 21 deletions Modix.Bot/Behaviors/InteractionListeningBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ private async Task ExecuteInteractionAsync(SocketInteraction interaction, IInter
var stopwatch = Stopwatch.StartNew();

string interactionName;
var isDeferred = false;
var defer = false;
var requiresAuthentication = true;
var ephemeralErrors = false;

switch (interaction)
{
Expand All @@ -66,32 +67,33 @@ private async Task ExecuteInteractionAsync(SocketInteraction interaction, IInter
_ => null,
};

if (commandInfo is not null)
{
if (!commandInfo.Attributes.Any(x => x is DoNotDeferAttribute))
{
isDeferred = true;
await command.DeferAsync(ephemeral: command is not ISlashCommandInteraction);
}
}
else
{
Log.Error("Could not find command {Name}.", command.CommandName);
isDeferred = true;
await command.DeferAsync();
}

interactionName = commandInfo?.ToString() ?? command.CommandName;
(defer, ephemeralErrors) = GetCommandInfoData(commandInfo, interactionName);

if (defer)
await command.DeferAsync(ephemeral: command is not ISlashCommandInteraction);
break;
case SocketModal modal:
await interaction.DeferAsync();
isDeferred = true;
interactionName = modal.Data.CustomId;
var modalBaseCustomId = GetBaseCustomId(modal.Data.CustomId);
var modalCommandInfo = _interactionService.ModalCommands.FirstOrDefault(x => GetBaseCustomId(x.Name) == modalBaseCustomId);
(defer, ephemeralErrors) = GetCommandInfoData(modalCommandInfo, interactionName);

if (defer)
await modal.DeferAsync();
break;
case SocketAutocompleteInteraction autocomplete:
requiresAuthentication = false;
interactionName = $"Autocomplete for {autocomplete.Data.CommandName} command";
break;
case SocketMessageComponent messageComponent:
interactionName = messageComponent.Data.CustomId;
var messageComponentInfo = _interactionService.SearchComponentCommand(messageComponent).Command;
(defer, ephemeralErrors) = GetCommandInfoData(messageComponentInfo, interactionName);

if (defer)
await messageComponent.DeferAsync();
break;
default:
interactionName = interaction.Type.ToString();
break;
Expand All @@ -106,18 +108,48 @@ private async Task ExecuteInteractionAsync(SocketInteraction interaction, IInter
{
var errorMessage = $"Error: {result.ErrorReason}";

if (isDeferred)
if (defer)
{
await context.Interaction.FollowupAsync(errorMessage, allowedMentions: AllowedMentions.None);
await context.Interaction.FollowupAsync(errorMessage, ephemeral: ephemeralErrors, allowedMentions: AllowedMentions.None);
}
else
{
await context.Interaction.RespondAsync(errorMessage, allowedMentions: AllowedMentions.None);
await context.Interaction.RespondAsync(errorMessage, ephemeral: ephemeralErrors, allowedMentions: AllowedMentions.None);
}
}

stopwatch.Stop();
Log.Information("Interaction took {Duration}ms to process: {Name}", stopwatch.ElapsedMilliseconds, interactionName);

static (bool defer, bool ephemeralErrors) GetCommandInfoData(ICommandInfo? commandInfo, string commandName)
{
var (defer, ephemeralErrors) = (false, commandInfo is ComponentCommandInfo);

if (commandInfo is not null)
{
if (commandInfo.Attributes.Any(x => x is EphemeralErrorsAttribute))
ephemeralErrors = true;

if (!commandInfo.Attributes.Any(x => x is DoNotDeferAttribute))
defer = true;
}
else
{
Log.Error("Could not find command {Name}.", commandName);
defer = true;
}

return (defer, ephemeralErrors);
}

static string GetBaseCustomId(string customId)
{
var colonIndex = customId.IndexOf(':');

return colonIndex > -1
? customId[..colonIndex]
: customId;
}
}
}
}
24 changes: 14 additions & 10 deletions Modix.Bot/Behaviors/PromotionLoggingHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ private async Task<string> FormatPromotionLogEntryAsync(long promotionActionId)
if (!_logRenderTemplates.TryGetValue(key, out var renderTemplate))
return null;

return string.Format(renderTemplate,
var logMessage = string.Format(renderTemplate,
promotionAction.Created.UtcDateTime.ToString("HH:mm:ss"),
promotionAction.Campaign?.Id,
promotionAction.Campaign?.Subject.GetFullUsername(),
Expand All @@ -123,8 +123,12 @@ private async Task<string> FormatPromotionLogEntryAsync(long promotionActionId)
promotionAction.NewComment?.Campaign.Subject.GetFullUsername(),
promotionAction.NewComment?.Campaign.Subject.Id,
promotionAction.NewComment?.Campaign.TargetRole.Name,
promotionAction.NewComment?.Campaign.TargetRole.Id,
promotionAction.NewComment?.Content);
promotionAction.NewComment?.Campaign.TargetRole.Id);

if (!string.IsNullOrWhiteSpace(promotionAction.NewComment?.Content))
logMessage += $" ```{promotionAction.NewComment.Content}```";

return logMessage;
}

/// <summary>
Expand Down Expand Up @@ -158,15 +162,15 @@ private async Task<string> FormatPromotionLogEntryAsync(long promotionActionId)
internal protected ModixConfig ModixConfig { get; }

private static readonly Dictionary<(PromotionActionType, PromotionSentiment?, PromotionCampaignOutcome?), string> _logRenderTemplates
= new Dictionary<(PromotionActionType, PromotionSentiment?, PromotionCampaignOutcome?), string>()
= new()
{
{ (PromotionActionType.CampaignCreated, null, null), "`[{0}]` A campaign (`{1}`) was created to promote **{2}** (`{3}`) to **{4}** (`{5}`)." },
{ (PromotionActionType.CommentCreated, PromotionSentiment.Abstain, null), "`[{0}]` A comment was added to the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), abstaining from the campaign. ```{11}```" },
{ (PromotionActionType.CommentCreated, PromotionSentiment.Approve, null), "`[{0}]` A comment was added to the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), approving of the promotion. ```{11}```" },
{ (PromotionActionType.CommentCreated, PromotionSentiment.Oppose, null), "`[{0}]` A comment was added to the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), opposing the promotion. ```{11}```" },
{ (PromotionActionType.CommentModified, PromotionSentiment.Abstain, null), "`[{0}]` A comment was modified in the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), abstaining from the campaign. ```{11}```" },
{ (PromotionActionType.CommentModified, PromotionSentiment.Approve, null), "`[{0}]` A comment was modified in the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), approving of the promotion. ```{11}```" },
{ (PromotionActionType.CommentModified, PromotionSentiment.Oppose, null), "`[{0}]` A comment was modified in the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), opposing the promotion. ```{11}```" },
{ (PromotionActionType.CommentCreated, PromotionSentiment.Abstain, null), "`[{0}]` A comment was added to the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), abstaining from the campaign." },
{ (PromotionActionType.CommentCreated, PromotionSentiment.Approve, null), "`[{0}]` A comment was added to the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), approving of the promotion." },
{ (PromotionActionType.CommentCreated, PromotionSentiment.Oppose, null), "`[{0}]` A comment was added to the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), opposing the promotion." },
{ (PromotionActionType.CommentModified, PromotionSentiment.Abstain, null), "`[{0}]` A comment was modified in the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), abstaining from the campaign." },
{ (PromotionActionType.CommentModified, PromotionSentiment.Approve, null), "`[{0}]` A comment was modified in the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), approving of the promotion." },
{ (PromotionActionType.CommentModified, PromotionSentiment.Oppose, null), "`[{0}]` A comment was modified in the campaign (`{6}`) to promote **{7}** (`{8}`) to **{9}** (`{10}`), opposing the promotion." },
{ (PromotionActionType.CampaignClosed, null, PromotionCampaignOutcome.Accepted), "`[{0}]` The campaign (`{1}`) to promote **{2}** (`{3}`) to **{4}** (`{5}`) was accepted." },
{ (PromotionActionType.CampaignClosed, null, PromotionCampaignOutcome.Rejected), "`[{0}]` The campaign (`{1}`) to promote **{2}** (`{3}`) to **{4}** (`{5}`) was rejected." },
{ (PromotionActionType.CampaignClosed, null, PromotionCampaignOutcome.Failed), "`[{0}]` The campaign (`{1}`) to promote **{2}** (`{3}`) to **{4}** (`{5}`) failed to process." },
Expand Down
37 changes: 36 additions & 1 deletion Modix.Bot/Extensions/ContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;

Expand All @@ -14,7 +15,7 @@ public static class ContextExtensions
private static readonly Emoji _checkmarkEmoji = new("✅");
private static readonly Emoji _xEmoji = new("❌");

private const int ConfirmationTimeoutSeconds = 10;
private const int ConfirmationTimeoutSeconds = 30;

public static async Task AddConfirmationAsync(this ICommandContext context)
{
Expand Down Expand Up @@ -99,5 +100,39 @@ async Task RemoveReactionsAndUpdateMessage(string bottomMessage)
await confirmationMessage.ModifyAsync(m => m.Content = mainMessage + bottomMessage);
}
}

public static async Task GetUserConfirmationAsync(this IInteractionContext context, string message, string customIdSuffix)
{
message = $"{message}\nRespond in the next {ConfirmationTimeoutSeconds} seconds to finalize or cancel the operation.";

var embedBuilder = new EmbedBuilder()
.WithTitle("Confirmation")
.WithDescription(message);

var componentBuilder = new ComponentBuilder()
.WithButton("Confirm", style: ButtonStyle.Success, customId: $"button_confirm_{customIdSuffix}")
.WithButton("Cancel", style: ButtonStyle.Danger, customId: $"button_cancel_{customIdSuffix}");

var confirmationMessage = await context.Interaction.FollowupAsync(embed: embedBuilder.Build(), components: componentBuilder.Build());

_confirmationDialogs.TryAdd(confirmationMessage.Id, default);

await Task.Delay(TimeSpan.FromSeconds(ConfirmationTimeoutSeconds));

if (_confirmationDialogs.TryRemove(confirmationMessage.Id, out _))
{
await confirmationMessage.ModifyAsync(x =>
{
x.Content = "\\❌ Canceled.";
x.Embeds = null;
x.Components = null;
});
}
}

public static void StopMonitoringConfirmationDialog(this IInteractionContext context, IUserMessage dialogMessage)
=> _confirmationDialogs.TryRemove(dialogMessage.Id, out _);

private static readonly ConcurrentDictionary<ulong, byte> _confirmationDialogs = new();
}
}
1 change: 1 addition & 0 deletions Modix.Bot/ModixBot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
Log.LogInformation("Loaded {SlashCommands} slash commands.", modules.SelectMany(x => x.SlashCommands).Count());
Log.LogInformation("Loaded {ContextCommands} context commands.", modules.SelectMany(x => x.ContextCommands).Count());
Log.LogInformation("Loaded {ModalCommands} modal commands.", modules.SelectMany(x => x.ModalCommands).Count());
Log.LogInformation("Loaded {ComponentCommands} component commands.", modules.SelectMany(x => x.ComponentCommands).Count());

await Task.Delay(-1);
}
Expand Down
Loading

0 comments on commit cf8ec3d

Please sign in to comment.