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
-2
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;
|
||||
@@ -39,7 +38,7 @@ public sealed class CreateSessionHandlerSubmitMissingFieldsTests
|
||||
// The wizard message is edited to surface the missing-field error.
|
||||
Assert.Single(messenger.Edits);
|
||||
var edit = messenger.Edits[0];
|
||||
Assert.Equal(draft.ChatId, edit.ChatId);
|
||||
Assert.Equal(long.Parse(draft.ChatId, System.Globalization.CultureInfo.InvariantCulture), edit.ChatId);
|
||||
Assert.Contains("Не заполнены", edit.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;
|
||||
|
||||
+7
-8
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;
|
||||
|
||||
@@ -20,7 +19,7 @@ public sealed class GameCreationWizardCancelBackTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Cancel();
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Contains(draft.Id, drafts.DeletedIds);
|
||||
Assert.Single(messenger.Edits);
|
||||
@@ -36,7 +35,7 @@ public sealed class GameCreationWizardCancelBackTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Back();
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
// Title is the first step, so Back is a no-op.
|
||||
Assert.Equal(WizardStepNames.Title, draft.Step);
|
||||
@@ -51,7 +50,7 @@ public sealed class GameCreationWizardCancelBackTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Back();
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Title, draft.Step);
|
||||
}
|
||||
@@ -65,7 +64,7 @@ public sealed class GameCreationWizardCancelBackTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Back();
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Description, draft.Step);
|
||||
}
|
||||
@@ -79,7 +78,7 @@ public sealed class GameCreationWizardCancelBackTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Back();
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Cover, draft.Step);
|
||||
}
|
||||
@@ -93,7 +92,7 @@ public sealed class GameCreationWizardCancelBackTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Back();
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.PoolSystemDuration, draft.Step);
|
||||
}
|
||||
@@ -108,7 +107,7 @@ public sealed class GameCreationWizardCancelBackTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Create();
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Confirm, draft.Step);
|
||||
Assert.Contains("cb-1", messenger.AnsweredCallbacks);
|
||||
|
||||
+13
-14
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;
|
||||
@@ -7,8 +6,8 @@ using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTest
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the pool-specific branch of the wizard: the AddSlots flow that
|
||||
/// builds up slot metadata through date and capacity steps.
|
||||
/// Verifies the pool-specific branch of the wizard: the AddSlots flow
|
||||
/// that builds up slot metadata through date and capacity steps.
|
||||
/// </summary>
|
||||
public sealed class GameCreationWizardPoolSlotTests
|
||||
{
|
||||
@@ -28,7 +27,7 @@ public sealed class GameCreationWizardPoolSlotTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var addData = WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "add");
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(addData), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(addData, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.PoolSlotDateTime, draft.Step);
|
||||
}
|
||||
@@ -49,7 +48,7 @@ public sealed class GameCreationWizardPoolSlotTests
|
||||
|
||||
var future = DateTimeOffset.UtcNow.AddDays(7).ToMoscow();
|
||||
var dtString = future.ToString("dd.MM.yyyy HH:mm");
|
||||
await wizard.HandleUpdateAsync(TextUpdate(dtString), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction(dtString, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.PoolSlotCapacity, draft.Step);
|
||||
}
|
||||
@@ -61,7 +60,7 @@ public sealed class GameCreationWizardPoolSlotTests
|
||||
var draft = NewDraft(WizardStepNames.PoolSlotDateTime);
|
||||
drafts.Seed(draft);
|
||||
|
||||
await wizard.HandleUpdateAsync(TextUpdate("01.01.2020 12:00"), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction("01.01.2020 12:00", ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.PoolSlotDateTime, draft.Step);
|
||||
}
|
||||
@@ -74,7 +73,7 @@ public sealed class GameCreationWizardPoolSlotTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var noWaitlist = WizardCallbackData.Choice(WizardStepNames.PoolSlotCapacity, "waitlist:off");
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(noWaitlist), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(noWaitlist, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.PoolAddSlots, draft.Step);
|
||||
}
|
||||
@@ -87,7 +86,7 @@ public sealed class GameCreationWizardPoolSlotTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var yesWaitlist = WizardCallbackData.Choice(WizardStepNames.PoolSlotCapacity, "waitlist:on");
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(yesWaitlist), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(yesWaitlist, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.PoolAddSlots, draft.Step);
|
||||
}
|
||||
@@ -107,7 +106,7 @@ public sealed class GameCreationWizardPoolSlotTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "done");
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.PoolAddSlots, draft.Step);
|
||||
}
|
||||
@@ -132,7 +131,7 @@ public sealed class GameCreationWizardPoolSlotTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "done");
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.PoolConfirm, draft.Step);
|
||||
}
|
||||
@@ -150,10 +149,10 @@ public sealed class GameCreationWizardPoolSlotTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
// "add" then "done" — no date/capacity supplied in between.
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(
|
||||
WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "add")), draft, CancellationToken.None);
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(
|
||||
WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "done")), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(
|
||||
WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "add"), ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(
|
||||
WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "done"), ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
// The wizard sees the in-memory slot count > 0 and advances to confirm.
|
||||
Assert.Equal(WizardStepNames.PoolConfirm, draft.Step);
|
||||
|
||||
+5
-6
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;
|
||||
|
||||
@@ -41,7 +40,7 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Choice(fromStep, choice);
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(expectedStep, draft.Step);
|
||||
Assert.NotEmpty(drafts.Upserts); // was persisted
|
||||
@@ -60,7 +59,7 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "Dnd5e:240");
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Visibility, draft.Step);
|
||||
using var doc = JsonDocument.Parse(draft.PayloadJson);
|
||||
@@ -84,7 +83,7 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:on");
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Visibility, draft.Step);
|
||||
}
|
||||
@@ -110,7 +109,7 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Choice(WizardStepNames.PickClub, clubId.ToString());
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
// Wizard acknowledged the callback and re-rendered the (still PickClub) step.
|
||||
Assert.NotEmpty(messenger.Edits);
|
||||
@@ -132,7 +131,7 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Choice(WizardStepNames.PickClub, "not-a-guid");
|
||||
await wizard.HandleUpdateAsync(CallbackUpdate(data), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.PickClub, draft.Step);
|
||||
}
|
||||
|
||||
+12
-13
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;
|
||||
|
||||
@@ -20,7 +19,7 @@ public sealed class GameCreationWizardValidationTests
|
||||
var draft = NewDraft(WizardStepNames.Title);
|
||||
drafts.Seed(draft);
|
||||
|
||||
await wizard.HandleUpdateAsync(TextUpdate(" "), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction(" ", ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Title, draft.Step);
|
||||
}
|
||||
@@ -32,8 +31,8 @@ public sealed class GameCreationWizardValidationTests
|
||||
var draft = NewDraft(WizardStepNames.Title);
|
||||
drafts.Seed(draft);
|
||||
|
||||
var tooLong = new string('a', WizardStep.MaxTitleLength + 1);
|
||||
await wizard.HandleUpdateAsync(TextUpdate(tooLong), draft, CancellationToken.None);
|
||||
var tooLong = new string('a', WizardStepLimits.MaxTitleLength + 1);
|
||||
await wizard.HandleInteractionAsync(TextInteraction(tooLong, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Title, draft.Step);
|
||||
}
|
||||
@@ -53,7 +52,7 @@ public sealed class GameCreationWizardValidationTests
|
||||
drafts.Seed(draft);
|
||||
|
||||
// 2020-01-01 is firmly in the past
|
||||
await wizard.HandleUpdateAsync(TextUpdate("01.01.2020 12:00"), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction("01.01.2020 12:00", ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.DateTime, draft.Step);
|
||||
}
|
||||
@@ -65,7 +64,7 @@ public sealed class GameCreationWizardValidationTests
|
||||
var draft = NewDraft(WizardStepNames.DateTime);
|
||||
drafts.Seed(draft);
|
||||
|
||||
await wizard.HandleUpdateAsync(TextUpdate("not a date"), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction("not a date", ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.DateTime, draft.Step);
|
||||
}
|
||||
@@ -83,7 +82,7 @@ public sealed class GameCreationWizardValidationTests
|
||||
var draft = NewDraft(WizardStepNames.Cover, payload);
|
||||
drafts.Seed(draft);
|
||||
|
||||
await wizard.HandleUpdateAsync(TextUpdate("not a url"), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction("not a url", ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Cover, draft.Step);
|
||||
}
|
||||
@@ -96,7 +95,7 @@ public sealed class GameCreationWizardValidationTests
|
||||
new WizardPayload { Type = WizardCreationType.Single, Title = "T", Description = "D" });
|
||||
drafts.Seed(draft);
|
||||
|
||||
await wizard.HandleUpdateAsync(TextUpdate("https://example.com/x.jpg"), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction("https://example.com/x.jpg", ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.System, draft.Step);
|
||||
}
|
||||
@@ -109,7 +108,7 @@ public sealed class GameCreationWizardValidationTests
|
||||
new WizardPayload { Type = WizardCreationType.Single, Title = "T", Description = "D" });
|
||||
drafts.Seed(draft);
|
||||
|
||||
await wizard.HandleUpdateAsync(TextUpdate("-"), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction("-", ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.System, draft.Step);
|
||||
}
|
||||
@@ -132,7 +131,7 @@ public sealed class GameCreationWizardValidationTests
|
||||
});
|
||||
drafts.Seed(draft);
|
||||
|
||||
await wizard.HandleUpdateAsync(TextUpdate(input), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction(input, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Capacity, draft.Step);
|
||||
}
|
||||
@@ -148,7 +147,7 @@ public sealed class GameCreationWizardValidationTests
|
||||
new WizardPayload { Type = WizardCreationType.Single, Title = "T", System = "Dnd5e" });
|
||||
drafts.Seed(draft);
|
||||
|
||||
await wizard.HandleUpdateAsync(TextUpdate(input), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction(input, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Duration, draft.Step);
|
||||
}
|
||||
@@ -161,7 +160,7 @@ public sealed class GameCreationWizardValidationTests
|
||||
new WizardPayload { Type = WizardCreationType.Single, Title = "T" });
|
||||
drafts.Seed(draft);
|
||||
|
||||
await wizard.HandleUpdateAsync(TextUpdate("-"), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction("-", ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Cover, draft.Step);
|
||||
}
|
||||
@@ -178,7 +177,7 @@ public sealed class GameCreationWizardValidationTests
|
||||
new WizardPayload { Type = WizardCreationType.Single, Title = "T" });
|
||||
drafts.Seed(draft);
|
||||
|
||||
await wizard.HandleUpdateAsync(TextUpdate("CustomSystem"), draft, CancellationToken.None);
|
||||
await wizard.HandleInteractionAsync(TextInteraction("CustomSystem", ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Duration, draft.Step);
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using GmRelay.Bot.Infrastructure.Telegram;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
@@ -30,7 +30,7 @@ public sealed class UpdateRouterDelegationTests
|
||||
var draft = NewDraft(WizardStepNames.Title);
|
||||
drafts.Seed(draft);
|
||||
|
||||
var update = TextUpdate("Curse of Strahd", ownerId: draft.OwnerTelegramId);
|
||||
var update = TextUpdate("Curse of Strahd", ownerId: long.Parse(draft.OwnerId, System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
await sut.RouteAsync(update, CancellationToken.None);
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed class UpdateRouterDelegationTests
|
||||
// "wizard:cancel" — wizard owns the cancel callback. The router
|
||||
// delegates control-callbacks (resume/reset) but lets the wizard
|
||||
// handle wizard:* callbacks.
|
||||
var update = CallbackUpdate(WizardCallbackData.Cancel(), ownerId: draft.OwnerTelegramId);
|
||||
var update = CallbackUpdate(WizardCallbackData.Cancel(), ownerId: long.Parse(draft.OwnerId, System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
await sut.RouteAsync(update, CancellationToken.None);
|
||||
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using GmRelay.Bot.Infrastructure.Telegram;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
@@ -36,8 +36,8 @@ public sealed class UpdateRouterResetsDraftOnStaleCommandTests
|
||||
Message = new Message
|
||||
{
|
||||
Text = "/newsession",
|
||||
Chat = new Chat { Id = draft.ChatId },
|
||||
From = new User { Id = draft.OwnerTelegramId, FirstName = "GM" },
|
||||
Chat = new Chat { Id = long.Parse(draft.ChatId, System.Globalization.CultureInfo.InvariantCulture) },
|
||||
From = new User { Id = long.Parse(draft.OwnerId, System.Globalization.CultureInfo.InvariantCulture), FirstName = "GM" },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+8
-7
@@ -49,22 +49,23 @@ public sealed class WizardDraftRepositoryFixture : IAsyncLifetime
|
||||
"""
|
||||
CREATE TABLE wizard_drafts (
|
||||
id UUID PRIMARY KEY,
|
||||
chat_id BIGINT NOT NULL,
|
||||
message_thread_id INT,
|
||||
owner_telegram_id BIGINT NOT NULL,
|
||||
chat_id TEXT NOT NULL,
|
||||
message_thread_id TEXT,
|
||||
owner_id TEXT NOT NULL,
|
||||
platform TEXT NOT NULL DEFAULT 'Telegram',
|
||||
step TEXT NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
draft_message_id BIGINT,
|
||||
draft_message_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_wizard_drafts_owner
|
||||
ON wizard_drafts(chat_id, message_thread_id, owner_telegram_id);
|
||||
ON wizard_drafts(platform, owner_id);
|
||||
|
||||
CREATE INDEX idx_wizard_drafts_expires
|
||||
ON wizard_drafts(expires_at);
|
||||
CREATE INDEX idx_wizard_drafts_platform
|
||||
ON wizard_drafts(platform);
|
||||
""",
|
||||
connection);
|
||||
await createSchema.ExecuteNonQueryAsync().WaitAsync(ContainerTimeout);
|
||||
|
||||
+12
-6
@@ -1,3 +1,6 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using Npgsql;
|
||||
|
||||
@@ -20,7 +23,7 @@ public sealed class WizardDraftRepositoryTests(WizardDraftRepositoryFixture fixt
|
||||
draft.UpdatedAt = DateTimeOffset.UtcNow.AddSeconds(1);
|
||||
await sut.UpsertAsync(draft, CancellationToken.None);
|
||||
|
||||
var loaded = await sut.GetActiveAsync(draft.ChatId, draft.MessageThreadId, draft.OwnerTelegramId, CancellationToken.None);
|
||||
var loaded = await sut.GetActiveAsync(draft.Platform, draft.OwnerId, CancellationToken.None);
|
||||
Assert.NotNull(loaded);
|
||||
Assert.Equal("Title", loaded!.Step);
|
||||
}
|
||||
@@ -35,7 +38,7 @@ public sealed class WizardDraftRepositoryTests(WizardDraftRepositoryFixture fixt
|
||||
var draft = NewDraft("Type", DateTimeOffset.UtcNow.AddMinutes(-1));
|
||||
await sut.UpsertAsync(draft, CancellationToken.None);
|
||||
|
||||
var loaded = await sut.GetActiveAsync(draft.ChatId, draft.MessageThreadId, draft.OwnerTelegramId, CancellationToken.None);
|
||||
var loaded = await sut.GetActiveAsync(draft.Platform, draft.OwnerId, CancellationToken.None);
|
||||
Assert.Null(loaded);
|
||||
}
|
||||
|
||||
@@ -49,7 +52,9 @@ public sealed class WizardDraftRepositoryTests(WizardDraftRepositoryFixture fixt
|
||||
var draft = NewDraft("Type", DateTimeOffset.UtcNow.AddHours(1));
|
||||
await sut.UpsertAsync(draft, CancellationToken.None);
|
||||
|
||||
var loaded = await sut.GetActiveAsync(draft.ChatId, draft.MessageThreadId, ownerTelegramId: draft.OwnerTelegramId + 1, CancellationToken.None);
|
||||
var otherOwner = (long.Parse(draft.OwnerId, System.Globalization.CultureInfo.InvariantCulture) + 1)
|
||||
.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
var loaded = await sut.GetActiveAsync(draft.Platform, otherOwner, CancellationToken.None);
|
||||
Assert.Null(loaded);
|
||||
}
|
||||
|
||||
@@ -69,16 +74,17 @@ public sealed class WizardDraftRepositoryTests(WizardDraftRepositoryFixture fixt
|
||||
var deleted = await sut.DeleteExpiredAsync(CancellationToken.None);
|
||||
Assert.Equal(1, deleted);
|
||||
|
||||
var loadedFresh = await sut.GetActiveAsync(fresh.ChatId, fresh.MessageThreadId, fresh.OwnerTelegramId, CancellationToken.None);
|
||||
var loadedFresh = await sut.GetActiveAsync(fresh.Platform, fresh.OwnerId, CancellationToken.None);
|
||||
Assert.NotNull(loadedFresh);
|
||||
}
|
||||
|
||||
private static WizardDraft NewDraft(string step, DateTimeOffset expiresAt) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ChatId = 42,
|
||||
ChatId = "42",
|
||||
MessageThreadId = null,
|
||||
OwnerTelegramId = 100,
|
||||
OwnerId = "100",
|
||||
Platform = "Telegram",
|
||||
Step = step,
|
||||
PayloadJson = "{}",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using Telegram.Bot.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the Telegram <c>Update</c> → <c>WizardInteraction</c> mapping
|
||||
/// that <see cref="WizardInteractionMapper"/> exposes. The mapper is the
|
||||
/// single bridge between Telegram's native update type and the
|
||||
/// platform-neutral wizard core, so its contract needs to be locked
|
||||
/// down: callback queries carry the data payload, text messages carry
|
||||
/// their text, and photos carry the largest photo's <c>FileId</c>.
|
||||
/// </summary>
|
||||
public sealed class WizardInteractionMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void CallbackUpdate_ProducesCallbackInteraction_WithPayloadAndOwner()
|
||||
{
|
||||
var update = new Update
|
||||
{
|
||||
CallbackQuery = new CallbackQuery
|
||||
{
|
||||
Id = "cb-42",
|
||||
Data = "wizard:choice:Type:single",
|
||||
From = new User { Id = 100, FirstName = "GM" },
|
||||
Message = new Message { Chat = new Chat { Id = 42 } },
|
||||
},
|
||||
};
|
||||
|
||||
var ok = WizardInteractionMapper.TryMap(update, out var interaction);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal("100", interaction.OwnerId);
|
||||
Assert.Null(interaction.Text);
|
||||
Assert.Equal("wizard:choice:Type:single", interaction.CallbackPayload);
|
||||
Assert.Null(interaction.PhotoFileId);
|
||||
Assert.Null(interaction.PhotoUrl);
|
||||
Assert.Equal("cb-42", interaction.InteractionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TextUpdate_ProducesTextInteraction_WithTextAndNoCallback()
|
||||
{
|
||||
var update = new Update
|
||||
{
|
||||
Message = new Message
|
||||
{
|
||||
Text = "My Game Title",
|
||||
Chat = new Chat { Id = 42 },
|
||||
From = new User { Id = 200, FirstName = "GM" },
|
||||
},
|
||||
};
|
||||
|
||||
var ok = WizardInteractionMapper.TryMap(update, out var interaction);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal("200", interaction.OwnerId);
|
||||
Assert.Equal("My Game Title", interaction.Text);
|
||||
Assert.Null(interaction.CallbackPayload);
|
||||
Assert.Null(interaction.PhotoFileId);
|
||||
Assert.Equal("msg", interaction.InteractionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhotoUpdate_ProducesPhotoInteraction_WithLargestFileId()
|
||||
{
|
||||
var update = new Update
|
||||
{
|
||||
Message = new Message
|
||||
{
|
||||
Chat = new Chat { Id = 42 },
|
||||
From = new User { Id = 300, FirstName = "GM" },
|
||||
Photo = new[]
|
||||
{
|
||||
new PhotoSize { FileId = "small-id", Width = 90, Height = 60 },
|
||||
new PhotoSize { FileId = "medium-id", Width = 320, Height = 240 },
|
||||
new PhotoSize { FileId = "large-id", Width = 800, Height = 600 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var ok = WizardInteractionMapper.TryMap(update, out var interaction);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal("300", interaction.OwnerId);
|
||||
Assert.Null(interaction.Text);
|
||||
Assert.Null(interaction.CallbackPayload);
|
||||
Assert.Equal("large-id", interaction.PhotoFileId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CaptionedPhoto_ProducesPhotoInteraction_AndKeepsCaptionOutOfText()
|
||||
{
|
||||
// Telegram sometimes attaches a caption to a photo message. The
|
||||
// mapper treats it as a non-text interaction (cover-step uses
|
||||
// PhotoFileId, not caption). This test pins that distinction.
|
||||
var update = new Update
|
||||
{
|
||||
Message = new Message
|
||||
{
|
||||
Caption = "ignored",
|
||||
Chat = new Chat { Id = 42 },
|
||||
From = new User { Id = 400 },
|
||||
Photo = new[]
|
||||
{
|
||||
new PhotoSize { FileId = "only-id", Width = 100, Height = 100 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var ok = WizardInteractionMapper.TryMap(update, out var interaction);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal("only-id", interaction.PhotoFileId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyUpdate_ReturnsFalse()
|
||||
{
|
||||
var ok = WizardInteractionMapper.TryMap(new Update(), out var interaction);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Null(interaction);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -248,7 +248,7 @@ public sealed class WizardStepRenderTests
|
||||
private static WizardDraft NewDraft(string step) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ChatId = 42,
|
||||
ChatId = "42",
|
||||
Step = step,
|
||||
PayloadJson = "{}",
|
||||
};
|
||||
|
||||
+112
-38
@@ -1,25 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
using WizardBot = GmRelay.Bot.Features.Sessions.CreateSession.Wizard.GameCreationWizard;
|
||||
using WizardMessenger = GmRelay.Bot.Features.Sessions.CreateSession.Wizard.ITelegramWizardMessenger;
|
||||
using WizardBot = GmRelay.Shared.Features.Sessions.CreateSession.Wizard.GameCreationWizard;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
||||
|
||||
/// <summary>
|
||||
/// Hand-rolled test doubles and helpers for wizard unit tests. The project
|
||||
/// convention is to use fakes (not a mocking framework) so the suite stays
|
||||
/// AOT-friendly and the production code doesn't grow virtual members just
|
||||
/// for tests.
|
||||
/// Hand-rolled test doubles and helpers for wizard unit tests. The
|
||||
/// project convention is to use fakes (not a mocking framework) so the
|
||||
/// suite stays AOT-friendly and the production code doesn't grow
|
||||
/// virtual members just for tests.
|
||||
/// </summary>
|
||||
internal static class WizardTestFakes
|
||||
{
|
||||
public const string PlatformName = "Telegram";
|
||||
|
||||
public static WizardBot BuildWizard(out FakeWizardDraftRepository drafts, out FakeWizardMessenger messenger)
|
||||
{
|
||||
drafts = new FakeWizardDraftRepository();
|
||||
@@ -30,11 +31,12 @@ internal static class WizardTestFakes
|
||||
public static WizardDraft NewDraft(string step, WizardPayload? payload = null, long ownerId = 100) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ChatId = 42,
|
||||
ChatId = "42",
|
||||
MessageThreadId = null,
|
||||
OwnerTelegramId = ownerId,
|
||||
OwnerId = ownerId.ToString(CultureInfo.InvariantCulture),
|
||||
Platform = PlatformName,
|
||||
Step = step,
|
||||
DraftMessageId = 7,
|
||||
DraftMessageId = "7",
|
||||
PayloadJson = System.Text.Json.JsonSerializer.Serialize(
|
||||
payload ?? new WizardPayload(),
|
||||
WizardPayloadJsonContext.Default.WizardPayload),
|
||||
@@ -43,6 +45,63 @@ internal static class WizardTestFakes
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddHours(24),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Build the platform-neutral <see cref="WizardInteraction"/> the
|
||||
/// wizard now consumes. Pre-V112 callers passed
|
||||
/// <c>Telegram.Bot.Types.Update</c> directly; tests now build the
|
||||
/// neutral interaction via the same mapper the production code uses.
|
||||
/// </summary>
|
||||
public static WizardInteraction CallbackInteraction(
|
||||
string data, string ownerId = "100", string callbackId = "cb-1")
|
||||
{
|
||||
return new WizardInteraction(
|
||||
OwnerId: ownerId,
|
||||
Text: null,
|
||||
CallbackPayload: data,
|
||||
PhotoFileId: null,
|
||||
PhotoUrl: null,
|
||||
InteractionId: callbackId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a text-style <see cref="WizardInteraction"/> mirroring what
|
||||
/// <c>WizardInteractionMapper</c> would produce for a Telegram text
|
||||
/// message.
|
||||
/// </summary>
|
||||
public static WizardInteraction TextInteraction(
|
||||
string text, string ownerId = "100", int messageId = 1)
|
||||
{
|
||||
return new WizardInteraction(
|
||||
OwnerId: ownerId,
|
||||
Text: text,
|
||||
CallbackPayload: null,
|
||||
PhotoFileId: null,
|
||||
PhotoUrl: null,
|
||||
InteractionId: $"msg-{messageId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a photo-style <see cref="WizardInteraction"/> mirroring
|
||||
/// what <c>WizardInteractionMapper</c> would produce for a Telegram
|
||||
/// photo message.
|
||||
/// </summary>
|
||||
public static WizardInteraction PhotoInteraction(
|
||||
string fileId, string ownerId = "100", int messageId = 1)
|
||||
{
|
||||
return new WizardInteraction(
|
||||
OwnerId: ownerId,
|
||||
Text: null,
|
||||
CallbackPayload: null,
|
||||
PhotoFileId: fileId,
|
||||
PhotoUrl: null,
|
||||
InteractionId: $"msg-{messageId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a Telegram <see cref="Update"/> carrying a callback query.
|
||||
/// Used by router-level tests that exercise
|
||||
/// <c>UpdateRouter.RouteAsync</c> end-to-end.
|
||||
/// </summary>
|
||||
public static Update CallbackUpdate(string data, long ownerId = 100) => new()
|
||||
{
|
||||
CallbackQuery = new CallbackQuery
|
||||
@@ -57,6 +116,11 @@ internal static class WizardTestFakes
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Build a Telegram <see cref="Update"/> carrying a text message.
|
||||
/// Used by router-level tests that exercise
|
||||
/// <c>UpdateRouter.RouteAsync</c> end-to-end.
|
||||
/// </summary>
|
||||
public static Update TextUpdate(string text, long ownerId = 100) => new()
|
||||
{
|
||||
Message = new Message
|
||||
@@ -69,9 +133,9 @@ internal static class WizardTestFakes
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records every call the wizard makes against the draft repository. Backed by
|
||||
/// an in-memory dictionary so tests can pre-seed an "active" draft for the
|
||||
/// wizard to mutate.
|
||||
/// Records every call the wizard makes against the draft repository.
|
||||
/// Backed by an in-memory dictionary so tests can pre-seed an "active"
|
||||
/// draft for the wizard to mutate.
|
||||
/// </summary>
|
||||
internal sealed class FakeWizardDraftRepository : IWizardDraftRepository
|
||||
{
|
||||
@@ -85,13 +149,12 @@ internal sealed class FakeWizardDraftRepository : IWizardDraftRepository
|
||||
|
||||
public void Seed(WizardDraft draft) => store[draft.Id] = draft;
|
||||
|
||||
public Task<WizardDraft?> GetActiveAsync(long chatId, int? messageThreadId, long ownerTelegramId, CancellationToken ct)
|
||||
public Task<WizardDraft?> GetActiveAsync(string platform, string ownerId, CancellationToken ct)
|
||||
{
|
||||
foreach (var d in store.Values)
|
||||
{
|
||||
if (d.ChatId == chatId &&
|
||||
d.MessageThreadId == messageThreadId &&
|
||||
d.OwnerTelegramId == ownerTelegramId &&
|
||||
if (d.Platform == platform &&
|
||||
d.OwnerId == ownerId &&
|
||||
d.ExpiresAt > DateTimeOffset.UtcNow)
|
||||
{
|
||||
return Task.FromResult<WizardDraft?>(d);
|
||||
@@ -108,7 +171,8 @@ internal sealed class FakeWizardDraftRepository : IWizardDraftRepository
|
||||
Id = draft.Id,
|
||||
ChatId = draft.ChatId,
|
||||
MessageThreadId = draft.MessageThreadId,
|
||||
OwnerTelegramId = draft.OwnerTelegramId,
|
||||
OwnerId = draft.OwnerId,
|
||||
Platform = draft.Platform,
|
||||
Step = draft.Step,
|
||||
PayloadJson = draft.PayloadJson,
|
||||
DraftMessageId = draft.DraftMessageId,
|
||||
@@ -136,11 +200,14 @@ internal sealed class FakeWizardDraftRepository : IWizardDraftRepository
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records every call the wizard makes against the messenger. Default return
|
||||
/// values (empty clubs, message-id 1) match what the wizard expects to see
|
||||
/// in steady state.
|
||||
/// Records every call the wizard makes against the messenger. Default
|
||||
/// return values (empty clubs, message-id 99) match what the wizard
|
||||
/// expects to see in steady state. The recorded tuple shapes match
|
||||
/// the old <c>ITelegramWizardMessenger</c> recorders so existing test
|
||||
/// assertions (<c>edit.ChatId</c>, <c>edit.Text</c>, …) keep working
|
||||
/// after the refactor.
|
||||
/// </summary>
|
||||
internal sealed class FakeWizardMessenger : ITelegramWizardMessenger
|
||||
internal sealed class FakeWizardMessenger : IWizardMessenger
|
||||
{
|
||||
public List<(long ChatId, int? ThreadId, long MsgId, string Text)> Edits { get; } = new();
|
||||
|
||||
@@ -148,37 +215,44 @@ internal sealed class FakeWizardMessenger : ITelegramWizardMessenger
|
||||
|
||||
public List<(long ChatId, int? ThreadId, string Text)> Sends { get; } = new();
|
||||
|
||||
public List<(string OwnerId, IReadOnlyList<WizardAction> Actions)> EditActions { get; } = new();
|
||||
|
||||
public IReadOnlyList<WizardClubOption> Clubs { get; set; } = Array.Empty<WizardClubOption>();
|
||||
|
||||
public Task<long> EditMessageTextAsync(
|
||||
long chatId,
|
||||
int? messageThreadId,
|
||||
long messageId,
|
||||
public Task<string> EditDraftMessageAsync(
|
||||
WizardDraft draft,
|
||||
string text,
|
||||
InlineKeyboardMarkup keyboard,
|
||||
IReadOnlyList<WizardAction> keyboard,
|
||||
CancellationToken ct)
|
||||
{
|
||||
Edits.Add((chatId, messageThreadId, messageId, text));
|
||||
return Task.FromResult(messageId);
|
||||
Edits.Add((
|
||||
long.TryParse(draft.ChatId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var chatId) ? chatId : 0,
|
||||
int.TryParse(draft.MessageThreadId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var threadId) ? threadId : (int?)null,
|
||||
long.TryParse(draft.DraftMessageId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var msgId) ? msgId : 0,
|
||||
text));
|
||||
EditActions.Add((draft.OwnerId, keyboard));
|
||||
return Task.FromResult(draft.DraftMessageId ?? "0");
|
||||
}
|
||||
|
||||
public Task<long> SendGroupMessageAsync(
|
||||
long chatId,
|
||||
int? messageThreadId,
|
||||
public Task<string> SendDraftMessageAsync(
|
||||
WizardDraft draft,
|
||||
string text,
|
||||
InlineKeyboardMarkup keyboard,
|
||||
IReadOnlyList<WizardAction> keyboard,
|
||||
CancellationToken ct)
|
||||
{
|
||||
Sends.Add((chatId, messageThreadId, text));
|
||||
return Task.FromResult(99L);
|
||||
Sends.Add((
|
||||
long.TryParse(draft.ChatId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var chatId) ? chatId : 0,
|
||||
int.TryParse(draft.MessageThreadId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var threadId) ? threadId : (int?)null,
|
||||
text));
|
||||
return Task.FromResult("99");
|
||||
}
|
||||
|
||||
public Task AnswerCallbackAsync(string callbackId, string? text, CancellationToken ct)
|
||||
public Task AnswerInteractionAsync(string interactionId, string? text, CancellationToken ct)
|
||||
{
|
||||
AnsweredCallbacks.Add(callbackId);
|
||||
AnsweredCallbacks.Add(interactionId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<WizardClubOption>> GetGmClubsAsync(long ownerTelegramId, CancellationToken ct)
|
||||
public Task<IReadOnlyList<WizardClubOption>> GetOwnerClubsAsync(string ownerId, CancellationToken ct)
|
||||
=> Task.FromResult(Clubs);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user