Fix Discord co-GM management
Deploy Telegram Bot / build-and-push (push) Successful in 5m58s
Deploy Telegram Bot / scan-images (push) Successful in 3m39s
Deploy Telegram Bot / deploy (push) Successful in 34s

This commit is contained in:
2026-05-27 16:32:47 +03:00
parent 7a2965b43f
commit fac5d75c7e
5 changed files with 129 additions and 24 deletions
@@ -75,7 +75,9 @@ public sealed class DiscordNewSessionHandler(
FROM group_managers gm FROM group_managers gm
JOIN players p ON p.id = gm.player_id JOIN players p ON p.id = gm.player_id
JOIN game_groups g ON g.id = gm.group_id JOIN game_groups g ON g.id = gm.group_id
WHERE g.platform = 'Discord' AND g.external_group_id = @GuildId", WHERE g.platform = 'Discord'
AND p.platform = 'Discord'
AND g.external_group_id = @GuildId",
new { GuildId = guildId }); new { GuildId = guildId });
if (!permissionChecker.CanManageSchedule(guildOwnerId, userId, dbManagerUserIds, resolvedPermissions)) if (!permissionChecker.CanManageSchedule(guildOwnerId, userId, dbManagerUserIds, resolvedPermissions))
@@ -40,8 +40,8 @@
</span> </span>
@if (groupManagement.CurrentUserIsOwner && manager.Role == GroupManagerRoleExtensions.CoGmValue) @if (groupManagement.CurrentUserIsOwner && manager.Role == GroupManagerRoleExtensions.CoGmValue)
{ {
<button type="button" class="btn-gm btn-gm-outline" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" disabled="@(removingCoGmId == manager.ExternalUserId)" @onclick="() => RemoveCoGm(manager.ExternalUserId ?? manager.TelegramId.ToString())"> <button type="button" class="btn-gm btn-gm-outline" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" disabled="@(removingCoGmId == ManagerKey(manager))" @onclick="() => RemoveCoGm(manager)">
@(removingCoGmId == manager.ExternalUserId ? "⏳ Удаляем..." : "Убрать") @(removingCoGmId == ManagerKey(manager) ? "⏳ Удаляем..." : "Убрать")
</button> </button>
} }
} }
@@ -52,8 +52,8 @@
<EditForm Model="@coGmModel" OnValidSubmit="AddCoGm"> <EditForm Model="@coGmModel" OnValidSubmit="AddCoGm">
<div class="batch-bulk-fields"> <div class="batch-bulk-fields">
<div class="gm-form-group"> <div class="gm-form-group">
<label class="gm-form-label">Telegram ID co-GM</label> <label class="gm-form-label">@CoGmIdLabel</label>
<InputNumber @bind-Value="coGmModel.TelegramId" class="gm-form-control" min="1" /> <InputText @bind-Value="coGmModel.ExternalUserId" class="gm-form-control" />
</div> </div>
<div class="gm-form-group"> <div class="gm-form-group">
<label class="gm-form-label">Имя</label> <label class="gm-form-label">Имя</label>
@@ -61,7 +61,7 @@
</div> </div>
<div class="gm-form-group"> <div class="gm-form-group">
<label class="gm-form-label">Username</label> <label class="gm-form-label">Username</label>
<InputText @bind-Value="coGmModel.TelegramUsername" class="gm-form-control" /> <InputText @bind-Value="coGmModel.ExternalUsername" class="gm-form-control" />
</div> </div>
</div> </div>
<button type="submit" class="btn-gm btn-gm-primary" disabled="@isAddingCoGm"> <button type="submit" class="btn-gm btn-gm-primary" disabled="@isAddingCoGm">
@@ -405,6 +405,7 @@
private Guid? processingTemplateId; private Guid? processingTemplateId;
private string? removingCoGmId; private string? removingCoGmId;
private bool isAddingCoGm; private bool isAddingCoGm;
private string? currentPlatform;
private string? externalUserId; private string? externalUserId;
private string? errorMessage; private string? errorMessage;
private string? successMessage; private string? successMessage;
@@ -423,6 +424,7 @@
return; return;
} }
currentPlatform = platform;
await LoadSessions(); await LoadSessions();
} }
@@ -458,9 +460,16 @@
errorMessage = null; errorMessage = null;
successMessage = null; successMessage = null;
if (!coGmModel.TelegramId.HasValue || coGmModel.TelegramId.Value <= 0) var coGmExternalUserId = coGmModel.ExternalUserId.Trim();
if (coGmExternalUserId.Length == 0)
{ {
errorMessage = "Telegram ID co-GM должен быть положительным числом."; errorMessage = $"{CoGmIdLabel} должен быть заполнен.";
return;
}
if (!IsValidPlatformUserId(CoGmPlatform, coGmExternalUserId))
{
errorMessage = $"{CoGmIdLabel} должен быть положительным числом.";
return; return;
} }
@@ -470,10 +479,10 @@
{ {
await SessionService.AddCoGmForOwnerAsync( await SessionService.AddCoGmForOwnerAsync(
GroupId, GroupId,
"Telegram", CoGmPlatform,
coGmModel.TelegramId.Value.ToString(), coGmExternalUserId,
coGmModel.DisplayName, coGmModel.DisplayName,
coGmModel.TelegramUsername); coGmModel.ExternalUsername);
coGmModel = new(); coGmModel = new();
successMessage = "Co-GM добавлен."; successMessage = "Co-GM добавлен.";
@@ -493,15 +502,17 @@
} }
} }
private async Task RemoveCoGm(string coGmExternalUserId) private async Task RemoveCoGm(WebGroupManager manager)
{ {
errorMessage = null; errorMessage = null;
successMessage = null; successMessage = null;
removingCoGmId = coGmExternalUserId; removingCoGmId = ManagerKey(manager);
var platform = ManagerPlatform(manager);
var coGmExternalUserId = ManagerExternalUserId(manager);
try try
{ {
await SessionService.RemoveCoGmForOwnerAsync(GroupId, "Telegram", coGmExternalUserId); await SessionService.RemoveCoGmForOwnerAsync(GroupId, platform, coGmExternalUserId);
successMessage = "Co-GM удалён."; successMessage = "Co-GM удалён.";
await LoadSessions(); await LoadSessions();
} }
@@ -834,22 +845,51 @@
private bool IsTemplateBusy(CampaignTemplateUsageModel template) => processingTemplateId == template.Id; private bool IsTemplateBusy(CampaignTemplateUsageModel template) => processingTemplateId == template.Id;
private string CoGmPlatform =>
string.IsNullOrWhiteSpace(groupManagement?.Group.Platform)
? "Telegram"
: groupManagement.Group.Platform;
private string CoGmIdLabel => $"{CoGmPlatform} ID co-GM";
private string CurrentUserRole => private string CurrentUserRole =>
groupManagement?.Managers.FirstOrDefault(manager => manager.ExternalUserId == externalUserId)?.Role groupManagement?.Managers.FirstOrDefault(manager =>
string.Equals(ManagerPlatform(manager), currentPlatform, StringComparison.OrdinalIgnoreCase) &&
ManagerExternalUserId(manager) == externalUserId)?.Role
?? GroupManagerRoleExtensions.CoGmValue; ?? GroupManagerRoleExtensions.CoGmValue;
private static string FormatRole(string role) => private static string FormatRole(string role) =>
GroupManagerRoleExtensions.FromDatabaseValue(role).ToDisplayName(); GroupManagerRoleExtensions.FromDatabaseValue(role).ToDisplayName();
private static string FormatManager(WebGroupManager manager) private string FormatManager(WebGroupManager manager)
{ {
var username = string.IsNullOrWhiteSpace(manager.TelegramUsername) var username = string.IsNullOrWhiteSpace(manager.ExternalUsername)
? manager.TelegramId.ToString(System.Globalization.CultureInfo.InvariantCulture) ? manager.TelegramUsername
: "@" + manager.TelegramUsername; : manager.ExternalUsername;
var identity = string.IsNullOrWhiteSpace(username)
? $"{ManagerPlatform(manager)} {ManagerExternalUserId(manager)}"
: "@" + username.TrimStart('@');
return $"{FormatRole(manager.Role)} · {manager.DisplayName} · {username}"; return $"{FormatRole(manager.Role)} · {manager.DisplayName} · {identity}";
} }
private string ManagerPlatform(WebGroupManager manager) =>
string.IsNullOrWhiteSpace(manager.Platform) ? CoGmPlatform : manager.Platform;
private static string ManagerExternalUserId(WebGroupManager manager) =>
string.IsNullOrWhiteSpace(manager.ExternalUserId)
? manager.TelegramId.ToString(System.Globalization.CultureInfo.InvariantCulture)
: manager.ExternalUserId;
private string ManagerKey(WebGroupManager manager) =>
$"{ManagerPlatform(manager)}:{ManagerExternalUserId(manager)}";
private static bool IsValidPlatformUserId(string platform, string externalUserId) =>
string.Equals(platform, "Telegram", StringComparison.OrdinalIgnoreCase)
? long.TryParse(externalUserId, out var telegramId) && telegramId > 0
: !string.Equals(platform, "Discord", StringComparison.OrdinalIgnoreCase) ||
(ulong.TryParse(externalUserId, out var platformId) && platformId > 0);
private static int InferIntervalDays(IReadOnlyList<WebSession> orderedSessions) private static int InferIntervalDays(IReadOnlyList<WebSession> orderedSessions)
{ {
if (orderedSessions.Count < 2) if (orderedSessions.Count < 2)
@@ -944,8 +984,8 @@
private sealed class CoGmEditModel private sealed class CoGmEditModel
{ {
public long? TelegramId { get; set; } public string ExternalUserId { get; set; } = "";
public string DisplayName { get; set; } = ""; public string DisplayName { get; set; } = "";
public string? TelegramUsername { get; set; } public string? ExternalUsername { get; set; }
} }
} }
+18 -2
View File
@@ -28,6 +28,7 @@ public sealed record WebGameGroup(
public sealed record WebGroupManager( public sealed record WebGroupManager(
long TelegramId, long TelegramId,
string? ExternalUserId, string? ExternalUserId,
string? Platform,
string DisplayName, string DisplayName,
string? TelegramUsername, string? TelegramUsername,
string? ExternalUsername, string? ExternalUsername,
@@ -35,7 +36,17 @@ public sealed record WebGroupManager(
DateTime AddedAt) DateTime AddedAt)
{ {
public WebGroupManager(long telegramId, string displayName, string? telegramUsername, string role, DateTime addedAt) public WebGroupManager(long telegramId, string displayName, string? telegramUsername, string role, DateTime addedAt)
: this(telegramId, null, displayName, telegramUsername, null, role, addedAt) { } : this(telegramId, null, null, displayName, telegramUsername, null, role, addedAt) { }
public WebGroupManager(
long telegramId,
string? externalUserId,
string displayName,
string? telegramUsername,
string? externalUsername,
string role,
DateTime addedAt)
: this(telegramId, externalUserId, null, displayName, telegramUsername, externalUsername, role, addedAt) { }
} }
public sealed record WebGroupManagement( public sealed record WebGroupManagement(
@@ -215,8 +226,13 @@ public sealed class SessionService(
await using var conn = await dataSource.OpenConnectionAsync(); await using var conn = await dataSource.OpenConnectionAsync();
return (await conn.QueryAsync<WebGroupManager>( return (await conn.QueryAsync<WebGroupManager>(
""" """
SELECT COALESCE(p.external_user_id::BIGINT, 0) AS TelegramId, SELECT CASE
WHEN p.platform = 'Telegram' AND p.external_user_id ~ '^[0-9]+$'
THEN p.external_user_id::BIGINT
ELSE 0
END AS TelegramId,
p.external_user_id AS ExternalUserId, p.external_user_id AS ExternalUserId,
p.platform AS Platform,
p.display_name AS DisplayName, p.display_name AS DisplayName,
p.external_username AS TelegramUsername, p.external_username AS TelegramUsername,
p.external_username AS ExternalUsername, p.external_username AS ExternalUsername,
@@ -115,6 +115,18 @@ public sealed class DiscordNewSessionHandlerTests
Assert.Contains("UnauthorizedAccessException", source, StringComparison.Ordinal); Assert.Contains("UnauthorizedAccessException", source, StringComparison.Ordinal);
} }
[Fact]
public void Handler_ShouldLoadCoGmPermissionsFromDiscordPlayers()
{
var repoRoot = GetRepoRoot();
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
var source = File.ReadAllText(handlerPath);
Assert.Matches(
@"QueryAsync<ulong>[\s\S]*JOIN players p ON p\.id = gm\.player_id[\s\S]*p\.platform = 'Discord'[\s\S]*g\.external_group_id = @GuildId",
source);
}
[Fact] [Fact]
public void Handler_ShouldBePlatformNeutral() public void Handler_ShouldBePlatformNeutral()
{ {
@@ -0,0 +1,35 @@
namespace GmRelay.Bot.Tests.Web;
public sealed class GroupDetailsSourceTests
{
[Fact]
public async Task GroupDetails_ShouldManageCoGmUsingGroupPlatform()
{
var source = await ReadRepositoryFileAsync("src/GmRelay.Web/Components/Pages/GroupDetails.razor");
Assert.Contains("CoGmPlatform", source, StringComparison.Ordinal);
Assert.Contains("@CoGmIdLabel", source, StringComparison.Ordinal);
Assert.Contains("coGmModel.ExternalUserId", source, StringComparison.Ordinal);
Assert.Matches(@"SessionService\.AddCoGmForOwnerAsync\(\s*GroupId,\s*CoGmPlatform,\s*coGmExternalUserId", source);
Assert.Matches(@"SessionService\.RemoveCoGmForOwnerAsync\(GroupId,\s*platform,\s*coGmExternalUserId\)", source);
Assert.DoesNotContain("Telegram ID co-GM", source, StringComparison.Ordinal);
Assert.DoesNotContain("SessionService.RemoveCoGmForOwnerAsync(GroupId, \"Telegram\"", source, StringComparison.Ordinal);
}
private static async Task<string> ReadRepositoryFileAsync(string relativePath)
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
var candidate = Path.Combine(directory.FullName, relativePath);
if (File.Exists(candidate))
{
return await File.ReadAllTextAsync(candidate);
}
directory = directory.Parent;
}
throw new FileNotFoundException($"Could not locate repository file '{relativePath}'.");
}
}