Пример #1
0
        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");
            }
        }
Пример #2
0
        /// <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);
        }
Пример #3
0
        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);
        }
Пример #4
0
        /// <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();
            }
        }
Пример #5
0
        /// <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");
        }
Пример #6
0
        /// <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);
        }
Пример #7
0
        /// <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();
                }
            }
        }
Пример #8
0
        /// <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);
        }
Пример #9
0
        /// <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();
            }
        }
Пример #10
0
        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");
            }
        }
Пример #11
0
 /// <inheritdoc />
 public async Task <IReadOnlyCollection <PromotionCampaignSummary> > GetPromotionsForUserAsync(ulong guildId, ulong userId)
 => await PromotionCampaignRepository.GetPromotionsForUserAsync(guildId, userId);
Пример #12
0
        /// <inheritdoc />
        public async Task <IReadOnlyCollection <PromotionCampaignSummary> > SearchCampaignsAsync(PromotionCampaignSearchCriteria searchCriteria)
        {
            AuthorizationService.RequireClaims(AuthorizationClaim.PromotionsRead);

            return(await PromotionCampaignRepository.SearchSummariesAsync(searchCriteria));
        }
Пример #13
0
        /// <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);
        }
Пример #14
0
 /// <inheritdoc />
 public async Task <PromotionCampaignDetails> GetCampaignDetailsAsync(long campaignId)
 => await PromotionCampaignRepository.ReadDetailsAsync(campaignId);
Пример #15
0
 /// <inheritdoc />
 public Task <IReadOnlyCollection <PromotionCampaignSummary> > SearchCampaignsAsync(PromotionCampaignSearchCriteria searchCriteria)
 => PromotionCampaignRepository.SearchSummariesAsync(searchCriteria);
Пример #16
0
        /// <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();
            }
        }