014b5edd31
PR Checks / test-and-build (pull_request) Successful in 15m52s
Add format and location steps to the Telegram /newsession wizard, persist offline addresses in sessions.location_address, and render online links/offline addresses in schedule messages. Bump version to 3.10.0.
183 lines
7.4 KiB
C#
183 lines
7.4 KiB
C#
using Dapper;
|
|
using GmRelay.Shared.Domain;
|
|
using GmRelay.Shared.Platform;
|
|
using GmRelay.Shared.Rendering;
|
|
using Npgsql;
|
|
|
|
namespace GmRelay.Shared.Features.Sessions.CreateSession;
|
|
|
|
internal sealed record SessionCreationGroupAccessDto(Guid GroupId, bool CanManage);
|
|
|
|
public sealed class CreateSessionHandler(
|
|
NpgsqlDataSource dataSource)
|
|
{
|
|
public async Task<CreateSessionResult> HandleAsync(CreateSessionCommand command, CancellationToken ct)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
await using var transaction = await connection.BeginTransactionAsync(ct);
|
|
|
|
var transactionCommitted = false;
|
|
try
|
|
{
|
|
var platform = command.User.Platform.ToString();
|
|
var externalUserId = command.User.ExternalUserId;
|
|
var displayName = command.User.DisplayName;
|
|
var externalUsername = command.User.ExternalUsername;
|
|
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
INSERT INTO players (display_name, platform, external_user_id, external_username)
|
|
VALUES (@Name, @Platform, @ExternalId, @Username)
|
|
ON CONFLICT (platform, external_user_id)
|
|
WHERE platform IS NOT NULL AND external_user_id IS NOT NULL
|
|
DO UPDATE
|
|
SET display_name = EXCLUDED.display_name,
|
|
external_username = EXCLUDED.external_username;
|
|
""",
|
|
new { ExternalId = externalUserId, Name = displayName, Username = externalUsername, Platform = platform },
|
|
transaction);
|
|
|
|
var existingGroup = await connection.QuerySingleOrDefaultAsync<SessionCreationGroupAccessDto>(
|
|
"""
|
|
SELECT g.id AS GroupId,
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM group_managers gm
|
|
JOIN players p ON p.id = gm.player_id
|
|
WHERE gm.group_id = g.id
|
|
AND p.platform = @Platform
|
|
AND p.external_user_id = @ExternalGmId
|
|
) AS CanManage
|
|
FROM game_groups g
|
|
WHERE g.platform = @Platform
|
|
AND g.external_group_id = @ExternalGroupId
|
|
""",
|
|
new { Platform = platform, ExternalGroupId = command.Group.ExternalGroupId, ExternalGmId = externalUserId },
|
|
transaction);
|
|
|
|
Guid groupId;
|
|
if (existingGroup is null)
|
|
{
|
|
groupId = await connection.ExecuteScalarAsync<Guid>(
|
|
"""
|
|
INSERT INTO game_groups (name, platform, external_group_id, external_channel_id)
|
|
VALUES (@ChatName, @Platform, @ExternalGroupId, @ExternalChannelId)
|
|
RETURNING id;
|
|
""",
|
|
new
|
|
{
|
|
Platform = platform,
|
|
ExternalGroupId = command.Group.ExternalGroupId,
|
|
ExternalChannelId = command.Group.ExternalChannelId,
|
|
ChatName = command.Group.DisplayName
|
|
},
|
|
transaction);
|
|
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
INSERT INTO group_managers (group_id, player_id, role)
|
|
SELECT @GroupId, p.id, @OwnerRole
|
|
FROM players p
|
|
WHERE p.platform = @Platform
|
|
AND p.external_user_id = @ExternalGmId
|
|
ON CONFLICT (group_id, player_id) DO NOTHING
|
|
""",
|
|
new
|
|
{
|
|
GroupId = groupId,
|
|
Platform = platform,
|
|
ExternalGmId = externalUserId,
|
|
OwnerRole = GroupManagerRoleExtensions.OwnerValue
|
|
},
|
|
transaction);
|
|
}
|
|
else
|
|
{
|
|
if (!existingGroup.CanManage)
|
|
{
|
|
await transaction.RollbackAsync(ct);
|
|
return new CreateSessionResult(
|
|
false,
|
|
"⛔ Только owner или co-GM этой группы может создавать игровые сессии.",
|
|
null,
|
|
null,
|
|
null,
|
|
Array.Empty<string>());
|
|
}
|
|
|
|
groupId = existingGroup.GroupId;
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
UPDATE game_groups
|
|
SET name = @ChatName
|
|
WHERE id = @GroupId
|
|
""",
|
|
new { ChatName = command.Group.DisplayName, GroupId = groupId },
|
|
transaction);
|
|
}
|
|
|
|
var batchId = Guid.NewGuid();
|
|
var sessions = new List<SessionBatchDto>();
|
|
var orderedTimes = command.ScheduledTimes.OrderBy(v => v).ToList();
|
|
|
|
foreach (var scheduledAt in orderedTimes)
|
|
{
|
|
var sessionId = await connection.ExecuteScalarAsync<Guid>(
|
|
"""
|
|
INSERT INTO sessions (batch_id, group_id, title, join_link, scheduled_at, status, max_players, system, description, format, duration_minutes, is_one_shot, cover_image_url, location_address)
|
|
VALUES (@BatchId, @GroupId, @Title, @Link, @ScheduledAt, @Status, @MaxPlayers, @System, @Description, @Format, @DurationMinutes, @IsOneShot, @CoverImageUrl, @LocationAddress)
|
|
RETURNING id;
|
|
""",
|
|
new
|
|
{
|
|
BatchId = batchId,
|
|
GroupId = groupId,
|
|
command.Title,
|
|
Link = command.Link,
|
|
ScheduledAt = scheduledAt,
|
|
Status = SessionStatus.Planned,
|
|
MaxPlayers = command.MaxPlayers,
|
|
System = command.System?.ToString(),
|
|
command.Description,
|
|
command.Format,
|
|
DurationMinutes = command.DurationMinutes,
|
|
IsOneShot = command.IsOneShot,
|
|
CoverImageUrl = command.ImageReference,
|
|
command.LocationAddress
|
|
},
|
|
transaction);
|
|
|
|
sessions.Add(new SessionBatchDto(
|
|
sessionId,
|
|
scheduledAt.UtcDateTime,
|
|
SessionStatus.Planned,
|
|
command.MaxPlayers,
|
|
command.Link,
|
|
command.Format,
|
|
command.LocationAddress));
|
|
}
|
|
|
|
await transaction.CommitAsync(ct);
|
|
transactionCommitted = true;
|
|
|
|
var view = SessionBatchViewBuilder.Build(command.Title, sessions, Array.Empty<ParticipantBatchDto>());
|
|
|
|
return new CreateSessionResult(
|
|
true,
|
|
null,
|
|
view,
|
|
batchId,
|
|
groupId,
|
|
Array.Empty<string>());
|
|
}
|
|
catch
|
|
{
|
|
if (!transactionCommitted)
|
|
{
|
|
await transaction.RollbackAsync(ct);
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
}
|