@@ -6,7 +6,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VERSION: 1.1.3
|
VERSION: 1.1.4
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
|
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>1.1.3</Version>
|
<Version>1.1.4</Version>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
bot:
|
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
|
container_name: gmrelay_bot
|
||||||
restart: always
|
restart: always
|
||||||
network_mode: host
|
network_mode: host
|
||||||
@@ -30,7 +30,7 @@ services:
|
|||||||
- "Telegram__BotToken=${TELEGRAM_BOT_TOKEN}"
|
- "Telegram__BotToken=${TELEGRAM_BOT_TOKEN}"
|
||||||
|
|
||||||
web:
|
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
|
container_name: gmrelay_web
|
||||||
restart: always
|
restart: always
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ public sealed class CancelSessionHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Отменяем сессию
|
// 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. Загружаем весь батч для перерисовки
|
// 3. Загружаем весь батч для перерисовки
|
||||||
var batchSessions = await connection.QueryAsync<SessionBatchDto>(
|
var batchSessions = await connection.QueryAsync<SessionBatchDto>(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
|
using GmRelay.Shared.Domain;
|
||||||
using GmRelay.Shared.Rendering;
|
using GmRelay.Shared.Rendering;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
@@ -93,7 +94,7 @@ public sealed class CreateSessionHandler(
|
|||||||
var sessionId = await connection.ExecuteScalarAsync<Guid>(
|
var sessionId = await connection.ExecuteScalarAsync<Guid>(
|
||||||
"""
|
"""
|
||||||
INSERT INTO sessions (batch_id, group_id, title, join_link, scheduled_at, status, thread_id)
|
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;
|
RETURNING id;
|
||||||
""",
|
""",
|
||||||
new
|
new
|
||||||
@@ -103,11 +104,12 @@ public sealed class CreateSessionHandler(
|
|||||||
Title = title,
|
Title = title,
|
||||||
Link = link,
|
Link = link,
|
||||||
ScheduledAt = scheduledAt,
|
ScheduledAt = scheduledAt,
|
||||||
ThreadId = messageThreadId
|
ThreadId = messageThreadId,
|
||||||
|
Status = SessionStatus.Planned
|
||||||
},
|
},
|
||||||
transaction);
|
transaction);
|
||||||
|
|
||||||
sessions.Add(new SessionBatchDto(sessionId, scheduledAt.UtcDateTime, "Planned"));
|
sessions.Add(new SessionBatchDto(sessionId, scheduledAt.UtcDateTime, SessionStatus.Planned));
|
||||||
}
|
}
|
||||||
|
|
||||||
await transaction.CommitAsync(cancellationToken);
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
using GmRelay.Shared.Domain;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
using Telegram.Bot.Types;
|
using Telegram.Bot.Types;
|
||||||
@@ -21,10 +22,10 @@ public sealed class ExportCalendarHandler(
|
|||||||
FROM sessions s
|
FROM sessions s
|
||||||
JOIN game_groups g ON s.group_id = g.id
|
JOIN game_groups g ON s.group_id = g.id
|
||||||
WHERE g.telegram_chat_id = @ChatId
|
WHERE g.telegram_chat_id = @ChatId
|
||||||
AND s.status = 'Planned'
|
AND s.status = @Planned
|
||||||
AND s.scheduled_at > NOW()
|
AND s.scheduled_at > NOW()
|
||||||
ORDER BY s.scheduled_at ASC",
|
ORDER BY s.scheduled_at ASC",
|
||||||
new { ChatId = message.Chat.Id });
|
new { ChatId = message.Chat.Id, Planned = SessionStatus.Planned });
|
||||||
|
|
||||||
var sessionsList = sessions.ToList();
|
var sessionsList = sessions.ToList();
|
||||||
|
|
||||||
|
|||||||
@@ -80,10 +80,10 @@ public sealed class DeleteSessionHandler(
|
|||||||
FROM sessions s
|
FROM sessions s
|
||||||
JOIN game_groups g ON s.group_id = g.id
|
JOIN game_groups g ON s.group_id = g.id
|
||||||
LEFT JOIN session_participants sp ON s.id = sp.session_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
|
GROUP BY s.id, s.title, s.scheduled_at, s.status, g.gm_telegram_id
|
||||||
ORDER BY s.scheduled_at ASC",
|
ORDER BY s.scheduled_at ASC",
|
||||||
new { ChatId = command.ChatId });
|
new { ChatId = command.ChatId, Cancelled = SessionStatus.Cancelled });
|
||||||
|
|
||||||
var sessionsList = sessions.ToList();
|
var sessionsList = sessions.ToList();
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ public sealed class ListSessionsHandler(
|
|||||||
FROM sessions s
|
FROM sessions s
|
||||||
JOIN game_groups g ON s.group_id = g.id
|
JOIN game_groups g ON s.group_id = g.id
|
||||||
LEFT JOIN session_participants sp ON s.id = sp.session_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
|
GROUP BY s.id, s.title, s.scheduled_at, s.status, g.gm_telegram_id
|
||||||
ORDER BY s.scheduled_at ASC",
|
ORDER BY s.scheduled_at ASC",
|
||||||
new { ChatId = message.Chat.Id });
|
new { ChatId = message.Chat.Id, Cancelled = SessionStatus.Cancelled });
|
||||||
|
|
||||||
var sessionsList = sessions.ToList();
|
var sessionsList = sessions.ToList();
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -154,10 +154,10 @@ public sealed class HandleRescheduleTimeInputHandler(
|
|||||||
|
|
||||||
await connection.ExecuteAsync(
|
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
|
WHERE id = @SessionId
|
||||||
""",
|
""",
|
||||||
new { NewTime = newTime, proposal.SessionId },
|
new { NewTime = newTime, proposal.SessionId, Status = SessionStatus.Planned },
|
||||||
transaction);
|
transaction);
|
||||||
|
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
|
|||||||
@@ -165,13 +165,13 @@ public sealed class HandleRescheduleVoteHandler(
|
|||||||
"""
|
"""
|
||||||
UPDATE sessions
|
UPDATE sessions
|
||||||
SET scheduled_at = @NewTime,
|
SET scheduled_at = @NewTime,
|
||||||
status = 'Planned',
|
status = @Status,
|
||||||
confirmation_message_id = NULL,
|
confirmation_message_id = NULL,
|
||||||
link_message_id = NULL,
|
link_message_id = NULL,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = @SessionId
|
WHERE id = @SessionId
|
||||||
""",
|
""",
|
||||||
new { NewTime = newTime, proposal.SessionId },
|
new { NewTime = newTime, proposal.SessionId, Status = SessionStatus.Planned },
|
||||||
transaction);
|
transaction);
|
||||||
|
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
|
using GmRelay.Shared.Domain;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
|
|
||||||
@@ -39,9 +40,9 @@ public sealed class InitiateRescheduleHandler(
|
|||||||
SELECT s.title AS Title, g.gm_telegram_id AS GmId
|
SELECT s.title AS Title, g.gm_telegram_id AS GmId
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
JOIN game_groups g ON s.group_id = g.id
|
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)
|
if (session is null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Collections.Frozen;
|
||||||
|
|
||||||
namespace GmRelay.Shared.Domain;
|
namespace GmRelay.Shared.Domain;
|
||||||
|
|
||||||
public static class SessionStatus
|
public static class SessionStatus
|
||||||
@@ -6,4 +8,13 @@ public static class SessionStatus
|
|||||||
public const string ConfirmationSent = "ConfirmationSent";
|
public const string ConfirmationSent = "ConfirmationSent";
|
||||||
public const string Confirmed = "Confirmed";
|
public const string Confirmed = "Confirmed";
|
||||||
public const string Cancelled = "Cancelled";
|
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";
|
messageText += " <i>Пока никто не записался</i>\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.Status == "Cancelled")
|
if (SessionStatus.IsCancelled(session.Status))
|
||||||
{
|
{
|
||||||
messageText += "❌ <i>Сессия отменена</i>\n\n";
|
messageText += "❌ <i>Сессия отменена</i>\n\n";
|
||||||
}
|
}
|
||||||
else if (session.Status == "RecruitmentClosed")
|
|
||||||
{
|
|
||||||
messageText += "🔒 <i>Набор завершен</i>\n\n";
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
messageText += "\n";
|
messageText += "\n";
|
||||||
|
|||||||
@@ -134,15 +134,12 @@
|
|||||||
SessionStatus.Confirmed => "status-success",
|
SessionStatus.Confirmed => "status-success",
|
||||||
SessionStatus.Cancelled => "status-danger",
|
SessionStatus.Cancelled => "status-danger",
|
||||||
SessionStatus.ConfirmationSent => "status-warning",
|
SessionStatus.ConfirmationSent => "status-warning",
|
||||||
"Recruiting" => "status-info",
|
SessionStatus.Planned => "status-info",
|
||||||
"RecruitmentClosed" => "status-info",
|
|
||||||
_ => "status-neutral"
|
_ => "status-neutral"
|
||||||
};
|
};
|
||||||
|
|
||||||
private string TranslateStatus(string status) => status switch
|
private string TranslateStatus(string status) => status switch
|
||||||
{
|
{
|
||||||
"Recruiting" => "Набор",
|
|
||||||
"RecruitmentClosed" => "Набор закрыт",
|
|
||||||
SessionStatus.Planned => "Запланировано",
|
SessionStatus.Planned => "Запланировано",
|
||||||
SessionStatus.ConfirmationSent => "Ждём подтверждения",
|
SessionStatus.ConfirmationSent => "Ждём подтверждения",
|
||||||
SessionStatus.Confirmed => "Подтверждено",
|
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
|
public sealed class SessionBatchRendererTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Render_ShouldOrderSessionsAndSkipButtonsForClosedStatuses()
|
public void Render_ShouldOrderSessionsAndSkipButtonsForCancelledSessions()
|
||||||
{
|
{
|
||||||
var firstSessionId = Guid.NewGuid();
|
var firstSessionId = Guid.NewGuid();
|
||||||
var secondSessionId = Guid.NewGuid();
|
var secondSessionId = Guid.NewGuid();
|
||||||
@@ -14,9 +14,9 @@ public sealed class SessionBatchRendererTests
|
|||||||
|
|
||||||
var sessions = new[]
|
var sessions = new[]
|
||||||
{
|
{
|
||||||
new SessionBatchDto(secondSessionId, new DateTime(2026, 4, 27, 18, 0, 0, DateTimeKind.Utc), "Planned"),
|
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), "Cancelled"),
|
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), "RecruitmentClosed")
|
new SessionBatchDto(firstSessionId, new DateTime(2026, 4, 26, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned)
|
||||||
};
|
};
|
||||||
var participants = new[]
|
var participants = new[]
|
||||||
{
|
{
|
||||||
@@ -37,9 +37,12 @@ public sealed class SessionBatchRendererTests
|
|||||||
Assert.True(secondIndex < thirdIndex);
|
Assert.True(secondIndex < thirdIndex);
|
||||||
Assert.Contains("@alice", text);
|
Assert.Contains("@alice", text);
|
||||||
Assert.Contains("Bob", text);
|
Assert.Contains("Bob", text);
|
||||||
Assert.Single(result.Markup.InlineKeyboard);
|
Assert.Equal(2, result.Markup.InlineKeyboard.Count());
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
buttons.Select(button => button.CallbackData),
|
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($"join_session:{secondSessionId}", callbackData),
|
||||||
callbackData => Assert.Equal($"cancel_session:{secondSessionId}", callbackData),
|
callbackData => Assert.Equal($"cancel_session:{secondSessionId}", callbackData),
|
||||||
callbackData => Assert.Equal($"reschedule_session:{secondSessionId}", callbackData));
|
callbackData => Assert.Equal($"reschedule_session:{secondSessionId}", callbackData));
|
||||||
|
|||||||
Reference in New Issue
Block a user