Files
Radicade/Server/PlayerReporting/ContentReportAPI.cs
T
2026-10-16 23:33:13 -07:00

293 lines
8.9 KiB
C#

using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.Admin;
using RadicadeServer.Radicade.Discord;
using RadicadeServer.DB;
using RadicadeServer.Extensions;
using RadicadeServer.RecNet.Enums.Moderation;
using RadicadeServer.RecNet.Moderation;
using RadicadeServer.Discord.Webhooks;
namespace RadicadeServer.API.Moderation;
public class ContentReportAPI : Controller
{
private readonly RecDBContext db;
public ContentReportAPI(RecDBContext dbContext) {
db = dbContext;
}
[HttpPost("api/inventions/v1/report"), Authorize]
[HttpPost("api/inventions/v2/report"), Authorize]
public Task<IActionResult> ReportInvention() => HandleReport("Invention",
new[] { "InventionId", "inventionId", "Id", "id", "TargetId", "targetId", "ReporteeId", "reporteeId" },
async targetId =>
{
var invention = await db.Inventions.AsNoTracking().FirstOrDefaultAsync(x => x.InventionId == targetId);
return invention == null ? null : (OwnerId: invention.CreatorPlayerId, TargetName: invention.Name);
});
[HttpPost("api/rooms/v1/report"), Authorize]
[HttpPost("api/rooms/v2/report"), Authorize]
public Task<IActionResult> ReportRoom() => HandleReport("Room",
new[] { "RoomId", "roomId", "Id", "id", "TargetId", "targetId", "ReporteeId", "reporteeId" },
async targetId =>
{
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == targetId);
return room == null ? null : (OwnerId: room.CreatorAccountId, TargetName: room.Name);
});
[HttpPost("api/clubs/v1/report"), Authorize]
[HttpPost("api/clubs/v2/report"), Authorize]
[HttpPost("api/clubreporting/v1/report"), Authorize]
public Task<IActionResult> ReportClub() => HandleReport("Club",
new[] { "ClubId", "clubId", "Id", "id", "TargetId", "targetId", "ReporteeId", "reporteeId" },
_ => Task.FromResult<(long OwnerId, string TargetName)?>(null));
[HttpPost("api/board/v1/report"), Authorize]
[HttpPost("api/board/v2/report"), Authorize]
[HttpPost("api/communityboard/v1/report"), Authorize]
[HttpPost("api/communityboard/v2/report"), Authorize]
public Task<IActionResult> ReportBoard() => HandleReport("Board",
new[] { "BoardId", "boardId", "PostId", "postId", "CommentId", "commentId", "Id", "id", "TargetId", "targetId" },
_ => Task.FromResult<(long OwnerId, string TargetName)?>(null));
[HttpPost("api/playerevents/v2/report"), Authorize]
[HttpPost("api/events/v1/report"), Authorize]
[HttpPost("api/events/v2/report"), Authorize]
public Task<IActionResult> ReportEvent() => HandleReport("Event",
new[] { "EventId", "eventId", "PlayerEventId", "playerEventId", "Id", "id", "TargetId", "targetId", "ReporteeId", "reporteeId" },
async targetId =>
{
var ev = await db.PlayerEvents.AsNoTracking().FirstOrDefaultAsync(x => x.PlayerEventId == targetId);
return ev == null ? null : (OwnerId: ev.CreatorPlayerId, TargetName: ev.Name);
});
private async Task<IActionResult> HandleReport(
string reportType,
string[] idFieldNames,
Func<long, Task<(long OwnerId, string TargetName)?>> resolveTarget)
{
var reporterId = User.GetUserId();
if (reporterId == null) return Unauthorized();
var fields = await ReadFieldsAsync();
var targetId = GetLong(fields, idFieldNames);
long reportedId = 0;
string targetName = "";
if (targetId.HasValue)
{
var resolved = await resolveTarget(targetId.Value);
if (resolved.HasValue)
{
reportedId = resolved.Value.OwnerId;
targetName = resolved.Value.TargetName;
}
else
{
targetName = $"{reportType} {targetId.Value}";
}
}
return await SubmitReport(reportType, targetId, targetName, reportedId, reporterId.Value, fields);
}
[HttpPost("api/customAvatarItems/v1/{customAvatarItemId:guid}/report"), Authorize]
public async Task<IActionResult> ReportCustomAvatarItem(Guid customAvatarItemId)
{
var reporterId = User.GetUserId();
if (reporterId == null) return Unauthorized();
var fields = await ReadFieldsAsync();
var item = await db.CustomAvatarItems.AsNoTracking()
.FirstOrDefaultAsync(x => x.CustomAvatarItemId == customAvatarItemId);
return await SubmitReport(
"CustomAvatarItem",
null,
item?.Name ?? $"Custom Avatar Item {customAvatarItemId}",
item?.CreatorAccountId ?? 0,
reporterId.Value,
fields);
}
private async Task<IActionResult> SubmitReport(
string reportType,
long? targetId,
string targetName,
long reportedId,
long reporterId,
Dictionary<string, string> fields)
{
var reason = GetField(fields, "Reason", "reason", "ReportReason", "reportReason") ?? "";
var details = GetField(fields, "Details", "details", "Description", "description", "Message", "message", "Text", "text") ?? "";
var category = ParseCategory(fields);
var roomId = GetLong(fields, "RoomId", "roomId") ?? 0;
if (reportedId > 0 && reportedId != reporterId)
{
var reputation = await db.Reputations.FirstOrDefaultAsync(x => x.AccountId == reportedId);
if (reputation == null)
{
reputation = new Reputation { AccountId = reportedId };
await db.Reputations.AddAsync(reputation);
}
reputation.ReportCount += 1;
await db.SaveChangesAsync();
}
AdminPanelAPI.AddModerationReport(reporterId, reportedId, reason, details, category, roomId,
reportType, targetId, targetName);
ReportDiscord.SendAsync(WebhookConfig.ReportsUrl, $"🚩 {reportType} Report",
new DiscordPayload
{
Title = $"{reportType} Report",
ThumbnailUrl = WebhookImages.ForReport(reportType) ?? "",
Color = 0xF0B232,
Fields = new List<DiscordField>
{
new() { Name = "Report Type", Value = reportType, Inline = true },
new() { Name = "Reporter", Value = reporterId.ToString(), Inline = true },
new() { Name = "Reported Player", Value = reportedId > 0 ? reportedId.ToString() : "Unknown", Inline = true },
new() { Name = "Target", Value = targetId.HasValue ? $"{targetName} ({targetId})" : targetName, Inline = true },
new() { Name = "Category", Value = category.ToString(), Inline = true },
new() { Name = "Room", Value = roomId.ToString(), Inline = true },
new() { Name = "Reason", Value = reason, Inline = false },
new() { Name = "Details", Value = details, Inline = false }
},
TimestampUtc = DateTime.UtcNow.ToString("O")
});
return Ok(new OkResponse { Success = true });
}
private async Task<Dictionary<string, string>> ReadFieldsAsync()
{
var fields = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var kv in Request.Query)
fields[kv.Key] = kv.Value.ToString();
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
foreach (var kv in form)
fields[kv.Key] = kv.Value.ToString();
return fields;
}
if (Request.ContentLength is not > 0) return fields;
try
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
if (string.IsNullOrWhiteSpace(body)) return fields;
var trimmed = body.TrimStart();
if (trimmed.StartsWith("{", StringComparison.Ordinal))
{
using var doc = JsonDocument.Parse(body);
if (doc.RootElement.ValueKind == JsonValueKind.Object)
{
foreach (var prop in doc.RootElement.EnumerateObject())
{
fields[prop.Name] = prop.Value.ValueKind == JsonValueKind.String
? prop.Value.GetString() ?? ""
: prop.Value.ToString();
}
}
}
else if (trimmed.Contains('='))
{
foreach (var pair in body.Split('&', StringSplitOptions.RemoveEmptyEntries))
{
var eq = pair.IndexOf('=');
if (eq < 0)
{
fields[Uri.UnescapeDataString(pair)] = "";
continue;
}
var key = Uri.UnescapeDataString(pair.Substring(0, eq));
var value = Uri.UnescapeDataString(pair.Substring(eq + 1));
fields[key] = value;
}
}
}
catch
{
}
return fields;
}
private static string? GetField(Dictionary<string, string> fields, params string[] names)
{
foreach (var name in names)
{
if (fields.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value))
return value;
}
return null;
}
private static long? GetLong(Dictionary<string, string> fields, params string[] names)
{
var raw = GetField(fields, names);
if (raw == null) return null;
if (long.TryParse(raw, out var value)) return value;
var digits = new string(raw.Where(char.IsDigit).ToArray());
return long.TryParse(digits, out value) ? value : null;
}
private static ReportCategory ParseCategory(Dictionary<string, string> fields)
{
var raw = GetField(fields, "ReportCategory", "reportCategory", "Category", "category", "Reason", "reason", "ReportReason", "reportReason");
if (raw == null) return ReportCategory.Misc;
if (int.TryParse(raw, out var intValue) && Enum.IsDefined(typeof(ReportCategory), intValue))
return (ReportCategory)intValue;
if (Enum.TryParse<ReportCategory>(raw, ignoreCase: true, out var parsed))
return parsed;
return ReportCategory.Misc;
}
}