8f0f2ef7e7
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.
69 lines
2.8 KiB
C#
69 lines
2.8 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
|
using Telegram.Bot.Types;
|
|
|
|
namespace GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
|
|
|
/// <summary>
|
|
/// Converts a Telegram <see cref="Update"/> into the
|
|
/// platform-neutral <see cref="WizardInteraction"/> consumed by
|
|
/// <see cref="GameCreationWizard"/>. The mapping is the only place in
|
|
/// the bot that knows about both <c>Telegram.Bot.Types</c> and the
|
|
/// shared wizard contract, so a future Discord adapter can do the same
|
|
/// for its native event without changing the wizard core.
|
|
/// </summary>
|
|
public static class WizardInteractionMapper
|
|
{
|
|
/// <summary>
|
|
/// Returns <c>true</c> if <paramref name="update"/> carries a
|
|
/// wizard-relevant interaction (text message, photo, or
|
|
/// callback). Side-effect-free: the wizard state is not touched.
|
|
/// </summary>
|
|
public static bool TryMap(Update update, out WizardInteraction interaction)
|
|
{
|
|
interaction = default!;
|
|
if (update.CallbackQuery is { } cb && cb.From is not null)
|
|
{
|
|
interaction = new WizardInteraction(
|
|
OwnerId: cb.From.Id.ToString(CultureInfo.InvariantCulture),
|
|
Text: null,
|
|
CallbackPayload: cb.Data,
|
|
PhotoFileId: null,
|
|
PhotoUrl: null,
|
|
InteractionId: cb.Id);
|
|
return true;
|
|
}
|
|
|
|
if (update.Message is { From: not null } msg)
|
|
{
|
|
// The original Telegram wizard dispatched on
|
|
// `msg.Text is null` to identify a non-text update (photo,
|
|
// document, sticker, …) and only ran the text pipeline
|
|
// otherwise. We preserve that semantic: a message that
|
|
// carries a photo is a photo interaction even if it has a
|
|
// caption. Text is null for photos; the wizard checks
|
|
// PhotoFileId separately when Text is null.
|
|
//
|
|
// Note: `Message.MessageId` is exposed as a read-only
|
|
// property in Telegram.Bot, so the mapper cannot embed the
|
|
// numeric id in the interaction. Text interactions never
|
|
// need an ack, so the InteractionId is unused for them —
|
|
// we just emit a stable sentinel.
|
|
var hasPhoto = msg.Photo is { Length: > 0 };
|
|
var text = hasPhoto ? null : msg.Text;
|
|
var photoFileId = hasPhoto ? msg.Photo![^1].FileId : null;
|
|
interaction = new WizardInteraction(
|
|
OwnerId: msg.From!.Id.ToString(CultureInfo.InvariantCulture),
|
|
Text: text,
|
|
CallbackPayload: null,
|
|
PhotoFileId: photoFileId,
|
|
PhotoUrl: null,
|
|
InteractionId: "msg");
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|