Merge pull request #368 from spiralw/feat/yes-no-buttons

Use buttons for all yes/no prompts
This commit is contained in:
Astrid 2021-07-14 18:26:00 +02:00 committed by GitHub
commit 467d0746bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 120 additions and 77 deletions

View File

@ -18,5 +18,8 @@ namespace Myriad.Rest.Types.Requests
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<AllowedMentions> AllowedMentions { get; init; } public Optional<AllowedMentions> AllowedMentions { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<MessageComponent[]?> Components { get; init; }
} }
} }

View File

@ -51,7 +51,7 @@ namespace PluralKit.Bot
var existingGroup = await _repo.GetGroupByName(conn, ctx.System.Id, groupName); var existingGroup = await _repo.GetGroupByName(conn, ctx.System.Id, groupName);
if (existingGroup != null) { if (existingGroup != null) {
var msg = $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to create another group with the same name?"; var msg = $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to create another group with the same name?";
if (!await ctx.PromptYesNo(msg)) if (!await ctx.PromptYesNo(msg, "Create"))
throw new PKError("Group creation cancelled."); throw new PKError("Group creation cancelled.");
} }
@ -81,7 +81,7 @@ namespace PluralKit.Bot
var existingGroup = await _repo.GetGroupByName(conn, ctx.System.Id, newName); var existingGroup = await _repo.GetGroupByName(conn, ctx.System.Id, newName);
if (existingGroup != null && existingGroup.Id != target.Id) { if (existingGroup != null && existingGroup.Id != target.Id) {
var msg = $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to rename this member to that name too?"; var msg = $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to rename this member to that name too?";
if (!await ctx.PromptYesNo(msg)) if (!await ctx.PromptYesNo(msg, "Rename"))
throw new PKError("Group creation cancelled."); throw new PKError("Group creation cancelled.");
} }

View File

@ -73,7 +73,7 @@ namespace PluralKit.Bot
if (data.LinkedAccounts != null && !data.LinkedAccounts.Contains(ctx.Author.Id)) if (data.LinkedAccounts != null && !data.LinkedAccounts.Contains(ctx.Author.Id))
{ {
var msg = $"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?"; var msg = $"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?";
if (!await ctx.PromptYesNo(msg)) throw Errors.ImportCancelled; if (!await ctx.PromptYesNo(msg, "Import")) throw Errors.ImportCancelled;
} }
// If passed system is null, it'll create a new one // If passed system is null, it'll create a new one
@ -120,7 +120,7 @@ namespace PluralKit.Bot
issueStr += "\n- PluralKit does not support per-member system tags. Since you had multiple members with distinct tags, those tags will be applied to the members' *display names*/nicknames instead."; issueStr += "\n- PluralKit does not support per-member system tags. Since you had multiple members with distinct tags, those tags will be applied to the members' *display names*/nicknames instead.";
var msg = $"{issueStr}\n\nDo you want to proceed with the import?"; var msg = $"{issueStr}\n\nDo you want to proceed with the import?";
if (!await ctx.PromptYesNo(msg)) if (!await ctx.PromptYesNo(msg, "Proceed"))
throw Errors.ImportCancelled; throw Errors.ImportCancelled;
} }

View File

@ -41,7 +41,7 @@ namespace PluralKit.Bot
var existingMember = await _db.Execute(c => _repo.GetMemberByName(c, ctx.System.Id, memberName)); var existingMember = await _db.Execute(c => _repo.GetMemberByName(c, ctx.System.Id, memberName));
if (existingMember != null) { if (existingMember != null) {
var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?"; var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?";
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member creation cancelled."); if (!await ctx.PromptYesNo(msg, "Create")) throw new PKError("Member creation cancelled.");
} }
await using var conn = await _db.Obtain(); await using var conn = await _db.Obtain();

View File

@ -35,7 +35,7 @@ namespace PluralKit.Bot
if (existingMember != null && existingMember.Id != target.Id) if (existingMember != null && existingMember.Id != target.Id)
{ {
var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?"; var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?";
if (!await ctx.PromptYesNo(msg)) throw new PKError("Member renaming cancelled."); if (!await ctx.PromptYesNo(msg, "Rename")) throw new PKError("Member renaming cancelled.");
} }
// Rename the member // Rename the member

View File

@ -42,7 +42,7 @@ namespace PluralKit.Bot
var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**"); var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**");
var msg = $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?"; var msg = $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?";
return await ctx.PromptYesNo(msg); return await ctx.PromptYesNo(msg, "Proceed");
} }
// "Sub"command: clear flag // "Sub"command: clear flag
@ -52,7 +52,7 @@ namespace PluralKit.Bot
if (target.ProxyTags.Count > 1) if (target.ProxyTags.Count > 1)
{ {
var msg = $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?"; var msg = $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?";
if (!await ctx.PromptYesNo(msg)) if (!await ctx.PromptYesNo(msg, "Clear"))
throw Errors.GenericCancelled(); throw Errors.GenericCancelled();
} }
@ -117,7 +117,7 @@ namespace PluralKit.Bot
if (target.ProxyTags.Count > 1) if (target.ProxyTags.Count > 1)
{ {
var msg = $"This member already has more than one proxy tag set: {target.ProxyTagsString()}\nDo you want to replace them?"; var msg = $"This member already has more than one proxy tag set: {target.ProxyTagsString()}\nDo you want to replace them?";
if (!await ctx.PromptYesNo(msg)) if (!await ctx.PromptYesNo(msg, "Replace"))
throw Errors.GenericCancelled(); throw Errors.GenericCancelled();
} }

View File

@ -92,18 +92,18 @@ namespace PluralKit.Bot
// But, we do a prompt to confirm. // But, we do a prompt to confirm.
var lastSwitchMembers = _repo.GetSwitchMembers(conn, lastTwoSwitches[0].Id); var lastSwitchMembers = _repo.GetSwitchMembers(conn, lastTwoSwitches[0].Id);
var lastSwitchMemberStr = string.Join(", ", await lastSwitchMembers.Select(m => m.NameFor(ctx)).ToListAsync()); var lastSwitchMemberStr = string.Join(", ", await lastSwitchMembers.Select(m => m.NameFor(ctx)).ToListAsync());
var lastSwitchTimeStr = lastTwoSwitches[0].Timestamp.FormatZoned(ctx.System); var lastSwitchTime = lastTwoSwitches[0].Timestamp.ToUnixTimeSeconds(); // .FormatZoned(ctx.System)
var lastSwitchDeltaStr = (SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp).FormatDuration(); var lastSwitchDeltaStr = (SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp).FormatDuration();
var newSwitchTimeStr = time.FormatZoned(); var newSwitchTime = time.ToInstant().ToUnixTimeSeconds();
var newSwitchDeltaStr = (SystemClock.Instance.GetCurrentInstant() - time.ToInstant()).FormatDuration(); var newSwitchDeltaStr = (SystemClock.Instance.GetCurrentInstant() - time.ToInstant()).FormatDuration();
// yeet // yeet
var msg = $"{Emojis.Warn} This will move the latest switch ({lastSwitchMemberStr}) from {lastSwitchTimeStr} ({lastSwitchDeltaStr} ago) to {newSwitchTimeStr} ({newSwitchDeltaStr} ago). Is this OK?"; var msg = $"{Emojis.Warn} This will move the latest switch ({lastSwitchMemberStr}) from <t:{lastSwitchTime}> ({lastSwitchDeltaStr} ago) to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago). Is this OK?";
if (!await ctx.PromptYesNo(msg)) throw Errors.SwitchMoveCancelled; if (!await ctx.PromptYesNo(msg, "Move Switch")) throw Errors.SwitchMoveCancelled;
// aaaand *now* we do the move // aaaand *now* we do the move
await _repo.MoveSwitch(conn, lastTwoSwitches[0].Id, time.ToInstant()); await _repo.MoveSwitch(conn, lastTwoSwitches[0].Id, time.ToInstant());
await ctx.Reply($"{Emojis.Success} Switch moved to {newSwitchTimeStr} ({newSwitchDeltaStr} ago)."); await ctx.Reply($"{Emojis.Success} Switch moved to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago).");
} }
public async Task SwitchDelete(Context ctx) public async Task SwitchDelete(Context ctx)
@ -114,7 +114,7 @@ namespace PluralKit.Bot
{ {
// Subcommand: "delete all" // Subcommand: "delete all"
var purgeMsg = $"{Emojis.Warn} This will delete *all registered switches* in your system. Are you sure you want to proceed?"; var purgeMsg = $"{Emojis.Warn} This will delete *all registered switches* in your system. Are you sure you want to proceed?";
if (!await ctx.PromptYesNo(purgeMsg)) if (!await ctx.PromptYesNo(purgeMsg, "Clear Switches"))
throw Errors.GenericCancelled(); throw Errors.GenericCancelled();
await _db.Execute(c => _repo.DeleteAllSwitches(c, ctx.System.Id)); await _db.Execute(c => _repo.DeleteAllSwitches(c, ctx.System.Id));
await ctx.Reply($"{Emojis.Success} Cleared system switches!"); await ctx.Reply($"{Emojis.Success} Cleared system switches!");
@ -144,7 +144,7 @@ namespace PluralKit.Bot
msg = $"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr}, {lastSwitchDeltaStr} ago). The next latest switch is {secondSwitchMemberStr} ({secondSwitchDeltaStr} ago). Is this okay?"; msg = $"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr}, {lastSwitchDeltaStr} ago). The next latest switch is {secondSwitchMemberStr} ({secondSwitchDeltaStr} ago). Is this okay?";
} }
if (!await ctx.PromptYesNo(msg)) throw Errors.SwitchDeleteCancelled; if (!await ctx.PromptYesNo(msg, "Delete Switch")) throw Errors.SwitchDeleteCancelled;
await _repo.DeleteSwitch(conn, lastTwoSwitches[0].Id); await _repo.DeleteSwitch(conn, lastTwoSwitches[0].Id);
await ctx.Reply($"{Emojis.Success} Switch deleted."); await ctx.Reply($"{Emojis.Success} Switch deleted.");

View File

@ -294,7 +294,7 @@ namespace PluralKit.Bot
var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone); var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone);
var msg = $"This will change the system time zone to **{zone.Id}**. The current time is **{currentTime.FormatZoned()}**. Is this correct?"; var msg = $"This will change the system time zone to **{zone.Id}**. The current time is **{currentTime.FormatZoned()}**. Is this correct?";
if (!await ctx.PromptYesNo(msg)) throw Errors.TimezoneChangeCancelled; if (!await ctx.PromptYesNo(msg, "Change Timezone")) throw Errors.TimezoneChangeCancelled;
var patch = new SystemPatch {UiTz = zone.Id}; var patch = new SystemPatch {UiTz = zone.Id};
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch)); await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));

View File

@ -34,9 +34,8 @@ namespace PluralKit.Bot
if (existingAccount != null) if (existingAccount != null)
throw Errors.AccountInOtherSystem(existingAccount); throw Errors.AccountInOtherSystem(existingAccount);
var msg = $"{account.Mention()}, please confirm the link by clicking the {Emojis.Success} reaction on this message."; var msg = $"{account.Mention()}, please confirm the link.";
var mentions = new AllowedMentions {Users = new[] {account.Id}}; if (!await ctx.PromptYesNo(msg, "Confirm", user: account, matchFlag: false)) throw Errors.MemberLinkCancelled;
if (!await ctx.PromptYesNo(msg, user: account, mentions: mentions, matchFlag: false)) throw Errors.MemberLinkCancelled;
await _repo.AddAccount(conn, ctx.System.Id, account.Id); await _repo.AddAccount(conn, ctx.System.Id, account.Id);
await ctx.Reply($"{Emojis.Success} Account linked to system."); await ctx.Reply($"{Emojis.Success} Account linked to system.");
} }
@ -58,7 +57,7 @@ namespace PluralKit.Bot
if (accountIds.Count == 1) throw Errors.UnlinkingLastAccount; if (accountIds.Count == 1) throw Errors.UnlinkingLastAccount;
var msg = $"Are you sure you want to unlink <@{id}> from your system?"; var msg = $"Are you sure you want to unlink <@{id}> from your system?";
if (!await ctx.PromptYesNo(msg)) throw Errors.MemberUnlinkCancelled; if (!await ctx.PromptYesNo(msg, "Unlink")) throw Errors.MemberUnlinkCancelled;
await _repo.RemoveAccount(conn, ctx.System.Id, id); await _repo.RemoveAccount(conn, ctx.System.Id, id);
await ctx.Reply($"{Emojis.Success} Account unlinked."); await ctx.Reply($"{Emojis.Success} Account unlinked.");

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Autofac; using Autofac;
using Myriad.Rest.Types;
using Myriad.Rest.Types.Requests; using Myriad.Rest.Types.Requests;
using Myriad.Types; using Myriad.Types;
@ -15,10 +16,11 @@ namespace PluralKit.Bot.Interactive
{ {
public abstract class BaseInteractive public abstract class BaseInteractive
{ {
private readonly Context _ctx; protected readonly Context _ctx;
private readonly List<Button> _buttons = new(); protected readonly List<Button> _buttons = new();
private readonly TaskCompletionSource _tcs = new(); protected readonly TaskCompletionSource _tcs = new();
private bool _running; protected Message _message { get; private set; }
protected bool _running;
protected BaseInteractive(Context ctx) protected BaseInteractive(Context ctx)
{ {
@ -52,21 +54,28 @@ namespace PluralKit.Bot.Interactive
}); });
} }
protected async Task Finish(InteractionContext ctx) protected async Task Finish(InteractionContext? ctx = null)
{ {
foreach (var button in _buttons) foreach (var button in _buttons)
button.Disabled = true; button.Disabled = true;
await Update(ctx);
if (ctx != null)
await Update(ctx);
else
await _ctx.Rest.EditMessage(_message.ChannelId, _message.Id, new MessageEditRequest {
Components = GetComponents()
});
_tcs.TrySetResult(); _tcs.TrySetResult();
} }
protected async Task<Message> Send(string? content = null, Embed? embed = null) protected async Task Send(string? content = null, Embed? embed = null, AllowedMentions? mentions = null)
{ {
return await _ctx.Rest.CreateMessage(_ctx.Channel.Id, new MessageRequest _message = await _ctx.Rest.CreateMessage(_ctx.Channel.Id, new MessageRequest
{ {
Content = content, Content = content,
Embed = embed, Embed = embed,
AllowedMentions = mentions,
Components = GetComponents() Components = GetComponents()
}); });
} }
@ -113,7 +122,7 @@ namespace PluralKit.Bot.Interactive
} }
} }
private void Cleanup() protected void Cleanup()
{ {
var dispatch = _ctx.Services.Resolve<InteractionDispatchService>(); var dispatch = _ctx.Services.Resolve<InteractionDispatchService>();
foreach (var button in _buttons) foreach (var button in _buttons)

View File

@ -1,7 +1,15 @@
using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Myriad.Gateway;
using Myriad.Rest.Types;
using Myriad.Types; using Myriad.Types;
using PluralKit.Core;
using Autofac;
namespace PluralKit.Bot.Interactive namespace PluralKit.Bot.Interactive
{ {
public class YesNoPrompt: BaseInteractive public class YesNoPrompt: BaseInteractive
@ -20,7 +28,13 @@ namespace PluralKit.Bot.Interactive
{ {
AddButton(ctx => OnButtonClick(ctx, true), AcceptLabel, AcceptStyle); AddButton(ctx => OnButtonClick(ctx, true), AcceptLabel, AcceptStyle);
AddButton(ctx => OnButtonClick(ctx, false), CancelLabel, CancelStyle); AddButton(ctx => OnButtonClick(ctx, false), CancelLabel, CancelStyle);
await Send(Message);
AllowedMentions mentions = null;
if (User != _ctx.Author.Id)
mentions = new AllowedMentions {Users = new[] {User!.Value}};
await Send(Message, mentions: mentions);
} }
private async Task OnButtonClick(InteractionContext ctx, bool result) private async Task OnButtonClick(InteractionContext ctx, bool result)
@ -35,6 +49,58 @@ namespace PluralKit.Bot.Interactive
await Finish(ctx); await Finish(ctx);
} }
private bool MessagePredicate(MessageCreateEvent e)
{
if (e.ChannelId != _ctx.Channel.Id) return false;
if (e.Author.Id != User) return false;
var response = e.Content.ToLowerInvariant();
if (response == "y" || response == "yes")
{
Result = true;
return true;
}
if (response == "n" || response == "no")
{
Result = false;
return true;
}
return false;
}
public new async Task Run()
{
// todo: can we split this up somehow so it doesn't need to be *completely* copied from BaseInteractive?
var cts = new CancellationTokenSource(Timeout.ToTimeSpan());
if (_running)
throw new InvalidOperationException("Action is already running");
_running = true;
var queue = _ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>();
var messageDispatch = queue.WaitFor(MessagePredicate, Timeout, cts.Token);
await Start();
cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("Action timed out")));
try
{
var doneTask = await Task.WhenAny(_tcs.Task, messageDispatch);
if (doneTask == messageDispatch)
await Finish();
}
finally
{
Cleanup();
}
}
public YesNoPrompt(Context ctx): base(ctx) public YesNoPrompt(Context ctx): base(ctx)
{ {
User = ctx.Author.Id; User = ctx.Author.Id;

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Autofac; using Autofac;
@ -15,64 +14,31 @@ using Myriad.Types;
using NodaTime; using NodaTime;
using PluralKit.Bot.Interactive;
using PluralKit.Core; using PluralKit.Core;
namespace PluralKit.Bot { namespace PluralKit.Bot {
public static class ContextUtils { public static class ContextUtils {
public static async Task<bool> ConfirmClear(this Context ctx, string toClear) public static async Task<bool> ConfirmClear(this Context ctx, string toClear)
{ {
if (!(await ctx.PromptYesNo($"{Emojis.Warn} Are you sure you want to clear {toClear}?"))) throw Errors.GenericCancelled(); if (!(await ctx.PromptYesNo($"{Emojis.Warn} Are you sure you want to clear {toClear}?", "Clear"))) throw Errors.GenericCancelled();
else return true; else return true;
} }
public static async Task<bool> PromptYesNo(this Context ctx, string msgString, User user = null, Duration? timeout = null, AllowedMentions mentions = null, bool matchFlag = true) public static async Task<bool> PromptYesNo(this Context ctx, string msgString, string acceptButton, User user = null, bool matchFlag = true)
{ {
Message message;
if (matchFlag && ctx.MatchFlag("y", "yes")) return true; if (matchFlag && ctx.MatchFlag("y", "yes")) return true;
else message = await ctx.Reply(msgString, mentions: mentions);
var cts = new CancellationTokenSource();
if (user == null) user = ctx.Author;
if (timeout == null) timeout = Duration.FromMinutes(5);
if (!DiscordUtils.HasReactionPermissions(ctx)) var prompt = new YesNoPrompt(ctx)
await ctx.Reply($"{Emojis.Note} PluralKit does not have permissions to add reactions in this channel. \nPlease reply with 'yes' to confirm, or 'no' to cancel.");
else
// "Fork" the task adding the reactions off so we don't have to wait for them to be finished to start listening for presses
await ctx.Rest.CreateReactionsBulk(message, new[] {Emojis.Success, Emojis.Error});
bool ReactionPredicate(MessageReactionAddEvent e)
{ {
if (e.ChannelId != message.ChannelId || e.MessageId != message.Id) return false; Message = msgString,
if (e.UserId != user.Id) return false; AcceptLabel = acceptButton,
return true; User = user?.Id ?? ctx.Author.Id,
} };
bool MessagePredicate(MessageCreateEvent e) await prompt.Run();
{
if (e.ChannelId != message.ChannelId) return false;
if (e.Author.Id != user.Id) return false;
var strings = new [] {"y", "yes", "n", "no"}; return prompt.Result == true;
return strings.Any(str => string.Equals(e.Content, str, StringComparison.InvariantCultureIgnoreCase));
}
var messageTask = ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>().WaitFor(MessagePredicate, timeout, cts.Token);
var reactionTask = ctx.Services.Resolve<HandlerQueue<MessageReactionAddEvent>>().WaitFor(ReactionPredicate, timeout, cts.Token);
var theTask = await Task.WhenAny(messageTask, reactionTask);
cts.Cancel();
if (theTask == messageTask)
{
var responseMsg = (await messageTask);
var positives = new[] {"y", "yes"};
return positives.Any(p => string.Equals(responseMsg.Content, p, StringComparison.InvariantCultureIgnoreCase));
}
if (theTask == reactionTask)
return (await reactionTask).Emoji.Name == Emojis.Success;
return false;
} }
public static async Task<MessageReactionAddEvent> AwaitReaction(this Context ctx, Message message, User user = null, Func<MessageReactionAddEvent, bool> predicate = null, Duration? timeout = null) public static async Task<MessageReactionAddEvent> AwaitReaction(this Context ctx, Message message, User user = null, Func<MessageReactionAddEvent, bool> predicate = null, Duration? timeout = null)