Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RadicadeServer.RecNet.Moderation
{
public class AccountCreationBlock
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public string IpAddress { get; set; } = "";
public string Reason { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ExpiresAt { get; set; }
}
}
+242
View File
@@ -0,0 +1,242 @@
using Microsoft.EntityFrameworkCore;
using RadicadeServer.API.Notifications;
using RadicadeServer.DB;
using RadicadeServer.Radicade.Discord;
using RadicadeServer.RecNet.Enums.Messages;
using RadicadeServer.RecNet.Enums.Moderation;
using RadicadeServer.RecNet.Moderation;
using RadicadeServer.RecNet.Notifications;
namespace RadicadeServer.Utils
{
public sealed record AccountLockResult(bool Success, bool Changed, string? Error = null);
public static class AccountLock
{
public const string BanMethod = "Account Lock";
public const string Reason =
"Moderator (Account lock. Please submit a ticket with us to discuss. https://support.radicade.app/new/account-support/)";
public const string SupportUrl = "https://support.radicade.app";
public const string ErrorCode = "ACCOUNT_PERMALOCKED";
public const string NotAppealableMessage = "This ban cannot be appealed";
public static bool IsLockBan(ModerationDB ban) =>
string.Equals(ban.BanMethod, BanMethod, StringComparison.OrdinalIgnoreCase);
public const string FallbackUsername = "Account";
public static string LoginErrorMessage(string? username)
{
var name = (username ?? string.Empty).Trim();
if (name.StartsWith("@", StringComparison.Ordinal)) name = name[1..].Trim();
if (string.IsNullOrWhiteSpace(name)) name = FallbackUsername;
return $"@{name}: This account has been locked for security reasons. Contact us at {SupportUrl}.";
}
public static Task<ModerationDB?> FindActiveLockAsync(RecDBContext db, long accountId) =>
db.Moderation.AsNoTracking()
.Where(x => x.PlayerId == accountId
&& x.IsBan
&& x.BanMethod == BanMethod
&& x.ExpireDuration > DateTime.UtcNow)
.OrderByDescending(x => x.Id)
.FirstOrDefaultAsync();
public static async Task<bool> IsLockedAsync(RecDBContext db, long accountId)
{
try
{
return await FindActiveLockAsync(db, accountId) != null;
}
catch (Exception ex)
{
Log($"could not read lock state for account {accountId} - failing closed ({ex.GetType().Name})");
return true;
}
}
public static async Task<string?> ResolveUsernameAsync(RecDBContext db, long accountId)
{
try
{
var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == accountId);
return profile?.username;
}
catch
{
return null;
}
}
public static async Task<AccountLockResult> ApplyAsync(RecDBContext db, long accountId, string? staff, string? caseRef)
{
bool changed;
try
{
var existing = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == accountId && x.IsBan);
var note = NormalizeCase(caseRef);
if (existing == null)
{
existing = new ModerationDB
{
PlayerId = accountId,
IsBan = true,
Category = ReportCategory.Moderator,
Message = Reason,
BanMethod = BanMethod,
ExpireDuration = DateTime.MaxValue,
CreatedAt = DateTime.UtcNow,
ModeratorNote = note
};
await db.Moderation.AddAsync(existing);
changed = true;
}
else
{
changed = !IsAlreadyLocked(existing) || note != existing.ModeratorNote;
existing.IsBan = true;
existing.Category = ReportCategory.Moderator;
existing.Message = Reason;
existing.BanMethod = BanMethod;
existing.ExpireDuration = DateTime.MaxValue;
existing.ModeratorNote = note ?? existing.ModeratorNote;
}
await db.SaveChangesAsync();
}
catch (Exception ex)
{
Log($"failed to persist account lock for {accountId}: {ex.GetType().Name}");
AdminActionDiscord.LogAction("Account Lock", StaffName(staff),
$"FAILED to lock account {accountId}: the permanent ban could not be saved ({ex.GetType().Name}).");
return new AccountLockResult(false, false, "The lock could not be saved. Nothing was changed.");
}
var failures = new List<string>();
await EnforceOnlineAsync(db, accountId, failures);
var detail = $"Locked account {accountId}. Category: {ReportCategory.Moderator}. Method: {BanMethod}. Reason: {Reason}";
if (!string.IsNullOrWhiteSpace(caseRef)) detail += $" Case: {caseRef.Trim()}";
detail += failures.Count == 0
? " Result: ban persisted, online state handled, new sign-ins refused."
: $" Result: ban persisted; partial failures - {string.Join("; ", failures)}.";
AdminActionDiscord.LogAction("Account Lock", StaffName(staff), detail);
return new AccountLockResult(true, changed);
}
public static async Task<AccountLockResult> RemoveAsync(RecDBContext db, long accountId, string? staff, string? caseRef)
{
ModerationDB? existing;
try
{
existing = await db.Moderation.FirstOrDefaultAsync(x => x.PlayerId == accountId && x.IsBan);
}
catch (Exception ex)
{
Log($"failed to read ban state before unlock for {accountId}: {ex.GetType().Name}");
return new AccountLockResult(false, false, "The ban state could not be read.");
}
if (existing == null)
return new AccountLockResult(false, false, "This account does not have an active ban.");
if (!string.Equals(existing.BanMethod, BanMethod, StringComparison.OrdinalIgnoreCase))
return new AccountLockResult(false, false,
$"The active ban is '{existing.BanMethod}', not an account lock. Refusing to remove an unrelated ban.");
var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == accountId);
try
{
db.Moderation.Remove(existing);
await db.SaveChangesAsync();
}
catch (Exception ex)
{
Log($"failed to persist account unlock for {accountId}: {ex.GetType().Name}");
AdminActionDiscord.LogAction("Account Unlock", StaffName(staff),
$"FAILED to unlock account {accountId}: the ban row could not be removed ({ex.GetType().Name}). The account is still locked.");
return new AccountLockResult(false, false, "The unlock could not be saved. The account is still locked.");
}
var detail = $"Unlocked account {accountId}: the account lock was removed; hardware/IP bans and unrelated records were left untouched.";
if (!string.IsNullOrWhiteSpace(caseRef)) detail += $" Case: {caseRef.Trim()}";
AdminActionDiscord.LogAction("Account Unlock", StaffName(staff), detail);
await BestEffortAsync(() => Notify.SendNotify(accountId, new Notification { Id = NotificationType.RefreshLogin, Msg = profile }));
await BestEffortAsync(() => PlayerUtils.ReleaseModerationHoldAsync(accountId));
return new AccountLockResult(true, true);
}
private static bool IsAlreadyLocked(ModerationDB ban) =>
ban.IsBan
&& ban.Category == ReportCategory.Moderator
&& ban.Message == Reason
&& string.Equals(ban.BanMethod, BanMethod, StringComparison.OrdinalIgnoreCase)
&& ban.ExpireDuration >= DateTime.MaxValue.AddYears(-10);
private static string? NormalizeCase(string? caseRef)
{
var trimmed = (caseRef ?? string.Empty).Trim();
return trimmed.Length == 0 ? null : trimmed;
}
private static string StaffName(string? staff) =>
string.IsNullOrWhiteSpace(staff) ? "Unknown" : staff.Trim();
private static async Task EnforceOnlineAsync(RecDBContext db, long accountId, List<string> failures)
{
bool online;
try
{
var liveClient = Notify.GetClient(accountId) != null;
var presence = await db.Presences.AsNoTracking().FirstOrDefaultAsync(x => x.playerId == accountId);
online = liveClient || presence?.isOnline == true;
}
catch (Exception ex)
{
failures.Add($"online-state check failed ({ex.GetType().Name})");
return;
}
if (!online) return;
try { MatchmakingUtils.ClearInRoom(accountId); }
catch (Exception ex) { failures.Add($"clearing the in-room session failed ({ex.GetType().Name})"); }
var startedAt = DateTime.UtcNow;
try
{
var lockBan = await db.Moderation.AsNoTracking()
.FirstOrDefaultAsync(x => x.PlayerId == accountId && x.IsBan && x.BanMethod == BanMethod);
if (lockBan != null) startedAt = lockBan.CreatedAt;
}
catch { }
try { await PlayerUtils.EjectToDormAsync(db, accountId); }
catch (Exception ex) { failures.Add($"dorm transition failed ({ex.GetType().Name})"); }
try
{
var details = PlayerUtils.BuildBlockDetails(true, ReportCategory.Moderator, Reason, DateTime.MaxValue, startedAtUtc: startedAt);
await Notify.SendNotify(accountId, new Notification { Id = NotificationType.ModerationKick, Msg = details });
}
catch (Exception ex) { failures.Add($"moderation notification failed ({ex.GetType().Name})"); }
}
private static async Task BestEffortAsync(Func<Task> action)
{
try { await action(); }
catch { }
}
private static void Log(string message) =>
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Security] {message}");
}
}
+249
View File
@@ -0,0 +1,249 @@
using Microsoft.EntityFrameworkCore;
using RadicadeServer.API.Notifications;
using RadicadeServer.DB;
using RadicadeServer.RecNet.Auth;
using RadicadeServer.RecNet.Enums.Messages;
using RadicadeServer.RecNet.Enums.Moderation;
using RadicadeServer.RecNet.Moderation;
using RadicadeServer.RecNet.Notifications;
namespace RadicadeServer.Utils
{
public class BanStatusMonitor : BackgroundService
{
private static readonly TimeSpan Interval = TimeSpan.FromSeconds(15);
private readonly IDbContextFactory<RecDBContext> _dbFactory;
private static readonly System.Collections.Concurrent.ConcurrentDictionary<long, string> LastSeenBan = new();
private static readonly System.Collections.Concurrent.ConcurrentDictionary<long, long> LastSeenRoom = new();
private static readonly System.Collections.Concurrent.ConcurrentDictionary<long, string> LastSeenVoiceRestriction = new();
public BanStatusMonitor(IDbContextFactory<RecDBContext> dbFactory) {
_dbFactory = dbFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await CheckConnectedPlayersAsync(stoppingToken);
}
catch (Exception)
{
}
await Task.Delay(Interval, stoppingToken);
}
}
private async Task CheckConnectedPlayersAsync(CancellationToken ct)
{
var clients = Notify.SnapshotClients();
if (clients.Count == 0) return;
var accountIds = clients.Select(c => c.AccountId).Distinct().ToList();
await using var db = await _dbFactory.CreateDbContextAsync(ct);
var players = await db.Players.AsNoTracking()
.Where(x => accountIds.Contains(x.AccountId)).ToListAsync(ct);
var presences = await db.Presences.AsNoTracking()
.Where(x => accountIds.Contains(x.playerId)).ToListAsync(ct);
var moderationBans = await db.Moderation
.Where(x => x.IsBan && x.BanMethod != AccountLock.BanMethod && accountIds.Contains(x.PlayerId))
.ToListAsync(ct);
var hardwareBans = await db.HardwareBans.ToListAsync(ct);
var ipBans = await db.IpBans.ToListAsync(ct);
var voiceRestrictions = await db.VoiceChatRestrictions.AsNoTracking()
.Where(x => accountIds.Contains(x.PlayerId) && x.ClearedAt == null && x.ExpiresAt > DateTime.UtcNow)
.ToListAsync(ct);
var now = DateTime.UtcNow;
bool cleaned = false;
foreach (var m in moderationBans.Where(m => m.ExpireDuration < now).ToList())
{
db.Moderation.Remove(m);
moderationBans.Remove(m);
cleaned = true;
}
foreach (var h in hardwareBans.Where(h => h.ExpiresAt.HasValue && h.ExpiresAt.Value < now).ToList())
{
db.HardwareBans.Remove(h);
hardwareBans.Remove(h);
cleaned = true;
}
foreach (var i in ipBans.Where(i => i.ExpiresAt.HasValue && i.ExpiresAt.Value < now).ToList())
{
db.IpBans.Remove(i);
ipBans.Remove(i);
cleaned = true;
}
if (cleaned) await db.SaveChangesAsync(ct);
var connected = new HashSet<long>(accountIds);
foreach (var client in clients)
{
var id = client.AccountId;
var player = players.FirstOrDefault(p => p.AccountId == id);
if (player == null) continue;
var moderation = moderationBans.FirstOrDefault(m => m.PlayerId == id);
var hwBan = !string.IsNullOrWhiteSpace(player.DeviceId)
? hardwareBans.FirstOrDefault(h => h.DeviceId == player.DeviceId)
: null;
var ipBan = ResolveIpBan(ipBans, player);
string? signature = null;
string? reason = null;
DateTime? startedAt = null;
DateTime expiry = DateTime.MaxValue;
var banCategory = ReportCategory.Misc;
if (moderation != null)
{
signature = "mod:" + moderation.Id;
reason = moderation.Message ?? "";
startedAt = moderation.CreatedAt;
expiry = moderation.ExpireDuration;
banCategory = moderation.Category;
}
else if (hwBan != null)
{
signature = "hw:" + hwBan.Id;
reason = hwBan.Reason ?? "";
startedAt = hwBan.CreatedAt;
expiry = hwBan.ExpiresAt ?? DateTime.MaxValue;
}
else if (ipBan != null)
{
signature = "ip:" + ipBan.IpAddress;
reason = ipBan.Reason ?? "";
startedAt = ipBan.CreatedAt;
expiry = ipBan.ExpiresAt ?? DateTime.MaxValue;
}
var presence = presences.FirstOrDefault(p => p.playerId == id);
var last = LastSeenBan.GetValueOrDefault(id);
var roomKey = presence?.roomInstance ?? -1;
var lastRoom = LastSeenRoom.GetValueOrDefault(id, long.MinValue);
if (signature != null)
{
var banChanged = last != signature;
var roomChanged = lastRoom != roomKey;
LastSeenBan[id] = signature;
LastSeenRoom[id] = roomKey;
if (banChanged || roomChanged)
{
if (!await db.IsDeveloper(id))
await KickIfInRoomAsync(db, id, reason ?? "", expiry, startedAt, hwBan?.BannedAccountId, banChanged, banCategory);
}
}
else if (last != null)
{
LastSeenBan.TryRemove(id, out _);
LastSeenRoom.TryRemove(id, out _);
await PlayerUtils.ReleaseModerationHoldAsync(id, await db.IsDeveloper(id));
var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == id);
if (profile != null)
await Notify.SendNotify(id, new Notification { Id = NotificationType.RefreshLogin, Msg = profile });
}
var restriction = voiceRestrictions.FirstOrDefault(r => r.PlayerId == id);
var restrictionSignature = restriction == null
? null
: $"vr:{restriction.Id}:{restriction.ExpiresAt.Ticks}";
var known = LastSeenVoiceRestriction.TryGetValue(id, out var lastRestriction)
? lastRestriction
: null;
if (known != restrictionSignature)
{
if (restrictionSignature == null)
LastSeenVoiceRestriction.TryRemove(id, out _);
else
LastSeenVoiceRestriction[id] = restrictionSignature;
await VoiceChatRestrictions.PushAsync(id, restriction);
}
}
foreach (var key in LastSeenBan.Keys)
{
if (!connected.Contains(key))
LastSeenBan.TryRemove(key, out _);
LastSeenRoom.TryRemove(key, out _);
}
foreach (var key in LastSeenVoiceRestriction.Keys)
{
if (!connected.Contains(key))
LastSeenVoiceRestriction.TryRemove(key, out _);
}
}
private static IpBan? ResolveIpBan(List<IpBan> ipBans, PlayerData player)
{
if (string.IsNullOrWhiteSpace(player.LastIpAddress)) return null;
return ipBans.FirstOrDefault(b => b.IpAddress == player.LastIpAddress);
}
private async Task KickIfInRoomAsync(RecDBContext db, long accountId, string reason, DateTime expiry, DateTime? startedAt = null, long? relatedAccountId = null, bool banChanged = false, ReportCategory category = ReportCategory.Misc)
{
var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == accountId);
if (presence?.roomInstance != null)
{
var roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance);
var room = roomInstance != null ? await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId) : null;
bool isOwnDorm = room != null && room.IsDorm && room.CreatorAccountId == accountId;
if (isOwnDorm)
{
bool relatedBan = relatedAccountId.HasValue && relatedAccountId.Value != accountId;
if (!relatedBan || !banChanged) return;
}
else
{
await PlayerUtils.EjectToDormAsync(db, accountId);
}
}
await Notify.SendNotify(accountId, new Notification
{
Id = NotificationType.ModerationKick,
Msg = PlayerUtils.BuildBlockDetails(true, category, reason, expiry, startedAtUtc: startedAt,
relatedUsername: await PlayerUtils.RelatedAccountUsernameAsync(db, relatedAccountId, accountId))
});
}
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace RadicadeServer.RecNet.Moderation
{
public class DeviceId
{
public string oldDeviceId { get; set; } = null!;
public string newDeviceId { get; set; } = null!;
}
}
+25
View File
@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RadicadeServer.RecNet.Moderation
{
public class GameBlock
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public string IpAddress { get; set; } = "";
public string Reason { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ExpiresAt { get; set; }
}
}
+116
View File
@@ -0,0 +1,116 @@
using Microsoft.EntityFrameworkCore;
using RadicadeServer.DB;
namespace RadicadeServer.Utils
{
public static class GameBlockMonitor
{
private static readonly object Lock = new();
private static readonly SemaphoreSlim RefreshGate = new(1, 1);
private static Dictionary<string, DateTime?> Active = new();
private static DateTime LastRefresh = DateTime.MinValue;
private const double RefreshIntervalSeconds = 10;
public static async Task<bool> IsBlockedAsync(IDbContextFactory<RecDBContext> factory, string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress)) return false;
DateTime lastRefresh;
lock (Lock) lastRefresh = LastRefresh;
if ((DateTime.UtcNow - lastRefresh).TotalSeconds > RefreshIntervalSeconds)
await RefreshAsync(factory);
lock (Lock)
{
if (Active.TryGetValue(NormalizeIp(ipAddress), out var expiresAt))
return !expiresAt.HasValue || expiresAt.Value > DateTime.UtcNow;
return false;
}
}
public static void Invalidate()
{
lock (Lock) LastRefresh = DateTime.MinValue;
}
public static async Task RefreshAsync(IDbContextFactory<RecDBContext> factory)
{
await RefreshGate.WaitAsync();
try
{
DateTime lastRefresh;
lock (Lock) lastRefresh = LastRefresh;
if ((DateTime.UtcNow - lastRefresh).TotalSeconds <= RefreshIntervalSeconds)
return;
try
{
await using var db = await factory.CreateDbContextAsync();
var now = DateTime.UtcNow;
var rows = await db.GameBlocks.AsNoTracking().ToListAsync();
var expired = rows.Where(b => b.ExpiresAt.HasValue && b.ExpiresAt.Value < now).ToList();
if (expired.Count > 0)
{
db.GameBlocks.RemoveRange(expired);
await db.SaveChangesAsync();
}
var active = new Dictionary<string, DateTime?>();
foreach (var row in rows.Where(b => !expired.Contains(b)))
active[NormalizeIp(row.IpAddress)] = row.ExpiresAt;
lock (Lock)
{
Active = active;
LastRefresh = DateTime.UtcNow;
}
}
catch
{
lock (Lock) LastRefresh = DateTime.UtcNow;
}
}
finally
{
RefreshGate.Release();
}
}
public static string NormalizeIp(string ip)
{
ip = (ip ?? "").Trim();
if (ip.StartsWith("::ffff:", StringComparison.OrdinalIgnoreCase))
ip = ip.Substring("::ffff:".Length);
return ip;
}
}
}
+47
View File
@@ -0,0 +1,47 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RadicadeServer.RecNet.Moderation
{
public class HardwareBan
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public string DeviceId { get; set; } = "";
public string Reason { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ExpiresAt { get; set; }
public long? BannedAccountId { get; set; }
public int OffenseNumber { get; set; }
public string DetectionCategory { get; set; } = "";
public string Evidence { get; set; } = "";
public int DurationMinutes { get; set; }
}
}
+23
View File
@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RadicadeServer.RecNet.Moderation
{
public class IpBan
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public string IpAddress { get; set; } = "";
public string Reason { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ExpiresAt { get; set; }
}
}
+49
View File
@@ -0,0 +1,49 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using RadicadeServer.RecNet.Enums.Moderation;
namespace RadicadeServer.RecNet.Moderation
{
public class ModerationDB
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public DateTime ExpireDuration { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string Message { get; set; } = null!;
public string? ModeratorNote { get; set; }
public ReportCategory Category { get; set; }
public bool IsBan { get; set; }
public long PlayerId { get; set; }
public string BanMethod { get; set; } = "None";
public string? AppealCode { get; set; }
public string? AppealText { get; set; }
public string? AppealContact { get; set; }
public DateTime? AppealSubmittedAt { get; set; }
public string? AppealResolution { get; set; }
public DateTime? AppealResolvedAt { get; set; }
}
}
+58
View File
@@ -0,0 +1,58 @@
using System.Collections.Concurrent;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.DB;
namespace RadicadeServer.Utils
{
public static class TokenRevocation
{
private static readonly ConcurrentDictionary<long, int> Versions = new();
public static async Task<bool> IsValidAsync(IServiceProvider services, long accountId, int tokenVersion)
{
if (Versions.TryGetValue(accountId, out var cached))
return cached == tokenVersion;
var factory = services.GetRequiredService<IDbContextFactory<RecDBContext>>();
await using var db = await factory.CreateDbContextAsync();
var current = await db.Players
.AsNoTracking()
.Where(p => p.AccountId == accountId)
.Select(p => (int?)p.TokenVersion)
.FirstOrDefaultAsync();
var stored = current ?? 0;
Versions[accountId] = stored;
return stored == tokenVersion;
}
public static void Bump(long accountId, int newVersion)
{
Versions.TryRemove(accountId, out _);
}
}
}
+28
View File
@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using RadicadeServer.RecNet.Enums.Moderation;
namespace RadicadeServer.RecNet.Moderation
{
public class VoteKickLog
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public long PlayerId { get; set; }
public string Reason { get; set; } = "";
public ReportCategory Category { get; set; }
public bool IsForce { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime ExpiresAt { get; set; }
}
}
+30
View File
@@ -0,0 +1,30 @@
namespace RadicadeServer.RecNet.Moderation
{
public enum KickReason
{
Moderator = -1,
Unknown,
DEPRECATED_MicrophoneAbuse,
Harassment,
Cheating,
DEPRECATED_ImmatureBehavior,
AFK,
Misc,
Underage,
VoteKick = 10,
MisleadingPurchases,
CoC_Underage = 100,
CoC_Sexual,
CoC_Discrimination,
CoC_Trolling,
CoC_NameOrProfile,
InappropriateClothing = 200,
IssuingInaccurateReports = 1000
}
public class VoteKickReason
{
public string Reason { get; set; } = null!;
public KickReason ReportCategory { get; set; }
}
}