예제 #1
0
        /// <inheritdoc />
        public async Task RescindInfractionAsync(long infractionId, string reason)
        {
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationRescind);

            var infraction = await InfractionRepository.ReadAsync(infractionId);

            if (infraction == null)
            {
                throw new ArgumentException("Infraction does not exist", nameof(infractionId));
            }

            switch (infraction.Type)
            {
            case InfractionType.Mute:
                await DoDiscordUnMuteAsync(infraction.Subject.Id);

                break;

            case InfractionType.Ban:
                await DoDiscordUnBanAsync(infraction.Subject.Id);

                break;
            }

            var actionId = await ModerationActionRepository.CreateAsync(new ModerationActionCreationData()
            {
                Type         = ModerationActionType.InfractionRescinded,
                CreatedById  = AuthorizationService.CurrentUserId.Value,
                Reason       = reason,
                InfractionId = infractionId
            });

            // TODO: Log action to a channel, pulled from IModerationConfigRepository.
        }
예제 #2
0
        /// <inheritdoc />
        public async Task <ServiceResult <DateTimeOffset> > GetNextInfractionExpiration()
        {
            var result = await InfractionRepository.ReadExpiresFirstOrDefaultAsync(
                new InfractionSearchCriteria()
            {
                IsRescinded  = false,
                IsDeleted    = false,
                ExpiresRange = new DateTimeOffsetRange()
                {
                    From = DateTimeOffset.MinValue,
                    To   = DateTimeOffset.MaxValue,
                }
            },
                new[]
            {
                new SortingCriteria()
                {
                    PropertyName = nameof(InfractionSummary.Expires), Direction = SortDirection.Ascending
                }
            });

            if (result == null)
            {
                return(ServiceResult <DateTimeOffset> .FromError("No expiring infractions found."));
            }

            return(ServiceResult.FromResult(result.Value));
        }
예제 #3
0
        /// <inheritdoc />
        public async Task <ServiceResult> RescindInfractionAsync(InfractionType type, ulong subjectId)
        {
            var authResult = AuthorizationService.CheckClaims(AuthorizationClaim.ModerationRescind);

            if (authResult.IsFailure)
            {
                return(authResult);
            }

            var rankResult = await RequireSubjectRankLowerThanModeratorRankAsync(AuthorizationService.CurrentGuildId.Value, subjectId);

            if (rankResult.IsFailure)
            {
                return(rankResult);
            }

            await DoRescindInfractionAsync(
                (await InfractionRepository.SearchSummariesAsync(
                     new InfractionSearchCriteria()
            {
                GuildId = AuthorizationService.CurrentGuildId.Value,
                Types = new [] { type },
                SubjectId = subjectId,
                IsRescinded = false,
                IsDeleted = false,
            }))
                .FirstOrDefault());

            return(ServiceResult.FromSuccess());
        }
예제 #4
0
        /// <inheritdoc />
        public async Task DeleteInfractionAsync(long infractionId)
        {
            AuthorizationService.RequireAuthenticatedUser();
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationDelete);

            var infraction = await InfractionRepository.ReadSummaryAsync(infractionId);

            if (infraction == null)
            {
                throw new InvalidOperationException($"Infraction {infractionId} does not exist");
            }

            await InfractionRepository.TryDeleteAsync(infraction.Id, AuthorizationService.CurrentUserId.Value);

            var guild = await GuildService.GetGuildAsync(infraction.GuildId);

            var subject = await UserService.GetGuildUserAsync(guild.Id, infraction.Subject.Id);

            switch (infraction.Type)
            {
            case InfractionType.Mute:
                await subject.RemoveRoleAsync(
                    await GetOrCreateMuteRoleInGuildAsync(guild));

                break;

            case InfractionType.Ban:
                await guild.RemoveBanAsync(subject);

                break;
            }
        }
예제 #5
0
        private async Task DoRescindInfractionAsync(InfractionSummary infraction)
        {
            if (infraction == null)
            {
                throw new InvalidOperationException("Infraction does not exist");
            }

            await InfractionRepository.TryRescindAsync(infraction.Id, AuthorizationService.CurrentUserId.Value);

            var guild = await DiscordClient.GetGuildAsync(infraction.GuildId);

            switch (infraction.Type)
            {
            case InfractionType.Mute:
                if (!await UserService.GuildUserExistsAsync(guild.Id, infraction.Subject.Id))
                {
                    throw new InvalidOperationException("Cannot unmute a user who is not in the server.");
                }

                var subject = await UserService.GetGuildUserAsync(guild.Id, infraction.Subject.Id);

                await subject.RemoveRoleAsync(await GetDesignatedMuteRoleAsync(guild));

                break;

            case InfractionType.Ban:
                await guild.RemoveBanAsync(infraction.Subject.Id);

                break;

            default:
                throw new InvalidOperationException($"{infraction.Type} infractions cannot be rescinded.");
            }
        }
예제 #6
0
        public async Task <bool> UpdateInfractionAsync(long infractionId, string newReason, ulong currentUserId)
        {
            var infraction = await InfractionRepository.ReadSummaryAsync(infractionId);

            var editCutoff = DateTimeOffset.Now.AddDays(-1);

            if (infraction.CreateAction.Created <= editCutoff)
            {
                return(false);
            }

            AuthorizationService.RequireClaims(_createInfractionClaimsByType[infraction.Type]);

            // Allow users who created the infraction to bypass any further
            // validation and update their own infraction
            if (infraction.CreateAction.CreatedBy.Id == currentUserId)
            {
                return(await InfractionRepository.TryUpdateAync(infractionId, newReason, currentUserId));
            }

            // Else we know it's not the user's infraction
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationUpdateInfraction);

            return(await InfractionRepository.TryUpdateAync(infractionId, newReason, currentUserId));
        }
예제 #7
0
        /// <inheritdoc />
        public async Task RescindInfractionAsync(long infractionId)
        {
            AuthorizationService.RequireAuthenticatedUser();
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationRescind);

            await DoRescindInfractionAsync(
                await InfractionRepository.ReadSummaryAsync(infractionId));
        }
예제 #8
0
 public async Task <ServiceResult <IDictionary <InfractionType, int> > > GetInfractionCountsForUserAsync(ulong subjectId)
 => await AuthorizationService.CheckClaims(AuthorizationClaim.ModerationRead)
 .ShortCircuitAsync(InfractionRepository.GetInfractionCountsAsync(new InfractionSearchCriteria
 {
     GuildId   = AuthorizationService.CurrentGuildId,
     SubjectId = subjectId,
     IsDeleted = false
 }));
예제 #9
0
        public void Constructor_Always_InvokesBaseConstructor()
        {
            var modixContext = Substitute.For <ModixContext>();

            var uut = new InfractionRepository(modixContext);

            uut.ModixContext.ShouldBeSameAs(modixContext);
        }
예제 #10
0
        /// <inheritdoc />
        public async Task RescindInfractionAsync(long infractionId, string reason = null, bool isAutoRescind = false)
        {
            AuthorizationService.RequireAuthenticatedUser();
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationRescind);

            await DoRescindInfractionAsync(
                await InfractionRepository.ReadSummaryAsync(infractionId), reason, isAutoRescind);
        }
예제 #11
0
        public async Task <bool> AnyInfractionsAsync(InfractionSearchCriteria criteria)
        {
            if (criteria is null)
            {
                throw new ArgumentNullException(nameof(criteria));
            }

            return(await InfractionRepository.AnyAsync(criteria));
        }
        public void Constructor_Always_InvokesBaseConstructor()
        {
            var modixContext = Substitute.For <ModixContext>();
            var moderationActionEventHandlers = Enumerable.Empty <IModerationActionEventHandler>();
            var infractionEventHandlers       = Enumerable.Empty <IInfractionEventHandler>();

            var uut = new InfractionRepository(modixContext, moderationActionEventHandlers, infractionEventHandlers);

            uut.ModixContext.ShouldBeSameAs(modixContext);
        }
