/// <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. }
/// <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); }
/// <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 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; } }
/// <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()); }
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(); })); } });