public async Task CreateInfractionAsync(ulong guildId, ulong moderatorId, InfractionType type, ulong subjectId, string reason, TimeSpan?duration) { _authorizationService.RequireClaims(_createInfractionClaimsByType[type]); if (reason is null) { throw new ArgumentNullException(nameof(reason)); } if (reason.Length >= MaxReasonLength) { throw new ArgumentException($"Reason must be less than {MaxReasonLength} 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"); } var guild = await _discordClient.GetGuildAsync(guildId); var subject = await _userService.TryGetGuildUserAsync(guild, subjectId, default); await RequireSubjectRankLowerThanModeratorRankAsync(guild, moderatorId, subject); 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 { _dogStatsd.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 when subject is not null: await subject.AddRoleAsync( await GetDesignatedMuteRoleAsync(guild)); break; case InfractionType.Ban: await guild.AddBanAsync(subjectId, reason : reason); break; } }
/// <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; } }
/// <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()); }