예제 #13
0
        public async Task <bool> AnyInfractionsAsync(InfractionSearchCriteria criteria)
        {
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationRead);

            if (criteria is null)
            {
                throw new ArgumentNullException(nameof(criteria));
            }

            return(await InfractionRepository.AnyAsync(criteria));
        }
예제 #14
0
        public async Task <IDictionary <InfractionType, int> > GetInfractionCountsForUserAsync(ulong subjectId)
        {
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationRead);

            return(await InfractionRepository.GetInfractionCountsAsync(new InfractionSearchCriteria
            {
                GuildId = AuthorizationService.CurrentGuildId,
                SubjectId = subjectId,
                IsDeleted = false
            }));
        }
예제 #15
0
        /// <inheritdoc />
        public async Task <ServiceResult> RescindInfractionAsync(long infractionId, bool isAutoRescind = false)
        {
            var authResult = AuthorizationService.CheckClaims(AuthorizationClaim.ModerationRescind);

            if (authResult.IsFailure)
            {
                return(authResult);
            }

            await DoRescindInfractionAsync(
                await InfractionRepository.ReadSummaryAsync(infractionId), isAutoRescind);

            return(ServiceResult.FromSuccess());
        }
예제 #16
0
        /// <inheritdoc />
        public async Task DeleteInfractionAsync(long infractionId)
        {
            AuthorizationService.RequireAuthenticatedUser();
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationDeleteInfraction);

            var infraction = await InfractionRepository.ReadSummaryAsync(infractionId);

            if (infraction == null)
            {
                throw new InvalidOperationException($"Infraction {infractionId} does not exist");
            }

            await RequireSubjectRankLowerThanModeratorRankAsync(infraction.GuildId, AuthorizationService.CurrentUserId.Value, infraction.Subject.Id);

            await InfractionRepository.TryDeleteAsync(infraction.Id, AuthorizationService.CurrentUserId.Value);

            var guild = await DiscordClient.GetGuildAsync(infraction.GuildId);

            switch (infraction.Type)
            {
            case InfractionType.Mute:

                if (await UserService.GuildUserExistsAsync(guild.Id, infraction.Subject.Id))
                {
                    var subject = await UserService.GetGuildUserAsync(guild.Id, infraction.Subject.Id);

                    await subject.RemoveRoleAsync(await GetDesignatedMuteRoleAsync(guild));
                }
                else
                {
                    Log.Warning("Tried to unmute {User} while deleting mute infraction, but they weren't in the guild: {Guild}",
                                infraction.Subject.Id, guild.Id);
                }

                break;

            case InfractionType.Ban:

                //If the infraction has already been rescinded, we don't need to actually perform the unmute/unban
                //Doing so will return a 404 from Discord (trying to remove a nonexistant ban)
                if (infraction.RescindAction == null)
                {
                    await guild.RemoveBanAsync(infraction.Subject.Id);
                }

                break;
            }
        }
예제 #17
0
        private async Task DoRescindInfractionAsync(InfractionSummary infraction, string reason = null, bool isAutoRescind = false)
        {
            RequestOptions GetRequestOptions() => string.IsNullOrEmpty(reason) ? null : new RequestOptions
            {
                AuditLogReason = reason
            };

            if (infraction == null)
            {
                throw new InvalidOperationException("Infraction does not exist");
            }

            if (!isAutoRescind)
            {
                await RequireSubjectRankLowerThanModeratorRankAsync(infraction.GuildId, AuthorizationService.CurrentUserId.Value, infraction.Subject.Id);
            }

            await InfractionRepository.TryRescindAsync(infraction.Id, AuthorizationService.CurrentUserId.Value, reason);

            var guild = await DiscordClient.GetGuildAsync(infraction.GuildId);

            switch (infraction.Type)
            {
            case InfractionType.Mute:
                if (!await UserService.GuildUserExistsAsync(guild.Id, infraction.Subject.Id))
                {
                    Log.Information("Attempted to remove the mute role from {0} ({1}), but they were not in the server.",
                                    infraction.Subject.GetFullUsername(),
                                    infraction.Subject.Id);
                    break;
                }

                var subject = await UserService.GetGuildUserAsync(guild.Id, infraction.Subject.Id);

                await subject.RemoveRoleAsync(await GetDesignatedMuteRoleAsync(guild), GetRequestOptions());

                break;

            case InfractionType.Ban:
                await guild.RemoveBanAsync(infraction.Subject.Id, GetRequestOptions());

                break;

            default:
                throw new InvalidOperationException($"{infraction.Type} infractions cannot be rescinded.");
            }
        }
예제 #18
0
        /// <inheritdoc />
        public async Task AutoRescindExpiredInfractions()
        {
            var expiredInfractionIds = await InfractionRepository.SearchIdsAsync(new InfractionSearchCriteria()
            {
                ExpiresRange = new DateTimeOffsetRange()
                {
                    To = DateTimeOffset.Now
                },
                IsRescinded = false,
                IsDeleted   = false
            });

            foreach (var expiredInfractionId in expiredInfractionIds)
            {
                await RescindInfractionAsync(expiredInfractionId);
            }
        }
예제 #19
0
        /// <inheritdoc />
        public async Task <ServiceResult> DeleteInfractionAsync(long infractionId)
        {
            var authResult = AuthorizationService.CheckClaims(AuthorizationClaim.ModerationDeleteInfraction);

            if (authResult.IsFailure)
            {
                return(authResult);
            }

            var infraction = await InfractionRepository.ReadSummaryAsync(infractionId);

            if (infraction == null)
            {
                return(ServiceResult.FromError($"Infraction {infractionId} does not exist"));
            }

            var rankResult = await RequireSubjectRankLowerThanModeratorRankAsync(AuthorizationService.CurrentGuildId.Value, infraction.Subject.Id);

            if (rankResult.IsFailure)
            {
                return(rankResult);
            }

            await InfractionRepository.TryDeleteAsync(infraction.Id, AuthorizationService.CurrentUserId.Value);

            var guild = await DiscordClient.GetGuildAsync(infraction.GuildId);

            var subject = await UserService.GetGuildUserAsync(guild.Id, infraction.Subject.Id);

            switch (infraction.Type)
            {
            case InfractionType.Mute:
                await subject.RemoveRoleAsync(
                    await GetDesignatedMuteRoleAsync(guild));

                break;

            case InfractionType.Ban:
                await guild.RemoveBanAsync(subject);

                break;
            }

            return(ServiceResult.FromSuccess());
        }
예제 #20
0
        /// <inheritdoc />
        public async Task RescindInfractionAsync(InfractionType type, ulong subjectId)
        {
            AuthorizationService.RequireAuthenticatedGuild();
            AuthorizationService.RequireAuthenticatedUser();
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationRescind);

            await DoRescindInfractionAsync(
                (await InfractionRepository.SearchSummariesAsync(
                     new InfractionSearchCriteria()
            {
                GuildId = AuthorizationService.CurrentGuildId.Value,
                Types = new [] { type },
                SubjectId = subjectId,
                IsRescinded = false,
                IsDeleted = false,
            }))
                .FirstOrDefault());
        }
예제 #21
0
 /// <inheritdoc />
 public Task <DateTimeOffset?> GetNextInfractionExpiration()
 => InfractionRepository.ReadExpiresFirstOrDefaultAsync(
     new InfractionSearchCriteria()
 {
     IsRescinded  = false,
     IsDeleted    = false,
     ExpiresRange = new DateTimeOffsetRange()
     {
         From = DateTimeOffset.MinValue,
         To   = DateTimeOffset.MaxValue,
     }
 },
     new []
 {
     new SortingCriteria()
     {
         PropertyName = nameof(InfractionSummary.Expires), Direction = SortDirection.Ascending
     }
 });
