feat: moderation API
This commit is contained in:
parent
79b8c4799e
commit
36cb1d2043
24 changed files with 1535 additions and 45 deletions
292
Foxnouns.Backend/Services/ModerationService.cs
Normal file
292
Foxnouns.Backend/Services/ModerationService.cs
Normal file
|
@ -0,0 +1,292 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
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<ModerationService>();
|
||||
|
||||
public async Task<AuditLogEntry> 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<AuditLogEntry> 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<UserAvatarUpdateInvocable, AvatarUpdatePayload>(
|
||||
new AvatarUpdatePayload(target.Id, null)
|
||||
);
|
||||
|
||||
// TODO: also clear member profiles?
|
||||
|
||||
db.Update(target);
|
||||
await db.SaveChangesAsync();
|
||||
return entry;
|
||||
}
|
||||
|
||||
public async Task<AuditLogEntry> 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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue