d4n3436 / Fergun.Interactive

An addon that provides interactive functionality to Discord commands.
MIT License
31 stars 5 forks source link

Error with "Unknown action" #17

Closed 1NieR closed 2 years ago

1NieR commented 2 years ago

Hi. I get this error all the time. Can you help me please.

System.ArgumentException: Unknown action. (Parameter 'result')
   at Fergun.Interactive.InteractiveService.ApplyActionOnStopAsync[TOption](IInteractiveElement`1 element, IInteractiveMessageResult result, SocketInteraction lastInteraction, SocketMessageComponent stopInteraction) in D:\Program
 Files\DiscordBots\Fergun.Interactive\src\InteractiveService.cs:line 869
   at Fergun.Interactive.InteractiveService.WaitForSelectionResultAsync[TOption](SelectionCallback`1 callback) in D:\Program Files\DiscordBots\Fergun.Interactive\src\InteractiveService.cs:line 776
   at Fergun.Interactive.InteractiveService.SendSelectionInternalAsync[TOption](BaseSelection`1 selection, IMessageChannel channel, Nullable`1 timeout, IUserMessage message, Action`1 messageAction, CancellationToken cancellationT
oken) in D:\Program Files\DiscordBots\Fergun.Interactive\src\InteractiveService.cs:line 0
   at Fergun.Interactive.InteractiveService.SendSelectionAsync[TOption](BaseSelection`1 selection, IUserMessage message, Nullable`1 timeout, Action`1 messageAction, CancellationToken cancellationToken) in D:\Program Files\Discord
Bots\Fergun.Interactive\src\InteractiveService.cs:line 622
   at Bot.Modules.Gambling.EventCommands.MenuAsync() in D:\Program Files\[NF]Finalnextupdate 3.99\Bot\src\Bot\Modules\Gambling\EventCommands.cs:line 727
   at Discord.Commands.ModuleClassBuilder.<>c__DisplayClass6_0.<<BuildCommand>g__ExecuteCallback|0>d.MoveNext()
--- End of stack trace from previous location ---
   at Discord.Commands.CommandInfo.ExecuteInternalAsync(ICommandContext context, Object[] args, IServiceProvider services)
   at Discord.Commands.CommandInfo.ExecuteInternalAsync(ICommandContext context, Object[] args, IServiceProvider services)
   at Discord.Commands.CommandInfo.ExecuteInternalAsync(ICommandContext context, Object[] args, IServiceProvider services)
   at Discord.Commands.CommandInfo.ExecuteAsync(ICommandContext context, IEnumerable`1 argList, IEnumerable`1 paramList, IServiceProvider services)

code example:

        [Command("menu", RunMode = RunMode.Async)]
        public async Task MenuAsync()
        {
            var timeSpan = TimeSpan.FromSeconds(15);
            var cts = new CancellationTokenSource(timeSpan);

            InteractiveMessageResult<Item> result = null;
            IUserMessage message = null;

            var items = new Item[]
            {
                new(0,"apple", new Emoji("🍎")),
                new(1, "avocado", new Emoji("🥑")),
                new(2,"mandarin", new Emoji("🍊")),
            };

            while (result is null || result.Status == InteractiveStatus.Success)
            {
                var pageBuilder = new PageBuilder()
                    .WithTitle("Test shop")
                    .WithDescription("Test.");

                var selection = new SelectionBuilder<Item>()
                    .AddUser(Context.User)
                    .WithSelectionPage(pageBuilder)
                    .WithOptions(items)
                    .WithStringConverter(x => x.Name)
                    .WithEmoteConverter(x => x.Emote)
                    .WithActionOnCancellation(ActionOnStop.DisableInput)
                    .WithActionOnTimeout(ActionOnStop.DisableInput)
                    .Build();

                result = message is null
                    ? await Interactive.SendSelectionAsync(selection, Context.Channel, timeSpan, cancellationToken: cts.Token)
                    : await Interactive.SendSelectionAsync(selection, message, timeSpan, cancellationToken: cts.Token);

                message = result.Message;

                if (!result.IsSuccess) break;
                string item = items[result.Value.number].Name;

                var promptBuilder = new SelectionBuilder<string>()
                .AddUser(Context.User)
                .WithSelectionPage(pageBuilder.WithDescription($"You want to buy {item}?"))
                .AddOption("yes")
                .AddOption("no")
                .WithStringConverter(x => x)
                .WithAllowCancel(true)
                .WithActionOnCancellation(ActionOnStop.DisableInput)
                .WithActionOnTimeout(ActionOnStop.DisableInput)
                .Build();

                var prompt = message is null
                 ? await Interactive.SendSelectionAsync(promptBuilder, Context.Channel, timeSpan, cancellationToken: cts.Token)
                 : await Interactive.SendSelectionAsync(promptBuilder, message, timeSpan, cancellationToken: cts.Token);

                if (prompt.IsCanceled) continue;
                if (prompt.IsSuccess) return;
            }
        }
        private sealed record Item(int number, string Name, IEmote Emote);

rider64_EvlwUqte4U

d4n3436 commented 2 years ago

After some debugging, I've traced the root cause of this bug to the CancellationTokenSource that is being used in the selections.

After waiting for the second selection to be canceled, the CancellationTokenSource enters in a canceled state and the loop is repeated, then the message is modified to contain the first selection. Due to the state of CancellationTokenSource, when the internal TaskCompletionSource is created, the callback that gets registered for the CancellationToken is immediately called, setting the result of the TaskCompletionSource prematurely.

A way to solve this is to set the (eventual) canceled result before registering the callback. This means that if you pass a canceled CancellationToken to a selection/paginator, they will be canceled immediately, which in my opinion it's bad design.

The correct way to reuse a message for a selection is to use the CancellationTokenSource once, then create a new, fresh one.

I will not close this issue because this is technically a bug, and I will make some changes in the lib to throw a proper exception when using a canceled CancellationToken.