예제 #22
0
        /// <inheritdoc />
        public async Task DeleteInfractionAsync(long infractionId)
        {
            AuthorizationService.RequireAuthenticatedUser();
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationDeleteInfraction);

            var infraction = await InfractionRepository.ReadSummaryAsync(infractionId);

            if (infraction == null)
            {
                throw new InvalidOperationException($"Infraction {infractionId} does not exist");
            }

            await RequireSubjectRankLowerThanModeratorRankAsync(infraction.GuildId, infraction.Subject.Id);

            await InfractionRepository.TryDeleteAsync(infraction.Id, AuthorizationService.CurrentUserId.Value);

            var guild = await DiscordClient.GetGuildAsync(infraction.GuildId);

            switch (infraction.Type)
            {
            case InfractionType.Mute:

                if (await UserService.GuildUserExistsAsync(guild.Id, infraction.Subject.Id))
                {
                    var subject = await UserService.GetGuildUserAsync(guild.Id, infraction.Subject.Id);

                    await subject.RemoveRoleAsync(await GetDesignatedMuteRoleAsync(guild));
                }
                else
                {
                    Log.Warning("Tried to unmute {User} while deleting mute infraction, but they weren't in the guild: {Guild}",
                                infraction.Subject.Id, guild.Id);
                }

                break;

            case InfractionType.Ban:
                await guild.RemoveBanAsync(infraction.Subject.Id);

                break;
            }
        }
예제 #23
0
        /// <inheritdoc />
        public async Task CreateInfractionAsync(InfractionType type, ulong subjectId, string reason, TimeSpan?duration)
        {
            AuthorizationService.RequireClaims(_createInfractionClaimsByType[type]);

            switch (type)
            {
            case InfractionType.Mute:
                await DoDiscordMuteAsync(subjectId);

                break;

            case InfractionType.Ban:
                await DoDiscordBanAsync(subjectId);

                break;
            }

            var actionId = await ModerationActionRepository.CreateAsync(new ModerationActionCreationData()
            {
                Type        = ModerationActionType.InfractionCreated,
                CreatedById = AuthorizationService.CurrentUserId.Value,
                Reason      = reason
            });

            var infractionId = await InfractionRepository.CreateAsync(new InfractionCreationData()
            {
                Type           = type,
                SubjectId      = subjectId,
                Duration       = duration,
                CreateActionId = actionId
            });

            await ModerationActionRepository.UpdateAsync(actionId, data =>
            {
                data.InfractionId = infractionId;
            });

            // TODO: Log action to a channel, pulled from IModerationConfigRepository.

            // TODO: Implement InfractionAutoExpirationBehavior (or whatever) to automatically rescind infractions, based on Duration, and notify it here that a new infraction has been created, if it has a duration.
        }
예제 #24
0
        /// <summary>
        /// Imports the given <see cref="RowboatInfraction"/>s, mapping them to Modix infractions
        /// </summary>
        /// <param name="rowboatInfractions">The <see cref="IEnumerable{T}"/> of infractions to be imported</param>
        /// <returns>The count of imported infractions</returns>
        public async Task <int> ImportInfractionsAsync(IEnumerable <RowboatInfraction> rowboatInfractions)
        {
            AuthorizationService.RequireAuthenticatedGuild();
            AuthorizationService.RequireAuthenticatedUser();
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationConfigure, AuthorizationClaim.ModerationWarn,
                                               AuthorizationClaim.ModerationNote, AuthorizationClaim.ModerationBan);

            if (!AuthorizationService.CurrentGuildId.HasValue)
            {
                throw new InvalidOperationException("Cannot import infractions without a guild context");
            }

            var importCount = 0;

            using (var transaction = await InfractionRepository.BeginCreateTransactionAsync())
            {
                foreach (var infraction in rowboatInfractions.Where(d => d.Active))
                {
                    if (await GuildUserRepository.ReadSummaryAsync(infraction.User.Id, AuthorizationService.CurrentGuildId.Value) != null &&
                        await GuildUserRepository.ReadSummaryAsync(infraction.Actor.Id, AuthorizationService.CurrentGuildId.Value) != null)
                    {
                        await InfractionRepository.CreateAsync(
                            new InfractionCreationData()
                        {
                            GuildId     = AuthorizationService.CurrentGuildId.Value,
                            Type        = infraction.ModixInfractionType,
                            SubjectId   = infraction.User.Id,
                            Reason      = infraction.Reason,
                            CreatedById = infraction.Actor.Id
                        });

                        importCount++;
                    }
                }

                transaction.Commit();
            }

            return(importCount);
        }
예제 #25
0
        /// <inheritdoc />
        public async Task RescindInfractionAsync(InfractionType type, ulong subjectId, string reason = null)
        {
            AuthorizationService.RequireAuthenticatedGuild();
            AuthorizationService.RequireAuthenticatedUser();
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationRescind);

            if (reason?.Length >= MaxReasonLength)
            {
                throw new ArgumentException($"Reason must be less than {MaxReasonLength} characters in length", nameof(reason));
            }

            await DoRescindInfractionAsync(
                (await InfractionRepository.SearchSummariesAsync(
                     new InfractionSearchCriteria()
            {
                GuildId = AuthorizationService.CurrentGuildId.Value,
                Types = new[] { type },
                SubjectId = subjectId,
                IsRescinded = false,
                IsDeleted = false,
            }))
                .FirstOrDefault(), reason);
        }
예제 #26
0
        /// <inheritdoc />
        public Task <RecordsPage <InfractionSummary> > SearchInfractionsAsync(InfractionSearchCriteria searchCriteria, IEnumerable <SortingCriteria> sortingCriteria, PagingCriteria pagingCriteria)
        {
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationRead);

            return(InfractionRepository.SearchSummariesPagedAsync(searchCriteria, sortingCriteria, pagingCriteria));
        }
예제 #27
0
        /// <inheritdoc />
        public Task <IReadOnlyCollection <InfractionSummary> > SearchInfractionsAsync(InfractionSearchCriteria searchCriteria, IEnumerable <SortingCriteria> sortingCriteria = null)
        {
            AuthorizationService.RequireClaims(AuthorizationClaim.ModerationRead);

            return(InfractionRepository.SearchSummariesAsync(searchCriteria, sortingCriteria));
        }
예제 #28
0
        /// <inheritdoc />
        public async Task CreateInfractionAsync(InfractionType type, ulong subjectId, string reason, TimeSpan?duration)
        {
            AuthorizationService.RequireAuthenticatedGuild();
            AuthorizationService.RequireAuthenticatedUser();
            AuthorizationService.RequireClaims(_createInfractionClaimsByType[type]);

            var guild = await GuildService.GetGuildAsync(AuthorizationService.CurrentGuildId.Value);

            var subject = await UserService.GetGuildUserAsync(guild.Id, subjectId);

            if (reason == null)
            {
                throw new ArgumentNullException(nameof(reason));
            }

            if (((type == InfractionType.Notice) || (type == InfractionType.Warning)) &&
                string.IsNullOrWhiteSpace(reason))
            {
                throw new InvalidOperationException($"{type.ToString()} infractions require a reason to be given");
            }

            using (var transaction = await InfractionRepository.BeginCreateTransactionAsync())
            {
                if ((type == InfractionType.Mute) || (type == InfractionType.Ban))
                {
                    if (await InfractionRepository.AnyAsync(new InfractionSearchCriteria()
                    {
                        GuildId = guild.Id,
                        Types = new[] { type },
                        SubjectId = subject.Id,
                        IsRescinded = false,
                        IsDeleted = false
                    }))
                    {
                        throw new InvalidOperationException($"Discord user {subjectId} already has an active {type} infraction");
                    }
                }

                await InfractionRepository.CreateAsync(
                    new InfractionCreationData()
                {
                    GuildId     = guild.Id,
                    Type        = type,
                    SubjectId   = subjectId,
                    Reason      = reason,
                    Duration    = duration,
                    CreatedById = AuthorizationService.CurrentUserId.Value
                });

                transaction.Commit();
            }

            // TODO: Implement ModerationSyncBehavior to listen for mutes and bans that happen directly in Discord, instead of through bot commands,
            // and to read the Discord Audit Log to check for mutes and bans that were missed during downtime, and add all such actions to
            // the Infractions and ModerationActions repositories.
            // Note that we'll need to upgrade to the latest Discord.NET version to get access to the audit log.

            // Assuming that our Infractions repository is always correct, regarding the state of the Discord API.
            switch (type)
            {
            case InfractionType.Mute:
                await subject.AddRoleAsync(
                    await GetOrCreateMuteRoleInGuildAsync(guild));

                break;

            case InfractionType.Ban:
                await guild.AddBanAsync(subject, reason : reason);

                break;
            }
        }
