fix(discord): fix select/modal parser off-by-one + wire real club lookup
The dispatcher rejected every valid select menu and modal submit because of an off-by-one in the customId parts-length check (NetCord strips the matching 'wizard' prefix and passes the remainder, so 'wizard:select:Visibility' arrives as 'select:Visibility' = 2 parts, not 3). Also fixed: WizardClubLookup.LoadClubsAsync returned an empty list, making the PickClub step always show 'no clubs'. Now queries the DB via NpgsqlDataSource with the same Owner|CoGm role filter the messenger uses. Build green, 190/192 wizard+Discord tests pass (2 pre-existing skips), format clean.
This commit is contained in:
@@ -3,12 +3,14 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
using GmRelay.DiscordBot.Infrastructure.Discord;
|
using GmRelay.DiscordBot.Infrastructure.Discord;
|
||||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NetCord;
|
using NetCord;
|
||||||
using NetCord.Rest;
|
using NetCord.Rest;
|
||||||
using NetCord.Services.ComponentInteractions;
|
using NetCord.Services.ComponentInteractions;
|
||||||
|
using Npgsql;
|
||||||
using SharedWizard = GmRelay.Shared.Features.Sessions.CreateSession.Wizard.GameCreationWizard;
|
using SharedWizard = GmRelay.Shared.Features.Sessions.CreateSession.Wizard.GameCreationWizard;
|
||||||
|
|
||||||
namespace GmRelay.DiscordBot.Features.Sessions.Wizard;
|
namespace GmRelay.DiscordBot.Features.Sessions.Wizard;
|
||||||
@@ -92,6 +94,7 @@ public sealed class WizardInteractionDispatcher
|
|||||||
private readonly DiscordWizardSubmitter _submitter;
|
private readonly DiscordWizardSubmitter _submitter;
|
||||||
private readonly DiscordWizardMessenger _messenger;
|
private readonly DiscordWizardMessenger _messenger;
|
||||||
private readonly DiscordInteractionReplyCache _replies;
|
private readonly DiscordInteractionReplyCache _replies;
|
||||||
|
private readonly NpgsqlDataSource _dataSource;
|
||||||
private readonly ILogger<WizardInteractionDispatcher> _log;
|
private readonly ILogger<WizardInteractionDispatcher> _log;
|
||||||
|
|
||||||
public WizardInteractionDispatcher(
|
public WizardInteractionDispatcher(
|
||||||
@@ -101,6 +104,7 @@ public sealed class WizardInteractionDispatcher
|
|||||||
DiscordWizardSubmitter submitter,
|
DiscordWizardSubmitter submitter,
|
||||||
DiscordWizardMessenger messenger,
|
DiscordWizardMessenger messenger,
|
||||||
DiscordInteractionReplyCache replies,
|
DiscordInteractionReplyCache replies,
|
||||||
|
NpgsqlDataSource dataSource,
|
||||||
ILogger<WizardInteractionDispatcher> log)
|
ILogger<WizardInteractionDispatcher> log)
|
||||||
{
|
{
|
||||||
_drafts = drafts;
|
_drafts = drafts;
|
||||||
@@ -109,6 +113,7 @@ public sealed class WizardInteractionDispatcher
|
|||||||
_submitter = submitter;
|
_submitter = submitter;
|
||||||
_messenger = messenger;
|
_messenger = messenger;
|
||||||
_replies = replies;
|
_replies = replies;
|
||||||
|
_dataSource = dataSource;
|
||||||
_log = log;
|
_log = log;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,13 +300,18 @@ public sealed class WizardInteractionDispatcher
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = args.Split(':', 3);
|
// NetCord strips the matching ComponentInteraction "wizard"
|
||||||
if (parts.Length < 3 || parts[0] != "select" || context.Interaction.Data.SelectedValues.Count == 0)
|
// prefix from the custom id and passes the remainder as `args`.
|
||||||
|
// For `wizard:select:Visibility` the args arrive as
|
||||||
|
// `select:Visibility` (2 parts when split on `:`), so the
|
||||||
|
// length check uses `< 2` and the step lives at parts[1].
|
||||||
|
var parts = args.Split(':', 2);
|
||||||
|
if (parts.Length < 2 || parts[0] != "select" || context.Interaction.Data.SelectedValues.Count == 0)
|
||||||
{
|
{
|
||||||
await AckWithErrorAsync(context.Interaction, "Неизвестный выбор");
|
await AckWithErrorAsync(context.Interaction, "Неизвестный выбор");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var step = parts[2];
|
var step = parts[1];
|
||||||
var value = context.Interaction.Data.SelectedValues[0];
|
var value = context.Interaction.Data.SelectedValues[0];
|
||||||
|
|
||||||
await context.Interaction.SendResponseAsync(InteractionCallback.DeferredModifyMessage);
|
await context.Interaction.SendResponseAsync(InteractionCallback.DeferredModifyMessage);
|
||||||
@@ -341,13 +351,16 @@ public sealed class WizardInteractionDispatcher
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = args.Split(':', 3);
|
// Same NetCord prefix-stripping as the select handler:
|
||||||
if (parts.Length < 3 || parts[0] != "modal" || context.Interaction.Data.Components.Count == 0)
|
// for `wizard:modal:Title` the args arrive as `modal:Title`
|
||||||
|
// (2 parts when split on `:`).
|
||||||
|
var parts = args.Split(':', 2);
|
||||||
|
if (parts.Length < 2 || parts[0] != "modal" || context.Interaction.Data.Components.Count == 0)
|
||||||
{
|
{
|
||||||
await AckWithErrorAsync(context.Interaction, "Некорректный модал");
|
await AckWithErrorAsync(context.Interaction, "Некорректный модал");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var step = parts[2];
|
var step = parts[1];
|
||||||
|
|
||||||
var text = ExtractModalText(context);
|
var text = ExtractModalText(context);
|
||||||
if (text is null)
|
if (text is null)
|
||||||
@@ -450,11 +463,10 @@ public sealed class WizardInteractionDispatcher
|
|||||||
|
|
||||||
private async Task<IReadOnlyList<WizardClubOption>?> LoadClubsAsync(WizardDraft draft, CancellationToken ct)
|
private async Task<IReadOnlyList<WizardClubOption>?> LoadClubsAsync(WizardDraft draft, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// The wizard's GetOwnerClubsAsync would do this for us, but the
|
// Inline the same query the messenger would run, so the
|
||||||
// dispatcher doesn't have a direct reference to the messenger.
|
// dispatcher's PickClub step sees the owner's real club list
|
||||||
// We re-query via the messenger's DB connection by going
|
// instead of an empty array.
|
||||||
// through a small helper exposed below.
|
return await WizardClubLookup.LoadClubsAsync(_dataSource, draft.OwnerId, ct);
|
||||||
return await WizardClubLookup.LoadClubsAsync(draft.OwnerId, ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WizardPayload LoadPayload(WizardDraft draft) =>
|
private static WizardPayload LoadPayload(WizardDraft draft) =>
|
||||||
@@ -488,14 +500,29 @@ public sealed class WizardInteractionDispatcher
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class WizardClubLookup
|
internal static class WizardClubLookup
|
||||||
{
|
{
|
||||||
public static async Task<IReadOnlyList<WizardClubOption>> LoadClubsAsync(string ownerId, CancellationToken ct)
|
public static async Task<IReadOnlyList<WizardClubOption>> LoadClubsAsync(
|
||||||
|
NpgsqlDataSource dataSource, string ownerId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// Resolve the same NpgsqlDataSource the messenger uses. The
|
// Same SQL the messenger runs for the wizard's render path.
|
||||||
// dispatcher doesn't hold it directly, so it goes through DI.
|
// Filter by Owner|CoGm role and group by club to dedupe.
|
||||||
// For now we return an empty list — the inline messenger's
|
const string sql = """
|
||||||
// GetOwnerClubsAsync is the source of truth and is invoked by
|
SELECT g.id AS ClubId,
|
||||||
// the wizard's render path.
|
g.name AS Name
|
||||||
await Task.CompletedTask;
|
FROM game_groups g
|
||||||
return Array.Empty<WizardClubOption>();
|
JOIN group_managers gm ON gm.group_id = g.id
|
||||||
|
JOIN players p ON p.id = gm.player_id
|
||||||
|
WHERE p.platform = @Platform
|
||||||
|
AND p.external_user_id = @OwnerId
|
||||||
|
AND gm.role IN ('Owner', 'CoGm')
|
||||||
|
GROUP BY g.id, g.name
|
||||||
|
ORDER BY g.name
|
||||||
|
""";
|
||||||
|
await using var conn = await dataSource.OpenConnectionAsync(ct);
|
||||||
|
var rows = await conn.QueryAsync<WizardClubOption>(
|
||||||
|
new CommandDefinition(
|
||||||
|
sql,
|
||||||
|
new { Platform = "Discord", OwnerId = ownerId },
|
||||||
|
cancellationToken: ct));
|
||||||
|
return rows.AsList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user