mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user