feat(bot): add online/offline wizard locations
PR Checks / test-and-build (pull_request) Successful in 15m52s

Add format and location steps to the Telegram /newsession wizard, persist offline addresses in sessions.location_address, and render online links/offline addresses in schedule messages.

Bump version to 3.10.0.
This commit is contained in:
2026-06-10 11:29:25 +03:00
parent bbd58142db
commit 014b5edd31
36 changed files with 460 additions and 49 deletions
@@ -226,9 +226,20 @@ public sealed class GameCreationWizard
case WizardStepNames.Capacity when payload.Single?.MaxPlayers is null:
return int.TryParse(input, out var cap) && cap >= WizardStepLimits.MinCapacity && cap <= WizardStepLimits.MaxCapacity
? (WizardStepNames.Visibility, SetMaxPlayers(payload, cap), payload)
? (WizardStepNames.Format, SetMaxPlayers(payload, cap), payload)
: (null, "Лимит должен быть 1..50", payload);
case WizardStepNames.Location when payload.Format == WizardSessionFormat.Online:
return Uri.TryCreate(input.Trim(), UriKind.Absolute, out var locationUri) &&
(locationUri.Scheme == Uri.UriSchemeHttp || locationUri.Scheme == Uri.UriSchemeHttps)
? (WizardStepNames.Visibility, SetJoinLink(payload, input.Trim()), payload)
: (null, "Некорректная ссылка", payload);
case WizardStepNames.Location when payload.Format == WizardSessionFormat.Offline:
return ValidateText(input, WizardStepLimits.MaxLocationLength, "Адрес не может быть пустым", "Слишком длинный адрес", out var address)
? (WizardStepNames.Visibility, SetLocationAddress(payload, address), payload)
: (null, address, payload);
case WizardStepNames.PoolSystemDuration when payload.System is null:
return ValidateText(input, WizardStepLimits.MaxSystemLength, "Слишком длинное название системы", "Слишком длинное название системы", out var psys)
? (WizardStepNames.PoolSystemDuration, SetSystem(payload, psys), payload)
@@ -236,7 +247,7 @@ public sealed class GameCreationWizard
case WizardStepNames.PoolSystemDuration when payload.DurationMinutes is null:
return TryParseHours(input, out var pdur)
? (WizardStepNames.Visibility, SetDurationMinutes(payload, pdur), payload)
? (WizardStepNames.Format, SetDurationMinutes(payload, pdur), payload)
: (null, "Неверная длительность (1..12 ч)", payload);
case WizardStepNames.PoolSlotDateTime:
@@ -264,6 +275,7 @@ public sealed class GameCreationWizard
WizardStepNames.System => ApplySystemChoice(payload, choice),
WizardStepNames.Duration => ApplyDurationChoice(payload, choice),
WizardStepNames.Capacity => ApplyCapacityChoice(payload, choice),
WizardStepNames.Format => ApplyFormatChoice(payload, choice),
WizardStepNames.Visibility => ApplyVisibilityChoice(payload, choice),
WizardStepNames.PickClub => ApplyPickClubChoice(payload, choice),
WizardStepNames.Publish => ApplyPublishChoice(payload, choice),
@@ -302,7 +314,7 @@ public sealed class GameCreationWizard
{
if (choice is "no_limit")
{
return (WizardStepNames.Visibility, SetMaxPlayers(p, null));
return (WizardStepNames.Format, SetMaxPlayers(p, null));
}
if (choice is "waitlist:on" or "waitlist:off" && p.Single?.MaxPlayers is null)
@@ -312,12 +324,19 @@ public sealed class GameCreationWizard
return choice switch
{
"waitlist:on" => (WizardStepNames.Visibility, SetWaitlist(p, true)),
"waitlist:off" => (WizardStepNames.Visibility, SetWaitlist(p, false)),
"waitlist:on" => (WizardStepNames.Format, SetWaitlist(p, true)),
"waitlist:off" => (WizardStepNames.Format, SetWaitlist(p, false)),
_ => (null, "Неизвестный выбор"),
};
}
private static (string?, string?) ApplyFormatChoice(WizardPayload p, string choice) => choice switch
{
"online" => (WizardStepNames.Location, SetFormat(p, WizardSessionFormat.Online)),
"offline" => (WizardStepNames.Location, SetFormat(p, WizardSessionFormat.Offline)),
_ => (null, "Неизвестный выбор"),
};
private static (string?, string?) ApplyVisibilityChoice(WizardPayload p, string choice) => choice switch
{
"public" => (NextAfterVisibility(p), SetVisibility(p, WizardVisibility.Public)),
@@ -349,7 +368,7 @@ public sealed class GameCreationWizard
{
"_custom" => (WizardStepNames.PoolSystemDuration, null),
{ } c when c.Contains(':') => SplitSystemDuration(c) is (var sys, var dur)
? (WizardStepNames.Visibility, SetSystem(p, sys) ?? SetDurationMinutes(p, dur))
? (WizardStepNames.Format, SetSystem(p, sys) ?? SetDurationMinutes(p, dur))
: (null, "Неверный выбор"),
_ => (null, "Неизвестный выбор"),
};
@@ -391,13 +410,15 @@ public sealed class GameCreationWizard
WizardStepNames.Duration => WizardStepNames.System,
WizardStepNames.DateTime => WizardStepNames.Duration,
WizardStepNames.Capacity => WizardStepNames.DateTime,
WizardStepNames.Visibility => WizardStepNames.Capacity,
WizardStepNames.Format => p.Type == WizardCreationType.Pool ? WizardStepNames.PoolSystemDuration : WizardStepNames.Capacity,
WizardStepNames.Location => WizardStepNames.Format,
WizardStepNames.Visibility => WizardStepNames.Location,
WizardStepNames.PickClub => WizardStepNames.Visibility,
WizardStepNames.Publish => WizardStepNames.PickClub,
WizardStepNames.Confirm => WizardStepNames.Publish,
WizardStepNames.PoolSystemDuration => null, // first pool step
WizardStepNames.PoolAddSlots => WizardStepNames.PoolSystemDuration,
WizardStepNames.PoolAddSlots => WizardStepNames.Visibility,
WizardStepNames.PoolSlotDateTime => WizardStepNames.PoolAddSlots,
WizardStepNames.PoolSlotCapacity => WizardStepNames.PoolSlotDateTime,
WizardStepNames.PoolConfirm => WizardStepNames.PoolAddSlots,
@@ -442,6 +463,15 @@ public sealed class GameCreationWizard
private static string? SetClubId(WizardPayload p, Guid v) { p.ClubId = v; return null; }
private static string? SetType(WizardPayload p, WizardCreationType v) { p.Type = v; return null; }
private static string? SetPublishInShowcase(WizardPayload p, bool v) { p.PublishInShowcase = v; return null; }
private static string? SetFormat(WizardPayload p, WizardSessionFormat v)
{
p.Format = v;
p.JoinLink = null;
p.LocationAddress = null;
return null;
}
private static string? SetJoinLink(WizardPayload p, string v) { p.JoinLink = v; p.LocationAddress = null; return null; }
private static string? SetLocationAddress(WizardPayload p, string v) { p.LocationAddress = v; p.JoinLink = null; return null; }
private static string? SetCurrentSlotDateTime(WizardPayload p, DateTimeOffset v)
{
@@ -488,8 +518,8 @@ public sealed class GameCreationWizard
private static string? NextAfterSystem(WizardPayload p) => WizardStepNames.Duration;
private static string? NextAfterDuration(WizardPayload p)
{
if (p.Type == WizardCreationType.Pool) return WizardStepNames.Visibility;
return p.Single?.MaxPlayers is not null ? WizardStepNames.Visibility : WizardStepNames.DateTime;
if (p.Type == WizardCreationType.Pool) return WizardStepNames.Format;
return p.Single?.MaxPlayers is not null ? WizardStepNames.Format : WizardStepNames.DateTime;
}
private static string? NextAfterVisibility(WizardPayload p)
{