Files
GmRelayBot/src/GmRelay.Shared/Features/Sessions/ListSessions/DeleteSessionHandler.cs
T

112 lines
3.9 KiB
C#

using Dapper;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Platform;
using Microsoft.Extensions.Logging;
using Npgsql;
namespace GmRelay.Shared.Features.Sessions.ListSessions;
internal sealed record DeleteSessionInfoDto(
string Title,
Guid BatchId,
Guid GroupId,
bool CanManage,
int? ThreadId,
bool TopicCreatedByBot);
public sealed record DeleteSessionResult(
bool Success,
string? ReplyText,
string? Title,
Guid? GroupId,
int? ThreadId,
bool TopicCreatedByBot,
int RemainingInTopic);
public sealed class DeleteSessionHandler(
NpgsqlDataSource dataSource)
{
public async Task<DeleteSessionResult> HandleAsync(DeleteSessionCommand command, CancellationToken ct)
{
await using var connection = await dataSource.OpenConnectionAsync(ct);
await using var transaction = await connection.BeginTransactionAsync(ct);
// 1. Use the database mutation order before locking the session or linked portfolio cards.
await connection.ExecuteAsync(
"SELECT pg_advisory_xact_lock(20260530, 108)",
transaction: transaction);
// 2. Lock the session before any linked portfolio card and verify group manager.
var session = await connection.QuerySingleOrDefaultAsync<DeleteSessionInfoDto>(
"""
SELECT s.title AS Title,
s.batch_id AS BatchId,
s.group_id AS GroupId,
s.thread_id AS ThreadId,
s.topic_created_by_bot AS TopicCreatedByBot,
EXISTS (
SELECT 1
FROM group_managers gm
JOIN players p ON p.id = gm.player_id
WHERE gm.group_id = s.group_id
AND p.platform = @Platform
AND p.external_user_id = @ExternalUserId
) AS CanManage
FROM sessions s
WHERE s.id = @SessionId
FOR UPDATE OF s
""",
new { command.SessionId, Platform = command.User.Platform.ToString(), ExternalUserId = command.User.ExternalUserId }, transaction);
if (session == null)
{
return new DeleteSessionResult(false, "Сессия не найдена.", null, null, null, false, 0);
}
if (!session.CanManage)
{
return new DeleteSessionResult(false, "Только owner или co-GM может удалять сессию.", null, null, null, false, 0);
}
// 3. Unpublish a linked portfolio card before its required session link cascades away.
await connection.ExecuteAsync(
"""
UPDATE portfolio_games pg
SET is_public = false,
updated_at = now()
FROM portfolio_game_sessions pgs
WHERE pgs.portfolio_game_id = pg.id
AND pgs.session_id = @SessionId
AND pg.is_public = true
""",
new { command.SessionId },
transaction);
// 4. Delete session
await connection.ExecuteAsync("DELETE FROM sessions WHERE id = @Id", new { Id = command.SessionId }, transaction);
var remainingInTopic = session.ThreadId.HasValue
? await connection.ExecuteScalarAsync<int>(
"""
SELECT COUNT(*)
FROM sessions
WHERE group_id = @GroupId
AND thread_id = @ThreadId
""",
new { session.GroupId, ThreadId = session.ThreadId.Value },
transaction)
: 0;
await transaction.CommitAsync(ct);
return new DeleteSessionResult(
true,
"Сессия удалена!",
session.Title,
session.GroupId,
session.ThreadId,
session.TopicCreatedByBot,
remainingInTopic);
}
}