// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Dto; using Foxnouns.Backend.Jobs; using Humanizer; using NodaTime; namespace Foxnouns.Backend.Services; public class ModerationService( ILogger logger, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator, IQueue queue, IClock clock ) { private readonly ILogger _logger = logger.ForContext(); public async Task IgnoreReportAsync( User moderator, Report report, string? reason = null ) { _logger.Information( "Moderator {ModeratorId} is ignoring report {ReportId} on user {TargetId}", moderator.Id, report.Id, report.TargetUserId ); var entry = new AuditLogEntry { Id = snowflakeGenerator.GenerateSnowflake(), ModeratorId = moderator.Id, ModeratorUsername = moderator.Username, ReportId = report.Id, Type = AuditLogEntryType.IgnoreReport, Reason = reason, }; db.AuditLog.Add(entry); report.Status = ReportStatus.Closed; db.Update(report); await db.SaveChangesAsync(); return entry; } public async Task ExecuteSuspensionAsync( User moderator, User target, Report? report, string reason, bool clearProfile ) { _logger.Information( "Moderator {ModeratorId} is suspending user {TargetId}", moderator.Id, target.Id ); var entry = new AuditLogEntry { Id = snowflakeGenerator.GenerateSnowflake(), ModeratorId = moderator.Id, ModeratorUsername = moderator.Username, TargetUserId = target.Id, TargetUsername = target.Username, ReportId = report?.Id, Type = AuditLogEntryType.SuspendUser, Reason = reason, }; db.AuditLog.Add(entry); db.Notifications.Add( new Notification { Id = snowflakeGenerator.GenerateSnowflake(), TargetId = target.Id, Type = NotificationType.Warning, Message = null, LocalizationKey = "notification.suspension", LocalizationParams = { { "reason", reason } }, } ); target.Deleted = true; target.DeletedAt = clock.GetCurrentInstant(); target.DeletedBy = moderator.Id; if (!clearProfile) { db.Update(target); await db.SaveChangesAsync(); return entry; } _logger.Information("Clearing profile of user {TargetId}", target.Id); target.Username = $"deleted-user-{target.Id}"; target.DisplayName = null; target.Bio = null; target.MemberTitle = null; target.Links = []; target.Timezone = null; target.Names = []; target.Pronouns = []; target.Fields = []; target.CustomPreferences = []; target.ProfileFlags = []; queue.QueueInvocableWithPayload( new AvatarUpdatePayload(target.Id, null) ); // TODO: also clear member profiles? db.Update(target); await db.SaveChangesAsync(); return entry; } public async Task ExecuteWarningAsync( User moderator, User targetUser, Member? targetMember, Report? report, string reason, FieldsToClear[]? fieldsToClear ) { _logger.Information( "Moderator {ModeratorId} is warning user {TargetId} (member {TargetMemberId})", moderator.Id, targetUser.Id, targetMember?.Id ); string[]? fields = fieldsToClear?.Select(f => f.Humanize(LetterCasing.LowerCase)).ToArray(); var entry = new AuditLogEntry { Id = snowflakeGenerator.GenerateSnowflake(), ModeratorId = moderator.Id, ModeratorUsername = moderator.Username, TargetUserId = targetUser.Id, TargetUsername = targetUser.Username, TargetMemberId = targetMember?.Id, TargetMemberName = targetMember?.Name, ReportId = report?.Id, Type = fields != null ? AuditLogEntryType.WarnUserAndClearProfile : AuditLogEntryType.WarnUser, Reason = reason, ClearedFields = fields, }; db.AuditLog.Add(entry); db.Notifications.Add( new Notification { Id = snowflakeGenerator.GenerateSnowflake(), TargetId = targetUser.Id, Type = NotificationType.Warning, Message = null, LocalizationKey = fieldsToClear != null ? "notification.warning-cleared-fields" : "notification.warning", LocalizationParams = { { "reason", reason }, { "clearedFields", string.Join( "\n", fieldsToClear?.Select(f => f.Humanize(LetterCasing.LowerCase)) ?? [] ) }, }, } ); if (targetMember != null && fieldsToClear != null) { foreach (FieldsToClear field in fieldsToClear) { switch (field) { case FieldsToClear.DisplayName: targetMember.DisplayName = null; break; case FieldsToClear.Avatar: queue.QueueInvocableWithPayload< MemberAvatarUpdateInvocable, AvatarUpdatePayload >(new AvatarUpdatePayload(targetMember.Id, null)); break; case FieldsToClear.Bio: targetMember.Bio = null; break; case FieldsToClear.Links: targetMember.Links = []; break; case FieldsToClear.Names: targetMember.Names = []; break; case FieldsToClear.Pronouns: targetMember.Pronouns = []; break; case FieldsToClear.Fields: targetMember.Fields = []; break; case FieldsToClear.Flags: targetMember.ProfileFlags = []; break; // custom preferences can't be cleared on member-scoped warnings case FieldsToClear.CustomPreferences: default: break; } } db.Update(targetMember); } else if (fieldsToClear != null) { foreach (FieldsToClear field in fieldsToClear) { switch (field) { case FieldsToClear.DisplayName: targetUser.DisplayName = null; break; case FieldsToClear.Avatar: queue.QueueInvocableWithPayload< UserAvatarUpdateInvocable, AvatarUpdatePayload >(new AvatarUpdatePayload(targetUser.Id, null)); break; case FieldsToClear.Bio: targetUser.Bio = null; break; case FieldsToClear.Links: targetUser.Links = []; break; case FieldsToClear.Names: targetUser.Names = []; break; case FieldsToClear.Pronouns: targetUser.Pronouns = []; break; case FieldsToClear.Fields: targetUser.Fields = []; break; case FieldsToClear.Flags: targetUser.ProfileFlags = []; break; case FieldsToClear.CustomPreferences: targetUser.CustomPreferences = []; break; default: break; } } db.Update(targetUser); } await db.SaveChangesAsync(); return entry; } }