Fix Discord co-GM management
This commit is contained in:
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user