|
|
|
@@ -15,12 +15,12 @@ namespace GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed class GameCreationWizard
|
|
|
|
|
{
|
|
|
|
|
private readonly WizardDraftRepository _drafts;
|
|
|
|
|
private readonly IWizardDraftRepository _drafts;
|
|
|
|
|
private readonly ITelegramWizardMessenger _messenger;
|
|
|
|
|
private readonly ILogger<GameCreationWizard> _log;
|
|
|
|
|
|
|
|
|
|
public GameCreationWizard(
|
|
|
|
|
WizardDraftRepository drafts,
|
|
|
|
|
IWizardDraftRepository drafts,
|
|
|
|
|
ITelegramWizardMessenger messenger,
|
|
|
|
|
ILogger<GameCreationWizard> log)
|
|
|
|
|
{
|
|
|
|
@@ -111,12 +111,12 @@ public sealed class GameCreationWizard
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (nextStep, error) = ApplyText(draft, text);
|
|
|
|
|
var (nextStep, error, payload) = ApplyText(draft, text);
|
|
|
|
|
if (payload is { } p) SavePayload(draft, p);
|
|
|
|
|
if (error is { } errMsg && draft.DraftMessageId is { } mid)
|
|
|
|
|
{
|
|
|
|
|
// Re-render the same step with ⚠️ prefix.
|
|
|
|
|
var payload = LoadPayload(draft);
|
|
|
|
|
var (rendered, kb) = WizardStep.Render(draft, payload, null);
|
|
|
|
|
var (rendered, kb) = WizardStep.Render(draft, LoadPayload(draft), null);
|
|
|
|
|
await _messenger.EditMessageTextAsync(
|
|
|
|
|
draft.ChatId, draft.MessageThreadId, mid,
|
|
|
|
|
"⚠️ " + errMsg + "\n\n" + rendered, kb, ct);
|
|
|
|
@@ -132,12 +132,13 @@ public sealed class GameCreationWizard
|
|
|
|
|
|
|
|
|
|
private async Task ApplyChoiceAsync(WizardDraft draft, string step, string choice, string callbackId, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
var (nextStep, error) = ApplyChoice(draft, step, choice);
|
|
|
|
|
var (nextStep, error, payload) = ApplyChoice(draft, step, choice);
|
|
|
|
|
if (error is { } err)
|
|
|
|
|
{
|
|
|
|
|
await _messenger.AnswerCallbackAsync(callbackId, err, ct);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (payload is { } p) SavePayload(draft, p);
|
|
|
|
|
if (nextStep is { } s)
|
|
|
|
|
{
|
|
|
|
|
draft.Step = s;
|
|
|
|
@@ -166,79 +167,79 @@ public sealed class GameCreationWizard
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Text input dispatcher ─────────────────────────────────────────
|
|
|
|
|
private static (string? nextStep, string? error) ApplyText(WizardDraft draft, string input)
|
|
|
|
|
private static (string? nextStep, string? error, WizardPayload payload) ApplyText(WizardDraft draft, string input)
|
|
|
|
|
{
|
|
|
|
|
var payload = LoadPayload(draft);
|
|
|
|
|
switch (draft.Step)
|
|
|
|
|
{
|
|
|
|
|
case WizardStepNames.Title:
|
|
|
|
|
return ValidateText(input, WizardStep.MaxTitleLength, "Название не может быть пустым", "Слишком длинное название", out var title)
|
|
|
|
|
? (WizardStepNames.Description, SetTitle(payload, title))
|
|
|
|
|
: (null, title);
|
|
|
|
|
? (WizardStepNames.Description, SetTitle(payload, title), payload)
|
|
|
|
|
: (null, title, payload);
|
|
|
|
|
|
|
|
|
|
case WizardStepNames.Description:
|
|
|
|
|
if (input == "-") return (WizardStepNames.Cover, SetDescription(payload, null));
|
|
|
|
|
if (input == "-") return (WizardStepNames.Cover, SetDescription(payload, null), payload);
|
|
|
|
|
return ValidateText(input, WizardStep.MaxDescriptionLength, "Описание не может быть пустым", "Слишком длинное описание", out var desc)
|
|
|
|
|
? (WizardStepNames.Cover, SetDescription(payload, desc))
|
|
|
|
|
: (null, desc);
|
|
|
|
|
? (WizardStepNames.Cover, SetDescription(payload, desc), payload)
|
|
|
|
|
: (null, desc, payload);
|
|
|
|
|
|
|
|
|
|
case WizardStepNames.Cover:
|
|
|
|
|
if (input == "-") return (NextAfterCover(payload), SetImageUrl(payload, null));
|
|
|
|
|
if (input == "-") return (NextAfterCover(payload), SetImageUrl(payload, null), payload);
|
|
|
|
|
if (Uri.TryCreate(input, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
|
|
|
|
return (NextAfterCover(payload), SetImageUrl(payload, input));
|
|
|
|
|
return (null, "Некорректный URL");
|
|
|
|
|
return (NextAfterCover(payload), SetImageUrl(payload, input), payload);
|
|
|
|
|
return (null, "Некорректный URL", payload);
|
|
|
|
|
|
|
|
|
|
case WizardStepNames.System when payload.System is null:
|
|
|
|
|
// "Other" branch — only active if free-text was offered.
|
|
|
|
|
return ValidateText(input, WizardStep.MaxSystemLength, "Слишком длинное название системы", "Слишком длинное название системы", out var sys)
|
|
|
|
|
? (WizardStepNames.Duration, SetSystem(payload, sys))
|
|
|
|
|
: (null, sys);
|
|
|
|
|
? (WizardStepNames.Duration, SetSystem(payload, sys), payload)
|
|
|
|
|
: (null, sys, payload);
|
|
|
|
|
|
|
|
|
|
case WizardStepNames.Duration when payload.DurationMinutes is null:
|
|
|
|
|
return TryParseHours(input, out var durMin)
|
|
|
|
|
? (WizardStepNames.DateTime, SetDurationMinutes(payload, durMin))
|
|
|
|
|
: (null, "Неверная длительность (1..12 ч)");
|
|
|
|
|
? (WizardStepNames.DateTime, SetDurationMinutes(payload, durMin), payload)
|
|
|
|
|
: (null, "Неверная длительность (1..12 ч)", payload);
|
|
|
|
|
|
|
|
|
|
case WizardStepNames.DateTime:
|
|
|
|
|
return MoscowTime.TryParseMoscow(input, out var dt) && dt > DateTimeOffset.UtcNow
|
|
|
|
|
? (WizardStepNames.Capacity, SetScheduledAt(payload, dt))
|
|
|
|
|
: (null, dt == default ? "Не удалось разобрать дату" : "Дата в прошлом");
|
|
|
|
|
? (WizardStepNames.Capacity, SetScheduledAt(payload, dt), payload)
|
|
|
|
|
: (null, dt == default ? "Не удалось разобрать дату" : "Дата в прошлом", payload);
|
|
|
|
|
|
|
|
|
|
case WizardStepNames.Capacity when payload.Single?.MaxPlayers is null:
|
|
|
|
|
return int.TryParse(input, out var cap) && cap >= WizardStep.MinCapacity && cap <= WizardStep.MaxCapacity
|
|
|
|
|
? (WizardStepNames.Visibility, SetMaxPlayers(payload, cap))
|
|
|
|
|
: (null, "Лимит должен быть 1..50");
|
|
|
|
|
? (WizardStepNames.Visibility, SetMaxPlayers(payload, cap), payload)
|
|
|
|
|
: (null, "Лимит должен быть 1..50", payload);
|
|
|
|
|
|
|
|
|
|
case WizardStepNames.PoolSystemDuration when payload.System is null:
|
|
|
|
|
return ValidateText(input, WizardStep.MaxSystemLength, "Слишком длинное название системы", "Слишком длинное название системы", out var psys)
|
|
|
|
|
? (WizardStepNames.PoolSystemDuration, SetSystem(payload, psys))
|
|
|
|
|
: (null, psys);
|
|
|
|
|
? (WizardStepNames.PoolSystemDuration, SetSystem(payload, psys), payload)
|
|
|
|
|
: (null, psys, payload);
|
|
|
|
|
|
|
|
|
|
case WizardStepNames.PoolSystemDuration when payload.DurationMinutes is null:
|
|
|
|
|
return TryParseHours(input, out var pdur)
|
|
|
|
|
? (WizardStepNames.Visibility, SetDurationMinutes(payload, pdur))
|
|
|
|
|
: (null, "Неверная длительность (1..12 ч)");
|
|
|
|
|
? (WizardStepNames.Visibility, SetDurationMinutes(payload, pdur), payload)
|
|
|
|
|
: (null, "Неверная длительность (1..12 ч)", payload);
|
|
|
|
|
|
|
|
|
|
case WizardStepNames.PoolSlotDateTime:
|
|
|
|
|
return MoscowTime.TryParseMoscow(input, out var slotDt) && slotDt > DateTimeOffset.UtcNow
|
|
|
|
|
? (WizardStepNames.PoolSlotCapacity, SetCurrentSlotDateTime(payload, slotDt))
|
|
|
|
|
: (null, slotDt == default ? "Не удалось разобрать дату" : "Дата в прошлом");
|
|
|
|
|
? (WizardStepNames.PoolSlotCapacity, SetCurrentSlotDateTime(payload, slotDt), payload)
|
|
|
|
|
: (null, slotDt == default ? "Не удалось разобрать дату" : "Дата в прошлом", payload);
|
|
|
|
|
|
|
|
|
|
case WizardStepNames.PoolSlotCapacity:
|
|
|
|
|
return int.TryParse(input, out var slotCap) && slotCap >= WizardStep.MinCapacity && slotCap <= WizardStep.MaxCapacity
|
|
|
|
|
? (WizardStepNames.PoolAddSlots, SetCurrentSlotMaxPlayers(payload, slotCap))
|
|
|
|
|
: (null, "Лимит должен быть 1..50");
|
|
|
|
|
? (WizardStepNames.PoolAddSlots, SetCurrentSlotMaxPlayers(payload, slotCap), payload)
|
|
|
|
|
: (null, "Лимит должен быть 1..50", payload);
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return (null, "Ожидается выбор кнопкой");
|
|
|
|
|
return (null, "Ожидается выбор кнопкой", payload);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Callback (button) dispatcher ──────────────────────────────────
|
|
|
|
|
private static (string? nextStep, string? error) ApplyChoice(WizardDraft draft, string step, string choice)
|
|
|
|
|
private static (string? nextStep, string? error, WizardPayload payload) ApplyChoice(WizardDraft draft, string step, string choice)
|
|
|
|
|
{
|
|
|
|
|
var payload = LoadPayload(draft);
|
|
|
|
|
return step switch
|
|
|
|
|
var (next, err) = step switch
|
|
|
|
|
{
|
|
|
|
|
WizardStepNames.Type => ApplyTypeChoice(payload, choice),
|
|
|
|
|
WizardStepNames.System => ApplySystemChoice(payload, choice),
|
|
|
|
@@ -252,6 +253,7 @@ public sealed class GameCreationWizard
|
|
|
|
|
WizardStepNames.PoolSlotCapacity => ApplyPoolSlotCapacityChoice(payload, choice),
|
|
|
|
|
_ => (null, "Неизвестный шаг"),
|
|
|
|
|
};
|
|
|
|
|
return (next, err, payload);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static (string?, string?) ApplyTypeChoice(WizardPayload p, string choice) => choice switch
|
|
|
|
@@ -420,8 +422,7 @@ public sealed class GameCreationWizard
|
|
|
|
|
private static string? CommitCurrentPoolSlot(WizardPayload p, bool waitlist)
|
|
|
|
|
{
|
|
|
|
|
p.Pool ??= new WizardPoolInput();
|
|
|
|
|
var current = p.Pool.Slots.LastOrDefault();
|
|
|
|
|
if (current is null) return "Слот не начат";
|
|
|
|
|
var current = EnsureCurrentPoolSlot(p);
|
|
|
|
|
current.Waitlist = waitlist;
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|