private async Task PerformCommonCreateCampaignValidationsAsync(IGuildUser subject, GuildRoleBrief targetRankRole, IEnumerable <GuildRoleBrief> rankRoles) { if (await PromotionCampaignRepository.AnyAsync(new PromotionCampaignSearchCriteria() { GuildId = AuthorizationService.CurrentGuildId.Value, SubjectId = subject.Id, TargetRoleId = targetRankRole.Id, IsClosed = false })) { throw new InvalidOperationException($"An active campaign already exists for {subject.GetFullUsername()} to be promoted to {targetRankRole.Name}"); } // JoinedAt is null, when it cannot be obtained if (subject.JoinedAt.HasValue) { if (subject.JoinedAt.Value.DateTime > (DateTimeOffset.Now - TimeSpan.FromDays(20))) { throw new InvalidOperationException($"{subject.GetFullUsername()} has joined less than 20 days prior"); } } if (!await CheckIfUserIsRankOrHigherAsync(rankRoles, AuthorizationService.CurrentUserId.Value, targetRankRole.Id)) { throw new InvalidOperationException($"Creating a promotion campaign requires a rank at least as high as the proposed target rank"); } }
/// <inheritdoc /> public async Task UpdateCommentAsync(long commentId, PromotionSentiment newSentiment, string newContent) { AuthorizationService.RequireAuthenticatedUser(); AuthorizationService.RequireClaims(AuthorizationClaim.PromotionsComment); ValidateComment(newContent); PromotionActionSummary resultAction; using (var transaction = await PromotionCommentRepository.BeginUpdateTransactionAsync()) { var oldComment = await PromotionCommentRepository.ReadSummaryAsync(commentId); var campaign = await PromotionCampaignRepository.ReadDetailsAsync(oldComment.Campaign.Id); if (!(campaign.CloseAction is null)) { throw new InvalidOperationException($"Campaign {oldComment.Campaign.Id} has already been closed"); } resultAction = await PromotionCommentRepository.TryUpdateAsync(commentId, AuthorizationService.CurrentUserId.Value, x => { x.Sentiment = newSentiment; x.Content = newContent; }); transaction.Commit(); } PublishActionNotificationAsync(resultAction); }
private async Task FinalizeCreateCampaignAsync(ulong subjectId, ulong targetRoleId, string comment) { PromotionActionSummary campaignResultAction; PromotionActionSummary commentResultAction; using (var campaignTransaction = await PromotionCampaignRepository.BeginCreateTransactionAsync()) using (var commentTransaction = await PromotionCommentRepository.BeginCreateTransactionAsync()) { campaignResultAction = await PromotionCampaignRepository.CreateAsync(new PromotionCampaignCreationData() { GuildId = AuthorizationService.CurrentGuildId.Value, SubjectId = subjectId, TargetRoleId = targetRoleId, CreatedById = AuthorizationService.CurrentUserId.Value }); commentResultAction = await PromotionCommentRepository.CreateAsync(new PromotionCommentCreationData() { GuildId = AuthorizationService.CurrentGuildId.Value, CampaignId = campaignResultAction.Campaign.Id, Sentiment = PromotionSentiment.Approve, Content = comment, CreatedById = AuthorizationService.CurrentUserId.Value }); campaignTransaction.Commit(); commentTransaction.Commit(); } PublishActionNotificationAsync(campaignResultAction); PublishActionNotificationAsync(commentResultAction); }
/// <inheritdoc /> public async Task UpdateCommentAsync(long commentId, PromotionSentiment newSentiment, string newContent) { AuthorizationService.RequireAuthenticatedUser(); AuthorizationService.RequireClaims(AuthorizationClaim.PromotionsComment); if (newContent is null || newContent.Length <= 3) { throw new InvalidOperationException("Comment content must be longer than 3 characters."); } using (var transaction = await PromotionCommentRepository.BeginUpdateTransactionAsync()) { var oldComment = await PromotionCommentRepository.ReadSummaryAsync(commentId); var campaign = await PromotionCampaignRepository.ReadDetailsAsync(oldComment.Campaign.Id); if (!(campaign.CloseAction is null)) { throw new InvalidOperationException($"Campaign {oldComment.Campaign.Id} has already been closed"); } await PromotionCommentRepository.TryUpdateAsync(commentId, AuthorizationService.CurrentUserId.Value, x => { x.Sentiment = newSentiment; x.Content = newContent; }); transaction.Commit(); } }
/// <inheritdoc /> public async Task RejectCampaignAsync(long campaignId) { AuthorizationService.RequireAuthenticatedUser(); AuthorizationService.RequireClaims(AuthorizationClaim.PromotionsCloseCampaign); if (!(await PromotionCampaignRepository.TryCloseAsync(campaignId, AuthorizationService.CurrentUserId.Value, PromotionCampaignOutcome.Rejected))) throw new InvalidOperationException($"Campaign {campaignId} doesn't exist or is already closed"); }
/// <inheritdoc /> public async Task AddCommentAsync(long campaignId, PromotionSentiment sentiment, string content) { AuthorizationService.RequireAuthenticatedUser(); AuthorizationService.RequireClaims(AuthorizationClaim.PromotionsComment); ValidateComment(content); if (await PromotionCommentRepository.AnyAsync(new PromotionCommentSearchCriteria() { CampaignId = campaignId, CreatedById = AuthorizationService.CurrentUserId.Value, IsModified = false })) { throw new InvalidOperationException("Only one comment can be made per user, per campaign"); } var campaign = await PromotionCampaignRepository.ReadDetailsAsync(campaignId); if (campaign.Subject.Id == AuthorizationService.CurrentUserId) { throw new InvalidOperationException("You aren't allowed to comment on your own campaign"); } if (!(campaign.CloseAction is null)) { throw new InvalidOperationException($"Campaign {campaignId} has already been closed"); } var rankRoles = await GetRankRolesAsync(AuthorizationService.CurrentGuildId.Value); if (!await CheckIfUserIsRankOrHigherAsync(rankRoles, AuthorizationService.CurrentUserId.Value, campaign.TargetRole.Id)) { throw new InvalidOperationException($"Commenting on a promotion campaign requires a rank at least as high as the proposed target rank"); } PromotionActionSummary resultAction; using (var transaction = await PromotionCommentRepository.BeginCreateTransactionAsync()) { resultAction = await PromotionCommentRepository.CreateAsync(new PromotionCommentCreationData() { GuildId = campaign.GuildId, CampaignId = campaignId, Sentiment = sentiment, Content = content, CreatedById = AuthorizationService.CurrentUserId.Value }); transaction.Commit(); } PublishActionNotificationAsync(resultAction); }
/// <inheritdoc /> public async Task AcceptCampaignAsync(long campaignId) { AuthorizationService.RequireAuthenticatedUser(); AuthorizationService.RequireClaims(AuthorizationClaim.PromotionsCloseCampaign); using (var transaction = await PromotionCampaignRepository.BeginCloseTransactionAsync()) { var campaign = await PromotionCampaignRepository.ReadDetailsAsync(campaignId); if (campaign is null) throw new InvalidOperationException($"Campaign {campaignId} does not exist"); if (!(campaign.CloseAction is null)) throw new InvalidOperationException($"Campaign {campaignId} is already closed"); var timeSince = DateTime.UtcNow - campaign.CreateAction.Created; if (timeSince < TimeSpan.FromHours(48)) throw new InvalidOperationException($"Campaign {campaignId} cannot be accepted until 48 hours after its creation ({48 - timeSince.TotalHours:#.##} hrs remain)"); try { var subject = await UserService.GetGuildUserAsync(campaign.GuildId, campaign.Subject.Id); if (subject.RoleIds.Contains(campaign.TargetRole.Id)) throw new InvalidOperationException($"User {campaign.Subject.DisplayName} is already a member of role {campaign.TargetRole.Name}"); var guild = await DiscordClient.GetGuildAsync(campaign.GuildId); var targetRole = guild.GetRole(campaign.TargetRole.Id); if (targetRole is null) throw new InvalidOperationException($"Role {campaign.TargetRole.Name} no longer exists"); await subject.AddRoleAsync(targetRole); foreach (var lowerRankRole in (await GetRankRolesAsync(AuthorizationService.CurrentGuildId.Value)) .TakeWhile(x => x.Id != targetRole.Id)) { var lowerRole = guild.GetRole(lowerRankRole.Id); if (!(lowerRole is null) && subject.RoleIds.Contains(lowerRole.Id)) await subject.RemoveRoleAsync(lowerRole); } await PromotionCampaignRepository.TryCloseAsync(campaignId, AuthorizationService.CurrentUserId.Value, PromotionCampaignOutcome.Accepted); } catch { await PromotionCampaignRepository.TryCloseAsync(campaignId, AuthorizationService.CurrentUserId.Value, PromotionCampaignOutcome.Failed); throw; } finally { transaction.Commit(); } } }
/// <inheritdoc /> public async Task <PromotionCampaignDetails> GetCampaignDetailsAsync(long campaignId) { AuthorizationService.RequireClaims(AuthorizationClaim.PromotionsRead); var result = await PromotionCampaignRepository.ReadDetailsAsync(campaignId); if (result.Subject.Id == AuthorizationService.CurrentUserId) { throw new InvalidOperationException("You can't view comments on your own campaign."); } return(result); }
/// <inheritdoc /> public async Task AddCommentAsync(long campaignId, PromotionSentiment sentiment, string content) { AuthorizationService.RequireClaims(AuthorizationClaim.PromotionsComment); if (content == null || content.Length <= 3) { throw new InvalidOperationException("Comment content must be longer than 3 characters."); } using (var transaction = await PromotionCommentRepository.BeginCreateTransactionAsync()) { if (await PromotionCommentRepository.AnyAsync(new PromotionCommentSearchCriteria() { CampaignId = campaignId, CreatedById = AuthorizationService.CurrentUserId.Value })) { throw new InvalidOperationException("Only one comment can be made per user, per campaign"); } var campaign = await PromotionCampaignRepository.ReadDetailsAsync(campaignId); if (!(campaign.CloseAction is null)) { throw new InvalidOperationException($"Campaign {campaignId} has already been closed"); } var rankRoles = await GetRankRolesAsync(AuthorizationService.CurrentGuildId.Value); if (!await CheckIfUserIsRankOrHigher(rankRoles, AuthorizationService.CurrentUserId.Value, campaign.TargetRole.Id)) { throw new InvalidOperationException($"Commenting on a promotion campaign requires a rank at least as high as the proposed target rank"); } await PromotionCommentRepository.CreateAsync(new PromotionCommentCreationData() { GuildId = campaign.GuildId, CampaignId = campaignId, Sentiment = sentiment, Content = content, CreatedById = AuthorizationService.CurrentUserId.Value }); transaction.Commit(); } }
private async Task PerformCommonCreateCampaignValidationsAsync(IGuildUser subject, GuildRoleBrief targetRankRole, IEnumerable <GuildRoleBrief> rankRoles) { if (await PromotionCampaignRepository.AnyAsync(new PromotionCampaignSearchCriteria() { GuildId = AuthorizationService.CurrentGuildId.Value, SubjectId = subject.Id, TargetRoleId = targetRankRole.Id, IsClosed = false })) { throw new InvalidOperationException($"An active campaign already exists for {subject.GetDisplayNameWithDiscriminator()} to be promoted to {targetRankRole.Name}"); } if (!await CheckIfUserIsRankOrHigherAsync(rankRoles, AuthorizationService.CurrentUserId.Value, targetRankRole.Id)) { throw new InvalidOperationException($"Creating a promotion campaign requires a rank at least as high as the proposed target rank"); } }
/// <inheritdoc /> public async Task <IReadOnlyCollection <PromotionCampaignSummary> > GetPromotionsForUserAsync(ulong guildId, ulong userId) => await PromotionCampaignRepository.GetPromotionsForUserAsync(guildId, userId);
/// <inheritdoc /> public async Task <IReadOnlyCollection <PromotionCampaignSummary> > SearchCampaignsAsync(PromotionCampaignSearchCriteria searchCriteria) { AuthorizationService.RequireClaims(AuthorizationClaim.PromotionsRead); return(await PromotionCampaignRepository.SearchSummariesAsync(searchCriteria)); }
/// <inheritdoc /> public async Task AcceptCampaignAsync(long campaignId, bool force) { AuthorizationService.RequireAuthenticatedUser(); AuthorizationService.RequireClaims(AuthorizationClaim.PromotionsCloseCampaign); PromotionActionSummary resultAction; using (var transaction = await PromotionCampaignRepository.BeginCloseTransactionAsync()) { var campaign = await PromotionCampaignRepository.ReadDetailsAsync(campaignId); if (campaign is null) { throw new InvalidOperationException($"Campaign {campaignId} does not exist"); } if (!(campaign.CloseAction is null)) { throw new InvalidOperationException($"Campaign {campaignId} is already closed"); } var timeSince = DateTime.UtcNow - campaign.CreateAction.Created; if (timeSince < PromotionCampaignEntityExtensions.CampaignAcceptCooldown && !force) { throw new InvalidOperationException($"Campaign {campaignId} cannot be accepted until {PromotionCampaignEntityExtensions.CampaignAcceptCooldown.TotalHours} hours after its creation ({(PromotionCampaignEntityExtensions.CampaignAcceptCooldown - timeSince).Humanize(4)} remain)"); } try { var subject = await UserService.GetGuildUserAsync(campaign.GuildId, campaign.Subject.Id); if (subject.RoleIds.Contains(campaign.TargetRole.Id)) { throw new InvalidOperationException($"User {campaign.Subject.GetFullUsername()} is already a member of role {campaign.TargetRole.Name}"); } var guild = await DiscordClient.GetGuildAsync(campaign.GuildId); var targetRole = guild.GetRole(campaign.TargetRole.Id); if (targetRole is null) { throw new InvalidOperationException($"Role {campaign.TargetRole.Name} no longer exists"); } await subject.AddRoleAsync(targetRole); foreach (var lowerRankRole in (await GetRankRolesAsync(AuthorizationService.CurrentGuildId.Value)) .TakeWhile(x => x.Id != targetRole.Id)) { var lowerRole = guild.GetRole(lowerRankRole.Id); if (!(lowerRole is null) && subject.RoleIds.Contains(lowerRole.Id)) { await subject.RemoveRoleAsync(lowerRole); } } resultAction = await PromotionCampaignRepository.TryCloseAsync(campaignId, AuthorizationService.CurrentUserId.Value, PromotionCampaignOutcome.Accepted); } catch { resultAction = await PromotionCampaignRepository.TryCloseAsync(campaignId, AuthorizationService.CurrentUserId.Value, PromotionCampaignOutcome.Failed); PublishActionNotificationAsync(resultAction); throw; } finally { transaction.Commit(); } } PublishActionNotificationAsync(resultAction); }
/// <inheritdoc /> public async Task <PromotionCampaignDetails> GetCampaignDetailsAsync(long campaignId) => await PromotionCampaignRepository.ReadDetailsAsync(campaignId);
/// <inheritdoc /> public Task <IReadOnlyCollection <PromotionCampaignSummary> > SearchCampaignsAsync(PromotionCampaignSearchCriteria searchCriteria) => PromotionCampaignRepository.SearchSummariesAsync(searchCriteria);
/// <inheritdoc /> public async Task CreateCampaignAsync(ulong subjectId, ulong targetRoleId, string comment) { AuthorizationService.RequireAuthenticatedGuild(); AuthorizationService.RequireAuthenticatedUser(); AuthorizationService.RequireClaims(AuthorizationClaim.PromotionsCreateCampaign); using (var campaignTransaction = await PromotionCampaignRepository.BeginCreateTransactionAsync()) using (var commentTransaction = await PromotionCommentRepository.BeginCreateTransactionAsync()) { var rankRoles = await GetRankRolesAsync(AuthorizationService.CurrentGuildId.Value); var targetRankRoleIndex = rankRoles .Select((x, i) => (role: x, index: (int?)i)) .FirstOrDefault(x => x.role.Id == targetRoleId) .index; if (targetRankRoleIndex is null) throw new InvalidOperationException($"Role {targetRoleId} is not a defined promotion rank"); var targetRankRole = rankRoles[targetRankRoleIndex.Value]; if (await PromotionCampaignRepository.AnyAsync(new PromotionCampaignSearchCriteria() { GuildId = AuthorizationService.CurrentGuildId.Value, SubjectId = subjectId, TargetRoleId = targetRankRole.Id, IsClosed = false })) throw new InvalidOperationException($"An active campaign already exists for user {subjectId} to be promoted to {targetRoleId}"); var subject = await UserService.GetGuildUserAsync(AuthorizationService.CurrentGuildId.Value, subjectId); if (subject.RoleIds.Contains(targetRoleId)) throw new InvalidOperationException($"User {subjectId} is already a member of role {targetRoleId}"); if(targetRankRoleIndex > 0) { var previousRankRole = rankRoles[targetRankRoleIndex.Value - 1]; if (!subject.RoleIds.Contains(previousRankRole.Id)) throw new InvalidOperationException($"The proposed promotion would skip over rank {previousRankRole.Name}"); } else if (subject.RoleIds.Intersect(rankRoles.Select(x => x.Id)).Any()) throw new InvalidOperationException($"User {subjectId} is already ranked"); if(!(await CheckIfUserIsRankOrHigher(rankRoles, AuthorizationService.CurrentUserId.Value, targetRankRole.Id))) throw new InvalidOperationException($"Creating a promotion campaign requires a rank at least as high as the proposed target rank"); var campaignId = await PromotionCampaignRepository.CreateAsync(new PromotionCampaignCreationData() { GuildId = AuthorizationService.CurrentGuildId.Value, SubjectId = subjectId, TargetRoleId = targetRankRole.Id, CreatedById = AuthorizationService.CurrentUserId.Value }); await PromotionCommentRepository.CreateAsync(new PromotionCommentCreationData() { GuildId = AuthorizationService.CurrentGuildId.Value, CampaignId = campaignId, Sentiment = PromotionSentiment.Approve, Content = comment, CreatedById = AuthorizationService.CurrentUserId.Value }); campaignTransaction.Commit(); commentTransaction.Commit(); } }