diff --git a/Content.Client/Administration/UI/BanList/BanListControl.xaml b/Content.Client/Administration/UI/BanList/BanListControl.xaml
new file mode 100644
index 0000000000..b202e92e57
--- /dev/null
+++ b/Content.Client/Administration/UI/BanList/BanListControl.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/BanList/BanListControl.xaml.cs b/Content.Client/Administration/UI/BanList/BanListControl.xaml.cs
new file mode 100644
index 0000000000..d9a892ffad
--- /dev/null
+++ b/Content.Client/Administration/UI/BanList/BanListControl.xaml.cs
@@ -0,0 +1,72 @@
+using System.Linq;
+using Content.Client.Administration.UI.CustomControls;
+using Content.Shared.Administration.BanList;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Administration.UI.BanList;
+
+[GenerateTypedNameReferences]
+public sealed partial class BanListControl : Control
+{
+ private BanListIdsPopup? _popup;
+
+ public BanListControl()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void SetBans(List bans)
+ {
+ foreach (var control in Bans.Children.ToArray()[1..])
+ {
+ control.Orphan();
+ }
+
+ foreach (var ban in bans)
+ {
+ Bans.AddChild(new HSeparator());
+
+ var line = new BanListLine(ban);
+ line.OnIdsClicked += LineIdsClicked;
+
+ Bans.AddChild(line);
+ }
+ }
+
+ private void ClosePopup()
+ {
+ _popup?.Close();
+ _popup = null;
+ }
+
+ private bool LineIdsClicked(BanListLine line)
+ {
+ ClosePopup();
+
+ var ban = line.Ban;
+ var ip = ban.Address == null
+ ? string.Empty
+ : Loc.GetString("ban-list-ip", ("ip", ban.Address.Value.address));
+ var hwid = ban.HWId == null ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", ban.HWId));
+ var guid = ban.UserId == null ? string.Empty : Loc.GetString("ban-list-guid", ("guid", ban.UserId.Value.ToString()));
+
+ _popup = new BanListIdsPopup(ip, hwid, guid);
+
+ var box = UIBox2.FromDimensions(UserInterfaceManager.MousePositionScaled.Position, (1, 1));
+ _popup.Open(box);
+
+ return true;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (_popup != null)
+ {
+ UserInterfaceManager.PopupRoot.RemoveChild(_popup);
+ }
+ }
+}
diff --git a/Content.Client/Administration/UI/BanList/BanListEui.cs b/Content.Client/Administration/UI/BanList/BanListEui.cs
new file mode 100644
index 0000000000..86f2541e51
--- /dev/null
+++ b/Content.Client/Administration/UI/BanList/BanListEui.cs
@@ -0,0 +1,34 @@
+using Content.Client.Eui;
+using Content.Shared.Administration.BanList;
+using Content.Shared.Eui;
+
+namespace Content.Client.Administration.UI.BanList;
+
+public sealed class BanListEui : BaseEui
+{
+ public BanListEui()
+ {
+ BanWindow = new BanListWindow();
+ BanControl = BanWindow.BanList;
+ }
+
+ private BanListWindow BanWindow { get; }
+
+ private BanListControl BanControl { get; }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not BanListEuiState s)
+ return;
+
+ BanWindow.SetTitlePlayer(s.BanListPlayerName);
+
+ s.Bans.Sort((a, b) => a.BanTime.CompareTo(b.BanTime));
+ BanControl.SetBans(s.Bans);
+ }
+
+ public override void Opened()
+ {
+ BanWindow.OpenCentered();
+ }
+}
diff --git a/Content.Client/Administration/UI/BanList/BanListHeader.xaml b/Content.Client/Administration/UI/BanList/BanListHeader.xaml
new file mode 100644
index 0000000000..af02027758
--- /dev/null
+++ b/Content.Client/Administration/UI/BanList/BanListHeader.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/BanList/BanListHeader.xaml.cs b/Content.Client/Administration/UI/BanList/BanListHeader.xaml.cs
new file mode 100644
index 0000000000..6c49ff3e5b
--- /dev/null
+++ b/Content.Client/Administration/UI/BanList/BanListHeader.xaml.cs
@@ -0,0 +1,14 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Administration.UI.BanList;
+
+[GenerateTypedNameReferences]
+public sealed partial class BanListHeader : ContainerButton
+{
+ public BanListHeader()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
diff --git a/Content.Client/Administration/UI/BanList/BanListIdsPopup.xaml b/Content.Client/Administration/UI/BanList/BanListIdsPopup.xaml
new file mode 100644
index 0000000000..dbd58b8c27
--- /dev/null
+++ b/Content.Client/Administration/UI/BanList/BanListIdsPopup.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/BanList/BanListIdsPopup.xaml.cs b/Content.Client/Administration/UI/BanList/BanListIdsPopup.xaml.cs
new file mode 100644
index 0000000000..6fb2257e97
--- /dev/null
+++ b/Content.Client/Administration/UI/BanList/BanListIdsPopup.xaml.cs
@@ -0,0 +1,20 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Administration.UI.BanList;
+
+[GenerateTypedNameReferences]
+public sealed partial class BanListIdsPopup : Popup
+{
+ public BanListIdsPopup(string? ip, string? hwid, string? guid)
+ {
+ RobustXamlLoader.Load(this);
+
+ IP.Text = ip;
+ HWId.Text = hwid;
+ GUID.Text = guid;
+
+ UserInterfaceManager.ModalRoot.AddChild(this);
+ }
+}
diff --git a/Content.Client/Administration/UI/BanList/BanListLine.xaml b/Content.Client/Administration/UI/BanList/BanListLine.xaml
new file mode 100644
index 0000000000..8d324f12b6
--- /dev/null
+++ b/Content.Client/Administration/UI/BanList/BanListLine.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/BanList/BanListLine.xaml.cs b/Content.Client/Administration/UI/BanList/BanListLine.xaml.cs
new file mode 100644
index 0000000000..90503fda22
--- /dev/null
+++ b/Content.Client/Administration/UI/BanList/BanListLine.xaml.cs
@@ -0,0 +1,70 @@
+using Content.Shared.Administration.BanList;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Input;
+
+namespace Content.Client.Administration.UI.BanList;
+
+[GenerateTypedNameReferences]
+public sealed partial class BanListLine : BoxContainer
+{
+ public readonly SharedServerBan Ban;
+
+ public event Func? OnIdsClicked;
+
+ public BanListLine(SharedServerBan ban)
+ {
+ RobustXamlLoader.Load(this);
+
+ Ban = ban;
+
+ IdsHidden.OnKeyBindDown += IdsKeyBindDown;
+
+ Reason.Text = ban.Reason;
+ BanTime.Text = FormatDate(ban.BanTime);
+ Expires.Text = ban.ExpirationTime == null
+ ? Loc.GetString("ban-list-permanent")
+ : FormatDate(ban.ExpirationTime.Value);
+
+ if (ban.Unban is { } unban)
+ {
+ var unbanned = Loc.GetString("ban-list-unbanned", ("date", FormatDate(unban.UnbanTime)));
+ var unbannedBy = unban.UnbanningAdmin == null
+ ? string.Empty
+ : $"\n{Loc.GetString("ban-list-unbanned-by", ("unbanner", unban.UnbanningAdmin))}";
+
+ Expires.Text += $"\n{unbanned}{unbannedBy}";
+ }
+
+ BanningAdmin.Text = ban.BanningAdminName;
+ }
+
+ private static string FormatDate(DateTimeOffset date)
+ {
+ return date.ToString("MM/dd/yyyy h:mm tt");
+ }
+
+ private void IdsKeyBindDown(GUIBoundKeyEventArgs args)
+ {
+ if (args.Function != EngineKeyFunctions.UIRightClick &&
+ args.Function != EngineKeyFunctions.UIClick)
+ {
+ return;
+ }
+
+ if (OnIdsClicked?.Invoke(this) == true)
+ {
+ args.Handle();
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ IdsHidden.OnKeyBindDown -= IdsKeyBindDown;
+ OnIdsClicked = null;
+ }
+}
diff --git a/Content.Client/Administration/UI/BanList/BanListWindow.xaml b/Content.Client/Administration/UI/BanList/BanListWindow.xaml
new file mode 100644
index 0000000000..d246a3e3a2
--- /dev/null
+++ b/Content.Client/Administration/UI/BanList/BanListWindow.xaml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/Content.Client/Administration/UI/BanList/BanListWindow.xaml.cs b/Content.Client/Administration/UI/BanList/BanListWindow.xaml.cs
new file mode 100644
index 0000000000..2bb08a8825
--- /dev/null
+++ b/Content.Client/Administration/UI/BanList/BanListWindow.xaml.cs
@@ -0,0 +1,19 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Administration.UI.BanList;
+
+[GenerateTypedNameReferences]
+public sealed partial class BanListWindow : DefaultWindow
+{
+ public BanListWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void SetTitlePlayer(string playerName)
+ {
+ Title = Loc.GetString("ban-list-title", ("player", playerName));
+ }
+}
diff --git a/Content.Client/Administration/UI/BwoinkWindow.xaml b/Content.Client/Administration/UI/BwoinkWindow.xaml
index ead47e6905..5b40e57513 100644
--- a/Content.Client/Administration/UI/BwoinkWindow.xaml
+++ b/Content.Client/Administration/UI/BwoinkWindow.xaml
@@ -10,6 +10,7 @@
+
diff --git a/Content.Client/Administration/UI/BwoinkWindow.xaml.cs b/Content.Client/Administration/UI/BwoinkWindow.xaml.cs
index 9c2c1a474e..7b80b6d536 100644
--- a/Content.Client/Administration/UI/BwoinkWindow.xaml.cs
+++ b/Content.Client/Administration/UI/BwoinkWindow.xaml.cs
@@ -1,8 +1,6 @@
-#nullable enable
using System.Text;
using System.Threading;
using Content.Client.Administration.Managers;
-using Content.Client.Administration.Systems;
using Content.Client.Administration.UI.CustomControls;
using Content.Client.Administration.UI.Tabs.AdminTab;
using Content.Client.Stylesheets;
@@ -62,7 +60,7 @@ namespace Content.Client.Administration.UI
sb.Append('●');
else
sb.Append(info.ActiveThisRound ? '○' : '·');
-
+
sb.Append(' ');
if (_adminAHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
{
@@ -106,6 +104,12 @@ namespace Content.Client.Administration.UI
return bch!.LastMessage.CompareTo(ach!.LastMessage);
};
+ Bans.OnPressed += _ =>
+ {
+ if (_currentPlayer is not null)
+ _console.ExecuteCommand($"banlist \"{_currentPlayer.SessionId}\"");
+ };
+
Notes.OnPressed += _ =>
{
if (_currentPlayer is not null)
@@ -170,6 +174,9 @@ namespace Content.Client.Administration.UI
private void FixButtons()
{
+ Bans.Visible = _adminManager.HasFlag(AdminFlags.Ban);
+ Bans.Disabled = !Bans.Visible;
+
Notes.Visible = _adminManager.HasFlag(AdminFlags.ViewNotes);
Notes.Disabled = !Notes.Visible;
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml.cs
index 7f8fcca88a..dacc042e62 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml.cs
+++ b/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml.cs
@@ -10,8 +10,6 @@ namespace Content.Client.Administration.UI.Notes;
[GenerateTypedNameReferences]
public sealed partial class AdminNotesControl : Control
{
- [Dependency] private readonly IUserInterfaceManager _ui = default!;
-
public event Action? OnNoteChanged;
public event Action? OnNewNoteEntered;
public event Action? OnNoteDeleted;
@@ -53,7 +51,7 @@ public sealed partial class AdminNotesControl : Control
OnNoteChanged?.Invoke(input.Id, text);
}
- private bool NoteRightClicked(AdminNotesLine line)
+ private bool NoteClicked(AdminNotesLine line)
{
ClosePopup();
@@ -69,7 +67,7 @@ public sealed partial class AdminNotesControl : Control
};
_popup.OnDeletePressed += noteId => OnNoteDeleted?.Invoke(noteId);
- var box = UIBox2.FromDimensions(_ui.MousePositionScaled.Position, (1, 1));
+ var box = UIBox2.FromDimensions(UserInterfaceManager.MousePositionScaled.Position, (1, 1));
_popup.Open(box);
return true;
@@ -102,7 +100,7 @@ public sealed partial class AdminNotesControl : Control
input = new AdminNotesLine(note);
input.OnSubmitted += NoteSubmitted;
- input.OnRightClicked += NoteRightClicked;
+ input.OnClicked += NoteClicked;
Notes.AddChild(input);
Inputs[note.Id] = input;
}
@@ -136,7 +134,7 @@ public sealed partial class AdminNotesControl : Control
if (_popup != null)
{
- _ui.PopupRoot.RemoveChild(_popup);
+ UserInterfaceManager.PopupRoot.RemoveChild(_popup);
}
OnNoteChanged = null;
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs
index b9ed5e96a8..476747e07e 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs
+++ b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs
@@ -30,7 +30,7 @@ public sealed partial class AdminNotesLine : BoxContainer
public string EditText => _edit?.Text ?? OriginalMessage;
public event Action? OnSubmitted;
- public event Func? OnRightClicked;
+ public event Func? OnClicked;
private void AddLabel()
{
@@ -92,7 +92,7 @@ public sealed partial class AdminNotesLine : BoxContainer
return;
}
- if (OnRightClicked?.Invoke(this) == true)
+ if (OnClicked?.Invoke(this) == true)
{
args.Handle();
}
@@ -137,6 +137,6 @@ public sealed partial class AdminNotesLine : BoxContainer
}
OnSubmitted = null;
- OnRightClicked = null;
+ OnClicked = null;
}
}
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml
index 8f23025e67..48a0baff11 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml
+++ b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml
@@ -1,5 +1,5 @@
+ xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
diff --git a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
index 2d643c9f7c..2573a2cf69 100644
--- a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
@@ -15,7 +15,6 @@
-
diff --git a/Content.Server/Administration/BanList/BanListEui.cs b/Content.Server/Administration/BanList/BanListEui.cs
new file mode 100644
index 0000000000..a3edd4ec48
--- /dev/null
+++ b/Content.Server/Administration/BanList/BanListEui.cs
@@ -0,0 +1,95 @@
+using System.Threading.Tasks;
+using Content.Server.Administration.Managers;
+using Content.Server.Database;
+using Content.Server.EUI;
+using Content.Shared.Administration;
+using Content.Shared.Administration.BanList;
+using Content.Shared.Eui;
+using Robust.Shared.Network;
+
+namespace Content.Server.Administration.BanList;
+
+public sealed class BanListEui : BaseEui
+{
+ [Dependency] private readonly IAdminManager _admins = default!;
+ [Dependency] private readonly IPlayerLocator _playerLocator = default!;
+ [Dependency] private readonly IServerDbManager _db = default!;
+
+ public BanListEui()
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ private Guid BanListPlayer { get; set; }
+ private string BanListPlayerName { get; set; } = string.Empty;
+ private List Bans { get; set; } = new();
+
+ public override void Opened()
+ {
+ base.Opened();
+
+ _admins.OnPermsChanged += OnPermsChanged;
+ }
+
+ public override EuiStateBase GetNewState()
+ {
+ return new BanListEuiState(BanListPlayerName, Bans);
+ }
+
+ private void OnPermsChanged(AdminPermsChangedEventArgs args)
+ {
+ if (args.Player == Player && !_admins.HasAdminFlag(Player, AdminFlags.Ban))
+ {
+ Close();
+ }
+ else
+ {
+ StateDirty();
+ }
+ }
+
+ private async Task LoadFromDb()
+ {
+ Bans.Clear();
+
+ var userId = new NetUserId(BanListPlayer);
+ BanListPlayerName = (await _playerLocator.LookupIdAsync(userId))?.Username ??
+ string.Empty;
+
+ foreach (var ban in await _db.GetServerBansAsync(null, userId, null))
+ {
+ SharedServerUnban? unban = null;
+ if (ban.Unban is { } unbanDef)
+ {
+ var unbanningAdmin = unbanDef.UnbanningAdmin == null
+ ? null
+ : (await _playerLocator.LookupIdAsync(unbanDef.UnbanningAdmin.Value))?.Username;
+ unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime);
+ }
+
+ Bans.Add(new SharedServerBan(
+ ban.Id,
+ ban.UserId,
+ ban.Address is { } address
+ ? (address.address.ToString(), address.cidrMask)
+ : null,
+ ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan()),
+ ban.BanTime.UtcDateTime,
+ ban.ExpirationTime?.UtcDateTime,
+ ban.Reason,
+ ban.BanningAdmin == null
+ ? null
+ : (await _playerLocator.LookupIdAsync(ban.BanningAdmin.Value))?.Username,
+ unban
+ ));
+ }
+
+ StateDirty();
+ }
+
+ public async Task ChangeBanListPlayer(Guid banListPlayer)
+ {
+ BanListPlayer = banListPlayer;
+ await LoadFromDb();
+ }
+}
diff --git a/Content.Server/Administration/Commands/BanListCommand.cs b/Content.Server/Administration/Commands/BanListCommand.cs
index 316848ee46..e52b84296a 100644
--- a/Content.Server/Administration/Commands/BanListCommand.cs
+++ b/Content.Server/Administration/Commands/BanListCommand.cs
@@ -1,9 +1,8 @@
-using System.Text;
-using Content.Server.Database;
+using Content.Server.Administration.BanList;
+using Content.Server.EUI;
using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
-using Robust.Shared.Network;
namespace Content.Server.Administration.Commands
{
@@ -11,77 +10,44 @@ namespace Content.Server.Administration.Commands
public sealed class BanListCommand : IConsoleCommand
{
public string Command => "banlist";
- public string Description => "Lists somebody's bans";
- public string Help => "Usage: ";
+ public string Description => "Opens the ban list panel.";
+ public string Help => $"Usage: {Command} ";
public async void Execute(IConsoleShell shell, string argStr, string[] args)
{
- if (args.Length != 1)
+ if (shell.Player is not IPlayerSession player)
{
- shell.WriteLine($"Invalid amount of args. {Help}");
+ shell.WriteError("This does not work from the server console.");
return;
}
- var plyMgr = IoCManager.Resolve();
- var dbMan = IoCManager.Resolve();
+ Guid banListPlayer;
- var target = args[0];
- NetUserId targetUid;
+ switch (args.Length)
+ {
+ case 1 when Guid.TryParse(args[0], out banListPlayer):
+ break;
+ case 1:
+ var locator = IoCManager.Resolve();
+ var dbGuid = await locator.LookupIdByNameAsync(args[0]);
- if (plyMgr.TryGetSessionByUsername(target, out var targetSession))
- {
- targetUid = targetSession.UserId;
- }
- else if (Guid.TryParse(target, out var targetGuid))
- {
- targetUid = new NetUserId(targetGuid);
- }
- else
- {
- shell.WriteLine("Unable to find user with that name.");
- return;
+ if (dbGuid == null)
+ {
+ shell.WriteError($"Unable to find {args[0]} netuserid");
+ return;
+ }
+
+ banListPlayer = dbGuid.UserId;
+ break;
+ default:
+ shell.WriteError($"Invalid arguments.\n{Help}");
+ return;
}
- var bans = await dbMan.GetServerBansAsync(null, targetUid, null);
-
- if (bans.Count == 0)
- {
- shell.WriteLine("That user has no bans in their record.");
- return;
- }
-
- var bansString = new StringBuilder("Bans in record:\n");
-
- foreach (var ban in bans)
- {
- bansString
- .Append("Ban ID: ")
- .Append(ban.Id)
- .Append("\n")
- .Append("Banned in ")
- .Append(ban.BanTime);
-
- if (ban.ExpirationTime == null)
- {
- bansString.Append(".");
- }
- else
- {
- bansString
- .Append(" until ")
- .Append(ban.ExpirationTime.Value)
- .Append(".");
- }
-
- bansString.Append("\n");
-
- bansString
- .Append("Reason: ")
- .Append(ban.Reason)
- .Append("\n\n");
- }
-
- shell.WriteLine(bansString.ToString());
+ var euis = IoCManager.Resolve();
+ var ui = new BanListEui();
+ euis.OpenEui(ui, player);
+ await ui.ChangeBanListPlayer(banListPlayer);
}
}
}
diff --git a/Content.Server/Administration/Commands/PardonCommand.cs b/Content.Server/Administration/Commands/PardonCommand.cs
index f3b89c1973..869024eb7c 100644
--- a/Content.Server/Administration/Commands/PardonCommand.cs
+++ b/Content.Server/Administration/Commands/PardonCommand.cs
@@ -4,7 +4,6 @@ using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
-
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Ban)]
@@ -27,7 +26,7 @@ namespace Content.Server.Administration.Commands
if (!int.TryParse(args[0], out var banId))
{
- shell.WriteLine($"Unable to parse {args[1]} as a ban id integer.\n{Help}");
+ shell.WriteLine($"Unable to parse {args[0]} as a ban id integer.\n{Help}");
return;
}
diff --git a/Content.Shared/Administration/BanList/BanListEuiState.cs b/Content.Shared/Administration/BanList/BanListEuiState.cs
new file mode 100644
index 0000000000..750043bb7d
--- /dev/null
+++ b/Content.Shared/Administration/BanList/BanListEuiState.cs
@@ -0,0 +1,17 @@
+using Content.Shared.Eui;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Administration.BanList;
+
+[Serializable, NetSerializable]
+public sealed class BanListEuiState : EuiStateBase
+{
+ public BanListEuiState(string banListPlayerName, List bans)
+ {
+ BanListPlayerName = banListPlayerName;
+ Bans = bans;
+ }
+
+ public string BanListPlayerName { get; }
+ public List Bans { get; }
+}
diff --git a/Content.Shared/Administration/BanList/SharedServerBan.cs b/Content.Shared/Administration/BanList/SharedServerBan.cs
new file mode 100644
index 0000000000..4970869966
--- /dev/null
+++ b/Content.Shared/Administration/BanList/SharedServerBan.cs
@@ -0,0 +1,17 @@
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Administration.BanList;
+
+[Serializable, NetSerializable]
+public sealed record SharedServerBan(
+ int? Id,
+ NetUserId? UserId,
+ (string address, int cidrMask)? Address,
+ string? HWId,
+ DateTime BanTime,
+ DateTime? ExpirationTime,
+ string Reason,
+ string? BanningAdminName,
+ SharedServerUnban? Unban
+);
diff --git a/Content.Shared/Administration/BanList/SharedServerUnban.cs b/Content.Shared/Administration/BanList/SharedServerUnban.cs
new file mode 100644
index 0000000000..f3a57e4159
--- /dev/null
+++ b/Content.Shared/Administration/BanList/SharedServerUnban.cs
@@ -0,0 +1,9 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Administration.BanList;
+
+[Serializable, NetSerializable]
+public sealed record SharedServerUnban(
+ string? UnbanningAdmin,
+ DateTime UnbanTime
+);
diff --git a/Resources/Locale/en-US/administration/ui/actions.ftl b/Resources/Locale/en-US/administration/ui/actions.ftl
index fe43e68243..86a5e15039 100644
--- a/Resources/Locale/en-US/administration/ui/actions.ftl
+++ b/Resources/Locale/en-US/administration/ui/actions.ftl
@@ -1,3 +1,4 @@
+admin-player-actions-bans = Ban List
admin-player-actions-notes = Notes
admin-player-actions-kick = Kick
admin-player-actions-ban = Ban
diff --git a/Resources/Locale/en-US/administration/ui/ban-list.ftl b/Resources/Locale/en-US/administration/ui/ban-list.ftl
new file mode 100644
index 0000000000..435dbf4131
--- /dev/null
+++ b/Resources/Locale/en-US/administration/ui/ban-list.ftl
@@ -0,0 +1,15 @@
+# UI
+ban-list-header-ids = Ids
+ban-list-header-reason = Reason
+ban-list-header-time = Ban time
+ban-list-header-expires = Expires
+ban-list-header-banning-admin = Banning admin
+
+ban-list-title = Bans for {$player}
+ban-list-hidden = Hidden
+ban-list-ip = IP: {$ip}
+ban-list-hwid = HWID: {$hwid}
+ban-list-guid = GUID: {$guid}
+ban-list-permanent = PERMANENT
+ban-list-unbanned = Unbanned: {$date}
+ban-list-unbanned-by = By {$unbanner}