Compare commits

..

1 Commits

Author SHA1 Message Date
Toutsu 2ba411a04b ci(deploy): increase trivy image scan timeout to 30m
Slow ARM64 runners hit the default timeout while initializing the
container image scan after pulling. Extend the timeout so image scans
can complete reliably.
2026-06-13 20:22:05 +03:00
6 changed files with 13 additions and 198 deletions
@@ -23,18 +23,11 @@ internal static class SessionListMessageRenderer
public static IReadOnlyList<PlatformMessageAction> RenderActions(IReadOnlyList<SessionListItemDto> sessions)
{
if (sessions.Count == 0)
if (sessions.Count == 0 || !sessions.First().CanManage)
{
return [];
}
return sessions.First().CanManage
? RenderManagerActions(sessions)
: RenderPlayerActions(sessions);
}
private static IReadOnlyList<PlatformMessageAction> RenderManagerActions(IReadOnlyList<SessionListItemDto> sessions)
{
var actions = new List<PlatformMessageAction>();
foreach (var session in sessions)
@@ -43,19 +36,19 @@ internal static class SessionListMessageRenderer
actions.Add(new PlatformMessageAction(
$"cancel_session:{session.Id}",
$"❌ Отменить {dateTitle}",
$"❌ {dateTitle}",
$"cancel_session:{session.Id}"));
actions.Add(new PlatformMessageAction(
$"reschedule_session:{session.Id}",
$"⏰ Перенести {dateTitle}",
$"⏰ {dateTitle}",
$"reschedule_session:{session.Id}"));
if (SessionCapacityRules.CanPromoteWaitlistedPlayer(session.MaxPlayers, session.PlayerCount, session.WaitlistCount))
{
actions.Add(new PlatformMessageAction(
$"promote_waitlist:{session.Id}",
$"⬆️ С ожидания {dateTitle}",
$"⬆️ Из ожидания {dateTitle}",
$"promote_waitlist:{session.Id}"));
}
@@ -67,31 +60,4 @@ internal static class SessionListMessageRenderer
return actions;
}
private static IReadOnlyList<PlatformMessageAction> RenderPlayerActions(IReadOnlyList<SessionListItemDto> sessions)
{
var actions = new List<PlatformMessageAction>();
foreach (var session in sessions)
{
var dateTitle = session.ScheduledAt.FormatMoscowShort();
if (session.IsUserActive || session.IsUserWaitlisted)
{
actions.Add(new PlatformMessageAction(
$"leave_session:{session.Id}",
session.IsUserWaitlisted ? $"✖️ Выйти из ожидания {dateTitle}" : $"✖️ Выйти {dateTitle}",
$"leave_session:{session.Id}"));
}
else
{
actions.Add(new PlatformMessageAction(
$"join_session:{session.Id}",
$"✅ Записаться {dateTitle}",
$"join_session:{session.Id}"));
}
}
return actions;
}
}
@@ -1,46 +0,0 @@
using Telegram.Bot;
using Telegram.Bot.Types;
namespace GmRelay.Bot.Infrastructure.Telegram;
/// <summary>
/// Registers the bot's command list with Telegram so users see the
/// command menu when they type "/" in a chat.
/// </summary>
public sealed class TelegramCommandsSetupService(
ITelegramBotClient bot,
ILogger<TelegramCommandsSetupService> logger) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
var commands = new[]
{
new BotCommand { Command = "start", Description = "Начать работу с ботом" },
new BotCommand { Command = "newsession", Description = "Создать новую игровую сессию" },
new BotCommand { Command = "listsessions", Description = "Список предстоящих сессий" },
new BotCommand { Command = "exportcalendar", Description = "Экспортировать расписание в ICS" },
new BotCommand { Command = "help", Description = "Справка по командам" }
};
try
{
await bot.SetMyCommands(
commands,
scope: new BotCommandScopeAllPrivateChats(),
cancellationToken: cancellationToken);
await bot.SetMyCommands(
commands,
scope: new BotCommandScopeAllGroupChats(),
cancellationToken: cancellationToken);
logger.LogInformation("Telegram command menu registered for private chats and groups.");
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to register Telegram command menu.");
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
@@ -366,13 +366,6 @@ public sealed class UpdateRouter(
text: """
GM-Relay бот для управления игровыми сессиями.
/start начать работу с ботом
/newsession создать новую игровую сессию
/listsessions список предстоящих сессий
/exportcalendar экспортировать расписание в ICS
/help эта справка
Пример создания сессии:
/newsession
Название: My Game
Время: 15.05.2026 19:30
@@ -384,8 +377,10 @@ public sealed class UpdateRouter(
Игр: 4
Интервал: 7
/listsessions список предстоящих сессий
Для owner/co-GM /listsessions показывает кнопки отмены, переноса, удаления и повышения из листа ожидания.
Игроки могут записаться кнопкой «На дату» и сняться кнопкой «Выйти».
/help эта справка
""",
cancellationToken: ct);
break;
-1
View File
@@ -98,7 +98,6 @@ builder.Services.AddSingleton<DirectSessionNotificationSender>();
// ── Telegram infrastructure ──────────────────────────────────────────
builder.Services.AddSingleton<UpdateRouter>();
builder.Services.AddSingleton<ITelegramUpdateHandler>(sp => sp.GetRequiredService<UpdateRouter>());
builder.Services.AddHostedService<TelegramCommandsSetupService>();
builder.Services.AddHostedService<TelegramMiniAppMenuButtonService>();
builder.Services.AddHostedService<TelegramBotService>();
@@ -5,17 +5,7 @@ using Npgsql;
namespace GmRelay.Shared.Features.Sessions.ListSessions;
public sealed record SessionListItemDto(
Guid Id,
string Title,
DateTime ScheduledAt,
string Status,
int? MaxPlayers,
int PlayerCount,
int WaitlistCount,
bool CanManage,
bool IsUserActive,
bool IsUserWaitlisted);
public sealed record SessionListItemDto(Guid Id, string Title, DateTime ScheduledAt, string Status, int? MaxPlayers, int PlayerCount, int WaitlistCount, bool CanManage);
public sealed record SessionListResult(
IReadOnlyList<SessionListItemDto> Sessions,
@@ -39,27 +29,7 @@ public sealed class ListSessionsHandler(
WHERE gm.group_id = s.group_id
AND manager_player.platform = @Platform
AND manager_player.external_user_id = @ExternalUserId
) AS CanManage,
EXISTS (
SELECT 1
FROM session_participants user_sp
JOIN players user_p ON user_p.id = user_sp.player_id
WHERE user_sp.session_id = s.id
AND user_sp.is_gm = false
AND user_sp.registration_status = @Active
AND user_p.platform = @Platform
AND user_p.external_user_id = @ExternalUserId
) AS IsUserActive,
EXISTS (
SELECT 1
FROM session_participants user_sp
JOIN players user_p ON user_p.id = user_sp.player_id
WHERE user_sp.session_id = s.id
AND user_sp.is_gm = false
AND user_sp.registration_status = @Waitlisted
AND user_p.platform = @Platform
AND user_p.external_user_id = @ExternalUserId
) AS IsUserWaitlisted
) AS CanManage
FROM sessions s
JOIN game_groups g ON s.group_id = g.id
LEFT JOIN session_participants sp ON s.id = sp.session_id
@@ -20,9 +20,7 @@ public sealed class SessionListMessageRendererTests
4,
3,
1,
true,
false,
false)
true)
};
var text = SessionListMessageRenderer.RenderText(sessions);
@@ -34,92 +32,25 @@ public sealed class SessionListMessageRendererTests
Assert.Contains(actions, a => a.Payload == $"reschedule_session:{sessionId}");
Assert.Contains(actions, a => a.Payload == $"promote_waitlist:{sessionId}");
Assert.Contains(actions, a => a.Payload == $"delete_session:{sessionId}");
var shortDate = new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc).FormatMoscowShort();
Assert.Contains(actions, a => a.Label == $"❌ Отменить {shortDate}");
Assert.Contains(actions, a => a.Label == $"⏰ Перенести {shortDate}");
Assert.Contains(actions, a => a.Label == $"⬆️ С ожидания {shortDate}");
Assert.Contains(actions, a => a.Label == $"🗑 Удалить {shortDate}");
}
[Fact]
public void Render_ShouldIncludeJoinAction_WhenPlayerIsNotRegistered()
public void Render_ShouldHideManagerActions_WhenUserCannotManage()
{
var sessionId = Guid.NewGuid();
var sessions = new[]
{
new SessionListItemDto(
sessionId,
"Ravenloft",
new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc),
SessionStatus.Planned,
4,
3,
0,
false,
false,
false)
};
var actions = SessionListMessageRenderer.RenderActions(sessions);
var shortDate = new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc).FormatMoscowShort();
Assert.Single(actions);
Assert.Contains(actions, a => a.Payload == $"join_session:{sessionId}");
Assert.Contains(actions, a => a.Label == $"✅ Записаться {shortDate}");
}
[Fact]
public void Render_ShouldIncludeLeaveAction_WhenPlayerIsActive()
{
var sessionId = Guid.NewGuid();
var sessions = new[]
{
new SessionListItemDto(
sessionId,
"Ravenloft",
new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc),
SessionStatus.Planned,
4,
3,
0,
false,
true,
false)
};
var actions = SessionListMessageRenderer.RenderActions(sessions);
var shortDate = new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc).FormatMoscowShort();
Assert.Single(actions);
Assert.Contains(actions, a => a.Payload == $"leave_session:{sessionId}");
Assert.Contains(actions, a => a.Label == $"✖️ Выйти {shortDate}");
}
[Fact]
public void Render_ShouldIncludeLeaveWaitlistAction_WhenPlayerIsWaitlisted()
{
var sessionId = Guid.NewGuid();
var sessions = new[]
{
new SessionListItemDto(
sessionId,
Guid.NewGuid(),
"Ravenloft",
new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc),
SessionStatus.Planned,
4,
3,
1,
false,
false,
true)
false)
};
var actions = SessionListMessageRenderer.RenderActions(sessions);
var shortDate = new DateTime(2026, 5, 7, 16, 30, 0, DateTimeKind.Utc).FormatMoscowShort();
Assert.Single(actions);
Assert.Contains(actions, a => a.Payload == $"leave_session:{sessionId}");
Assert.Contains(actions, a => a.Label == $"✖️ Выйти из ожидания {shortDate}");
Assert.Empty(actions);
}
}