Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b80002aa36 |
@@ -6,7 +6,7 @@ on:
|
||||
- main
|
||||
|
||||
env:
|
||||
VERSION: 1.1.3
|
||||
VERSION: 1.1.4
|
||||
|
||||
jobs:
|
||||
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.1.3</Version>
|
||||
<Version>1.1.4</Version>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ services:
|
||||
retries: 10
|
||||
|
||||
bot:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:1.1.3
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:1.1.4
|
||||
container_name: gmrelay_bot
|
||||
restart: always
|
||||
network_mode: host
|
||||
@@ -30,7 +30,7 @@ services:
|
||||
- "Telegram__BotToken=${TELEGRAM_BOT_TOKEN}"
|
||||
|
||||
web:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:1.1.3
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:1.1.4
|
||||
container_name: gmrelay_web
|
||||
restart: always
|
||||
network_mode: host
|
||||
|
||||
@@ -48,7 +48,10 @@ public sealed class CancelSessionHandler(
|
||||
}
|
||||
|
||||
// 2. Отменяем сессию
|
||||
await connection.ExecuteAsync("UPDATE sessions SET status = 'Cancelled' WHERE id = @Id", new { Id = command.SessionId }, transaction);
|
||||
await connection.ExecuteAsync(
|
||||
"UPDATE sessions SET status = @Status WHERE id = @Id",
|
||||
new { Id = command.SessionId, Status = SessionStatus.Cancelled },
|
||||
transaction);
|
||||
|
||||
// 3. Загружаем весь батч для перерисовки
|
||||
var batchSessions = await connection.QueryAsync<SessionBatchDto>(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dapper;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Rendering;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
@@ -93,7 +94,7 @@ public sealed class CreateSessionHandler(
|
||||
var sessionId = await connection.ExecuteScalarAsync<Guid>(
|
||||
"""
|
||||
INSERT INTO sessions (batch_id, group_id, title, join_link, scheduled_at, status, thread_id)
|
||||
VALUES (@BatchId, @GroupId, @Title, @Link, @ScheduledAt, 'Planned', @ThreadId)
|
||||
VALUES (@BatchId, @GroupId, @Title, @Link, @ScheduledAt, @Status, @ThreadId)
|
||||
RETURNING id;
|
||||
""",
|
||||
new
|
||||
@@ -103,11 +104,12 @@ public sealed class CreateSessionHandler(
|
||||
Title = title,
|
||||
Link = link,
|
||||
ScheduledAt = scheduledAt,
|
||||
ThreadId = messageThreadId
|
||||
ThreadId = messageThreadId,
|
||||
Status = SessionStatus.Planned
|
||||
},
|
||||
transaction);
|
||||
|
||||
sessions.Add(new SessionBatchDto(sessionId, scheduledAt.UtcDateTime, "Planned"));
|
||||
sessions.Add(new SessionBatchDto(sessionId, scheduledAt.UtcDateTime, SessionStatus.Planned));
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text;
|
||||
using Dapper;
|
||||
using GmRelay.Shared.Domain;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
@@ -21,10 +22,10 @@ public sealed class ExportCalendarHandler(
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON s.group_id = g.id
|
||||
WHERE g.telegram_chat_id = @ChatId
|
||||
AND s.status = 'Planned'
|
||||
AND s.status = @Planned
|
||||
AND s.scheduled_at > NOW()
|
||||
ORDER BY s.scheduled_at ASC",
|
||||
new { ChatId = message.Chat.Id });
|
||||
new { ChatId = message.Chat.Id, Planned = SessionStatus.Planned });
|
||||
|
||||
var sessionsList = sessions.ToList();
|
||||
|
||||
|
||||
@@ -80,10 +80,10 @@ public sealed class DeleteSessionHandler(
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON s.group_id = g.id
|
||||
LEFT JOIN session_participants sp ON s.id = sp.session_id
|
||||
WHERE g.telegram_chat_id = @ChatId AND s.status != 'Cancelled' AND s.scheduled_at > NOW()
|
||||
WHERE g.telegram_chat_id = @ChatId AND s.status != @Cancelled AND s.scheduled_at > NOW()
|
||||
GROUP BY s.id, s.title, s.scheduled_at, s.status, g.gm_telegram_id
|
||||
ORDER BY s.scheduled_at ASC",
|
||||
new { ChatId = command.ChatId });
|
||||
new { ChatId = command.ChatId, Cancelled = SessionStatus.Cancelled });
|
||||
|
||||
var sessionsList = sessions.ToList();
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ public sealed class ListSessionsHandler(
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON s.group_id = g.id
|
||||
LEFT JOIN session_participants sp ON s.id = sp.session_id
|
||||
WHERE g.telegram_chat_id = @ChatId AND s.status != 'Cancelled' AND s.scheduled_at > NOW()
|
||||
WHERE g.telegram_chat_id = @ChatId AND s.status != @Cancelled AND s.scheduled_at > NOW()
|
||||
GROUP BY s.id, s.title, s.scheduled_at, s.status, g.gm_telegram_id
|
||||
ORDER BY s.scheduled_at ASC",
|
||||
new { ChatId = message.Chat.Id });
|
||||
new { ChatId = message.Chat.Id, Cancelled = SessionStatus.Cancelled });
|
||||
|
||||
var sessionsList = sessions.ToList();
|
||||
|
||||
|
||||
+2
-2
@@ -154,10 +154,10 @@ public sealed class HandleRescheduleTimeInputHandler(
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
"""
|
||||
UPDATE sessions SET scheduled_at = @NewTime, status = 'Planned', updated_at = now()
|
||||
UPDATE sessions SET scheduled_at = @NewTime, status = @Status, updated_at = now()
|
||||
WHERE id = @SessionId
|
||||
""",
|
||||
new { NewTime = newTime, proposal.SessionId },
|
||||
new { NewTime = newTime, proposal.SessionId, Status = SessionStatus.Planned },
|
||||
transaction);
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
|
||||
@@ -165,13 +165,13 @@ public sealed class HandleRescheduleVoteHandler(
|
||||
"""
|
||||
UPDATE sessions
|
||||
SET scheduled_at = @NewTime,
|
||||
status = 'Planned',
|
||||
status = @Status,
|
||||
confirmation_message_id = NULL,
|
||||
link_message_id = NULL,
|
||||
updated_at = now()
|
||||
WHERE id = @SessionId
|
||||
""",
|
||||
new { NewTime = newTime, proposal.SessionId },
|
||||
new { NewTime = newTime, proposal.SessionId, Status = SessionStatus.Planned },
|
||||
transaction);
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dapper;
|
||||
using GmRelay.Shared.Domain;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
|
||||
@@ -39,9 +40,9 @@ public sealed class InitiateRescheduleHandler(
|
||||
SELECT s.title AS Title, g.gm_telegram_id AS GmId
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON s.group_id = g.id
|
||||
WHERE s.id = @SessionId AND s.status != 'Cancelled'
|
||||
WHERE s.id = @SessionId AND s.status != @Cancelled
|
||||
""",
|
||||
new { command.SessionId });
|
||||
new { command.SessionId, Cancelled = SessionStatus.Cancelled });
|
||||
|
||||
if (session is null)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Frozen;
|
||||
|
||||
namespace GmRelay.Shared.Domain;
|
||||
|
||||
public static class SessionStatus
|
||||
@@ -6,4 +8,13 @@ public static class SessionStatus
|
||||
public const string ConfirmationSent = "ConfirmationSent";
|
||||
public const string Confirmed = "Confirmed";
|
||||
public const string Cancelled = "Cancelled";
|
||||
|
||||
public static IReadOnlySet<string> All { get; } =
|
||||
new[] { Planned, ConfirmationSent, Confirmed, Cancelled }
|
||||
.ToFrozenSet(StringComparer.Ordinal);
|
||||
|
||||
public static bool IsKnown(string status) => All.Contains(status);
|
||||
|
||||
public static bool IsCancelled(string status) =>
|
||||
string.Equals(status, Cancelled, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
@@ -36,14 +36,10 @@ public static class SessionBatchRenderer
|
||||
messageText += " <i>Пока никто не записался</i>\n";
|
||||
}
|
||||
|
||||
if (session.Status == "Cancelled")
|
||||
if (SessionStatus.IsCancelled(session.Status))
|
||||
{
|
||||
messageText += "❌ <i>Сессия отменена</i>\n\n";
|
||||
}
|
||||
else if (session.Status == "RecruitmentClosed")
|
||||
{
|
||||
messageText += "🔒 <i>Набор завершен</i>\n\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
messageText += "\n";
|
||||
|
||||
@@ -134,15 +134,12 @@
|
||||
SessionStatus.Confirmed => "status-success",
|
||||
SessionStatus.Cancelled => "status-danger",
|
||||
SessionStatus.ConfirmationSent => "status-warning",
|
||||
"Recruiting" => "status-info",
|
||||
"RecruitmentClosed" => "status-info",
|
||||
SessionStatus.Planned => "status-info",
|
||||
_ => "status-neutral"
|
||||
};
|
||||
|
||||
private string TranslateStatus(string status) => status switch
|
||||
{
|
||||
"Recruiting" => "Набор",
|
||||
"RecruitmentClosed" => "Набор закрыт",
|
||||
SessionStatus.Planned => "Запланировано",
|
||||
SessionStatus.ConfirmationSent => "Ждём подтверждения",
|
||||
SessionStatus.Confirmed => "Подтверждено",
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Reflection;
|
||||
using GmRelay.Shared.Domain;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Domain;
|
||||
|
||||
public sealed class SessionStatusTests
|
||||
{
|
||||
[Fact]
|
||||
public void All_ShouldContainOnlyCanonicalSessionStatuses()
|
||||
{
|
||||
var allProperty = typeof(SessionStatus).GetProperty(
|
||||
"All",
|
||||
BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
Assert.NotNull(allProperty);
|
||||
|
||||
var allStatusValues = Assert.IsAssignableFrom<IReadOnlySet<string>>(allProperty.GetValue(null));
|
||||
var expectedStatusValues = new[]
|
||||
{
|
||||
SessionStatus.Planned,
|
||||
SessionStatus.ConfirmationSent,
|
||||
SessionStatus.Confirmed,
|
||||
SessionStatus.Cancelled
|
||||
}
|
||||
.Order(StringComparer.Ordinal);
|
||||
|
||||
Assert.Equal(expectedStatusValues, allStatusValues.Order(StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductionSources_ShouldNotReferenceLegacySessionStatuses()
|
||||
{
|
||||
var repositoryRoot = FindRepositoryRoot();
|
||||
var productionFiles = Directory.EnumerateFiles(repositoryRoot, "*.*", SearchOption.AllDirectories)
|
||||
.Where(path => IsProductionSource(path))
|
||||
.ToList();
|
||||
|
||||
var legacyStatuses = new[] { "Recruit" + "ing", "Recruitment" + "Closed" };
|
||||
var offenders = productionFiles
|
||||
.SelectMany(path => legacyStatuses
|
||||
.Where(status => File.ReadAllText(path).Contains(status, StringComparison.Ordinal))
|
||||
.Select(status => $"{Path.GetRelativePath(repositoryRoot, path)} contains {status}"))
|
||||
.ToList();
|
||||
|
||||
Assert.Empty(offenders);
|
||||
}
|
||||
|
||||
private static bool IsProductionSource(string path)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
var separator = Path.DirectorySeparatorChar;
|
||||
|
||||
return path.Contains($"{separator}src{separator}", StringComparison.Ordinal)
|
||||
&& !path.Contains($"{separator}bin{separator}", StringComparison.Ordinal)
|
||||
&& !path.Contains($"{separator}obj{separator}", StringComparison.Ordinal)
|
||||
&& extension is ".cs" or ".razor" or ".sql";
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "GM-Relay.slnx")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could not find repository root.");
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace GmRelay.Bot.Tests.Rendering;
|
||||
public sealed class SessionBatchRendererTests
|
||||
{
|
||||
[Fact]
|
||||
public void Render_ShouldOrderSessionsAndSkipButtonsForClosedStatuses()
|
||||
public void Render_ShouldOrderSessionsAndSkipButtonsForCancelledSessions()
|
||||
{
|
||||
var firstSessionId = Guid.NewGuid();
|
||||
var secondSessionId = Guid.NewGuid();
|
||||
@@ -14,9 +14,9 @@ public sealed class SessionBatchRendererTests
|
||||
|
||||
var sessions = new[]
|
||||
{
|
||||
new SessionBatchDto(secondSessionId, new DateTime(2026, 4, 27, 18, 0, 0, DateTimeKind.Utc), "Planned"),
|
||||
new SessionBatchDto(cancelledSessionId, new DateTime(2026, 4, 28, 18, 0, 0, DateTimeKind.Utc), "Cancelled"),
|
||||
new SessionBatchDto(firstSessionId, new DateTime(2026, 4, 26, 18, 0, 0, DateTimeKind.Utc), "RecruitmentClosed")
|
||||
new SessionBatchDto(secondSessionId, new DateTime(2026, 4, 27, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned),
|
||||
new SessionBatchDto(cancelledSessionId, new DateTime(2026, 4, 28, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Cancelled),
|
||||
new SessionBatchDto(firstSessionId, new DateTime(2026, 4, 26, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned)
|
||||
};
|
||||
var participants = new[]
|
||||
{
|
||||
@@ -37,9 +37,12 @@ public sealed class SessionBatchRendererTests
|
||||
Assert.True(secondIndex < thirdIndex);
|
||||
Assert.Contains("@alice", text);
|
||||
Assert.Contains("Bob", text);
|
||||
Assert.Single(result.Markup.InlineKeyboard);
|
||||
Assert.Equal(2, result.Markup.InlineKeyboard.Count());
|
||||
Assert.Collection(
|
||||
buttons.Select(button => button.CallbackData),
|
||||
callbackData => Assert.Equal($"join_session:{firstSessionId}", callbackData),
|
||||
callbackData => Assert.Equal($"cancel_session:{firstSessionId}", callbackData),
|
||||
callbackData => Assert.Equal($"reschedule_session:{firstSessionId}", callbackData),
|
||||
callbackData => Assert.Equal($"join_session:{secondSessionId}", callbackData),
|
||||
callbackData => Assert.Equal($"cancel_session:{secondSessionId}", callbackData),
|
||||
callbackData => Assert.Equal($"reschedule_session:{secondSessionId}", callbackData));
|
||||
|
||||
Reference in New Issue
Block a user