refactor(wizard): move core to Shared, add IWizardMessenger contract (issue #112)
Moves the game-creation wizard state machine, view builder, and platform-neutral contracts (callback data, step names, storage exception, club option, step limits) from GmRelay.Bot to GmRelay.Shared. Telegram continues to work through a new TelegramWizardMessenger implementing IWizardMessenger and a WizardInteractionMapper that converts Update → WizardInteraction. Wires the new platform column on wizard_drafts (V032 migration) and switches chat/owner/thread/message ids to TEXT so the same table can hold Discord snowflakes later. - GameCreationWizard: now in Shared, takes IWizardMessenger + IWizardDraftRepository, dispatches on WizardInteraction. - New IWizardMessenger contract with Edit/Send/Answer/GetOwnerClubs (returns string ids so Telegram longs and Discord snowflakes both fit). - New WizardStepViewBuilder in Shared returns (text, IReadOnlyList<WizardAction>); TelegramWizardMessenger renders actions into InlineKeyboardMarkup via a new Bot-side ToInlineKeyboard helper. - New WizardInteractionMapper in Bot (5-case test) converts Telegram Update to WizardInteraction. - WizardDraft gains a Platform column; ChatId/MessageThreadId/OwnerId/ DraftMessageId switched to string. V032 migrates existing rows and rebuilds the owner lookup index on (platform, owner_id). - All existing wizard / create-session tests updated to the new contract (HandleInteractionAsync + WizardInteraction). Wizard callback-data format preserved. - dotnet build clean, dotnet format --verify-no-changes clean, all 101 wizard tests pass.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
// ... UpdateRouter will have CancelSessionHandler and cancel_session route instead of close_recruitment
|
||||
using System.Globalization;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Features.Confirmation.HandleRsvp;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession;
|
||||
@@ -16,6 +17,7 @@ using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
using SharedWizard = GmRelay.Shared.Features.Sessions.CreateSession.Wizard.GameCreationWizard;
|
||||
|
||||
namespace GmRelay.Bot.Infrastructure.Telegram;
|
||||
|
||||
@@ -36,7 +38,7 @@ public sealed class UpdateRouter(
|
||||
InitiateRescheduleHandler initiateRescheduleHandler,
|
||||
BotRescheduleTimeInputHandler rescheduleTimeInputHandler,
|
||||
BotRescheduleVoteHandler rescheduleVoteHandler,
|
||||
GameCreationWizard wizard,
|
||||
SharedWizard wizard,
|
||||
IWizardDraftRepository drafts,
|
||||
ITelegramBotClient bot,
|
||||
IConfiguration configuration,
|
||||
@@ -47,9 +49,9 @@ public sealed class UpdateRouter(
|
||||
// 1) Wizard delegation. If the GM has an active (non-expired) draft for this
|
||||
// (chat, thread, owner), every update routes to the wizard. The wizard is
|
||||
// responsible for both text input and callback handling.
|
||||
if (TryGetWizardContext(update, out var chatId, out var threadId, out var ownerId))
|
||||
if (TryGetWizardContext(update, out _, out _, out var ownerId))
|
||||
{
|
||||
var draft = await drafts.GetActiveAsync(chatId, threadId, ownerId, ct);
|
||||
var draft = await drafts.GetActiveAsync("Telegram", ownerId, ct);
|
||||
if (draft is not null)
|
||||
{
|
||||
// Resume / Reset / Cancel menu callbacks live in the router because
|
||||
@@ -60,7 +62,10 @@ public sealed class UpdateRouter(
|
||||
return;
|
||||
}
|
||||
|
||||
await wizard.HandleUpdateAsync(update, draft, ct);
|
||||
if (WizardInteractionMapper.TryMap(update, out var interaction))
|
||||
{
|
||||
await wizard.HandleInteractionAsync(interaction, draft, ct);
|
||||
}
|
||||
|
||||
// The "✅ Создать" / "✅ Создать пул" button — the wizard only
|
||||
// acknowledges the callback; the actual session creation lives in
|
||||
@@ -157,7 +162,7 @@ public sealed class UpdateRouter(
|
||||
};
|
||||
|
||||
private static WizardPayload LoadPayload(WizardDraft draft) =>
|
||||
GameCreationWizard.LoadPayload(draft);
|
||||
SharedWizard.LoadPayload(draft);
|
||||
|
||||
internal static string GetCommandText(Message message)
|
||||
=> (message.Text ?? message.Caption ?? string.Empty).TrimStart();
|
||||
@@ -166,30 +171,30 @@ public sealed class UpdateRouter(
|
||||
/// Extracts the (chat, thread, owner) triple from an update for wizard lookups.
|
||||
/// Returns false for updates that carry no usable origin (e.g. inline queries).
|
||||
/// </summary>
|
||||
private static bool TryGetWizardContext(Update update, out long chatId, out int? messageThreadId, out long ownerId)
|
||||
private static bool TryGetWizardContext(Update update, out long chatId, out int? messageThreadId, out string ownerId)
|
||||
{
|
||||
chatId = 0;
|
||||
messageThreadId = null;
|
||||
ownerId = 0;
|
||||
ownerId = string.Empty;
|
||||
|
||||
switch (update)
|
||||
{
|
||||
case { Message: { From: not null, Chat: { } chat } msg }:
|
||||
chatId = chat.Id;
|
||||
messageThreadId = msg.MessageThreadId;
|
||||
ownerId = msg.From!.Id;
|
||||
ownerId = msg.From!.Id.ToString(CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
|
||||
case { CallbackQuery: { From: not null, Message: { Chat: { } cbmChat } } cb }:
|
||||
chatId = cbmChat.Id;
|
||||
messageThreadId = cb.Message?.MessageThreadId;
|
||||
ownerId = cb.From!.Id;
|
||||
ownerId = cb.From!.Id.ToString(CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
|
||||
case { CallbackQuery: { From: not null } cb2 }:
|
||||
// Callback arrived without a message (e.g. from a Mini App). No chat
|
||||
// context → wizard cannot run on this update.
|
||||
ownerId = cb2.From!.Id;
|
||||
ownerId = cb2.From!.Id.ToString(CultureInfo.InvariantCulture);
|
||||
return false;
|
||||
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user