mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
343 lines
12 KiB
C#
343 lines
12 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.Radicade.Discord;
|
|
using RadicadeServer.API.Notifications;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.Extensions;
|
|
using RadicadeServer.RecNet.Enums.Messages;
|
|
using RadicadeServer.RecNet.Moderation;
|
|
using RadicadeServer.RecNet.Notifications;
|
|
using RadicadeServer.Utils;
|
|
using RadicadeServer.Discord.Webhooks;
|
|
|
|
namespace RadicadeServer.API.Moderation
|
|
{
|
|
public class SubmitAppealRequest
|
|
{
|
|
|
|
public string Code { get; set; } = string.Empty;
|
|
|
|
|
|
public string Text { get; set; } = string.Empty;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[Route("api/Appeal/")]
|
|
public class AppealAPI(RecDBContext db) : Controller
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[HttpPost("submit")]
|
|
public async Task<ActionResult<OkResponse>> Submit([FromBody] SubmitAppealRequest request)
|
|
{
|
|
var code = request?.Code?.Trim().ToUpperInvariant() ?? string.Empty;
|
|
if (code.Length == 0)
|
|
return BadRequest(new OkResponse { Success = false, Message = "Missing appeal code." });
|
|
|
|
var text = request?.Text?.Trim() ?? string.Empty;
|
|
if (text.Length < 10)
|
|
return BadRequest(new OkResponse { Success = false, Message = "Please describe your appeal in at least 10 characters." });
|
|
|
|
var ban = await db.Moderation.FirstOrDefaultAsync(x => x.AppealCode == code);
|
|
if (ban == null)
|
|
return NotFound(new OkResponse { Success = false, Message = "Unknown appeal code." });
|
|
|
|
|
|
|
|
|
|
if (!ban.IsBan || ban.ExpireDuration < DateTime.UtcNow)
|
|
return BadRequest(new OkResponse { Success = false, Message = "Player is not banned!" });
|
|
|
|
if (AccountLock.IsLockBan(ban))
|
|
return BadRequest(new OkResponse { Success = false, Message = AccountLock.NotAppealableMessage });
|
|
|
|
|
|
if (!ModerationPolicy.IssuesAppealCode(ban.BanMethod) || ModerationPolicy.RefusesAppeal(ban.BanMethod))
|
|
return BadRequest(new OkResponse { Success = false, Message = AccountLock.NotAppealableMessage });
|
|
|
|
|
|
if (ban.AppealText != null && ban.AppealResolution == null)
|
|
return BadRequest(new OkResponse { Success = false, Message = "Your appeal is already pending review." });
|
|
|
|
if (ban.AppealResolution == "Accepted")
|
|
return BadRequest(new OkResponse { Success = false, Message = "Player is not banned!" });
|
|
|
|
|
|
|
|
|
|
|
|
var cooldownError = await PlayerUtils.GetAppealCooldownErrorAsync(db, ban.PlayerId);
|
|
if (cooldownError != null)
|
|
{
|
|
return BadRequest(new OkResponse { Success = false, Message = cooldownError });
|
|
}
|
|
|
|
ban.AppealText = text;
|
|
ban.AppealSubmittedAt = DateTime.UtcNow;
|
|
ban.AppealResolution = null;
|
|
ban.AppealResolvedAt = null;
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
|
|
var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == ban.PlayerId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (profile != null)
|
|
{
|
|
await Notify.SendNotify(ban.PlayerId, new Notification { Id = NotificationType.RefreshLogin, Msg = profile });
|
|
}
|
|
|
|
ReportDiscord.SendAsync(WebhookConfig.BanAppealsUrl, "🎟️ Ban Appeal",
|
|
new DiscordPayload
|
|
{
|
|
Title = "New Ban Appeal",
|
|
ThumbnailUrl = WebhookImages.BanAppeal,
|
|
Color = 0x5865F2,
|
|
Fields = new List<DiscordField>
|
|
{
|
|
new() { Name = "Player", Value = profile?.username ?? ban.PlayerId.ToString(), Inline = true },
|
|
new() { Name = "Player ID", Value = ban.PlayerId.ToString(), Inline = true },
|
|
new() { Name = "Appeal Code", Value = code, Inline = true },
|
|
new() { Name = "Ban Reason", Value = ban.Message ?? "—", Inline = false },
|
|
new() { Name = "Appeal", Value = text, Inline = false }
|
|
},
|
|
TimestampUtc = DateTime.UtcNow.ToString("O")
|
|
});
|
|
|
|
return Ok(new OkResponse { Success = true, Message = "Appeal submitted. A moderator will review it shortly." });
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("status")]
|
|
public async Task<ActionResult<object>> Status([FromQuery] string code)
|
|
{
|
|
var normalized = code?.Trim().ToUpperInvariant() ?? string.Empty;
|
|
if (normalized.Length == 0)
|
|
return Ok(new { found = false });
|
|
|
|
var ban = await db.Moderation.AsNoTracking().FirstOrDefaultAsync(x => x.AppealCode == normalized);
|
|
if (ban == null)
|
|
return Ok(new { found = false });
|
|
|
|
var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == ban.PlayerId);
|
|
|
|
bool banned = ban.IsBan && ban.ExpireDuration > DateTime.UtcNow;
|
|
string? cooldownText = null;
|
|
DateTime? cooldownUntilUtc = null;
|
|
|
|
|
|
if (banned
|
|
&& (ban.AppealResolution == "Rejected"
|
|
|| ban.AppealResolution == "Reduced"
|
|
|| ban.AppealResolution == "Extended"
|
|
|| ban.AppealResolution == "Reviewed")
|
|
&& ban.AppealResolvedAt.HasValue)
|
|
{
|
|
var cooldownEnds = ban.AppealResolvedAt.Value + PlayerUtils.AppealCooldownDuration;
|
|
if (cooldownEnds > DateTime.UtcNow)
|
|
{
|
|
cooldownUntilUtc = cooldownEnds;
|
|
cooldownText = $"{PlayerUtils.AppealCooldownPrefix}{PlayerUtils.FormatAppealCooldown(cooldownEnds - DateTime.UtcNow)}";
|
|
}
|
|
}
|
|
|
|
return Ok(new
|
|
{
|
|
found = true,
|
|
player = profile?.username ?? "unknown",
|
|
reason = ban.Message ?? string.Empty,
|
|
category = ban.Category.ToString(),
|
|
permanent = ban.ExpireDuration >= DateTime.MaxValue.AddYears(-10),
|
|
expiresAtUtc = ban.ExpireDuration,
|
|
banned,
|
|
hasAppeal = ban.AppealText != null,
|
|
submittedAt = ban.AppealSubmittedAt,
|
|
resolution = ban.AppealResolution,
|
|
resolvedAtUtc = ban.AppealResolvedAt,
|
|
cooldownUntilUtc,
|
|
cooldownText
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[Route("api/banappeal/")]
|
|
public class BanAppealAPI(RecDBContext db) : Controller
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static object AppealResponse(bool success, string? error, object? value) => new
|
|
{
|
|
success,
|
|
error,
|
|
value
|
|
};
|
|
|
|
[HttpGet("generateCode"), Authorize]
|
|
[HttpPost("generateCode"), Authorize]
|
|
public async Task<ActionResult<object>> GenerateCode()
|
|
{
|
|
var userId = User.GetUserId();
|
|
if (userId == null)
|
|
return Unauthorized(new { success = false, error = "Not signed in.", value = (object?)null });
|
|
|
|
var ban = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == userId.Value && x.IsBan && x.ExpireDuration > DateTime.UtcNow);
|
|
if (ban == null)
|
|
return Ok(AppealResponse(false, "Account is not banned.", null));
|
|
|
|
if (AccountLock.IsLockBan(ban))
|
|
return Ok(AppealResponse(false, AccountLock.NotAppealableMessage, null));
|
|
|
|
|
|
if (!ModerationPolicy.IssuesAppealCode(ban.BanMethod) || ModerationPolicy.RefusesAppeal(ban.BanMethod))
|
|
return Ok(AppealResponse(false, AccountLock.NotAppealableMessage, null));
|
|
|
|
var cooldownError = await PlayerUtils.GetAppealCooldownErrorAsync(db, userId.Value);
|
|
if (cooldownError != null)
|
|
return Ok(AppealResponse(false, cooldownError, null));
|
|
|
|
|
|
|
|
|
|
|
|
if (ban.AppealText != null && ban.AppealResolution == null)
|
|
return Ok(AppealResponse(false, "Your appeal is already pending review.", null));
|
|
|
|
ban.AppealCode = PlayerUtils.GenerateAppealCode();
|
|
await db.SaveChangesAsync();
|
|
|
|
var url = $"{PlayerUtils.AppealUrl()}?code={ban.AppealCode}";
|
|
return Ok(AppealResponse(true, null, url));
|
|
}
|
|
|
|
private async Task<(ModerationDB? Ban, ActionResult? Stop)> LookUpAppeal(string? code, string? key)
|
|
{
|
|
if (!key.IsSupportKey())
|
|
return (null, Unauthorized(new { success = false, error = "Invalid secret.", value = (object?)null }));
|
|
|
|
var normalized = code?.Trim().ToUpperInvariant() ?? string.Empty;
|
|
if (normalized.Length == 0)
|
|
return (null, Ok(AppealResponse(false, "Missing appeal code.", null)));
|
|
|
|
var ban = await db.Moderation.FirstOrDefaultAsync(x => x.AppealCode == normalized);
|
|
if (ban == null)
|
|
return (null, Ok(AppealResponse(false, "Unknown appeal code.", null)));
|
|
|
|
|
|
if (!ModerationPolicy.IssuesAppealCode(ban.BanMethod) || ModerationPolicy.RefusesAppeal(ban.BanMethod))
|
|
return (null, Ok(AppealResponse(false, AccountLock.NotAppealableMessage, null)));
|
|
|
|
return (ban, null);
|
|
}
|
|
|
|
private string SupportKeyFrom(string? queryKey)
|
|
{
|
|
return !string.IsNullOrWhiteSpace(queryKey)
|
|
? queryKey
|
|
: Request.Headers["X-Support-Key"].ToString().Trim();
|
|
}
|
|
|
|
[HttpPost("pending")]
|
|
public async Task<ActionResult<object>> Pending([FromQuery] string code, [FromQuery] string key)
|
|
{
|
|
var (ban, stop) = await LookUpAppeal(code, SupportKeyFrom(key));
|
|
if (stop != null) return stop;
|
|
if (ban == null) return Ok(AppealResponse(false, "Unknown appeal code.", null));
|
|
|
|
if (ban.AppealResolution != null)
|
|
return Ok(AppealResponse(false, "This appeal has already been reviewed.", ban.AppealResolution));
|
|
|
|
if (ban.AppealText == null)
|
|
{
|
|
ban.AppealText = PlayerUtils.SupportSiteAppealText;
|
|
ban.AppealSubmittedAt = DateTime.UtcNow;
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
return Ok(AppealResponse(true, null, "pending"));
|
|
}
|
|
|
|
[HttpPost("resolve")]
|
|
public async Task<ActionResult<object>> Resolve([FromQuery] string code, [FromQuery] string key, [FromQuery] string? decision = null)
|
|
{
|
|
var (ban, stop) = await LookUpAppeal(code, SupportKeyFrom(key));
|
|
if (stop != null) return stop;
|
|
if (ban == null) return Ok(AppealResponse(false, "Unknown appeal code.", null));
|
|
|
|
if (ban.AppealResolution != null)
|
|
return Ok(AppealResponse(true, null, ban.AppealResolution));
|
|
|
|
var verdict = (decision ?? string.Empty).Trim().ToLowerInvariant();
|
|
|
|
if (verdict == "accept")
|
|
{
|
|
ban.IsBan = false;
|
|
ban.AppealResolution = "Accepted";
|
|
}
|
|
else if (verdict == "deny")
|
|
{
|
|
ban.AppealResolution = "Rejected";
|
|
}
|
|
else if (verdict == "reviewed" || verdict == "review")
|
|
{
|
|
ban.AppealResolution = "Reviewed";
|
|
}
|
|
else
|
|
{
|
|
return Ok(AppealResponse(false, "Unknown decision.", null));
|
|
}
|
|
|
|
ban.AppealResolvedAt = DateTime.UtcNow;
|
|
await db.SaveChangesAsync();
|
|
|
|
if (!ban.IsBan)
|
|
{
|
|
var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == ban.PlayerId);
|
|
if (profile != null)
|
|
await Notify.SendNotify(ban.PlayerId, new Notification { Id = NotificationType.RefreshLogin, Msg = profile });
|
|
}
|
|
|
|
return Ok(AppealResponse(true, null, ban.AppealResolution));
|
|
}
|
|
}
|
|
}
|