using GmRelay.Shared.Domain; namespace GmRelay.Web.Services; public sealed class AuthorizedSessionService(ISessionStore sessionStore) { public Task> GetGroupsForGmAsync(long gmId) => sessionStore.GetGroupsForGmAsync(gmId); public async Task GetGroupManagementForGmAsync(Guid groupId, long gmId) { if (!await GroupBelongsToGmAsync(groupId, gmId)) { return null; } var group = await sessionStore.GetGroupAsync(groupId); if (group is null) { return null; } var managers = await sessionStore.GetGroupManagersAsync(groupId); var isOwner = await sessionStore.IsGroupOwnerAsync(groupId, gmId); return new WebGroupManagement(group, managers, isOwner); } public async Task?> GetUpcomingSessionsForGmAsync(Guid groupId, long gmId) { if (!await GroupBelongsToGmAsync(groupId, gmId)) { return null; } return await sessionStore.GetUpcomingSessionsAsync(groupId); } public async Task GetSessionForGmAsync(Guid sessionId, long gmId) { var session = await sessionStore.GetSessionAsync(sessionId); if (session is null) { return null; } return await GroupBelongsToGmAsync(session.GroupId, gmId) ? session : null; } public async Task GetBatchForGmAsync(Guid batchId, long gmId) { var batch = await sessionStore.GetBatchAsync(batchId); if (batch is null) { return null; } return await GroupBelongsToGmAsync(batch.GroupId, gmId) ? batch : null; } public async Task UpdateSessionForGmAsync(Guid sessionId, long gmId, string title, DateTime scheduledAt, string joinLink, int? maxPlayers) { var session = await GetSessionForGmAsync(sessionId, gmId); if (session is null) { throw new SessionAccessDeniedException(sessionId, gmId); } await sessionStore.UpdateSessionAsync(sessionId, session.GroupId, title, scheduledAt, joinLink, maxPlayers); } public async Task PromoteWaitlistedPlayerForGmAsync(Guid sessionId, long gmId) { var session = await GetSessionForGmAsync(sessionId, gmId); if (session is null) { throw new SessionAccessDeniedException(sessionId, gmId); } await sessionStore.PromoteWaitlistedPlayerAsync(sessionId, session.GroupId); } public async Task UpdateBatchDetailsForGmAsync(Guid batchId, long gmId, string title, string joinLink) { var batch = await GetBatchForGmAsync(batchId, gmId); if (batch is null) { throw new SessionAccessDeniedException(batchId, gmId); } await sessionStore.UpdateBatchDetailsAsync(batchId, batch.GroupId, title.Trim(), joinLink.Trim()); } public async Task UpdateBatchNotificationModeForGmAsync(Guid batchId, long gmId, SessionNotificationMode notificationMode) { var batch = await GetBatchForGmAsync(batchId, gmId); if (batch is null) { throw new SessionAccessDeniedException(batchId, gmId); } await sessionStore.UpdateBatchNotificationModeAsync(batchId, batch.GroupId, notificationMode); } public async Task RescheduleBatchForGmAsync(Guid batchId, long gmId, DateTime firstScheduledAt, int intervalDays) { if (intervalDays <= 0) { throw new ArgumentOutOfRangeException(nameof(intervalDays), intervalDays, "Interval must be greater than zero."); } var batch = await GetBatchForGmAsync(batchId, gmId); if (batch is null) { throw new SessionAccessDeniedException(batchId, gmId); } await sessionStore.RescheduleBatchAsync(batchId, batch.GroupId, firstScheduledAt, intervalDays); } public async Task CloneBatchForGmAsync(Guid batchId, long gmId, BatchCloneInterval interval) { var batch = await GetBatchForGmAsync(batchId, gmId); if (batch is null) { throw new SessionAccessDeniedException(batchId, gmId); } return await sessionStore.CloneBatchAsync(batchId, batch.GroupId, interval); } public async Task?> GetCampaignTemplatesForGmAsync(Guid groupId, long gmId) { if (!await GroupBelongsToGmAsync(groupId, gmId)) { return null; } return await sessionStore.GetCampaignTemplatesAsync(groupId); } public async Task CreateCampaignTemplateForGmAsync( Guid groupId, long gmId, CreateCampaignTemplateRequest request) { if (!await GroupBelongsToGmAsync(groupId, gmId)) { throw new SessionAccessDeniedException(groupId, gmId); } var normalizedRequest = NormalizeCampaignTemplateRequest(request); return await sessionStore.CreateCampaignTemplateAsync(groupId, normalizedRequest); } public async Task DeleteCampaignTemplateForGmAsync(Guid templateId, long gmId) { var template = await sessionStore.GetCampaignTemplateAsync(templateId); if (template is null || !await GroupBelongsToGmAsync(template.GroupId, gmId)) { throw new SessionAccessDeniedException(templateId, gmId); } await sessionStore.DeleteCampaignTemplateAsync(templateId, template.GroupId); } public async Task CreateBatchFromCampaignTemplateForGmAsync( Guid templateId, long gmId, DateTime firstScheduledAt) { if (firstScheduledAt <= DateTime.UtcNow) { throw new ArgumentOutOfRangeException(nameof(firstScheduledAt), firstScheduledAt, "First scheduled time must be in the future."); } var template = await sessionStore.GetCampaignTemplateAsync(templateId); if (template is null || !await GroupBelongsToGmAsync(template.GroupId, gmId)) { throw new SessionAccessDeniedException(templateId, gmId); } return await sessionStore.CreateBatchFromTemplateAsync(templateId, template.GroupId, firstScheduledAt); } public async Task AddCoGmForOwnerAsync(Guid groupId, long ownerTelegramId, long coGmTelegramId, string displayName, string? telegramUsername) { if (coGmTelegramId <= 0) { throw new ArgumentOutOfRangeException(nameof(coGmTelegramId), coGmTelegramId, "Telegram id must be greater than zero."); } if (ownerTelegramId == coGmTelegramId) { throw new InvalidOperationException("Owner is already a group manager."); } if (!await sessionStore.IsGroupOwnerAsync(groupId, ownerTelegramId)) { throw new SessionAccessDeniedException(groupId, ownerTelegramId); } var normalizedName = string.IsNullOrWhiteSpace(displayName) ? $"Telegram {coGmTelegramId.ToString(System.Globalization.CultureInfo.InvariantCulture)}" : displayName.Trim(); var normalizedUsername = string.IsNullOrWhiteSpace(telegramUsername) ? null : telegramUsername.Trim().TrimStart('@'); await sessionStore.AddGroupCoGmAsync(groupId, ownerTelegramId, coGmTelegramId, normalizedName, normalizedUsername); } public async Task RemoveCoGmForOwnerAsync(Guid groupId, long ownerTelegramId, long coGmTelegramId) { if (!await sessionStore.IsGroupOwnerAsync(groupId, ownerTelegramId)) { throw new SessionAccessDeniedException(groupId, ownerTelegramId); } await sessionStore.RemoveGroupCoGmAsync(groupId, coGmTelegramId); } private async Task GroupBelongsToGmAsync(Guid groupId, long gmId) { return await sessionStore.IsGroupManagerAsync(groupId, gmId); } private static CreateCampaignTemplateRequest NormalizeCampaignTemplateRequest(CreateCampaignTemplateRequest request) { var name = request.Name.Trim(); var title = request.Title.Trim(); var joinLink = request.JoinLink.Trim(); if (name.Length == 0) { throw new ArgumentException("Template name must not be empty.", nameof(request)); } if (title.Length == 0) { throw new ArgumentException("Session title must not be empty.", nameof(request)); } if (joinLink.Length == 0) { throw new ArgumentException("Join link must not be empty.", nameof(request)); } if (request.SessionCount is < 1 or > 52) { throw new ArgumentOutOfRangeException(nameof(request), request.SessionCount, "Session count must be between 1 and 52."); } if (request.IntervalDays is < 1 or > 365) { throw new ArgumentOutOfRangeException(nameof(request), request.IntervalDays, "Interval must be between 1 and 365 days."); } if (request.MaxPlayers is <= 0) { throw new ArgumentOutOfRangeException(nameof(request), request.MaxPlayers, "Seat limit must be greater than zero."); } return request with { Name = name, Title = title, JoinLink = joinLink }; } }