Automated whitelists (#23985)
* Beginnings of making the breadmemes jobs easier * stuff * stuff pt. 2 * Stuff pt.3 * Stuff I forgot last time * Basic whitelist Only people that are added to the whitelist with the addwhitelist command will be able to join. I call this the "legacy" whitelist * Remove always deny condition in favor of just breaking if playtime check fails * Change default whitelist Default whitelist is now the "legacy" whitelist. * localization * Admin check * minor spelling change * Fix build * Whitelist message * Fix vars not being datafield and spelling mistakes * Minor spelling mistake * Change config for salamander * Reviews and stuff * Add summaries * Fix whitelists * Forgot to add a datafield * Fixing stuff I guess * Reuse admin remarks to reduce load when connecting. * Update log messages to be verbose instead of debug * Reviews * whoops * Explain a bit more how whitelist checking works * Apply CE's review * Append Membership to Blacklist and Whitelist conditions * Fix review comments * Uncapitalize playerConnectionWhitelist, add to ignored client prototypes * Make note count field work * Fix cvar for thingy --------- Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
This commit is contained in:
@@ -108,6 +108,7 @@ namespace Content.Client.Entry
|
||||
_prototypeManager.RegisterIgnore("lobbyBackground");
|
||||
_prototypeManager.RegisterIgnore("gamePreset");
|
||||
_prototypeManager.RegisterIgnore("noiseChannel");
|
||||
_prototypeManager.RegisterIgnore("playerConnectionWhitelist");
|
||||
_prototypeManager.RegisterIgnore("spaceBiome");
|
||||
_prototypeManager.RegisterIgnore("worldgenConfig");
|
||||
_prototypeManager.RegisterIgnore("gameRule");
|
||||
|
||||
1769
Content.Server.Database/Migrations/Postgres/20240112194620_Blacklist.Designer.cs
generated
Normal file
1769
Content.Server.Database/Migrations/Postgres/20240112194620_Blacklist.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Blacklist : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "blacklist",
|
||||
columns: table => new
|
||||
{
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_blacklist", x => x.user_id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "blacklist");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -512,6 +512,20 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.ToTable("assigned_user_id", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Blacklist",
|
||||
b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("UserId")
|
||||
.HasName("PK_blacklist");
|
||||
|
||||
b.ToTable("blacklist", (string) null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
1701
Content.Server.Database/Migrations/Sqlite/20240112194612_Blacklist.Designer.cs
generated
Normal file
1701
Content.Server.Database/Migrations/Sqlite/20240112194612_Blacklist.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Blacklist : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "blacklist",
|
||||
columns: table => new
|
||||
{
|
||||
user_id = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_blacklist", x => x.user_id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "blacklist");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -483,6 +483,19 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.ToTable("assigned_user_id", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Blacklist",
|
||||
b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("UserId")
|
||||
.HasName("PK_blacklist");
|
||||
|
||||
b.ToTable("blacklist", (string) null);
|
||||
});
|
||||
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace Content.Server.Database
|
||||
public DbSet<AdminLog> AdminLog { get; set; } = null!;
|
||||
public DbSet<AdminLogPlayer> AdminLogPlayer { get; set; } = null!;
|
||||
public DbSet<Whitelist> Whitelist { get; set; } = null!;
|
||||
public DbSet<Blacklist> Blacklist { get; set; } = null!;
|
||||
public DbSet<ServerBan> Ban { get; set; } = default!;
|
||||
public DbSet<ServerUnban> Unban { get; set; } = default!;
|
||||
public DbSet<ServerBanExemption> BanExemption { get; set; } = default!;
|
||||
@@ -551,6 +552,15 @@ namespace Content.Server.Database
|
||||
[Required, Key] public Guid UserId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of users who are on the "blacklist". This is a list that may be used by Whitelist implementations to deny access to certain users.
|
||||
/// </summary>
|
||||
[Table("blacklist")]
|
||||
public class Blacklist
|
||||
{
|
||||
[Required, Key] public Guid UserId { get; set; }
|
||||
}
|
||||
|
||||
public class Admin
|
||||
{
|
||||
[Key] public Guid UserId { get; set; }
|
||||
|
||||
221
Content.Server/Connection/ConnectionManager.Whitelist.cs
Normal file
221
Content.Server/Connection/ConnectionManager.Whitelist.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Connection.Whitelist;
|
||||
using Content.Server.Connection.Whitelist.Conditions;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Connection;
|
||||
|
||||
/// <summary>
|
||||
/// Handles whitelist conditions for incoming connections.
|
||||
/// </summary>
|
||||
public sealed partial class ConnectionManager
|
||||
{
|
||||
private PlayerConnectionWhitelistPrototype[]? _whitelists;
|
||||
|
||||
public void PostInit()
|
||||
{
|
||||
_cfg.OnValueChanged(CCVars.WhitelistPrototypeList, UpdateWhitelists, true);
|
||||
}
|
||||
|
||||
private void UpdateWhitelists(string s)
|
||||
{
|
||||
var list = new List<PlayerConnectionWhitelistPrototype>();
|
||||
foreach (var id in s.Split(','))
|
||||
{
|
||||
if (_prototypeManager.TryIndex(id, out PlayerConnectionWhitelistPrototype? prototype))
|
||||
{
|
||||
list.Add(prototype);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sawmill.Fatal($"Whitelist prototype {id} does not exist. Denying all connections.");
|
||||
_whitelists = null; // Invalidate the list, causes deny on all connections.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_whitelists = list.ToArray();
|
||||
}
|
||||
|
||||
private bool IsValid(PlayerConnectionWhitelistPrototype whitelist, int playerCount)
|
||||
{
|
||||
return playerCount >= whitelist.MinimumPlayers && playerCount <= whitelist.MaximumPlayers;
|
||||
}
|
||||
|
||||
public async Task<(bool isWhitelisted, string? denyMessage)> IsWhitelisted(PlayerConnectionWhitelistPrototype whitelist, NetUserData data, ISawmill sawmill)
|
||||
{
|
||||
var cacheRemarks = await _db.GetAllAdminRemarks(data.UserId);
|
||||
var cachePlaytime = await _db.GetPlayTimes(data.UserId);
|
||||
|
||||
foreach (var condition in whitelist.Conditions)
|
||||
{
|
||||
bool matched;
|
||||
string denyMessage;
|
||||
switch (condition)
|
||||
{
|
||||
case ConditionAlwaysMatch:
|
||||
matched = true;
|
||||
denyMessage = Loc.GetString("whitelist-always-deny");
|
||||
break;
|
||||
case ConditionManualWhitelistMembership:
|
||||
matched = await CheckConditionManualWhitelist(data);
|
||||
denyMessage = Loc.GetString("whitelist-manual");
|
||||
break;
|
||||
case ConditionManualBlacklistMembership:
|
||||
matched = await CheckConditionManualBlacklist(data);
|
||||
denyMessage = Loc.GetString("whitelist-blacklisted");
|
||||
break;
|
||||
case ConditionNotesDateRange conditionNotes:
|
||||
matched = CheckConditionNotesDateRange(conditionNotes, cacheRemarks);
|
||||
denyMessage = Loc.GetString("whitelist-notes");
|
||||
break;
|
||||
case ConditionPlayerCount conditionPlayerCount:
|
||||
matched = CheckConditionPlayerCount(conditionPlayerCount);
|
||||
denyMessage = Loc.GetString("whitelist-player-count");
|
||||
break;
|
||||
case ConditionPlaytime conditionPlaytime:
|
||||
matched = CheckConditionPlaytime(conditionPlaytime, cachePlaytime);
|
||||
denyMessage = Loc.GetString("whitelist-playtime", ("minutes", conditionPlaytime.MinimumPlaytime));
|
||||
break;
|
||||
case ConditionNotesPlaytimeRange conditionNotesPlaytimeRange:
|
||||
matched = CheckConditionNotesPlaytimeRange(conditionNotesPlaytimeRange, cacheRemarks, cachePlaytime);
|
||||
denyMessage = Loc.GetString("whitelist-notes");
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException($"Whitelist condition {condition.GetType().Name} not implemented");
|
||||
}
|
||||
|
||||
sawmill.Verbose($"User {data.UserName} whitelist condition {condition.GetType().Name} result: {matched}");
|
||||
sawmill.Verbose($"Action: {condition.Action.ToString()}");
|
||||
switch (condition.Action)
|
||||
{
|
||||
case ConditionAction.Allow:
|
||||
if (matched)
|
||||
{
|
||||
sawmill.Verbose($"User {data.UserName} passed whitelist condition {condition.GetType().Name} and it's a breaking condition");
|
||||
return (true, denyMessage);
|
||||
}
|
||||
break;
|
||||
case ConditionAction.Deny:
|
||||
if (matched)
|
||||
{
|
||||
sawmill.Verbose($"User {data.UserName} failed whitelist condition {condition.GetType().Name}");
|
||||
return (false, denyMessage);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
sawmill.Verbose($"User {data.UserName} failed whitelist condition {condition.GetType().Name} but it's not a breaking condition");
|
||||
break;
|
||||
}
|
||||
}
|
||||
sawmill.Verbose($"User {data.UserName} passed all whitelist conditions");
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
#region Condition Checking
|
||||
|
||||
private async Task<bool> CheckConditionManualWhitelist(NetUserData data)
|
||||
{
|
||||
return !(await _db.GetWhitelistStatusAsync(data.UserId));
|
||||
}
|
||||
|
||||
private async Task<bool> CheckConditionManualBlacklist(NetUserData data)
|
||||
{
|
||||
return await _db.GetBlacklistStatusAsync(data.UserId);
|
||||
}
|
||||
|
||||
private bool CheckConditionNotesDateRange(ConditionNotesDateRange conditionNotes, List<IAdminRemarksRecord> remarks)
|
||||
{
|
||||
var range = DateTime.UtcNow.AddDays(-conditionNotes.Range);
|
||||
|
||||
return CheckRemarks(remarks,
|
||||
conditionNotes.IncludeExpired,
|
||||
conditionNotes.IncludeSecret,
|
||||
conditionNotes.MinimumSeverity,
|
||||
conditionNotes.MinimumNotes,
|
||||
adminRemarksRecord => adminRemarksRecord.CreatedAt > range);
|
||||
}
|
||||
|
||||
private bool CheckConditionPlayerCount(ConditionPlayerCount conditionPlayerCount)
|
||||
{
|
||||
var count = _plyMgr.PlayerCount;
|
||||
return count >= conditionPlayerCount.MinimumPlayers && count <= conditionPlayerCount.MaximumPlayers;
|
||||
}
|
||||
|
||||
private bool CheckConditionPlaytime(ConditionPlaytime conditionPlaytime, List<PlayTime> playtime)
|
||||
{
|
||||
var tracker = playtime.Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
|
||||
if (tracker is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return tracker.TimeSpent.TotalMinutes >= conditionPlaytime.MinimumPlaytime;
|
||||
}
|
||||
|
||||
private bool CheckConditionNotesPlaytimeRange(
|
||||
ConditionNotesPlaytimeRange conditionNotesPlaytimeRange,
|
||||
List<IAdminRemarksRecord> remarks,
|
||||
List<PlayTime> playtime)
|
||||
{
|
||||
var overallTracker = playtime.Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
|
||||
if (overallTracker is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return CheckRemarks(remarks,
|
||||
conditionNotesPlaytimeRange.IncludeExpired,
|
||||
conditionNotesPlaytimeRange.IncludeSecret,
|
||||
conditionNotesPlaytimeRange.MinimumSeverity,
|
||||
conditionNotesPlaytimeRange.MinimumNotes,
|
||||
adminRemarksRecord => adminRemarksRecord.PlaytimeAtNote >= overallTracker.TimeSpent - TimeSpan.FromMinutes(conditionNotesPlaytimeRange.Range));
|
||||
}
|
||||
|
||||
private bool CheckRemarks(List<IAdminRemarksRecord> remarks, bool includeExpired, bool includeSecret, NoteSeverity minimumSeverity, int MinimumNotes, Func<IAdminRemarksRecord, bool> additionalCheck)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
var notes = remarks.Count(r => r is AdminNoteRecord note && note.Severity >= minimumSeverity && (includeSecret || !note.Secret) && (includeExpired || note.ExpirationTime == null || note.ExpirationTime > utcNow));
|
||||
if (notes < MinimumNotes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var adminRemarksRecord in remarks)
|
||||
{
|
||||
// If we're not including expired notes, skip them
|
||||
if (!includeExpired && (adminRemarksRecord.ExpirationTime == null || adminRemarksRecord.ExpirationTime <= utcNow))
|
||||
continue;
|
||||
|
||||
// In order to get the severity of the remark, we need to see if it's an AdminNoteRecord.
|
||||
if (adminRemarksRecord is not AdminNoteRecord adminNoteRecord)
|
||||
continue;
|
||||
|
||||
// We want to filter out secret notes if we're not including them.
|
||||
if (!includeSecret && adminNoteRecord.Secret)
|
||||
continue;
|
||||
|
||||
// At this point, we need to remove the note if it's not within the severity range.
|
||||
if (adminNoteRecord.Severity < minimumSeverity)
|
||||
continue;
|
||||
|
||||
// Perform the additional check specific to each method
|
||||
if (!additionalCheck(adminRemarksRecord))
|
||||
continue;
|
||||
|
||||
// If we've made it this far, we have a match
|
||||
return true;
|
||||
}
|
||||
|
||||
// No matches
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Connection.Whitelist;
|
||||
using Content.Server.Connection.Whitelist.Conditions;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
@@ -14,6 +18,7 @@ using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -26,6 +31,7 @@ namespace Content.Server.Connection
|
||||
public interface IConnectionManager
|
||||
{
|
||||
void Initialize();
|
||||
void PostInit();
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily allow a user to bypass regular connection requirements.
|
||||
@@ -43,7 +49,7 @@ namespace Content.Server.Connection
|
||||
/// <summary>
|
||||
/// Handles various duties like guest username assignment, bans, connection logs, etc...
|
||||
/// </summary>
|
||||
public sealed class ConnectionManager : IConnectionManager
|
||||
public sealed partial class ConnectionManager : IConnectionManager
|
||||
{
|
||||
[Dependency] private readonly IServerDbManager _dbManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _plyMgr = default!;
|
||||
@@ -52,12 +58,14 @@ namespace Content.Server.Connection
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly ServerDbEntryManager _serverDbEntry = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
|
||||
private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = [];
|
||||
private ISawmill _sawmill = default!;
|
||||
private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = [];
|
||||
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
@@ -268,20 +276,33 @@ namespace Content.Server.Connection
|
||||
return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null);
|
||||
}
|
||||
|
||||
if (_cfg.GetCVar(CCVars.WhitelistEnabled))
|
||||
// Checks for whitelist IF it's enabled AND the user isn't an admin. Admins are always allowed.
|
||||
if (_cfg.GetCVar(CCVars.WhitelistEnabled) && adminData is null)
|
||||
{
|
||||
var min = _cfg.GetCVar(CCVars.WhitelistMinPlayers);
|
||||
var max = _cfg.GetCVar(CCVars.WhitelistMaxPlayers);
|
||||
var playerCountValid = _plyMgr.PlayerCount >= min && _plyMgr.PlayerCount < max;
|
||||
|
||||
if (playerCountValid && await _db.GetWhitelistStatusAsync(userId) == false
|
||||
&& adminData is null)
|
||||
if (_whitelists is null)
|
||||
{
|
||||
var msg = Loc.GetString(_cfg.GetCVar(CCVars.WhitelistReason));
|
||||
// was the whitelist playercount changed?
|
||||
if (min > 0 || max < int.MaxValue)
|
||||
msg += "\n" + Loc.GetString("whitelist-playercount-invalid", ("min", min), ("max", max));
|
||||
return (ConnectionDenyReason.Whitelist, msg, null);
|
||||
_sawmill.Error("Whitelist enabled but no whitelists loaded.");
|
||||
// Misconfigured, deny everyone.
|
||||
return (ConnectionDenyReason.Whitelist, Loc.GetString("whitelist-misconfigured"), null);
|
||||
}
|
||||
|
||||
foreach (var whitelist in _whitelists)
|
||||
{
|
||||
if (!IsValid(whitelist, _plyMgr.PlayerCount))
|
||||
{
|
||||
// Not valid for current player count.
|
||||
continue;
|
||||
}
|
||||
|
||||
var whitelistStatus = await IsWhitelisted(whitelist, e.UserData, _sawmill);
|
||||
if (!whitelistStatus.isWhitelisted)
|
||||
{
|
||||
// Not whitelisted.
|
||||
return (ConnectionDenyReason.Whitelist, Loc.GetString("whitelist-fail-prefix", ("msg", whitelistStatus.denyMessage!)), null);
|
||||
}
|
||||
|
||||
// Whitelisted, don't check any more.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
117
Content.Server/Connection/Whitelist/BlacklistCommands.cs
Normal file
117
Content.Server/Connection/Whitelist/BlacklistCommands.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Connection.Whitelist;
|
||||
|
||||
[AdminCommand(AdminFlags.Ban)]
|
||||
public sealed class AddBlacklistCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
|
||||
[Dependency] private readonly IServerDbManager _db = default!;
|
||||
|
||||
public override string Command => "blacklistadd";
|
||||
|
||||
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-need-minimum-one-argument"));
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length > 1)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-need-exactly-one-argument"));
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
var data = await _playerLocator.LookupIdByNameAsync(name);
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-blacklistadd-not-found", ("username", args[0])));
|
||||
return;
|
||||
}
|
||||
var guid = data.UserId;
|
||||
var isBlacklisted = await _db.GetBlacklistStatusAsync(guid);
|
||||
if (isBlacklisted)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-blacklistadd-existing", ("username", data.Username)));
|
||||
return;
|
||||
}
|
||||
|
||||
await _db.AddToBlacklistAsync(guid);
|
||||
shell.WriteLine(Loc.GetString("cmd-blacklistadd-added", ("username", data.Username)));
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHint(Loc.GetString("cmd-blacklistadd-arg-player"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Ban)]
|
||||
public sealed class RemoveBlacklistCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
|
||||
[Dependency] private readonly IServerDbManager _db = default!;
|
||||
|
||||
public override string Command => "blacklistremove";
|
||||
|
||||
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-need-minimum-one-argument"));
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length > 1)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-need-exactly-one-argument"));
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
var data = await _playerLocator.LookupIdByNameAsync(name);
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-blacklistremove-not-found", ("username", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
var guid = data.UserId;
|
||||
var isBlacklisted = await _db.GetBlacklistStatusAsync(guid);
|
||||
if (!isBlacklisted)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-blacklistremove-existing", ("username", data.Username)));
|
||||
return;
|
||||
}
|
||||
|
||||
await _db.RemoveFromBlacklistAsync(guid);
|
||||
shell.WriteLine(Loc.GetString("cmd-blacklistremove-removed", ("username", data.Username)));
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHint(Loc.GetString("cmd-blacklistremove-arg-player"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Connection.Whitelist.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that always matches
|
||||
/// </summary>
|
||||
public sealed partial class ConditionAlwaysMatch : WhitelistCondition
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Database;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Connection.Whitelist.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that matches if the player is in the manual blacklist.
|
||||
/// </summary>
|
||||
public sealed partial class ConditionManualBlacklistMembership : WhitelistCondition
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Database;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Connection.Whitelist.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that matches if the player is in the manual whitelist.
|
||||
/// </summary>
|
||||
public sealed partial class ConditionManualWhitelistMembership : WhitelistCondition
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.Database;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Connection.Whitelist.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that matches if the player has notes within a certain date range.
|
||||
/// </summary>
|
||||
public sealed partial class ConditionNotesDateRange : WhitelistCondition
|
||||
{
|
||||
[DataField]
|
||||
public bool IncludeExpired = false;
|
||||
|
||||
[DataField]
|
||||
public NoteSeverity MinimumSeverity = NoteSeverity.Minor;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum number of notes required.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MinimumNotes = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Range in days to check for notes.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Range = int.MaxValue;
|
||||
|
||||
[DataField]
|
||||
public bool IncludeSecret = false;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Content.Shared.Database;
|
||||
|
||||
namespace Content.Server.Connection.Whitelist.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that matches if the player has notes within a certain playtime range.
|
||||
/// </summary>
|
||||
public sealed partial class ConditionNotesPlaytimeRange : WhitelistCondition
|
||||
{
|
||||
[DataField]
|
||||
public bool IncludeExpired = false;
|
||||
|
||||
[DataField]
|
||||
public NoteSeverity MinimumSeverity = NoteSeverity.Minor;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum number of notes required.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MinimumNotes = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The range in minutes to check for notes.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Range = int.MaxValue;
|
||||
|
||||
[DataField]
|
||||
public bool IncludeSecret = false;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Connection.Whitelist.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that matches if the player count is within a certain range.
|
||||
/// </summary>
|
||||
public sealed partial class ConditionPlayerCount : WhitelistCondition
|
||||
{
|
||||
[DataField]
|
||||
public int MinimumPlayers = 0;
|
||||
[DataField]
|
||||
public int MaximumPlayers = int.MaxValue;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Connection.Whitelist.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that matches if the player has played for a certain amount of time.
|
||||
/// </summary>
|
||||
public sealed partial class ConditionPlaytime : WhitelistCondition
|
||||
{
|
||||
[DataField]
|
||||
public int MinimumPlaytime = 0; // In minutes
|
||||
}
|
||||
41
Content.Server/Connection/Whitelist/WhitelistCondition.cs
Normal file
41
Content.Server/Connection/Whitelist/WhitelistCondition.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Connection.Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// This class is used to determine if a player should be allowed to join the server.
|
||||
/// It is used in <see cref="PlayerConnectionWhitelistPrototype"/>
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
[MeansImplicitUse]
|
||||
public abstract partial class WhitelistCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// What action should be taken if this condition is met?
|
||||
/// Defaults to <see cref="ConditionAction.Next"/>.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ConditionAction Action { get; set; } = ConditionAction.Next;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines what action should be taken if a condition is met.
|
||||
/// </summary>
|
||||
public enum ConditionAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The player is allowed to join, and the next conditions will be skipped.
|
||||
/// </summary>
|
||||
Allow,
|
||||
/// <summary>
|
||||
/// The player is denied to join, and the next conditions will be skipped.
|
||||
/// </summary>
|
||||
Deny,
|
||||
/// <summary>
|
||||
/// The next condition should be checked.
|
||||
/// </summary>
|
||||
Next
|
||||
}
|
||||
42
Content.Server/Connection/Whitelist/WhitelistPrototype.cs
Normal file
42
Content.Server/Connection/Whitelist/WhitelistPrototype.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Connection.Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// Used by the <see cref="ConnectionManager"/> to determine if a player should be allowed to join the server.
|
||||
/// Used in the whitelist.prototype_list CVar.
|
||||
///
|
||||
/// Whitelists are used to determine if a player is allowed to connect.
|
||||
/// You define a PlayerConnectionWhitelist with a list of conditions.
|
||||
/// Every condition has a type and a <see cref="ConditionAction"/> along with other parameters depending on the type.
|
||||
/// Action must either be Allow, Deny or Next.
|
||||
/// Allow means the player is instantly allowed to connect if the condition is met.
|
||||
/// Deny means the player is instantly denied to connect if the condition is met.
|
||||
/// Next means the next condition in the list is checked.
|
||||
/// If the condition doesn't match, the next condition is checked.
|
||||
/// </summary>
|
||||
[Prototype("playerConnectionWhitelist")]
|
||||
public sealed class PlayerConnectionWhitelistPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum number of players required for this whitelist to be active.
|
||||
/// If there are less players than this, the whitelist will be ignored and the next one in the list will be used.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MinimumPlayers { get; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of players allowed for this whitelist to be active.
|
||||
/// If there are more players than this, the whitelist will be ignored and the next one in the list will be used.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MaximumPlayers { get; } = int.MaxValue;
|
||||
|
||||
[DataField]
|
||||
public WhitelistCondition[] Conditions { get; } = default!;
|
||||
}
|
||||
@@ -1066,6 +1066,29 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> GetBlacklistStatusAsync(NetUserId player)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
return await db.DbContext.Blacklist.AnyAsync(w => w.UserId == player);
|
||||
}
|
||||
|
||||
public async Task AddToBlacklistAsync(NetUserId player)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
db.DbContext.Blacklist.Add(new Blacklist() { UserId = player });
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task RemoveFromBlacklistAsync(NetUserId player)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
var entry = await db.DbContext.Blacklist.SingleAsync(w => w.UserId == player);
|
||||
db.DbContext.Blacklist.Remove(entry);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Uploaded Resources Logs
|
||||
|
||||
@@ -244,6 +244,16 @@ namespace Content.Server.Database
|
||||
|
||||
#endregion
|
||||
|
||||
#region Blacklist
|
||||
|
||||
Task<bool> GetBlacklistStatusAsync(NetUserId player);
|
||||
|
||||
Task AddToBlacklistAsync(NetUserId player);
|
||||
|
||||
Task RemoveFromBlacklistAsync(NetUserId player);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Uploaded Resources Logs
|
||||
|
||||
Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data);
|
||||
@@ -740,6 +750,24 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.RemoveFromWhitelistAsync(player));
|
||||
}
|
||||
|
||||
public Task<bool> GetBlacklistStatusAsync(NetUserId player)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.GetBlacklistStatusAsync(player));
|
||||
}
|
||||
|
||||
public Task AddToBlacklistAsync(NetUserId player)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.AddToBlacklistAsync(player));
|
||||
}
|
||||
|
||||
public Task RemoveFromBlacklistAsync(NetUserId player)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.RemoveFromBlacklistAsync(player));
|
||||
}
|
||||
|
||||
public Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
|
||||
@@ -145,6 +145,7 @@ namespace Content.Server.Entry
|
||||
IoCManager.Resolve<IGameMapManager>().Initialize();
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
|
||||
IoCManager.Resolve<IBanManager>().Initialize();
|
||||
IoCManager.Resolve<IConnectionManager>().PostInit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1326,24 +1326,12 @@ namespace Content.Shared.CCVar
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> WhitelistEnabled =
|
||||
CVarDef.Create("whitelist.enabled", false, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// The loc string to display as a disconnect reason when someone is not whitelisted.
|
||||
/// Specifies the whitelist prototypes to be used by the server. This should be a comma-separated list of prototypes.
|
||||
/// If a whitelists conditions to be active fail (for example player count), the next whitelist will be used instead. If no whitelist is valid, the player will be allowed to connect.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> WhitelistReason =
|
||||
CVarDef.Create("whitelist.reason", "whitelist-not-whitelisted", CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// If the playercount is below this number, the whitelist will not apply.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> WhitelistMinPlayers =
|
||||
CVarDef.Create("whitelist.min_players", 0, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// If the playercount is above this number, the whitelist will not apply.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> WhitelistMaxPlayers =
|
||||
CVarDef.Create("whitelist.max_players", int.MaxValue, CVar.SERVERONLY);
|
||||
public static readonly CVarDef<string> WhitelistPrototypeList =
|
||||
CVarDef.Create("whitelist.prototype_list", "basicWhitelist", CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* VOTE
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Configuration preset used on Wizard's Den Salamander
|
||||
|
||||
[game]
|
||||
desc = "Official English Space Station 14 servers. Medium roleplay ruleset. You must be whitelisted through Discord to play if there are more than 15 online players."
|
||||
desc = "Official English Space Station 14 servers. Medium roleplay ruleset. you must be whitelisted by playing on other Wizard's Den servers if there are more than 15 online players."
|
||||
hostname = "[EN] Wizard's Den Salamander [US West RP]"
|
||||
soft_max_players = 130
|
||||
|
||||
@@ -10,8 +10,7 @@ rules_file = "MRPRuleset"
|
||||
|
||||
[whitelist]
|
||||
enabled = true
|
||||
reason = "whitelist-not-whitelisted-rp"
|
||||
min_players = 15
|
||||
prototype_list = "salamanderMrpWhitelist"
|
||||
|
||||
[shuttle]
|
||||
emergency_early_launch_allowed = true
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
whitelist-not-whitelisted = You are not whitelisted.
|
||||
|
||||
# proper handling for having a min/max or not
|
||||
whitelist-playercount-invalid = {$min ->
|
||||
[0] The whitelist for this server only applies below {$max} players.
|
||||
*[other] The whitelist for this server only applies above {$min} {$max ->
|
||||
[2147483647] -> players, so you may be able to join later.
|
||||
*[other] -> players and below {$max} players, so you may be able to join later.
|
||||
}
|
||||
}
|
||||
whitelist-not-whitelisted-rp = You are not whitelisted. To become whitelisted, visit our Discord (which can be found at https://spacestation14.io) and check the #rp-whitelist channel.
|
||||
|
||||
cmd-whitelistadd-desc = Adds the player with the given username to the server whitelist.
|
||||
cmd-whitelistadd-desc = Adds the player with the given username to the server whitelist.
|
||||
cmd-whitelistadd-help = Usage: whitelistadd <username or User ID>
|
||||
cmd-whitelistadd-existing = {$username} is already on the whitelist!
|
||||
cmd-whitelistadd-added = {$username} added to the whitelist
|
||||
@@ -40,8 +28,30 @@ panic-bunker-account-denied-reason = This server is in panic bunker mode, often
|
||||
panic-bunker-account-reason-account = Your Space Station 14 account is too new. It must be older than {$minutes} minutes
|
||||
panic-bunker-account-reason-overall = Your overall playtime on the server must be greater than {$minutes} $minutes
|
||||
|
||||
whitelist-playtime = You do not have enough playtime to join this server. You need at least {$minutes} minutes of playtime to join this server.
|
||||
whitelist-player-count = This server is currently not accepting players. Please try again later.
|
||||
whitelist-notes = You currently have too many admin notes to join this server. You can check your notes by typing /adminremarks in chat.
|
||||
whitelist-manual = You are not whitelisted on this server.
|
||||
whitelist-blacklisted = You are blacklisted from this server.
|
||||
whitelist-always-deny = You are not allowed to join this server.
|
||||
whitelist-fail-prefix = Not whitelisted: {$msg}
|
||||
whitelist-misconfigured = The server is misconfigured and is not accepting players. Please contact the server owner and try again later.
|
||||
|
||||
cmd-blacklistadd-desc = Adds the player with the given username to the server blacklist.
|
||||
cmd-blacklistadd-help = Usage: blacklistadd <username>
|
||||
cmd-blacklistadd-existing = {$username} is already on the blacklist!
|
||||
cmd-blacklistadd-added = {$username} added to the blacklist
|
||||
cmd-blacklistadd-not-found = Unable to find '{$username}'
|
||||
cmd-blacklistadd-arg-player = [player]
|
||||
|
||||
cmd-blacklistremove-desc = Removes the player with the given username from the server blacklist.
|
||||
cmd-blacklistremove-help = Usage: blacklistremove <username>
|
||||
cmd-blacklistremove-existing = {$username} is not on the blacklist!
|
||||
cmd-blacklistremove-removed = {$username} removed from the blacklist
|
||||
cmd-blacklistremove-not-found = Unable to find '{$username}'
|
||||
cmd-blacklistremove-arg-player = [player]
|
||||
|
||||
baby-jail-account-denied = This server is a newbie server, intended for new players and those who want to help them. New connections by accounts that are too old or are not on a whitelist are not accepted. Check out some other servers and see everything Space Station 14 has to offer. Have fun!
|
||||
baby-jail-account-denied-reason = This server is a newbie server, intended for new players and those who want to help them. New connections by accounts that are too old or are not on a whitelist are not accepted. Check out some other servers and see everything Space Station 14 has to offer. Have fun! Reason: "{$reason}"
|
||||
baby-jail-account-reason-account = Your Space Station 14 account is too old. It must be younger than {$minutes} minutes
|
||||
baby-jail-account-reason-overall = Your overall playtime on the server must be younger than {$minutes} $minutes
|
||||
|
||||
|
||||
7
Resources/Prototypes/whitelists.yml
Normal file
7
Resources/Prototypes/whitelists.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
- type: playerConnectionWhitelist
|
||||
id: basicWhitelist # Basic whitelist using only the ManualWhitelist condition
|
||||
conditions:
|
||||
- !type:ConditionManualWhitelistMembership
|
||||
action: Allow # Allow connection if matched
|
||||
- !type:ConditionAlwaysMatch # Condition that always matches
|
||||
action: Deny # Deny connection if matched
|
||||
39
Resources/Prototypes/wizardsDenWhitelists.yml
Normal file
39
Resources/Prototypes/wizardsDenWhitelists.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
# This is the whitelist used for Wizard's Den Salamander
|
||||
|
||||
- type: playerConnectionWhitelist
|
||||
id: salamanderMrpWhitelist
|
||||
conditions:
|
||||
- !type:ConditionManualBlacklistMembership # Deny blacklisted (MRP ban)
|
||||
action: Deny
|
||||
- !type:ConditionNotesPlaytimeRange # Deny for high severity notes in the last 30 days
|
||||
includeExpired: false
|
||||
minimumSeverity: 3 # High
|
||||
minimumNotes: 1
|
||||
range: 30 # 30 days
|
||||
action: Deny
|
||||
includeSecret: false
|
||||
- !type:ConditionNotesPlaytimeRange # Deny for >=2 medium severity notes in the last 14 days
|
||||
includeExpired: false
|
||||
minimumSeverity: 2 # Medium
|
||||
minimumNotes: 1
|
||||
range: 14 # 14 Days
|
||||
action: Deny
|
||||
includeSecret: false
|
||||
- !type:ConditionNotesPlaytimeRange # Deny for >=3 low severity notes in the last 14 days
|
||||
includeExpired: false
|
||||
minimumSeverity: 1 # Low
|
||||
minimumNotes: 3
|
||||
range: 14 # 14 Days
|
||||
action: Deny
|
||||
includeSecret: false
|
||||
- !type:ConditionManualWhitelistMembership # Allow whitelisted players
|
||||
action: Allow
|
||||
- !type:ConditionPlayerCount # Allow when <= 15 players are online
|
||||
minimumPlayers: 0
|
||||
maximumPlayers: 15
|
||||
action: Allow
|
||||
#- !type:ConditionPlaytime
|
||||
# minimumPlaytime: 1200 # 20 hours to be whitelisted
|
||||
# action: Deny
|
||||
- !type:ConditionAlwaysMatch
|
||||
action: Deny
|
||||
Reference in New Issue
Block a user