fix(web): show discord sessions and integration labels
This commit is contained in:
@@ -41,7 +41,7 @@ public sealed class DiscordListSessionsHandler(
|
||||
WHERE g.platform = 'Discord'
|
||||
AND g.external_group_id = @GuildId
|
||||
AND s.status != @Cancelled
|
||||
AND s.scheduled_at > NOW()
|
||||
AND s.scheduled_at > now() - interval '4 hours'
|
||||
GROUP BY s.id, s.title, s.scheduled_at, s.status, s.max_players
|
||||
ORDER BY s.scheduled_at ASC",
|
||||
new
|
||||
|
||||
@@ -44,10 +44,12 @@ public class DiscordNewSessionCommand : ApplicationCommandModule<SlashCommandCon
|
||||
_logger.LogInformation("Resolved permissions for user {UserId}: {Permissions}", Context.User.Id, resolvedPermissions);
|
||||
|
||||
ulong guildOwnerId = 0;
|
||||
var guildName = guildId.ToString();
|
||||
try
|
||||
{
|
||||
var guild = await Context.Client.Rest.GetGuildAsync(guildId);
|
||||
guildOwnerId = guild.OwnerId;
|
||||
guildName = guild.Name;
|
||||
_logger.LogInformation("Guild owner id: {OwnerId}", guildOwnerId);
|
||||
}
|
||||
catch (RestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
@@ -80,6 +82,7 @@ public class DiscordNewSessionCommand : ApplicationCommandModule<SlashCommandCon
|
||||
var view = await _handler.HandleAsync(
|
||||
guildId: guildId.ToString(),
|
||||
channelId: Context.Channel!.Id.ToString(),
|
||||
groupName: guildName,
|
||||
userId: Context.User.Id,
|
||||
userDisplayName: Context.User.GlobalName ?? Context.User.Username,
|
||||
resolvedPermissions: resolvedPermissions,
|
||||
|
||||
@@ -47,6 +47,7 @@ public sealed class DiscordNewSessionHandler(
|
||||
public async Task<SessionBatchViewModel> HandleAsync(
|
||||
string guildId,
|
||||
string channelId,
|
||||
string groupName,
|
||||
ulong userId,
|
||||
string userDisplayName,
|
||||
ulong resolvedPermissions,
|
||||
@@ -58,6 +59,9 @@ public sealed class DiscordNewSessionHandler(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);
|
||||
var displayGroupName = string.IsNullOrWhiteSpace(groupName) || string.Equals(groupName, guildId, StringComparison.Ordinal)
|
||||
? title
|
||||
: groupName.Trim();
|
||||
|
||||
var dbManagerUserIds = await connection.QueryAsync<ulong>(
|
||||
@"SELECT CAST(p.external_user_id AS BIGINT)
|
||||
@@ -88,13 +92,13 @@ public sealed class DiscordNewSessionHandler(
|
||||
|
||||
var groupId = await connection.ExecuteScalarAsync<Guid>(
|
||||
@"INSERT INTO game_groups (name, platform, external_group_id, external_channel_id)
|
||||
VALUES (@GuildId, 'Discord', @GuildId, @ChannelId)
|
||||
VALUES (@GroupName, 'Discord', @GuildId, @ChannelId)
|
||||
ON CONFLICT (platform, external_group_id)
|
||||
WHERE platform IS NOT NULL AND external_group_id IS NOT NULL
|
||||
DO UPDATE SET name = EXCLUDED.name,
|
||||
external_channel_id = COALESCE(EXCLUDED.external_channel_id, game_groups.external_channel_id)
|
||||
RETURNING id",
|
||||
new { GuildId = guildId, ChannelId = channelId },
|
||||
new { GroupName = displayGroupName, GuildId = guildId, ChannelId = channelId },
|
||||
transaction);
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
|
||||
@@ -44,9 +44,14 @@
|
||||
<div class="group-card-icon">🎮</div>
|
||||
<h3 class="group-card-title">@group.Name</h3>
|
||||
<p class="group-card-id">ID: @(group.Platform == "Discord" ? group.ExternalGroupId : group.TelegramChatId.ToString())</p>
|
||||
<span class="status-badge @(group.ManagerRole == GroupManagerRoleExtensions.OwnerValue ? "status-success" : "status-info")" style="align-self: flex-start; margin-bottom: 1rem;">
|
||||
@FormatRole(group.ManagerRole)
|
||||
</span>
|
||||
<div class="group-card-meta">
|
||||
<span class="status-badge platform-badge">
|
||||
@FormatPlatform(group.Platform)
|
||||
</span>
|
||||
<span class="status-badge @(group.ManagerRole == GroupManagerRoleExtensions.OwnerValue ? "status-success" : "status-info")">
|
||||
@FormatRole(group.ManagerRole)
|
||||
</span>
|
||||
</div>
|
||||
<a href="/group/@group.Id" class="btn-gm btn-gm-primary" style="width: 100%; justify-content: center; margin-top: auto;">
|
||||
Посмотреть игры →
|
||||
</a>
|
||||
@@ -81,6 +86,20 @@
|
||||
font-family: 'Courier New', monospace;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.group-card-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.platform-badge {
|
||||
background: rgba(88, 101, 242, 0.15);
|
||||
color: #9ea8ff;
|
||||
border-color: rgba(88, 101, 242, 0.35);
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
@@ -104,4 +123,7 @@
|
||||
|
||||
private static string FormatRole(string role) =>
|
||||
GroupManagerRoleExtensions.FromDatabaseValue(role).ToDisplayName();
|
||||
|
||||
private static string FormatPlatform(string? platform) =>
|
||||
string.Equals(platform, "Discord", StringComparison.OrdinalIgnoreCase) ? "Discord" : "Telegram";
|
||||
}
|
||||
|
||||
@@ -123,11 +123,18 @@ public sealed class SessionService(
|
||||
SELECT g.id,
|
||||
g.telegram_chat_id AS TelegramChatId,
|
||||
g.external_group_id AS ExternalGroupId,
|
||||
g.name,
|
||||
COALESCE(NULLIF(g.name, g.external_group_id), latest_session.title, g.name) AS Name,
|
||||
g.platform AS Platform,
|
||||
vg.ManagerRole
|
||||
FROM visible_groups vg
|
||||
JOIN game_groups g ON g.id = vg.group_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT s.title
|
||||
FROM sessions s
|
||||
WHERE s.group_id = g.id
|
||||
ORDER BY s.scheduled_at DESC
|
||||
LIMIT 1
|
||||
) latest_session ON true
|
||||
ORDER BY g.name
|
||||
""",
|
||||
new
|
||||
@@ -146,10 +153,17 @@ public sealed class SessionService(
|
||||
SELECT g.id,
|
||||
g.telegram_chat_id AS TelegramChatId,
|
||||
g.external_group_id AS ExternalGroupId,
|
||||
g.name,
|
||||
COALESCE(NULLIF(g.name, g.external_group_id), latest_session.title, g.name) AS Name,
|
||||
g.platform AS Platform,
|
||||
@OwnerRole AS ManagerRole
|
||||
FROM game_groups g
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT s.title
|
||||
FROM sessions s
|
||||
WHERE s.group_id = g.id
|
||||
ORDER BY s.scheduled_at DESC
|
||||
LIMIT 1
|
||||
) latest_session ON true
|
||||
WHERE g.id = @GroupId
|
||||
""",
|
||||
new { GroupId = groupId, OwnerRole = GroupManagerRoleExtensions.OwnerValue });
|
||||
|
||||
@@ -33,7 +33,17 @@ public sealed class DiscordListSessionsHandlerTests
|
||||
|
||||
Assert.Contains("platform = 'Discord'", handler, StringComparison.Ordinal);
|
||||
Assert.Contains("external_group_id = @GuildId", handler, StringComparison.Ordinal);
|
||||
Assert.Contains("scheduled_at > NOW()", handler, StringComparison.Ordinal);
|
||||
Assert.Contains("scheduled_at > now() - interval '4 hours'", handler, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldIncludeRecentlyStartedSessionsForCleanup()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordListSessionsHandler.cs");
|
||||
var handler = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.Contains("now() - interval '4 hours'", handler, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -171,6 +171,18 @@ public sealed class DiscordNewSessionHandlerTests
|
||||
Assert.DoesNotContain("PlatformScheduleMessage", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldStoreReadableDiscordGroupNameForWebCards()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
var source = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.Contains("groupName", source, StringComparison.Ordinal);
|
||||
Assert.Contains("displayGroupName", source, StringComparison.Ordinal);
|
||||
Assert.Contains("VALUES (@GroupName, 'Discord'", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static DateTimeOffset FutureDateAt1930()
|
||||
{
|
||||
var future = DateTimeOffset.UtcNow.AddDays(7);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace GmRelay.Bot.Tests.Web;
|
||||
|
||||
public sealed class HomePageSourceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task HomePage_ShouldShowPlatformBadgeForGroups()
|
||||
{
|
||||
var source = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/Home.razor");
|
||||
|
||||
Assert.Contains("platform-badge", source, StringComparison.Ordinal);
|
||||
Assert.Contains("FormatPlatform", source, StringComparison.Ordinal);
|
||||
Assert.Contains("group.Platform", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SessionService_ShouldUseSessionTitleWhenDiscordGroupNameIsOnlyId()
|
||||
{
|
||||
var source = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/SessionService.cs");
|
||||
|
||||
Assert.Contains("latest_session.title", source, StringComparison.Ordinal);
|
||||
Assert.Contains("NULLIF(g.name, g.external_group_id)", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadRepositoryFileAsync(string relativePath)
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (!string.IsNullOrEmpty(dir) && !File.Exists(Path.Combine(dir, "Directory.Build.props")))
|
||||
{
|
||||
dir = Directory.GetParent(dir)?.FullName;
|
||||
}
|
||||
|
||||
var repoRoot = dir ?? throw new InvalidOperationException("Could not find repo root");
|
||||
return await File.ReadAllTextAsync(Path.Combine(repoRoot, relativePath));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user