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 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 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 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 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 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 HandleReport( string reportType, string[] idFieldNames, Func> 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 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 SubmitReport( string reportType, long? targetId, string targetName, long reportedId, long reporterId, Dictionary 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 { 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> ReadFieldsAsync() { var fields = new Dictionary(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 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 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 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(raw, ignoreCase: true, out var parsed)) return parsed; return ReportCategory.Misc; } }