Esempio n. 1
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);
        }
Esempio n. 2
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;
            }
        }
Esempio n. 3
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;
            }
        }
Esempio n. 4
0
        /// <inheritdoc />
        public async Task <ServiceResult> CreateInfractionAsync(InfractionType type, ulong subjectId, string reason, TimeSpan?duration)
        {
            var authResult = AuthorizationService.CheckClaims(_createInfractionClaimsByType[type]);

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

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

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

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

            IGuildUser subject;

            if (!await UserService.GuildUserExistsAsync(guild.Id, subjectId))
            {
                subject = new EphemeralUser(subjectId, "[FORCED]", guild);
                await UserService.TrackUserAsync(subject);
            }
            else
            {
                subject = await UserService.GetGuildUserAsync(guild.Id, subjectId);
            }

            if (type == InfractionType.Notice || type == InfractionType.Warning)
            {
                if (string.IsNullOrWhiteSpace(reason))
                {
                    return(ServiceResult.FromError($"{type.ToString()} infractions require a reason to be given"));
                }
            }

            var lengthResult = new InvalidLengthResult("Reason", reason.Length, maximum: 1000);

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

            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
                    }))
                    {
                        return(ServiceResult.FromError($"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 GetDesignatedMuteRoleAsync(guild));

                break;

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

                break;
            }

            return(ServiceResult.FromSuccess());
        }
Esempio n. 5
0
 public Task <OperationResult <long> > CreateInfractionAsync(InfractionType type, ulong guildId, ulong subjectId, string reason, TimeSpan?duration)
 => Operation.Start
 // Validation
 .Require(!string.IsNullOrWhiteSpace(reason),
          () => new InfractionReasonMissingError())
 .Require(reason.Length <= 1000,
          () => new InfractionReasonTooLongError(actualLength: reason.Length, maxLength: 1000))
 // Authorization
 .ContinueOnSuccessAsync(() => AuthorizationService
                         .RequireClaimsAsync(_requiredClaimsByInfractionType[type]))
 .ContinueOnSuccessAsync(() => AuthorizationService
                         .RequireRankOverSubjectAsync(guildId, subjectId))
 // Acquire Guild API
 .ContinueOnSuccessAsync(() => UserService
                         .GetGuildUser(guildId, subjectId))
 // Try perform mute
 .DoOnSuccessWhenAsync(type == InfractionType.Mute,
                       guildUser => DesignatedRoleService
                       // Retrieve mute role
                       .SearchDesignatedRolesAsync(new DesignatedRoleMappingSearchCriteria()
 {
     GuildId   = guildId,
     Type      = DesignatedRoleType.ModerationMute,
     IsDeleted = false
 })
                       .AsSuccessAsync()
                       .RequireOnSuccessAsync(
                           roleMappings => roleMappings.Any(),
                           () => new ModerationMuteRoleNotConfiguredError())
                       .RequireOnSuccessAsync(
                           roleMappings => roleMappings.Count == 1,
                           () => new ModerationMuteRoleMultipleConfigurationsError())
                       .ContinueOnSuccessAsync(roleMappings => roleMappings
                                               .First()
                                               .Role.Id
                                               .AsSuccess())
                       // Verify user is not muted
                       .RequireOnSuccessAsync(
                           roleId => !guildUser.RoleIds.Contains(roleId),
                           () => new ModerationSubjectAlreadyMutedError())
                       // Discord API
                       .ContinueOnSuccessAsync(roleId => guildUser
                                               .Guild
                                               .GetRole(roleId)
                                               .AsSuccess())
                       // Perform mute
                       .BranchOnSuccessAsync(role =>
                                             guildUser.AddRoleAsync(role)))
 // Try perform ban
 .DoOnSuccessWhenAsync(type == InfractionType.Ban,
                       guildUser => guildUser.Guild
                       .AsSuccess()
                       // Verify user is not banned
                       .DoOnSuccessAsync(guild => guild
                                         .GetBansAsync()
                                         .AsSuccessAsync()
                                         .RequireOnSuccessAsync(bans => !bans.Any(x => x.User.Id == guildUser.Id),
                                                                () => new ModerationSubjectAlreadyBannedError()))
                       // Perform ban
                       .BranchOnSuccessAsync(guild =>
                                             guild.AddBanAsync(guildUser.Id)))
 // Perform database operations
 .ContinueOnSuccessAsync(async() =>
 {
     using (var deleteTransaction = await InfractionRepository.BeginDeleteTransactionAsync())
         using (var createTransaction = await InfractionRepository.BeginCreateTransactionAsync())
         {
             return(await Operation.Start
                    // Delete existing active Mute/Ban infractions, if any, so we can create a new one
                    .BranchWhenAsync((type == InfractionType.Mute) || (type == InfractionType.Ban),
                                     () => Operation.Start
                                     .ContinueAsync(InfractionRepository.SearchIdsAsync(new InfractionSearchCriteria()
             {
                 GuildId = guildId,
                 Types = new[] { type },
                 SubjectId = subjectId,
                 IsRescinded = false,
                 IsDeleted = false
             }).AsSuccessAsync())
                                     .BranchOnSuccessWhenAsync(bans => bans.Any(),
                                                               bans => InfractionRepository
                                                               .TryDeleteAsync(
                                                                   bans,
                                                                   SelfUser.Id)))
                    // Record new infraction
                    .ContinueOnSuccessAsync(() => InfractionRepository.CreateAsync(new InfractionCreationData()
             {
                 GuildId = guildId,
                 Type = type,
                 Reason = reason,
                 Duration = duration,
                 SubjectId = subjectId,
                 CreatedById = AuthorizationService.CurrentUserId
                               ?? SelfUser.Id
             }))
                    .BranchOnSuccess(() =>
             {
                 deleteTransaction.Commit();
                 createTransaction.Commit();
             }));
         }
 });