fix(bot): skip join-link reminders without links
PR Checks / test-and-build (pull_request) Failing after 20m55s
PR Checks / test-and-build (pull_request) Failing after 20m55s
Prevent offline sessions with empty join links from entering the 5-minute join-link notification flow, omit blank link lines from direct reminders, and add offline persistence/reminder regression coverage.
This commit is contained in:
@@ -405,19 +405,8 @@ public sealed class TelegramPlatformMessenger(
|
|||||||
|
|
||||||
Ответьте кнопкой в групповом сообщении расписания.
|
Ответьте кнопкой в групповом сообщении расписания.
|
||||||
""",
|
""",
|
||||||
PlatformDirectSessionNotificationKind.OneHourReminder => $"""
|
PlatformDirectSessionNotificationKind.OneHourReminder => BuildOneHourReminderDirectText(notification),
|
||||||
⏰ <b>Игра начнётся примерно через 1 час</b>
|
PlatformDirectSessionNotificationKind.JoinLink => BuildJoinLinkDirectText(notification),
|
||||||
|
|
||||||
📌 <b>{System.Net.WebUtility.HtmlEncode(notification.Title)}</b>
|
|
||||||
📅 {notification.ScheduledAt.FormatMoscow()} (МСК)
|
|
||||||
🔗 {System.Net.WebUtility.HtmlEncode(notification.JoinLink ?? string.Empty)}
|
|
||||||
""",
|
|
||||||
PlatformDirectSessionNotificationKind.JoinLink => $"""
|
|
||||||
🎮 <b>Игра начинается через 5 минут</b>
|
|
||||||
|
|
||||||
📌 <b>{System.Net.WebUtility.HtmlEncode(notification.Title)}</b>
|
|
||||||
🔗 {System.Net.WebUtility.HtmlEncode(notification.JoinLink ?? string.Empty)}
|
|
||||||
""",
|
|
||||||
PlatformDirectSessionNotificationKind.RescheduleApproved => $"""
|
PlatformDirectSessionNotificationKind.RescheduleApproved => $"""
|
||||||
✅ <b>Сессия перенесена по итогам голосования</b>
|
✅ <b>Сессия перенесена по итогам голосования</b>
|
||||||
|
|
||||||
@@ -434,6 +423,39 @@ public sealed class TelegramPlatformMessenger(
|
|||||||
_ => BuildFallbackDirectText(notification)
|
_ => BuildFallbackDirectText(notification)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string BuildOneHourReminderDirectText(PlatformDirectSessionNotification notification)
|
||||||
|
{
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
"⏰ <b>Игра начнётся примерно через 1 час</b>",
|
||||||
|
string.Empty,
|
||||||
|
$"📌 <b>{System.Net.WebUtility.HtmlEncode(notification.Title)}</b>",
|
||||||
|
$"📅 {notification.ScheduledAt.FormatMoscow()} (МСК)"
|
||||||
|
};
|
||||||
|
AppendJoinLinkLine(lines, notification.JoinLink);
|
||||||
|
return string.Join("\n", lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildJoinLinkDirectText(PlatformDirectSessionNotification notification)
|
||||||
|
{
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
"🎮 <b>Игра начинается через 5 минут</b>",
|
||||||
|
string.Empty,
|
||||||
|
$"📌 <b>{System.Net.WebUtility.HtmlEncode(notification.Title)}</b>"
|
||||||
|
};
|
||||||
|
AppendJoinLinkLine(lines, notification.JoinLink);
|
||||||
|
return string.Join("\n", lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendJoinLinkLine(List<string> lines, string? joinLink)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(joinLink))
|
||||||
|
{
|
||||||
|
lines.Add($"🔗 {System.Net.WebUtility.HtmlEncode(joinLink)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildFallbackDirectText(PlatformDirectSessionNotification notification) =>
|
private static string BuildFallbackDirectText(PlatformDirectSessionNotification notification) =>
|
||||||
$"<b>{System.Net.WebUtility.HtmlEncode(notification.Title)}</b>\n{notification.ScheduledAt.FormatMoscow()} (МСК)";
|
$"<b>{System.Net.WebUtility.HtmlEncode(notification.Title)}</b>\n{notification.ScheduledAt.FormatMoscow()} (МСК)";
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public sealed class SendJoinLinkHandler(
|
|||||||
JOIN game_groups g ON g.id = s.group_id
|
JOIN game_groups g ON g.id = s.group_id
|
||||||
WHERE s.id = @SessionId
|
WHERE s.id = @SessionId
|
||||||
AND s.status = @Confirmed
|
AND s.status = @Confirmed
|
||||||
|
AND btrim(s.join_link) <> ''
|
||||||
AND (
|
AND (
|
||||||
(g.platform = 'Telegram' AND s.link_message_id IS NULL)
|
(g.platform = 'Telegram' AND s.link_message_id IS NULL)
|
||||||
OR (
|
OR (
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ public sealed class DbSessionTriggerStore(
|
|||||||
JOIN game_groups g ON g.id = s.group_id
|
JOIN game_groups g ON g.id = s.group_id
|
||||||
WHERE g.platform = @Platform
|
WHERE g.platform = @Platform
|
||||||
AND s.status = @Confirmed
|
AND s.status = @Confirmed
|
||||||
|
AND btrim(s.join_link) <> ''
|
||||||
AND s.scheduled_at - @LeadTime <= @Now
|
AND s.scheduled_at - @LeadTime <= @Now
|
||||||
AND (
|
AND (
|
||||||
(g.platform = 'Telegram' AND s.link_message_id IS NULL)
|
(g.platform = 'Telegram' AND s.link_message_id IS NULL)
|
||||||
|
|||||||
+45
@@ -144,4 +144,49 @@ public sealed class CreateSessionHandlerIntegrationTests(CreateSessionHandlerPos
|
|||||||
Assert.Equal("Online room notes", reader.GetString(2));
|
Assert.Equal("Online room notes", reader.GetString(2));
|
||||||
Assert.False(await reader.ReadAsync());
|
Assert.False(await reader.ReadAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_OfflineSession_PersistsFormatAndLocationAddress()
|
||||||
|
{
|
||||||
|
var connectionString = await fixture.CreateMigratedDatabaseAsync();
|
||||||
|
await using var dataSource = NpgsqlDataSource.Create(connectionString);
|
||||||
|
var sut = new CreateSessionHandler(dataSource);
|
||||||
|
|
||||||
|
var result = await sut.HandleAsync(
|
||||||
|
new CreateSessionCommand(
|
||||||
|
new PlatformUser(PlatformKind.Telegram, "333333333", "Offline GM", "offline_gm"),
|
||||||
|
new PlatformGroup(PlatformKind.Telegram, "444444444", "Offline Group"),
|
||||||
|
"Offline Adventure",
|
||||||
|
string.Empty,
|
||||||
|
[DateTimeOffset.UtcNow.AddDays(1)],
|
||||||
|
4,
|
||||||
|
null,
|
||||||
|
GameSystem.Dnd5e,
|
||||||
|
"Offline integration regression test",
|
||||||
|
"Offline",
|
||||||
|
240,
|
||||||
|
true,
|
||||||
|
"Москва, ул. Кубиков, 12"),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(result.Success, result.ErrorMessage);
|
||||||
|
Assert.NotNull(result.BatchId);
|
||||||
|
|
||||||
|
await using var connection = await dataSource.OpenConnectionAsync();
|
||||||
|
await using var command = new NpgsqlCommand(
|
||||||
|
"""
|
||||||
|
SELECT join_link, format, location_address
|
||||||
|
FROM sessions
|
||||||
|
WHERE batch_id = @batch_id
|
||||||
|
""",
|
||||||
|
connection);
|
||||||
|
command.Parameters.AddWithValue("batch_id", result.BatchId.Value);
|
||||||
|
|
||||||
|
await using var reader = await command.ExecuteReaderAsync();
|
||||||
|
Assert.True(await reader.ReadAsync());
|
||||||
|
Assert.Equal(string.Empty, reader.GetString(0));
|
||||||
|
Assert.Equal("Offline", reader.GetString(1));
|
||||||
|
Assert.Equal("Москва, ул. Кубиков, 12", reader.GetString(2));
|
||||||
|
Assert.False(await reader.ReadAsync());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using GmRelay.Bot.Tests.Features.Sessions.CreateSession;
|
||||||
|
using GmRelay.Shared.Domain;
|
||||||
|
using GmRelay.Shared.Infrastructure.Scheduling;
|
||||||
|
using GmRelay.Shared.Platform;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
|
namespace GmRelay.Bot.Tests.Infrastructure.Scheduling;
|
||||||
|
|
||||||
|
[Collection(CreateSessionHandlerPostgresCollection.Name)]
|
||||||
|
public sealed class DbSessionTriggerStoreTests(CreateSessionHandlerPostgresFixture fixture)
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetSessionsNeedingJoinLinkAsync_IgnoresConfirmedSessionsWithoutJoinLink()
|
||||||
|
{
|
||||||
|
var connectionString = await fixture.CreateMigratedDatabaseAsync();
|
||||||
|
await using var dataSource = NpgsqlDataSource.Create(connectionString);
|
||||||
|
await using var connection = await dataSource.OpenConnectionAsync();
|
||||||
|
|
||||||
|
var groupId = await InsertTelegramGroupAsync(connection);
|
||||||
|
var dueAt = DateTimeOffset.UtcNow.AddMinutes(4).UtcDateTime;
|
||||||
|
var onlineSessionId = await InsertSessionAsync(connection, groupId, dueAt, "https://vtt.example/game", "Online");
|
||||||
|
var offlineSessionId = await InsertSessionAsync(connection, groupId, dueAt, string.Empty, "Offline");
|
||||||
|
|
||||||
|
var sut = new DbSessionTriggerStore(dataSource, new PlatformSchedulerOptions(PlatformKind.Telegram));
|
||||||
|
|
||||||
|
var result = await sut.GetSessionsNeedingJoinLinkAsync(DateTimeOffset.UtcNow, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Contains(onlineSessionId, result);
|
||||||
|
Assert.DoesNotContain(offlineSessionId, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Guid> InsertTelegramGroupAsync(NpgsqlConnection connection)
|
||||||
|
{
|
||||||
|
await using var command = new NpgsqlCommand(
|
||||||
|
"""
|
||||||
|
INSERT INTO game_groups (name, platform, external_group_id)
|
||||||
|
VALUES ('Trigger Test Group', 'Telegram', @ExternalGroupId)
|
||||||
|
RETURNING id
|
||||||
|
""",
|
||||||
|
connection);
|
||||||
|
command.Parameters.AddWithValue("ExternalGroupId", Guid.NewGuid().ToString("N"));
|
||||||
|
|
||||||
|
return (Guid)(await command.ExecuteScalarAsync() ?? throw new InvalidOperationException("Group insert failed."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Guid> InsertSessionAsync(
|
||||||
|
NpgsqlConnection connection,
|
||||||
|
Guid groupId,
|
||||||
|
DateTime scheduledAt,
|
||||||
|
string joinLink,
|
||||||
|
string format)
|
||||||
|
{
|
||||||
|
await using var command = new NpgsqlCommand(
|
||||||
|
"""
|
||||||
|
INSERT INTO sessions (group_id, title, join_link, scheduled_at, status, format)
|
||||||
|
VALUES (@GroupId, 'Trigger Test Session', @JoinLink, @ScheduledAt, @Status, @Format)
|
||||||
|
RETURNING id
|
||||||
|
""",
|
||||||
|
connection);
|
||||||
|
command.Parameters.AddWithValue("GroupId", groupId);
|
||||||
|
command.Parameters.AddWithValue("JoinLink", joinLink);
|
||||||
|
command.Parameters.AddWithValue("ScheduledAt", scheduledAt);
|
||||||
|
command.Parameters.AddWithValue("Status", SessionStatus.Confirmed);
|
||||||
|
command.Parameters.AddWithValue("Format", format);
|
||||||
|
|
||||||
|
return (Guid)(await command.ExecuteScalarAsync() ?? throw new InvalidOperationException("Session insert failed."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using GmRelay.Bot.Infrastructure.Telegram;
|
|||||||
using GmRelay.Shared.Platform;
|
using GmRelay.Shared.Platform;
|
||||||
using GmRelay.Shared.Rendering;
|
using GmRelay.Shared.Rendering;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace GmRelay.Bot.Tests.Infrastructure.Telegram;
|
namespace GmRelay.Bot.Tests.Infrastructure.Telegram;
|
||||||
|
|
||||||
@@ -36,9 +37,35 @@ public sealed class TelegramPlatformMessengerTests
|
|||||||
Assert.Contains("Existing schedule message reference must match the schedule group.", exception.Message);
|
Assert.Contains("Existing schedule message reference must match the schedule group.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildDirectNotificationText_OneHourReminderWithoutJoinLink_ShouldNotRenderBlankLinkLine()
|
||||||
|
{
|
||||||
|
var notification = new PlatformDirectSessionNotification(
|
||||||
|
PlatformDirectSessionNotificationKind.OneHourReminder,
|
||||||
|
new PlatformUser(PlatformKind.Telegram, "123", "Player", "player"),
|
||||||
|
Guid.NewGuid(),
|
||||||
|
"Offline Game",
|
||||||
|
DateTime.UtcNow,
|
||||||
|
JoinLink: string.Empty);
|
||||||
|
|
||||||
|
var text = InvokeBuildDirectNotificationText(notification);
|
||||||
|
|
||||||
|
Assert.DoesNotContain("🔗", text);
|
||||||
|
}
|
||||||
|
|
||||||
private static TelegramPlatformMessenger CreateMessenger() =>
|
private static TelegramPlatformMessenger CreateMessenger() =>
|
||||||
new(null!, NullLogger<TelegramPlatformMessenger>.Instance);
|
new(null!, NullLogger<TelegramPlatformMessenger>.Instance);
|
||||||
|
|
||||||
|
private static string InvokeBuildDirectNotificationText(PlatformDirectSessionNotification notification)
|
||||||
|
{
|
||||||
|
var method = typeof(TelegramPlatformMessenger).GetMethod(
|
||||||
|
"BuildDirectNotificationText",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static);
|
||||||
|
|
||||||
|
Assert.NotNull(method);
|
||||||
|
return Assert.IsType<string>(method.Invoke(null, new object[] { notification }));
|
||||||
|
}
|
||||||
|
|
||||||
private static SessionBatchViewModel CreateView() =>
|
private static SessionBatchViewModel CreateView() =>
|
||||||
new("Test batch", []);
|
new("Test batch", []);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user