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'
|
WHERE g.platform = 'Discord'
|
||||||
AND g.external_group_id = @GuildId
|
AND g.external_group_id = @GuildId
|
||||||
AND s.status != @Cancelled
|
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
|
GROUP BY s.id, s.title, s.scheduled_at, s.status, s.max_players
|
||||||
ORDER BY s.scheduled_at ASC",
|
ORDER BY s.scheduled_at ASC",
|
||||||
new
|
new
|
||||||
|
|||||||
@@ -44,10 +44,12 @@ public class DiscordNewSessionCommand : ApplicationCommandModule<SlashCommandCon
|
|||||||
_logger.LogInformation("Resolved permissions for user {UserId}: {Permissions}", Context.User.Id, resolvedPermissions);
|
_logger.LogInformation("Resolved permissions for user {UserId}: {Permissions}", Context.User.Id, resolvedPermissions);
|
||||||
|
|
||||||
ulong guildOwnerId = 0;
|
ulong guildOwnerId = 0;
|
||||||
|
var guildName = guildId.ToString();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var guild = await Context.Client.Rest.GetGuildAsync(guildId);
|
var guild = await Context.Client.Rest.GetGuildAsync(guildId);
|
||||||
guildOwnerId = guild.OwnerId;
|
guildOwnerId = guild.OwnerId;
|
||||||
|
guildName = guild.Name;
|
||||||
_logger.LogInformation("Guild owner id: {OwnerId}", guildOwnerId);
|
_logger.LogInformation("Guild owner id: {OwnerId}", guildOwnerId);
|
||||||
}
|
}
|
||||||
catch (RestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
|
catch (RestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
@@ -80,6 +82,7 @@ public class DiscordNewSessionCommand : ApplicationCommandModule<SlashCommandCon
|
|||||||
var view = await _handler.HandleAsync(
|
var view = await _handler.HandleAsync(
|
||||||
guildId: guildId.ToString(),
|
guildId: guildId.ToString(),
|
||||||
channelId: Context.Channel!.Id.ToString(),
|
channelId: Context.Channel!.Id.ToString(),
|
||||||
|
groupName: guildName,
|
||||||
userId: Context.User.Id,
|
userId: Context.User.Id,
|
||||||
userDisplayName: Context.User.GlobalName ?? Context.User.Username,
|
userDisplayName: Context.User.GlobalName ?? Context.User.Username,
|
||||||
resolvedPermissions: resolvedPermissions,
|
resolvedPermissions: resolvedPermissions,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public sealed class DiscordNewSessionHandler(
|
|||||||
public async Task<SessionBatchViewModel> HandleAsync(
|
public async Task<SessionBatchViewModel> HandleAsync(
|
||||||
string guildId,
|
string guildId,
|
||||||
string channelId,
|
string channelId,
|
||||||
|
string groupName,
|
||||||
ulong userId,
|
ulong userId,
|
||||||
string userDisplayName,
|
string userDisplayName,
|
||||||
ulong resolvedPermissions,
|
ulong resolvedPermissions,
|
||||||
@@ -58,6 +59,9 @@ public sealed class DiscordNewSessionHandler(
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using var connection = await dataSource.OpenConnectionAsync(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>(
|
var dbManagerUserIds = await connection.QueryAsync<ulong>(
|
||||||
@"SELECT CAST(p.external_user_id AS BIGINT)
|
@"SELECT CAST(p.external_user_id AS BIGINT)
|
||||||
@@ -88,13 +92,13 @@ public sealed class DiscordNewSessionHandler(
|
|||||||
|
|
||||||
var groupId = await connection.ExecuteScalarAsync<Guid>(
|
var groupId = await connection.ExecuteScalarAsync<Guid>(
|
||||||
@"INSERT INTO game_groups (name, platform, external_group_id, external_channel_id)
|
@"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)
|
ON CONFLICT (platform, external_group_id)
|
||||||
WHERE platform IS NOT NULL AND external_group_id IS NOT NULL
|
WHERE platform IS NOT NULL AND external_group_id IS NOT NULL
|
||||||
DO UPDATE SET name = EXCLUDED.name,
|
DO UPDATE SET name = EXCLUDED.name,
|
||||||
external_channel_id = COALESCE(EXCLUDED.external_channel_id, game_groups.external_channel_id)
|
external_channel_id = COALESCE(EXCLUDED.external_channel_id, game_groups.external_channel_id)
|
||||||
RETURNING id",
|
RETURNING id",
|
||||||
new { GuildId = guildId, ChannelId = channelId },
|
new { GroupName = displayGroupName, GuildId = guildId, ChannelId = channelId },
|
||||||
transaction);
|
transaction);
|
||||||
|
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
|
|||||||
@@ -44,9 +44,14 @@
|
|||||||
<div class="group-card-icon">🎮</div>
|
<div class="group-card-icon">🎮</div>
|
||||||
<h3 class="group-card-title">@group.Name</h3>
|
<h3 class="group-card-title">@group.Name</h3>
|
||||||
<p class="group-card-id">ID: @(group.Platform == "Discord" ? group.ExternalGroupId : group.TelegramChatId.ToString())</p>
|
<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;">
|
<div class="group-card-meta">
|
||||||
@FormatRole(group.ManagerRole)
|
<span class="status-badge platform-badge">
|
||||||
</span>
|
@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 href="/group/@group.Id" class="btn-gm btn-gm-primary" style="width: 100%; justify-content: center; margin-top: auto;">
|
||||||
Посмотреть игры →
|
Посмотреть игры →
|
||||||
</a>
|
</a>
|
||||||
@@ -81,6 +86,20 @@
|
|||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
margin-bottom: 1rem;
|
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>
|
</style>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
@@ -104,4 +123,7 @@
|
|||||||
|
|
||||||
private static string FormatRole(string role) =>
|
private static string FormatRole(string role) =>
|
||||||
GroupManagerRoleExtensions.FromDatabaseValue(role).ToDisplayName();
|
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,
|
SELECT g.id,
|
||||||
g.telegram_chat_id AS TelegramChatId,
|
g.telegram_chat_id AS TelegramChatId,
|
||||||
g.external_group_id AS ExternalGroupId,
|
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,
|
g.platform AS Platform,
|
||||||
vg.ManagerRole
|
vg.ManagerRole
|
||||||
FROM visible_groups vg
|
FROM visible_groups vg
|
||||||
JOIN game_groups g ON g.id = vg.group_id
|
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
|
ORDER BY g.name
|
||||||
""",
|
""",
|
||||||
new
|
new
|
||||||
@@ -146,10 +153,17 @@ public sealed class SessionService(
|
|||||||
SELECT g.id,
|
SELECT g.id,
|
||||||
g.telegram_chat_id AS TelegramChatId,
|
g.telegram_chat_id AS TelegramChatId,
|
||||||
g.external_group_id AS ExternalGroupId,
|
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,
|
g.platform AS Platform,
|
||||||
@OwnerRole AS ManagerRole
|
@OwnerRole AS ManagerRole
|
||||||
FROM game_groups g
|
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
|
WHERE g.id = @GroupId
|
||||||
""",
|
""",
|
||||||
new { GroupId = groupId, OwnerRole = GroupManagerRoleExtensions.OwnerValue });
|
new { GroupId = groupId, OwnerRole = GroupManagerRoleExtensions.OwnerValue });
|
||||||
|
|||||||
@@ -33,7 +33,17 @@ public sealed class DiscordListSessionsHandlerTests
|
|||||||
|
|
||||||
Assert.Contains("platform = 'Discord'", handler, StringComparison.Ordinal);
|
Assert.Contains("platform = 'Discord'", handler, StringComparison.Ordinal);
|
||||||
Assert.Contains("external_group_id = @GuildId", 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]
|
[Fact]
|
||||||
|
|||||||
@@ -171,6 +171,18 @@ public sealed class DiscordNewSessionHandlerTests
|
|||||||
Assert.DoesNotContain("PlatformScheduleMessage", source, StringComparison.Ordinal);
|
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()
|
private static DateTimeOffset FutureDateAt1930()
|
||||||
{
|
{
|
||||||
var future = DateTimeOffset.UtcNow.AddDays(7);
|
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