예제 #29
0
 /// <inheritdoc />
 public async Task <ServiceResult <RecordsPage <InfractionSummary> > > SearchInfractionsAsync(InfractionSearchCriteria searchCriteria, IEnumerable <SortingCriteria> sortingCriteria, PagingCriteria pagingCriteria)
 => await AuthorizationService.CheckClaims(AuthorizationClaim.ModerationRead)
 .ShortCircuitAsync(InfractionRepository.SearchSummariesPagedAsync(searchCriteria, sortingCriteria, pagingCriteria));
예제 #30
0
        /// <inheritdoc />
        public async Task CreateInfractionAsync(ulong guildId, ulong moderatorId, InfractionType type, ulong subjectId, string reason, TimeSpan?duration)
        {
            AuthorizationService.RequireClaims(_createInfractionClaimsByType[type]);

            await RequireSubjectRankLowerThanModeratorRankAsync(guildId, moderatorId, subjectId);

            var guild = await DiscordClient.GetGuildAsync(guildId);

            IGuildUser subject;

            if (!await UserService.GuildUserExistsAsync(guildId, subjectId))
            {
                subject = await UserService.GetUserInformationAsync(guildId, subjectId);

                if (subject == null)
                {
                    throw new InvalidOperationException($"The given subject was not valid, ID: {subjectId}");
                }

                await UserService.TrackUserAsync(subject);
            }
            else
            {
                subject = await UserService.GetGuildUserAsync(guildId, subjectId);
            }

            if (reason == null)
            {
                throw new ArgumentNullException(nameof(reason));
            }

            if (reason.Length > 1000)
            {
                throw new ArgumentException("Reason must be less than 1000 characters in length", nameof(reason));
            }

            if (((type == InfractionType.Notice) || (type == InfractionType.Warning)) &&
                string.IsNullOrWhiteSpace(reason))
            {
                throw new InvalidOperationException($"{type.ToString()} infractions require a reason to be given");
            }

            using (var transaction = await InfractionRepository.BeginCreateTransactionAsync())
            {
                if ((type == InfractionType.Mute) || (type == InfractionType.Ban))
                {
                    if (await InfractionRepository.AnyAsync(new InfractionSearchCriteria()
                    {
                        GuildId = guildId,
                        Types = new[] { type },
                        SubjectId = subjectId,
                        IsRescinded = false,
                        IsDeleted = false
                    }))
                    {
                        throw new InvalidOperationException($"Discord user {subjectId} already has an active {type} infraction");
                    }
                }

                await InfractionRepository.CreateAsync(
                    new InfractionCreationData()
                {
                    GuildId     = guildId,
                    Type        = type,
                    SubjectId   = subjectId,
                    Reason      = reason,
                    Duration    = duration,
                    CreatedById = moderatorId
                });

                transaction.Commit();

                try
                {
                    var guildName = await DiscordClient.GetGuildAsync(guildId);

                    Stats.Increment("infractions", tags: new[] { $"infraction_type:{type}", $"guild:{guild.Name}" });
                }
                catch (Exception)
                {
                    // The world mourned, but nothing of tremendous value was lost.
                }
            }

            // TODO: Implement ModerationSyncBehavior to listen for mutes and bans that happen directly in Discord, instead of through bot commands,
            // and to read the Discord Audit Log to check for mutes and bans that were missed during downtime, and add all such actions to
            // the Infractions and ModerationActions repositories.
            // Note that we'll need to upgrade to the latest Discord.NET version to get access to the audit log.

            // Assuming that our Infractions repository is always correct, regarding the state of the Discord API.
            switch (type)
            {
            case InfractionType.Mute:
                await subject.AddRoleAsync(
                    await GetDesignatedMuteRoleAsync(guild));

                break;

            case InfractionType.Ban:
                await guild.AddBanAsync(subject, reason : reason);

                break;
            }
        }