diff --git a/Content.Client/Administration/ClientAdminManager.cs b/Content.Client/Administration/ClientAdminManager.cs new file mode 100644 index 0000000000..4301bfe8d0 --- /dev/null +++ b/Content.Client/Administration/ClientAdminManager.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Administration; +using Content.Shared.Network.NetMessages; +using Robust.Client.Console; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; +using Robust.Shared.Log; + +#nullable enable + +namespace Content.Client.Administration +{ + public class ClientAdminManager : IClientAdminManager, IClientConGroupImplementation, IPostInjectInit + { + [Dependency] private readonly IClientNetManager _netMgr = default!; + [Dependency] private readonly IClientConGroupController _conGroup = default!; + + private AdminData? _adminData; + private readonly HashSet _availableCommands = new HashSet(); + + public event Action? AdminStatusUpdated; + + public bool HasFlag(AdminFlags flag) + { + return _adminData?.HasFlag(flag) ?? false; + } + + public bool CanCommand(string cmdName) + { + return _availableCommands.Contains(cmdName); + } + + public bool CanViewVar() + { + return _adminData?.CanViewVar() ?? false; + } + + public bool CanAdminPlace() + { + return _adminData?.CanAdminPlace() ?? false; + } + + public bool CanScript() + { + return _adminData?.CanScript() ?? false; + } + + public bool CanAdminMenu() + { + return _adminData?.CanAdminMenu() ?? false; + } + + public void Initialize() + { + _netMgr.RegisterNetMessage(MsgUpdateAdminStatus.NAME, UpdateMessageRx); + } + + private void UpdateMessageRx(MsgUpdateAdminStatus message) + { + _availableCommands.Clear(); + _availableCommands.UnionWith(message.AvailableCommands); + Logger.DebugS("admin", $"Have {message.AvailableCommands.Length} commands available"); + + _adminData = message.Admin; + if (_adminData != null) + { + var flagsText = string.Join("|", AdminFlagsHelper.FlagsToNames(_adminData.Flags)); + Logger.InfoS("admin", $"Updated admin status: {_adminData.Active}/{_adminData.Title}/{flagsText}"); + } + else + { + Logger.InfoS("admin", "Updated admin status: Not admin"); + } + + AdminStatusUpdated?.Invoke(); + ConGroupUpdated?.Invoke(); + } + + public event Action? ConGroupUpdated; + + void IPostInjectInit.PostInject() + { + _conGroup.Implementation = this; + } + } +} diff --git a/Content.Client/Administration/IClientAdminManager.cs b/Content.Client/Administration/IClientAdminManager.cs new file mode 100644 index 0000000000..e675e3f378 --- /dev/null +++ b/Content.Client/Administration/IClientAdminManager.cs @@ -0,0 +1,52 @@ +using System; +using Content.Shared.Administration; + +#nullable enable + +namespace Content.Client.Administration +{ + /// + /// Manages server admin permissions for the local player. + /// + public interface IClientAdminManager + { + /// + /// Fired when the admin status of the local player changes, such as losing admin privileges. + /// + event Action AdminStatusUpdated; + + /// + /// Checks whether the local player has an admin flag. + /// + /// The flags to check. Multiple flags can be specified, they must all be held. + /// False if the local player is not an admin, inactive, or does not have all the flags specified. + bool HasFlag(AdminFlags flag); + + /// + /// Check if a player can execute a specified console command. + /// + bool CanCommand(string cmdName); + + /// + /// Check if the local player can open the VV menu. + /// + bool CanViewVar(); + + /// + /// Check if the local player can spawn stuff in with the entity/tile spawn panel. + /// + bool CanAdminPlace(); + + /// + /// Check if the local player can execute server-side C# scripts. + /// + bool CanScript(); + + /// + /// Check if the local player can open the admin menu. + /// + bool CanAdminMenu(); + + void Initialize(); + } +} diff --git a/Content.Client/Chat/ChatBox.cs b/Content.Client/Chat/ChatBox.cs index 7eb1befb17..75e65ff87f 100644 --- a/Content.Client/Chat/ChatBox.cs +++ b/Content.Client/Chat/ChatBox.cs @@ -1,10 +1,8 @@ using Content.Shared.Chat; -using Robust.Client.Console; using Robust.Client.Graphics.Drawing; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Input; -using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -97,29 +95,23 @@ namespace Content.Client.Chat ToggleMode = true, }; - var groupController = IoCManager.Resolve(); - if(groupController.CanCommand("asay")) + AdminButton = new Button { - AdminButton = new Button - { - Text = Loc.GetString("Admin"), - Name = "Admin", - ToggleMode = true, - }; - } + Text = Loc.GetString("Admin"), + Name = "Admin", + ToggleMode = true, + Visible = false + }; AllButton.OnToggled += OnFilterToggled; LocalButton.OnToggled += OnFilterToggled; OOCButton.OnToggled += OnFilterToggled; + AdminButton.OnToggled += OnFilterToggled; hBox.AddChild(AllButton); hBox.AddChild(LocalButton); hBox.AddChild(OOCButton); - if(AdminButton != null) - { - AdminButton.OnToggled += OnFilterToggled; - hBox.AddChild(AdminButton); - } + hBox.AddChild(AdminButton); AddChild(outerVBox); } diff --git a/Content.Client/Chat/ChatManager.cs b/Content.Client/Chat/ChatManager.cs index 7f935150cf..aa8014b5e5 100644 --- a/Content.Client/Chat/ChatManager.cs +++ b/Content.Client/Chat/ChatManager.cs @@ -1,6 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using Content.Client.Administration; using Content.Client.Interfaces.Chat; +using Content.Shared.Administration; using Content.Shared.Chat; using Robust.Client.Console; using Robust.Client.Interfaces.Graphics.ClientEye; @@ -18,9 +19,11 @@ using Robust.Shared.Network; using Robust.Shared.Timing; using Robust.Shared.Utility; +#nullable enable + namespace Content.Client.Chat { - internal sealed class ChatManager : IChatManager + internal sealed class ChatManager : IChatManager, IPostInjectInit { private struct SpeechBubbleData { @@ -75,9 +78,10 @@ namespace Content.Client.Chat [Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IClientConGroupController _groupController = default!; + [Dependency] private readonly IClientAdminManager _adminMgr = default!; - private ChatBox _currentChatBox; - private Control _speechBubbleRoot; + private ChatBox? _currentChatBox; + private Control _speechBubbleRoot = null!; /// /// Speech bubbles that are currently visible on screen. @@ -103,7 +107,7 @@ namespace Content.Client.Chat _speechBubbleRoot.SetPositionFirst(); // When connexion is achieved, request the max chat message length - _netManager.Connected += new EventHandler(RequestMaxLength); + _netManager.Connected += RequestMaxLength; } public void FrameUpdate(FrameEventArgs delta) @@ -157,14 +161,15 @@ namespace Content.Client.Chat { _currentChatBox.TextSubmitted += _onChatBoxTextSubmitted; _currentChatBox.FilterToggled += _onFilterButtonToggled; + + _currentChatBox.AllButton.Pressed = !_allState; + _currentChatBox.LocalButton.Pressed = !_localState; + _currentChatBox.OOCButton.Pressed = !_oocState; + _currentChatBox.AdminButton.Pressed = !_adminState; + AdminStatusUpdated(); } RepopulateChat(filteredHistory); - _currentChatBox.AllButton.Pressed = !_allState; - _currentChatBox.LocalButton.Pressed = !_localState; - _currentChatBox.OOCButton.Pressed = !_oocState; - if(chatBox.AdminButton != null) - _currentChatBox.AdminButton.Pressed = !_adminState; } public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble) @@ -229,9 +234,12 @@ namespace Content.Client.Chat // Check if message is longer than the character limit if (text.Length > _maxMessageLength) { - string locWarning = Loc.GetString("Your message exceeds {0} character limit", _maxMessageLength); - _currentChatBox?.AddLine(locWarning, ChatChannel.Server, Color.Orange); - _currentChatBox.ClearOnEnter = false; // The text shouldn't be cleared if it hasn't been sent + if (_currentChatBox != null) + { + string locWarning = Loc.GetString("Your message exceeds {0} character limit", _maxMessageLength); + _currentChatBox.AddLine(locWarning, ChatChannel.Server, Color.Orange); + _currentChatBox.ClearOnEnter = false; // The text shouldn't be cleared if it hasn't been sent + } return; } @@ -257,13 +265,15 @@ namespace Content.Client.Chat var conInput = text.Substring(1); if (string.IsNullOrWhiteSpace(conInput)) return; - if (_groupController.CanCommand("asay")){ + if (_groupController.CanCommand("asay")) + { _console.ProcessCommand($"asay \"{CommandParsing.Escape(conInput)}\""); } else { _console.ProcessCommand($"ooc \"{CommandParsing.Escape(conInput)}\""); } + break; } case MeAlias: @@ -276,7 +286,7 @@ namespace Content.Client.Chat } default: { - var conInput = _currentChatBox.DefaultChatFormat != null + var conInput = _currentChatBox?.DefaultChatFormat != null ? string.Format(_currentChatBox.DefaultChatFormat, CommandParsing.Escape(text)) : text; _console.ProcessCommand(conInput); @@ -341,6 +351,11 @@ namespace Content.Client.Chat private void RepopulateChat(IEnumerable filteredMessages) { + if (_currentChatBox == null) + { + return; + } + _currentChatBox.Contents.Clear(); foreach (var msg in filteredMessages) @@ -463,7 +478,8 @@ namespace Content.Client.Chat private void CreateSpeechBubble(IEntity entity, SpeechBubbleData speechData) { - var bubble = SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this); + var bubble = + SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this); if (_activeSpeechBubbles.TryGetValue(entity.Uid, out var existing)) { @@ -496,6 +512,19 @@ namespace Content.Client.Chat return _allState ^ _filteredChannels.HasFlag(channel); } + void IPostInjectInit.PostInject() + { + _adminMgr.AdminStatusUpdated += AdminStatusUpdated; + } + + private void AdminStatusUpdated() + { + if (_currentChatBox != null) + { + _currentChatBox.AdminButton.Visible = _adminMgr.HasFlag(AdminFlags.Admin); + } + } + private sealed class SpeechBubbleQueueData { /// diff --git a/Content.Client/ClientContentIoC.cs b/Content.Client/ClientContentIoC.cs index 7c338bd292..d63ad75a32 100644 --- a/Content.Client/ClientContentIoC.cs +++ b/Content.Client/ClientContentIoC.cs @@ -1,4 +1,6 @@ -using Content.Client.Chat; +using Content.Client.Administration; +using Content.Client.Chat; +using Content.Client.Eui; using Content.Client.GameTicking; using Content.Client.Interfaces; using Content.Client.Interfaces.Chat; @@ -37,6 +39,8 @@ namespace Content.Client IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 91ad2d97e9..0020a6d7ce 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -1,4 +1,6 @@ using System; +using Content.Client.Administration; +using Content.Client.Eui; using Content.Client.GameObjects.Components.Actor; using Content.Client.Input; using Content.Client.Interfaces; @@ -88,6 +90,7 @@ namespace Content.Client IoCManager.BuildGraph(); + IoCManager.Resolve().Initialize(); IoCManager.Resolve().LoadParallax(); IoCManager.Resolve().PlayerJoinedServer += SubscribePlayerAttachmentEvents; IoCManager.Resolve().Initialize(); @@ -150,6 +153,7 @@ namespace Content.Client IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); _baseClient.RunLevelChanged += (sender, args) => diff --git a/Content.Client/Eui/BaseEui.cs b/Content.Client/Eui/BaseEui.cs new file mode 100644 index 0000000000..ee5ad3c974 --- /dev/null +++ b/Content.Client/Eui/BaseEui.cs @@ -0,0 +1,68 @@ +using Content.Shared.Eui; +using Content.Shared.Network.NetMessages; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Client.Eui +{ + public abstract class BaseEui + { + [Dependency] private readonly IClientNetManager _netManager = default!; + + public EuiManager Manager { get; private set; } = default!; + public uint Id { get; private set; } + + protected BaseEui() + { + IoCManager.InjectDependencies(this); + } + + internal void Initialize(EuiManager mgr, uint id) + { + Manager = mgr; + Id = id; + } + + /// + /// Called when the EUI is opened by the server. + /// + public virtual void Opened() + { + } + + /// + /// Called when the EUI is closed by the server. + /// + public virtual void Closed() + { + } + + /// + /// Called when a new state comes in from the server. + /// + public virtual void HandleState(EuiStateBase state) + { + } + + /// + /// Called when a message comes in from the server. + /// + public virtual void HandleMessage(EuiMessageBase msg) + { + } + + /// + /// Send a message to the server-side implementation. + /// + protected void SendMessage(EuiMessageBase msg) + { + var netMsg = _netManager.CreateNetMessage(); + netMsg.Id = Id; + netMsg.Message = msg; + + _netManager.ClientSendMessage(netMsg); + } + } +} diff --git a/Content.Client/Eui/EuiManager.cs b/Content.Client/Eui/EuiManager.cs new file mode 100644 index 0000000000..60e4d84047 --- /dev/null +++ b/Content.Client/Eui/EuiManager.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Network.NetMessages; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Reflection; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Client.Eui +{ + public sealed class EuiManager + { + [Dependency] private readonly IClientNetManager _net = default!; + [Dependency] private readonly IReflectionManager _refl = default!; + [Dependency] private readonly IDynamicTypeFactory _dtf = default!; + + private readonly Dictionary _openUis = new Dictionary(); + + public void Initialize() + { + _net.RegisterNetMessage(MsgEuiCtl.NAME, RxMsgCtl); + _net.RegisterNetMessage(MsgEuiState.NAME, RxMsgState); + _net.RegisterNetMessage(MsgEuiMessage.NAME, RxMsgMessage); + } + + private void RxMsgMessage(MsgEuiMessage message) + { + var ui = _openUis[message.Id].Eui; + ui.HandleMessage(message.Message); + } + + private void RxMsgState(MsgEuiState message) + { + var ui = _openUis[message.Id].Eui; + ui.HandleState(message.State); + } + + private void RxMsgCtl(MsgEuiCtl message) + { + switch (message.Type) + { + case MsgEuiCtl.CtlType.Open: + var euiType = _refl.LooseGetType(message.OpenType); + var instance = _dtf.CreateInstance(euiType); + instance.Initialize(this, message.Id); + instance.Opened(); + _openUis.Add(message.Id, new EuiData(instance)); + break; + + case MsgEuiCtl.CtlType.Close: + var dat = _openUis[message.Id]; + dat.Eui.Closed(); + _openUis.Remove(message.Id); + + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + private sealed class EuiData + { + public BaseEui Eui; + + public EuiData(BaseEui eui) + { + Eui = eui; + } + } + } +} diff --git a/Content.Client/UserInterface/AdminMenu/AdminMenuManager.cs b/Content.Client/UserInterface/AdminMenu/AdminMenuManager.cs index ebcb5cc7fb..94b29253cf 100644 --- a/Content.Client/UserInterface/AdminMenu/AdminMenuManager.cs +++ b/Content.Client/UserInterface/AdminMenu/AdminMenuManager.cs @@ -11,14 +11,14 @@ namespace Content.Client.UserInterface.AdminMenu { internal class AdminMenuManager : IAdminMenuManager { - [Dependency] private INetManager _netManager = default!; + [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IClientConGroupController _clientConGroupController = default!; private SS14Window _window; private List _commandWindows; - public void Initialize() + public void Initialize() { _commandWindows = new List(); // Reset the AdminMenu Window on disconnect diff --git a/Content.Client/UserInterface/Permissions/PermissionsEui.cs b/Content.Client/UserInterface/Permissions/PermissionsEui.cs new file mode 100644 index 0000000000..6751962dc7 --- /dev/null +++ b/Content.Client/UserInterface/Permissions/PermissionsEui.cs @@ -0,0 +1,601 @@ +using System.Collections.Generic; +using System.Linq; +using Content.Client.Administration; +using Content.Client.Eui; +using Content.Client.UserInterface.Stylesheets; +using Content.Shared.Administration; +using Content.Shared.Eui; +using JetBrains.Annotations; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Utility; +using static Content.Shared.Administration.PermissionsEuiMsg; + +#nullable enable + +namespace Content.Client.UserInterface.Permissions +{ + [UsedImplicitly] + public sealed class PermissionsEui : BaseEui + { + private const int NoRank = -1; + + [Dependency] private readonly IClientAdminManager _adminManager = default!; + + private readonly Menu _menu; + private readonly List _subWindows = new List(); + + private Dictionary _ranks = + new Dictionary(); + + public PermissionsEui() + { + IoCManager.InjectDependencies(this); + + _menu = new Menu(this); + _menu.AddAdminButton.OnPressed += AddAdminPressed; + _menu.AddAdminRankButton.OnPressed += AddAdminRankPressed; + _menu.OnClose += CloseEverything; + } + + public override void Closed() + { + base.Closed(); + + CloseEverything(); + } + + private void CloseEverything() + { + foreach (var subWindow in _subWindows.ToArray()) + { + subWindow.Close(); + } + + _menu.Close(); + } + + private void AddAdminPressed(BaseButton.ButtonEventArgs obj) + { + OpenEditWindow(null); + } + + private void AddAdminRankPressed(BaseButton.ButtonEventArgs obj) + { + OpenRankEditWindow(null); + } + + + private void OnEditPressed(PermissionsEuiState.AdminData admin) + { + OpenEditWindow(admin); + } + + private void OpenEditWindow(PermissionsEuiState.AdminData? data) + { + var window = new EditAdminWindow(this, data); + window.SaveButton.OnPressed += _ => SaveAdminPressed(window); + window.OpenCentered(); + window.OnClose += () => _subWindows.Remove(window); + if (data != null) + { + window.RemoveButton!.OnPressed += _ => RemoveButtonPressed(window); + } + + _subWindows.Add(window); + } + + + private void OpenRankEditWindow(KeyValuePair? rank) + { + var window = new EditAdminRankWindow(this, rank); + window.SaveButton.OnPressed += _ => SaveAdminRankPressed(window); + window.OpenCentered(); + window.OnClose += () => _subWindows.Remove(window); + if (rank != null) + { + window.RemoveButton!.OnPressed += _ => RemoveRankButtonPressed(window); + } + + _subWindows.Add(window); + } + + private void RemoveButtonPressed(EditAdminWindow window) + { + SendMessage(new RemoveAdmin {UserId = window.SourceData!.Value.UserId}); + + window.Close(); + } + + private void RemoveRankButtonPressed(EditAdminRankWindow window) + { + SendMessage(new RemoveAdminRank {Id = window.SourceId!.Value}); + + window.Close(); + } + + private void SaveAdminPressed(EditAdminWindow popup) + { + popup.CollectSetFlags(out var pos, out var neg); + + int? rank = popup.RankButton.SelectedId; + if (rank == NoRank) + { + rank = null; + } + + var title = string.IsNullOrWhiteSpace(popup.TitleEdit.Text) ? null : popup.TitleEdit.Text; + + if (popup.SourceData is { } src) + { + SendMessage(new UpdateAdmin + { + UserId = src.UserId, + Title = title, + PosFlags = pos, + NegFlags = neg, + RankId = rank + }); + } + else + { + DebugTools.AssertNotNull(popup.NameEdit); + + SendMessage(new AddAdmin + { + UserNameOrId = popup.NameEdit!.Text, + Title = title, + PosFlags = pos, + NegFlags = neg, + RankId = rank + }); + } + + popup.Close(); + } + + + private void SaveAdminRankPressed(EditAdminRankWindow popup) + { + var flags = popup.CollectSetFlags(); + var name = popup.NameEdit.Text; + + if (popup.SourceId is { } src) + { + SendMessage(new UpdateAdminRank + { + Id = src, + Flags = flags, + Name = name + }); + } + else + { + SendMessage(new AddAdminRank + { + Flags = flags, + Name = name + }); + } + + popup.Close(); + } + + public override void Opened() + { + _menu.OpenCentered(); + } + + public override void HandleState(EuiStateBase state) + { + var s = (PermissionsEuiState) state; + + if (s.IsLoading) + { + return; + } + + _ranks = s.AdminRanks; + + _menu.AdminsList.RemoveAllChildren(); + foreach (var admin in s.Admins) + { + var al = _menu.AdminsList; + var name = admin.UserName ?? admin.UserId.ToString(); + + al.AddChild(new Label {Text = name}); + + var titleControl = new Label {Text = admin.Title ?? Loc.GetString("none")}; + if (admin.Title == null) // none + { + titleControl.StyleClasses.Add(StyleBase.StyleClassItalic); + } + + al.AddChild(titleControl); + + bool italic; + string rank; + var combinedFlags = admin.PosFlags; + if (admin.RankId is { } rankId) + { + italic = false; + var rankData = s.AdminRanks[rankId]; + rank = rankData.Name; + combinedFlags |= rankData.Flags; + } + else + { + italic = true; + rank = Loc.GetString("none"); + } + + var rankControl = new Label {Text = rank}; + if (italic) + { + rankControl.StyleClasses.Add(StyleBase.StyleClassItalic); + } + + al.AddChild(rankControl); + + var flagsText = AdminFlagsHelper.PosNegFlagsText(admin.PosFlags, admin.NegFlags); + + al.AddChild(new Label + { + Text = flagsText, + SizeFlagsHorizontal = Control.SizeFlags.ShrinkCenter | Control.SizeFlags.Expand + }); + + var editButton = new Button {Text = Loc.GetString("Edit")}; + editButton.OnPressed += _ => OnEditPressed(admin); + al.AddChild(editButton); + + if (!_adminManager.HasFlag(combinedFlags)) + { + editButton.Disabled = true; + editButton.ToolTip = Loc.GetString("You do not have the required flags to edit this admin."); + } + } + + _menu.AdminRanksList.RemoveAllChildren(); + foreach (var kv in s.AdminRanks) + { + var rank = kv.Value; + var flagsText = string.Join(' ', AdminFlagsHelper.FlagsToNames(rank.Flags).Select(f => $"+{f}")); + _menu.AdminRanksList.AddChild(new Label {Text = rank.Name}); + _menu.AdminRanksList.AddChild(new Label + { + Text = flagsText, + SizeFlagsHorizontal = Control.SizeFlags.ShrinkCenter | Control.SizeFlags.Expand + }); + var editButton = new Button {Text = Loc.GetString("Edit")}; + editButton.OnPressed += _ => OnEditRankPressed(kv); + _menu.AdminRanksList.AddChild(editButton); + + if (!_adminManager.HasFlag(rank.Flags)) + { + editButton.Disabled = true; + editButton.ToolTip = Loc.GetString("You do not have the required flags to edit this rank."); + } + } + } + + private void OnEditRankPressed(KeyValuePair rank) + { + OpenRankEditWindow(rank); + } + + private sealed class Menu : SS14Window + { + private readonly PermissionsEui _ui; + public readonly GridContainer AdminsList; + public readonly GridContainer AdminRanksList; + public readonly Button AddAdminButton; + public readonly Button AddAdminRankButton; + + public Menu(PermissionsEui ui) + { + _ui = ui; + Title = Loc.GetString("Permissions Panel"); + + var tab = new TabContainer(); + + AddAdminButton = new Button + { + Text = Loc.GetString("Add Admin"), + SizeFlagsHorizontal = SizeFlags.ShrinkEnd + }; + + AddAdminRankButton = new Button + { + Text = Loc.GetString("Add Admin Rank"), + SizeFlagsHorizontal = SizeFlags.ShrinkEnd + }; + + AdminsList = new GridContainer {Columns = 5, SizeFlagsVertical = SizeFlags.FillExpand}; + var adminVBox = new VBoxContainer + { + Children = {AdminsList, AddAdminButton}, + }; + TabContainer.SetTabTitle(adminVBox, Loc.GetString("Admins")); + + AdminRanksList = new GridContainer {Columns = 3}; + var rankVBox = new VBoxContainer + { + Children = { AdminRanksList, AddAdminRankButton} + }; + TabContainer.SetTabTitle(rankVBox, Loc.GetString("Admin Ranks")); + + tab.AddChild(adminVBox); + tab.AddChild(rankVBox); + + Contents.AddChild(tab); + } + + protected override Vector2 ContentsMinimumSize => (600, 400); + } + + private sealed class EditAdminWindow : SS14Window + { + public readonly PermissionsEuiState.AdminData? SourceData; + public readonly LineEdit? NameEdit; + public readonly LineEdit TitleEdit; + public readonly OptionButton RankButton; + public readonly Button SaveButton; + public readonly Button? RemoveButton; + + public readonly Dictionary FlagButtons + = new Dictionary(); + + public EditAdminWindow(PermissionsEui ui, PermissionsEuiState.AdminData? data) + { + SourceData = data; + + Control nameControl; + + if (data is { } dat) + { + var name = dat.UserName ?? dat.UserId.ToString(); + Title = Loc.GetString("Edit admin {0}", name); + + nameControl = new Label {Text = name}; + } + else + { + Title = Loc.GetString("Add admin"); + + nameControl = NameEdit = new LineEdit {PlaceHolder = Loc.GetString("Username/User ID")}; + } + + TitleEdit = new LineEdit {PlaceHolder = Loc.GetString("Custom title, leave blank to inherit rank title.")}; + RankButton = new OptionButton(); + SaveButton = new Button {Text = Loc.GetString("Save"), SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Expand}; + + RankButton.AddItem(Loc.GetString("No rank"), NoRank); + foreach (var (rId, rank) in ui._ranks) + { + RankButton.AddItem(rank.Name, rId); + } + + RankButton.SelectId(data?.RankId ?? NoRank); + RankButton.OnItemSelected += RankSelected; + + var permGrid = new GridContainer + { + Columns = 4, + HSeparationOverride = 0, + VSeparationOverride = 0 + }; + + foreach (var flag in AdminFlagsHelper.AllFlags) + { + // Can only grant out perms you also have yourself. + // Primarily intended to prevent people giving themselves +HOST with +PERMISSIONS but generalized. + var disable = !ui._adminManager.HasFlag(flag); + var flagName = flag.ToString().ToUpper(); + + var group = new ButtonGroup(); + + var inherit = new Button + { + Text = "I", + StyleClasses = {StyleBase.ButtonOpenRight}, + Disabled = disable, + Group = group, + }; + var sub = new Button + { + Text = "-", + StyleClasses = {StyleBase.ButtonOpenBoth}, + Disabled = disable, + Group = group + }; + var plus = new Button + { + Text = "+", + StyleClasses = {StyleBase.ButtonOpenLeft}, + Disabled = disable, + Group = group + }; + + if (data is { } d) + { + if ((d.NegFlags & flag) != 0) + { + sub.Pressed = true; + } + else if ((d.PosFlags & flag) != 0) + { + plus.Pressed = true; + } + else + { + inherit.Pressed = true; + } + } + else + { + inherit.Pressed = true; + } + + permGrid.AddChild(new Label {Text = flagName}); + permGrid.AddChild(inherit); + permGrid.AddChild(sub); + permGrid.AddChild(plus); + + FlagButtons.Add(flag, (inherit, sub, plus)); + } + + var bottomButtons = new HBoxContainer(); + if (data != null) + { + // show remove button. + RemoveButton = new Button {Text = Loc.GetString("Remove")}; + bottomButtons.AddChild(RemoveButton); + } + + bottomButtons.AddChild(SaveButton); + + Contents.AddChild(new VBoxContainer + { + Children = + { + new HBoxContainer + { + SeparationOverride = 2, + Children = + { + new VBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + nameControl, + TitleEdit, + RankButton + } + }, + permGrid + }, + SizeFlagsVertical = SizeFlags.FillExpand + }, + bottomButtons + } + }); + } + + private void RankSelected(OptionButton.ItemSelectedEventArgs obj) + { + RankButton.SelectId(obj.Id); + } + + public void CollectSetFlags(out AdminFlags pos, out AdminFlags neg) + { + pos = default; + neg = default; + + foreach (var (flag, (_, s, p)) in FlagButtons) + { + if (s.Pressed) + { + neg |= flag; + } + else if (p.Pressed) + { + pos |= flag; + } + } + } + + protected override Vector2? CustomSize => (600, 400); + } + + private sealed class EditAdminRankWindow : SS14Window + { + public readonly int? SourceId; + public readonly LineEdit NameEdit; + public readonly Button SaveButton; + public readonly Button? RemoveButton; + public readonly Dictionary FlagCheckBoxes = new Dictionary(); + + public EditAdminRankWindow(PermissionsEui ui, KeyValuePair? data) + { + SourceId = data?.Key; + + NameEdit = new LineEdit + { + PlaceHolder = Loc.GetString("Rank name"), + }; + + if (data != null) + { + NameEdit.Text = data.Value.Value.Name; + } + + SaveButton = new Button {Text = Loc.GetString("Save"), SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Expand}; + var flagsBox = new VBoxContainer(); + + foreach (var flag in AdminFlagsHelper.AllFlags) + { + // Can only grant out perms you also have yourself. + // Primarily intended to prevent people giving themselves +HOST with +PERMISSIONS but generalized. + var disable = !ui._adminManager.HasFlag(flag); + var flagName = flag.ToString().ToUpper(); + + var checkBox = new CheckBox + { + Disabled = disable, + Text = flagName + }; + + if (data != null && (data.Value.Value.Flags & flag) != 0) + { + checkBox.Pressed = true; + } + + FlagCheckBoxes.Add(flag, checkBox); + flagsBox.AddChild(checkBox); + } + + var bottomButtons = new HBoxContainer(); + if (data != null) + { + // show remove button. + RemoveButton = new Button {Text = Loc.GetString("Remove")}; + bottomButtons.AddChild(RemoveButton); + } + + bottomButtons.AddChild(SaveButton); + + Contents.AddChild(new VBoxContainer + { + Children = + { + NameEdit, + flagsBox, + bottomButtons + } + }); + } + + public AdminFlags CollectSetFlags() + { + AdminFlags flags = default; + foreach (var (flag, chk) in FlagCheckBoxes) + { + if (chk.Pressed) + { + flags |= flag; + } + } + + return flags; + } + + protected override Vector2? CustomSize => (600, 400); + } + } +} diff --git a/Content.Client/UserInterface/Stylesheets/StyleBase.cs b/Content.Client/UserInterface/Stylesheets/StyleBase.cs index 1cc9aa0769..83dd9c26cb 100644 --- a/Content.Client/UserInterface/Stylesheets/StyleBase.cs +++ b/Content.Client/UserInterface/Stylesheets/StyleBase.cs @@ -12,6 +12,7 @@ namespace Content.Client.UserInterface.Stylesheets public const string ClassHighDivider = "HighDivider"; public const string StyleClassLabelHeading = "LabelHeading"; public const string StyleClassLabelSubText = "LabelSubText"; + public const string StyleClassItalic = "Italic"; public const string ButtonOpenRight = "OpenRight"; public const string ButtonOpenLeft = "OpenLeft"; @@ -31,6 +32,7 @@ namespace Content.Client.UserInterface.Stylesheets protected StyleBase(IResourceCache resCache) { var notoSans12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12); + var notoSans12Italic = resCache.GetFont("/Fonts/NotoSans/NotoSans-Italic.ttf", 12); // Button styles. var buttonTex = resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png"); @@ -77,6 +79,14 @@ namespace Content.Client.UserInterface.Stylesheets { new StyleProperty("font", notoSans12), }), + + // Default font. + new StyleRule( + new SelectorElement(null, new[] {StyleClassItalic}, null, null), + new[] + { + new StyleProperty("font", notoSans12Italic), + }), }; } } diff --git a/Content.IntegrationTests/ContentIntegrationTest.cs b/Content.IntegrationTests/ContentIntegrationTest.cs index ed2986ca79..3db9c53884 100644 --- a/Content.IntegrationTests/ContentIntegrationTest.cs +++ b/Content.IntegrationTests/ContentIntegrationTest.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Threading.Tasks; using Content.Client; using Content.Client.Interfaces.Parallax; @@ -13,7 +12,6 @@ using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Network; using Robust.Shared.IoC; using Robust.Shared.Map; -using Robust.Shared.Prototypes; using Robust.UnitTesting; using EntryPoint = Content.Client.EntryPoint; diff --git a/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.Designer.cs b/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.Designer.cs new file mode 100644 index 0000000000..ae0daea2ad --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.Designer.cs @@ -0,0 +1,509 @@ +// +using System; +using System.Net; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20201028210620_Admins")] + partial class Admins + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("uuid"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin_rank_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("antag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AntagName") + .IsRequired() + .HasColumnName("antag_name") + .HasColumnType("text"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag"); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("assigned_user_id_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("job_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("JobName") + .IsRequired() + .HasColumnName("job_name") + .HasColumnType("text"); + + b.Property("Priority") + .HasColumnName("priority") + .HasColumnType("integer"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("job"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("connection_log_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Address") + .IsRequired() + .HasColumnName("address") + .HasColumnType("inet"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log"); + + b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("player_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("FirstSeenTime") + .HasColumnName("first_seen_time") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnName("last_seen_address") + .HasColumnType("inet"); + + b.Property("LastSeenTime") + .HasColumnName("last_seen_time") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnName("last_seen_user_name") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player"); + + b.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("server_ban_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property?>("Address") + .HasColumnName("address") + .HasColumnType("inet"); + + b.Property("BanTime") + .HasColumnName("ban_time") + .HasColumnType("timestamp with time zone"); + + b.Property("BanningAdmin") + .HasColumnName("banning_admin") + .HasColumnType("uuid"); + + b.Property("ExpirationTime") + .HasColumnName("expiration_time") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnName("reason") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("UserId"); + + b.ToTable("server_ban"); + + b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + b.HasCheckConstraint("HaveEitherAddressOrUserId", "address IS NOT NULL OR user_id IS NOT NULL"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("unban_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("BanId") + .HasColumnName("ban_id") + .HasColumnType("integer"); + + b.Property("UnbanTime") + .HasColumnName("unban_time") + .HasColumnType("timestamp with time zone"); + + b.Property("UnbanningAdmin") + .HasColumnName("unbanning_admin") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("preference_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("SelectedCharacterSlot") + .HasColumnName("selected_character_slot") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("profile_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Age") + .HasColumnName("age") + .HasColumnType("integer"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnName("char_name") + .HasColumnType("text"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnName("eye_color") + .HasColumnType("text"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnName("facial_hair_color") + .HasColumnType("text"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnName("facial_hair_name") + .HasColumnType("text"); + + b.Property("HairColor") + .IsRequired() + .HasColumnName("hair_color") + .HasColumnType("text"); + + b.Property("HairName") + .IsRequired() + .HasColumnName("hair_name") + .HasColumnType("text"); + + b.Property("PreferenceId") + .HasColumnName("preference_id") + .HasColumnType("integer"); + + b.Property("PreferenceUnavailable") + .HasColumnName("pref_unavailable") + .HasColumnType("integer"); + + b.Property("Sex") + .IsRequired() + .HasColumnName("sex") + .HasColumnType("text"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnName("skin_color") + .HasColumnType("text"); + + b.Property("Slot") + .HasColumnName("slot") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PreferenceId"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b => + { + b.HasOne("Content.Server.Database.PostgresServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.PostgresServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.cs b/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.cs new file mode 100644 index 0000000000..02514652da --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.cs @@ -0,0 +1,115 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Content.Server.Database.Migrations.Postgres +{ + public partial class Admins : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "admin_rank", + columns: table => new + { + admin_rank_id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_rank", x => x.admin_rank_id); + }); + + migrationBuilder.CreateTable( + name: "admin", + columns: table => new + { + user_id = table.Column(nullable: false), + title = table.Column(nullable: true), + admin_rank_id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_admin", x => x.user_id); + table.ForeignKey( + name: "FK_admin_admin_rank_admin_rank_id", + column: x => x.admin_rank_id, + principalTable: "admin_rank", + principalColumn: "admin_rank_id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "admin_rank_flag", + columns: table => new + { + admin_rank_flag_id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + flag = table.Column(nullable: false), + admin_rank_id = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_rank_flag", x => x.admin_rank_flag_id); + table.ForeignKey( + name: "FK_admin_rank_flag_admin_rank_admin_rank_id", + column: x => x.admin_rank_id, + principalTable: "admin_rank", + principalColumn: "admin_rank_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "admin_flag", + columns: table => new + { + admin_flag_id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + flag = table.Column(nullable: false), + negative = table.Column(nullable: false), + admin_id = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_flag", x => x.admin_flag_id); + table.ForeignKey( + name: "FK_admin_flag_admin_admin_id", + column: x => x.admin_id, + principalTable: "admin", + principalColumn: "user_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_admin_admin_rank_id", + table: "admin", + column: "admin_rank_id"); + + migrationBuilder.CreateIndex( + name: "IX_admin_flag_admin_id", + table: "admin_flag", + column: "admin_id"); + + migrationBuilder.CreateIndex( + name: "IX_admin_rank_flag_admin_rank_id", + table: "admin_rank_flag", + column: "admin_rank_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "admin_flag"); + + migrationBuilder.DropTable( + name: "admin_rank_flag"); + + migrationBuilder.DropTable( + name: "admin"); + + migrationBuilder.DropTable( + name: "admin_rank"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.Designer.cs b/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.Designer.cs new file mode 100644 index 0000000000..ce41855793 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.Designer.cs @@ -0,0 +1,517 @@ +// +using System; +using System.Net; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20201109092921_ExtraIndices")] + partial class ExtraIndices + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("uuid"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("antag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AntagName") + .IsRequired() + .HasColumnName("antag_name") + .HasColumnType("text"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag"); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("assigned_user_id_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("job_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("JobName") + .IsRequired() + .HasColumnName("job_name") + .HasColumnType("text"); + + b.Property("Priority") + .HasColumnName("priority") + .HasColumnType("integer"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("job"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("connection_log_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Address") + .IsRequired() + .HasColumnName("address") + .HasColumnType("inet"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log"); + + b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("player_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("FirstSeenTime") + .HasColumnName("first_seen_time") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnName("last_seen_address") + .HasColumnType("inet"); + + b.Property("LastSeenTime") + .HasColumnName("last_seen_time") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnName("last_seen_user_name") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player"); + + b.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("server_ban_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property?>("Address") + .HasColumnName("address") + .HasColumnType("inet"); + + b.Property("BanTime") + .HasColumnName("ban_time") + .HasColumnType("timestamp with time zone"); + + b.Property("BanningAdmin") + .HasColumnName("banning_admin") + .HasColumnType("uuid"); + + b.Property("ExpirationTime") + .HasColumnName("expiration_time") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnName("reason") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("UserId"); + + b.ToTable("server_ban"); + + b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + b.HasCheckConstraint("HaveEitherAddressOrUserId", "address IS NOT NULL OR user_id IS NOT NULL"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("unban_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("BanId") + .HasColumnName("ban_id") + .HasColumnType("integer"); + + b.Property("UnbanTime") + .HasColumnName("unban_time") + .HasColumnType("timestamp with time zone"); + + b.Property("UnbanningAdmin") + .HasColumnName("unbanning_admin") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("preference_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("SelectedCharacterSlot") + .HasColumnName("selected_character_slot") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("profile_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Age") + .HasColumnName("age") + .HasColumnType("integer"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnName("char_name") + .HasColumnType("text"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnName("eye_color") + .HasColumnType("text"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnName("facial_hair_color") + .HasColumnType("text"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnName("facial_hair_name") + .HasColumnType("text"); + + b.Property("HairColor") + .IsRequired() + .HasColumnName("hair_color") + .HasColumnType("text"); + + b.Property("HairName") + .IsRequired() + .HasColumnName("hair_name") + .HasColumnType("text"); + + b.Property("PreferenceId") + .HasColumnName("preference_id") + .HasColumnType("integer"); + + b.Property("PreferenceUnavailable") + .HasColumnName("pref_unavailable") + .HasColumnType("integer"); + + b.Property("Sex") + .IsRequired() + .HasColumnName("sex") + .HasColumnType("text"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnName("skin_color") + .HasColumnType("text"); + + b.Property("Slot") + .HasColumnName("slot") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PreferenceId"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b => + { + b.HasOne("Content.Server.Database.PostgresServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.PostgresServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.cs b/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.cs new file mode 100644 index 0000000000..5521f82d27 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Content.Server.Database.Migrations.Postgres +{ + public partial class ExtraIndices : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_player_last_seen_user_name", + table: "player", + column: "last_seen_user_name"); + + migrationBuilder.CreateIndex( + name: "IX_admin_rank_flag_flag_admin_rank_id", + table: "admin_rank_flag", + columns: new[] { "flag", "admin_rank_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_admin_flag_flag_admin_id", + table: "admin_flag", + columns: new[] { "flag", "admin_id" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_player_last_seen_user_name", + table: "player"); + + migrationBuilder.DropIndex( + name: "IX_admin_rank_flag_flag_admin_rank_id", + table: "admin_rank_flag"); + + migrationBuilder.DropIndex( + name: "IX_admin_flag_flag_admin_id", + table: "admin_flag"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs index 59ae3c45f4..16f84e3e27 100644 --- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs @@ -20,6 +20,104 @@ namespace Content.Server.Database.Migrations.Postgres .HasAnnotation("ProductVersion", "3.1.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("uuid"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag"); + }); + modelBuilder.Entity("Content.Server.Database.Antag", b => { b.Property("Id") @@ -168,6 +266,8 @@ namespace Content.Server.Database.Migrations.Postgres b.HasKey("Id"); + b.HasIndex("LastSeenUserName"); + b.HasIndex("UserId") .IsUnique(); @@ -348,6 +448,32 @@ namespace Content.Server.Database.Migrations.Postgres b.ToTable("profile"); }); + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Content.Server.Database.Antag", b => { b.HasOne("Content.Server.Database.Profile", "Profile") diff --git a/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.Designer.cs new file mode 100644 index 0000000000..23ebe0d41b --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.Designer.cs @@ -0,0 +1,476 @@ +// +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20201028210616_Admins")] + partial class Admins + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.4"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("TEXT"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("TEXT"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin_rank_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("antag_id") + .HasColumnType("INTEGER"); + + b.Property("AntagName") + .IsRequired() + .HasColumnName("antag_name") + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag"); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("assigned_user_id_id") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("job_id") + .HasColumnType("INTEGER"); + + b.Property("JobName") + .IsRequired() + .HasColumnName("job_name") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnName("priority") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("job"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("preference_id") + .HasColumnType("INTEGER"); + + b.Property("SelectedCharacterSlot") + .HasColumnName("selected_character_slot") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.Property("Age") + .HasColumnName("age") + .HasColumnType("INTEGER"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnName("char_name") + .HasColumnType("TEXT"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnName("eye_color") + .HasColumnType("TEXT"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnName("facial_hair_color") + .HasColumnType("TEXT"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnName("facial_hair_name") + .HasColumnType("TEXT"); + + b.Property("HairColor") + .IsRequired() + .HasColumnName("hair_color") + .HasColumnType("TEXT"); + + b.Property("HairName") + .IsRequired() + .HasColumnName("hair_name") + .HasColumnType("TEXT"); + + b.Property("PreferenceId") + .HasColumnName("preference_id") + .HasColumnType("INTEGER"); + + b.Property("PreferenceUnavailable") + .HasColumnName("pref_unavailable") + .HasColumnType("INTEGER"); + + b.Property("Sex") + .IsRequired() + .HasColumnName("sex") + .HasColumnType("TEXT"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnName("skin_color") + .HasColumnType("TEXT"); + + b.Property("Slot") + .HasColumnName("slot") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PreferenceId"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("connection_log_id") + .HasColumnType("INTEGER"); + + b.Property("Address") + .IsRequired() + .HasColumnName("address") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("connection_log"); + }); + + modelBuilder.Entity("Content.Server.Database.SqlitePlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("player_id") + .HasColumnType("INTEGER"); + + b.Property("FirstSeenTime") + .HasColumnName("first_seen_time") + .HasColumnType("TEXT"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnName("last_seen_address") + .HasColumnType("TEXT"); + + b.Property("LastSeenTime") + .HasColumnName("last_seen_time") + .HasColumnType("TEXT"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnName("last_seen_user_name") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("player"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("ban_id") + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnName("address") + .HasColumnType("TEXT"); + + b.Property("BanTime") + .HasColumnName("ban_time") + .HasColumnType("TEXT"); + + b.Property("BanningAdmin") + .HasColumnName("banning_admin") + .HasColumnType("TEXT"); + + b.Property("ExpirationTime") + .HasColumnName("expiration_time") + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasColumnName("reason") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ban"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("unban_id") + .HasColumnType("INTEGER"); + + b.Property("BanId") + .HasColumnName("ban_id") + .HasColumnType("INTEGER"); + + b.Property("UnbanTime") + .HasColumnName("unban_time") + .HasColumnType("TEXT"); + + b.Property("UnbanningAdmin") + .HasColumnName("unbanning_admin") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("unban"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b => + { + b.HasOne("Content.Server.Database.SqliteServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.SqliteServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.cs b/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.cs new file mode 100644 index 0000000000..c9c65034d4 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.cs @@ -0,0 +1,114 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Content.Server.Database.Migrations.Sqlite +{ + public partial class Admins : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "admin_rank", + columns: table => new + { + admin_rank_id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + name = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_rank", x => x.admin_rank_id); + }); + + migrationBuilder.CreateTable( + name: "admin", + columns: table => new + { + user_id = table.Column(nullable: false), + title = table.Column(nullable: true), + admin_rank_id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_admin", x => x.user_id); + table.ForeignKey( + name: "FK_admin_admin_rank_admin_rank_id", + column: x => x.admin_rank_id, + principalTable: "admin_rank", + principalColumn: "admin_rank_id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "admin_rank_flag", + columns: table => new + { + admin_rank_flag_id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + flag = table.Column(nullable: false), + admin_rank_id = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_rank_flag", x => x.admin_rank_flag_id); + table.ForeignKey( + name: "FK_admin_rank_flag_admin_rank_admin_rank_id", + column: x => x.admin_rank_id, + principalTable: "admin_rank", + principalColumn: "admin_rank_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "admin_flag", + columns: table => new + { + admin_flag_id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + flag = table.Column(nullable: false), + negative = table.Column(nullable: false), + admin_id = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_flag", x => x.admin_flag_id); + table.ForeignKey( + name: "FK_admin_flag_admin_admin_id", + column: x => x.admin_id, + principalTable: "admin", + principalColumn: "user_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_admin_admin_rank_id", + table: "admin", + column: "admin_rank_id"); + + migrationBuilder.CreateIndex( + name: "IX_admin_flag_admin_id", + table: "admin_flag", + column: "admin_id"); + + migrationBuilder.CreateIndex( + name: "IX_admin_rank_flag_admin_rank_id", + table: "admin_rank_flag", + column: "admin_rank_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "admin_flag"); + + migrationBuilder.DropTable( + name: "admin_rank_flag"); + + migrationBuilder.DropTable( + name: "admin"); + + migrationBuilder.DropTable( + name: "admin_rank"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.Designer.cs new file mode 100644 index 0000000000..ca7d412b93 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.Designer.cs @@ -0,0 +1,484 @@ +// +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20201109092917_ExtraIndices")] + partial class ExtraIndices + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.4"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("TEXT"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("TEXT"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("antag_id") + .HasColumnType("INTEGER"); + + b.Property("AntagName") + .IsRequired() + .HasColumnName("antag_name") + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag"); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("assigned_user_id_id") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("job_id") + .HasColumnType("INTEGER"); + + b.Property("JobName") + .IsRequired() + .HasColumnName("job_name") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnName("priority") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("job"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("preference_id") + .HasColumnType("INTEGER"); + + b.Property("SelectedCharacterSlot") + .HasColumnName("selected_character_slot") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.Property("Age") + .HasColumnName("age") + .HasColumnType("INTEGER"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnName("char_name") + .HasColumnType("TEXT"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnName("eye_color") + .HasColumnType("TEXT"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnName("facial_hair_color") + .HasColumnType("TEXT"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnName("facial_hair_name") + .HasColumnType("TEXT"); + + b.Property("HairColor") + .IsRequired() + .HasColumnName("hair_color") + .HasColumnType("TEXT"); + + b.Property("HairName") + .IsRequired() + .HasColumnName("hair_name") + .HasColumnType("TEXT"); + + b.Property("PreferenceId") + .HasColumnName("preference_id") + .HasColumnType("INTEGER"); + + b.Property("PreferenceUnavailable") + .HasColumnName("pref_unavailable") + .HasColumnType("INTEGER"); + + b.Property("Sex") + .IsRequired() + .HasColumnName("sex") + .HasColumnType("TEXT"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnName("skin_color") + .HasColumnType("TEXT"); + + b.Property("Slot") + .HasColumnName("slot") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PreferenceId"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("connection_log_id") + .HasColumnType("INTEGER"); + + b.Property("Address") + .IsRequired() + .HasColumnName("address") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("connection_log"); + }); + + modelBuilder.Entity("Content.Server.Database.SqlitePlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("player_id") + .HasColumnType("INTEGER"); + + b.Property("FirstSeenTime") + .HasColumnName("first_seen_time") + .HasColumnType("TEXT"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnName("last_seen_address") + .HasColumnType("TEXT"); + + b.Property("LastSeenTime") + .HasColumnName("last_seen_time") + .HasColumnType("TEXT"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnName("last_seen_user_name") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenUserName"); + + b.ToTable("player"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("ban_id") + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnName("address") + .HasColumnType("TEXT"); + + b.Property("BanTime") + .HasColumnName("ban_time") + .HasColumnType("TEXT"); + + b.Property("BanningAdmin") + .HasColumnName("banning_admin") + .HasColumnType("TEXT"); + + b.Property("ExpirationTime") + .HasColumnName("expiration_time") + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasColumnName("reason") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ban"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("unban_id") + .HasColumnType("INTEGER"); + + b.Property("BanId") + .HasColumnName("ban_id") + .HasColumnType("INTEGER"); + + b.Property("UnbanTime") + .HasColumnName("unban_time") + .HasColumnType("TEXT"); + + b.Property("UnbanningAdmin") + .HasColumnName("unbanning_admin") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("unban"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b => + { + b.HasOne("Content.Server.Database.SqliteServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.SqliteServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.cs b/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.cs new file mode 100644 index 0000000000..0c7368a51a --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Content.Server.Database.Migrations.Sqlite +{ + public partial class ExtraIndices : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_player_last_seen_user_name", + table: "player", + column: "last_seen_user_name"); + + migrationBuilder.CreateIndex( + name: "IX_admin_rank_flag_flag_admin_rank_id", + table: "admin_rank_flag", + columns: new[] { "flag", "admin_rank_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_admin_flag_flag_admin_id", + table: "admin_flag", + columns: new[] { "flag", "admin_id" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_player_last_seen_user_name", + table: "player"); + + migrationBuilder.DropIndex( + name: "IX_admin_rank_flag_flag_admin_rank_id", + table: "admin_rank_flag"); + + migrationBuilder.DropIndex( + name: "IX_admin_flag_flag_admin_id", + table: "admin_flag"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs index b00a7e32c7..bf24a18637 100644 --- a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs @@ -16,6 +16,101 @@ namespace Content.Server.Database.Migrations.Sqlite modelBuilder .HasAnnotation("ProductVersion", "3.1.4"); + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("TEXT"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("TEXT"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag"); + }); + modelBuilder.Entity("Content.Server.Database.Antag", b => { b.Property("Id") @@ -251,6 +346,8 @@ namespace Content.Server.Database.Migrations.Sqlite b.HasKey("Id"); + b.HasIndex("LastSeenUserName"); + b.ToTable("player"); }); @@ -318,6 +415,32 @@ namespace Content.Server.Database.Migrations.Sqlite b.ToTable("unban"); }); + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Content.Server.Database.Antag", b => { b.HasOne("Content.Server.Database.Profile", "Profile") diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index 5aa4f6ec81..c52a41fc0c 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; @@ -26,6 +27,8 @@ namespace Content.Server.Database public DbSet Preference { get; set; } = null!; public DbSet Profile { get; set; } = null!; public DbSet AssignedUserId { get; set; } = null!; + public DbSet Admin { get; set; } = null!; + public DbSet AdminRank { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -49,6 +52,19 @@ namespace Content.Server.Database modelBuilder.Entity() .HasIndex(p => p.UserId) .IsUnique(); + + modelBuilder.Entity() + .HasOne(p => p.AdminRank) + .WithMany(p => p!.Admins) + .OnDelete(DeleteBehavior.SetNull); + + modelBuilder.Entity() + .HasIndex(f => new {f.Flag, f.AdminId}) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(f => new {f.Flag, f.AdminRankId}) + .IsUnique(); } } @@ -135,4 +151,46 @@ namespace Content.Server.Database [Column("user_id")] public Guid UserId { get; set; } } + + [Table("admin")] + public class Admin + { + [Column("user_id"), Key] public Guid UserId { get; set; } + [Column("title")] public string? Title { get; set; } + + [Column("admin_rank_id")] public int? AdminRankId { get; set; } + public AdminRank? AdminRank { get; set; } + public List Flags { get; set; } = default!; + } + + [Table("admin_flag")] + public class AdminFlag + { + [Column("admin_flag_id")] public int Id { get; set; } + [Column("flag")] public string Flag { get; set; } = default!; + [Column("negative")] public bool Negative { get; set; } + + [Column("admin_id")] public Guid AdminId { get; set; } + public Admin Admin { get; set; } = default!; + } + + [Table("admin_rank")] + public class AdminRank + { + [Column("admin_rank_id")] public int Id { get; set; } + [Column("name")] public string Name { get; set; } = default!; + + public List Admins { get; set; } = default!; + public List Flags { get; set; } = default!; + } + + [Table("admin_rank_flag")] + public class AdminRankFlag + { + [Column("admin_rank_flag_id")] public int Id { get; set; } + [Column("flag")] public string Flag { get; set; } = default!; + + [Column("admin_rank_id")] public int AdminRankId { get; set; } + public AdminRank Rank { get; set; } = default!; + } } diff --git a/Content.Server.Database/ModelPostgres.cs b/Content.Server.Database/ModelPostgres.cs index 998c517c51..400768227c 100644 --- a/Content.Server.Database/ModelPostgres.cs +++ b/Content.Server.Database/ModelPostgres.cs @@ -65,6 +65,9 @@ namespace Content.Server.Database .HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + modelBuilder.Entity() + .HasIndex(p => p.LastSeenUserName); + modelBuilder.Entity() .HasIndex(p => p.UserId); diff --git a/Content.Server.Database/ModelSqlite.cs b/Content.Server.Database/ModelSqlite.cs index 0a7ca69e48..4032bb2b28 100644 --- a/Content.Server.Database/ModelSqlite.cs +++ b/Content.Server.Database/ModelSqlite.cs @@ -21,6 +21,14 @@ namespace Content.Server.Database options.UseSqlite("dummy connection string"); } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasIndex(p => p.LastSeenUserName); + } + public SqliteServerDbContext(DbContextOptions options) : base(options) { } diff --git a/Content.Server/Administration/AdminCommandAttribute.cs b/Content.Server/Administration/AdminCommandAttribute.cs new file mode 100644 index 0000000000..79ae7fc769 --- /dev/null +++ b/Content.Server/Administration/AdminCommandAttribute.cs @@ -0,0 +1,27 @@ +using System; +using Content.Shared.Administration; +using JetBrains.Annotations; +using Robust.Server.Interfaces.Console; + +namespace Content.Server.Administration +{ + /// + /// Specifies that a command can only be executed by an admin with the specified flags. + /// + /// + /// If this attribute is used multiple times, either attribute's flag sets can be used to get access. + /// + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [BaseTypeRequired(typeof(IClientCommand))] + [MeansImplicitUse] + public sealed class AdminCommandAttribute : Attribute + { + public AdminCommandAttribute(AdminFlags flags) + { + Flags = flags; + } + + public AdminFlags Flags { get; } + } +} diff --git a/Content.Server/Administration/AdminManager.cs b/Content.Server/Administration/AdminManager.cs new file mode 100644 index 0000000000..4c1b9fc4d5 --- /dev/null +++ b/Content.Server/Administration/AdminManager.cs @@ -0,0 +1,478 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using Content.Server.Database; +using Content.Server.Interfaces.Chat; +using Content.Server.Players; +using Content.Shared; +using Content.Shared.Administration; +using Content.Shared.Network.NetMessages; +using Robust.Server.Console; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.Interfaces.Configuration; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Resources; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Utility; +using YamlDotNet.RepresentationModel; + +#nullable enable + +namespace Content.Server.Administration +{ + public sealed class AdminManager : IAdminManager, IPostInjectInit, IConGroupControllerImplementation + { + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IServerDbManager _dbManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IServerNetManager _netMgr = default!; + [Dependency] private readonly IConGroupController _conGroup = default!; + [Dependency] private readonly IResourceManager _res = default!; + [Dependency] private readonly IConsoleShell _consoleShell = default!; + [Dependency] private readonly IChatManager _chat = default!; + + private readonly Dictionary _admins = new Dictionary(); + + public event Action? OnPermsChanged; + + public IEnumerable ActiveAdmins => _admins + .Where(p => p.Value.Data.Active) + .Select(p => p.Key); + + // If a command isn't in this list it's server-console only. + // if a command is in but the flags value is null it's available to everybody. + private readonly HashSet _anyCommands = new HashSet(); + private readonly Dictionary _adminCommands = new Dictionary(); + + public AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false) + { + if (_admins.TryGetValue(session, out var reg) && (reg.Data.Active || includeDeAdmin)) + { + return reg.Data; + } + + return null; + } + + public void DeAdmin(IPlayerSession session) + { + if (!_admins.TryGetValue(session, out var reg)) + { + throw new ArgumentException($"Player {session} is not an admin"); + } + + if (!reg.Data.Active) + { + return; + } + + _chat.SendAdminAnnouncement(Loc.GetString("{0} de-adminned themselves.", session.Name)); + _chat.DispatchServerMessage(session, Loc.GetString("You are now a normal player.")); + + var plyData = session.ContentData()!; + plyData.ExplicitlyDeadminned = true; + reg.Data.Active = false; + + SendPermsChangedEvent(session); + UpdateAdminStatus(session); + } + + public void ReAdmin(IPlayerSession session) + { + if (!_admins.TryGetValue(session, out var reg)) + { + throw new ArgumentException($"Player {session} is not an admin"); + } + + _chat.DispatchServerMessage(session, Loc.GetString("You are now an admin.")); + + var plyData = session.ContentData()!; + plyData.ExplicitlyDeadminned = false; + reg.Data.Active = true; + + _chat.SendAdminAnnouncement(Loc.GetString("{0} re-adminned themselves.", session.Name)); + + SendPermsChangedEvent(session); + UpdateAdminStatus(session); + } + + public async void ReloadAdmin(IPlayerSession player) + { + var data = await LoadAdminData(player); + var curAdmin = _admins.GetValueOrDefault(player); + + if (data == null && curAdmin == null) + { + // Wasn't admin before or after. + return; + } + + if (data == null) + { + // No longer admin. + _admins.Remove(player); + _chat.DispatchServerMessage(player, Loc.GetString("You are no longer an admin.")); + } + else + { + var (aData, rankId, special) = data.Value; + + if (curAdmin == null) + { + // Now an admin. + var reg = new AdminReg(player, aData) + { + IsSpecialLogin = special, + RankId = rankId + }; + _admins.Add(player, reg); + _chat.DispatchServerMessage(player, Loc.GetString("You are now an admin.")); + } + else + { + // Perms changed. + curAdmin.IsSpecialLogin = special; + curAdmin.RankId = rankId; + curAdmin.Data = aData; + } + + if (!player.ContentData()!.ExplicitlyDeadminned) + { + aData.Active = true; + + _chat.DispatchServerMessage(player, Loc.GetString("Your admin permissions have been updated.")); + } + } + + SendPermsChangedEvent(player); + UpdateAdminStatus(player); + } + + public void ReloadAdminsWithRank(int rankId) + { + foreach (var dat in _admins.Values.Where(p => p.RankId == rankId).ToArray()) + { + ReloadAdmin(dat.Session); + } + } + + public void Initialize() + { + _netMgr.RegisterNetMessage(MsgUpdateAdminStatus.NAME); + + // Cache permissions for loaded console commands with the requisite attributes. + foreach (var (cmdName, cmd) in _consoleShell.AvailableCommands) + { + var (isAvail, flagsReq) = GetRequiredFlag(cmd); + + if (!isAvail) + { + continue; + } + + if (flagsReq.Length != 0) + { + _adminCommands.Add(cmdName, flagsReq); + } + else + { + _anyCommands.Add(cmdName); + } + } + + // Load flags for engine commands, since those don't have the attributes. + if (_res.TryContentFileRead(new ResourcePath("/engineCommandPerms.yml"), out var fs)) + { + using var reader = new StreamReader(fs, EncodingHelpers.UTF8); + var yStream = new YamlStream(); + yStream.Load(reader); + var root = (YamlSequenceNode) yStream.Documents[0].RootNode; + + foreach (var child in root) + { + var map = (YamlMappingNode) child; + var commands = map.GetNode("Commands").Select(p => p.AsString()); + if (map.TryGetNode("Flags", out var flagsNode)) + { + var flagNames = flagsNode.AsString().Split(",", StringSplitOptions.RemoveEmptyEntries); + var flags = AdminFlagsHelper.NamesToFlags(flagNames); + foreach (var cmd in commands) + { + if (!_adminCommands.TryGetValue(cmd, out var exFlags)) + { + _adminCommands.Add(cmd, new[] {flags}); + } + else + { + var newArr = new AdminFlags[exFlags.Length + 1]; + exFlags.CopyTo(newArr, 0); + exFlags[^1] = flags; + _adminCommands[cmd] = newArr; + } + } + } + else + { + _anyCommands.UnionWith(commands); + } + } + } + } + + void IPostInjectInit.PostInject() + { + _playerManager.PlayerStatusChanged += PlayerStatusChanged; + _conGroup.Implementation = this; + } + + // NOTE: Also sends commands list for non admins.. + private void UpdateAdminStatus(IPlayerSession session) + { + var msg = _netMgr.CreateNetMessage(); + + var commands = new List(_anyCommands); + + if (_admins.TryGetValue(session, out var adminData)) + { + msg.Admin = adminData.Data; + + commands.AddRange(_adminCommands + .Where(p => p.Value.Any(f => adminData.Data.HasFlag(f))) + .Select(p => p.Key)); + } + + msg.AvailableCommands = commands.ToArray(); + + _netMgr.ServerSendMessage(msg, session.ConnectedClient); + } + + private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus == SessionStatus.Connected) + { + // Run this so that available commands list gets sent. + UpdateAdminStatus(e.Session); + } + else if (e.NewStatus == SessionStatus.InGame) + { + LoginAdminMaybe(e.Session); + } + else if (e.NewStatus == SessionStatus.Disconnected) + { + _admins.Remove(e.Session); + + if (_cfg.GetCVar(CCVars.AdminAnnounceLogout)) + { + _chat.SendAdminAnnouncement(Loc.GetString("Admin logout: {0}", e.Session.Name)); + } + } + } + + private async void LoginAdminMaybe(IPlayerSession session) + { + var adminDat = await LoadAdminData(session); + if (adminDat == null) + { + // Not an admin. + return; + } + + var (dat, rankId, specialLogin) = adminDat.Value; + var reg = new AdminReg(session, dat) + { + IsSpecialLogin = specialLogin, + RankId = rankId + }; + + _admins.Add(session, reg); + + if (!session.ContentData()!.ExplicitlyDeadminned) + { + reg.Data.Active = true; + + if (_cfg.GetCVar(CCVars.AdminAnnounceLogin)) + { + _chat.SendAdminAnnouncement(Loc.GetString("Admin login: {0}", session.Name)); + } + + SendPermsChangedEvent(session); + } + + UpdateAdminStatus(session); + } + + private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminData(IPlayerSession session) + { + if (IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal)) + { + var data = new AdminData + { + Title = Loc.GetString("Host"), + Flags = AdminFlagsHelper.Everything, + }; + + return (data, null, true); + } + else + { + var dbData = await _dbManager.GetAdminDataForAsync(session.UserId); + + if (dbData == null) + { + // Not an admin! + return null; + } + + var flags = AdminFlags.None; + + if (dbData.AdminRank != null) + { + flags = AdminFlagsHelper.NamesToFlags(dbData.AdminRank.Flags.Select(p => p.Flag)); + } + + foreach (var dbFlag in dbData.Flags) + { + var flag = AdminFlagsHelper.NameToFlag(dbFlag.Flag); + if (dbFlag.Negative) + { + flags &= ~flag; + } + else + { + flags |= flag; + } + } + + var data = new AdminData + { + Flags = flags + }; + + if (dbData.Title != null) + { + data.Title = dbData.Title; + } + else if (dbData.AdminRank != null) + { + data.Title = dbData.AdminRank.Name; + } + + return (data, dbData.AdminRankId, false); + } + } + + private static bool IsLocal(IPlayerSession player) + { + var ep = player.ConnectedClient.RemoteEndPoint; + var addr = ep.Address; + if (addr.IsIPv4MappedToIPv6) + { + addr = addr.MapToIPv4(); + } + + return Equals(addr, IPAddress.Loopback) || Equals(addr, IPAddress.IPv6Loopback); + } + + public bool CanCommand(IPlayerSession session, string cmdName) + { + if (_anyCommands.Contains(cmdName)) + { + // Anybody can use this command. + return true; + } + + if (!_adminCommands.TryGetValue(cmdName, out var flagsReq)) + { + // Server-console only. + return false; + } + + var data = GetAdminData(session); + if (data == null) + { + // Player isn't an admin. + return false; + } + + foreach (var flagReq in flagsReq) + { + if (data.HasFlag(flagReq)) + { + return true; + } + } + + return false; + } + + private static (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlag(IClientCommand cmd) + { + var type = cmd.GetType(); + if (Attribute.IsDefined(type, typeof(AnyCommandAttribute))) + { + // Available to everybody. + return (true, Array.Empty()); + } + + var attribs = type.GetCustomAttributes(typeof(AdminCommandAttribute)) + .Cast() + .Select(p => p.Flags) + .ToArray(); + + // If attribs.length == 0 then no access attribute is specified, + // and this is a server-only command. + return (attribs.Length != 0, attribs); + } + + public bool CanViewVar(IPlayerSession session) + { + return GetAdminData(session)?.CanViewVar() ?? false; + } + + public bool CanAdminPlace(IPlayerSession session) + { + return GetAdminData(session)?.CanAdminPlace() ?? false; + } + + public bool CanScript(IPlayerSession session) + { + return GetAdminData(session)?.CanScript() ?? false; + } + + public bool CanAdminMenu(IPlayerSession session) + { + return GetAdminData(session)?.CanAdminMenu() ?? false; + } + + private void SendPermsChangedEvent(IPlayerSession session) + { + var flags = GetAdminData(session)?.Flags; + OnPermsChanged?.Invoke(new AdminPermsChangedEventArgs(session, flags)); + } + + private sealed class AdminReg + { + public IPlayerSession Session; + + public AdminData Data; + public int? RankId; + + // Such as console.loginlocal + public bool IsSpecialLogin; + + public AdminReg(IPlayerSession session, AdminData data) + { + Data = data; + Session = session; + } + } + } +} diff --git a/Content.Server/Administration/AdminPermsChangedEventArgs.cs b/Content.Server/Administration/AdminPermsChangedEventArgs.cs new file mode 100644 index 0000000000..de2a7b0dbd --- /dev/null +++ b/Content.Server/Administration/AdminPermsChangedEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using Content.Shared.Administration; +using Robust.Server.Interfaces.Player; + +namespace Content.Server.Administration +{ + /// + /// Sealed when the permissions of an admin on the server change. + /// + public sealed class AdminPermsChangedEventArgs : EventArgs + { + public AdminPermsChangedEventArgs(IPlayerSession player, AdminFlags? flags) + { + Player = player; + Flags = flags; + } + + /// + /// The player that had their admin permissions changed. + /// + public IPlayerSession Player { get; } + + /// + /// The admin flags of the player. Null if the player is no longer an admin. + /// + public AdminFlags? Flags { get; } + + /// + /// Whether the player is now an admin. + /// + public bool IsAdmin => Flags.HasValue; + } +} diff --git a/Content.Server/Administration/AdminRank.cs b/Content.Server/Administration/AdminRank.cs new file mode 100644 index 0000000000..925022498e --- /dev/null +++ b/Content.Server/Administration/AdminRank.cs @@ -0,0 +1,18 @@ +using Content.Shared.Administration; + +#nullable enable + +namespace Content.Server.Administration +{ + public sealed class AdminRank + { + public AdminRank(string name, AdminFlags flags) + { + Name = name; + Flags = flags; + } + + public string Name { get; } + public AdminFlags Flags { get; } + } +} diff --git a/Content.Server/Administration/AnyCommandAttribute.cs b/Content.Server/Administration/AnyCommandAttribute.cs new file mode 100644 index 0000000000..ab9895f728 --- /dev/null +++ b/Content.Server/Administration/AnyCommandAttribute.cs @@ -0,0 +1,18 @@ +using System; +using JetBrains.Annotations; +using Robust.Server.Interfaces.Console; + +namespace Content.Server.Administration +{ + /// + /// Specifies that a command can be executed by any player. + /// + /// + [AttributeUsage(AttributeTargets.Class)] + [BaseTypeRequired(typeof(IClientCommand))] + [MeansImplicitUse] + public sealed class AnyCommandAttribute : Attribute + { + + } +} diff --git a/Content.Server/Administration/AGhost.cs b/Content.Server/Administration/Commands/AGhost.cs similarity index 93% rename from Content.Server/Administration/AGhost.cs rename to Content.Server/Administration/Commands/AGhost.cs index 5226c4637f..89c71d0346 100644 --- a/Content.Server/Administration/AGhost.cs +++ b/Content.Server/Administration/Commands/AGhost.cs @@ -1,12 +1,14 @@ using Content.Server.GameObjects.Components.Observer; using Content.Server.Players; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] public class AGhost : IClientCommand { public string Command => "aghost"; diff --git a/Content.Server/Administration/BanCommand.cs b/Content.Server/Administration/Commands/BanCommand.cs similarity index 93% rename from Content.Server/Administration/BanCommand.cs rename to Content.Server/Administration/Commands/BanCommand.cs index 2dbabd2302..f542d1c0c2 100644 --- a/Content.Server/Administration/BanCommand.cs +++ b/Content.Server/Administration/Commands/BanCommand.cs @@ -1,6 +1,6 @@ using System; -using System.Net; using Content.Server.Database; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.IoC; @@ -8,8 +8,9 @@ using Robust.Shared.Network; #nullable enable -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Ban)] public sealed class BanCommand : IClientCommand { public string Command => "ban"; diff --git a/Content.Server/Administration/ControlMob.cs b/Content.Server/Administration/Commands/ControlMob.cs similarity index 94% rename from Content.Server/Administration/ControlMob.cs rename to Content.Server/Administration/Commands/ControlMob.cs index 317f2d77ee..1e979e633a 100644 --- a/Content.Server/Administration/ControlMob.cs +++ b/Content.Server/Administration/Commands/ControlMob.cs @@ -1,6 +1,7 @@ using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Observer; using Content.Server.Players; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; @@ -8,8 +9,9 @@ using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] class ControlMob : IClientCommand { public string Command => "controlmob"; diff --git a/Content.Server/Administration/Commands/DeAdminCommand.cs b/Content.Server/Administration/Commands/DeAdminCommand.cs new file mode 100644 index 0000000000..81a61194cf --- /dev/null +++ b/Content.Server/Administration/Commands/DeAdminCommand.cs @@ -0,0 +1,31 @@ +using Content.Shared.Administration; +using JetBrains.Annotations; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Server.Administration.Commands +{ + [UsedImplicitly] + [AdminCommand(AdminFlags.None)] + public class DeAdminCommand : IClientCommand + { + public string Command => "deadmin"; + public string Description => "Temporarily de-admins you so you can experience the round as a normal player."; + public string Help => "Usage: deadmin\nUse readmin to re-admin after using this."; + + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (player == null) + { + shell.SendText(player, "You cannot use this command from the server console."); + return; + } + + var mgr = IoCManager.Resolve(); + mgr.DeAdmin(player); + } + } +} diff --git a/Content.Server/Administration/DeleteEntitiesWithComponent.cs b/Content.Server/Administration/Commands/DeleteEntitiesWithComponent.cs similarity index 93% rename from Content.Server/Administration/DeleteEntitiesWithComponent.cs rename to Content.Server/Administration/Commands/DeleteEntitiesWithComponent.cs index 0df4af06b8..f87bd7a99b 100644 --- a/Content.Server/Administration/DeleteEntitiesWithComponent.cs +++ b/Content.Server/Administration/Commands/DeleteEntitiesWithComponent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; @@ -7,8 +8,9 @@ using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] class DeleteEntitiesWithComponent : IClientCommand { public string Command => "deleteewc"; diff --git a/Content.Server/Administration/DeleteEntitiesWithId.cs b/Content.Server/Administration/Commands/DeleteEntitiesWithId.cs similarity index 90% rename from Content.Server/Administration/DeleteEntitiesWithId.cs rename to Content.Server/Administration/Commands/DeleteEntitiesWithId.cs index b2f3448167..d7ff635c10 100644 --- a/Content.Server/Administration/DeleteEntitiesWithId.cs +++ b/Content.Server/Administration/Commands/DeleteEntitiesWithId.cs @@ -1,12 +1,14 @@ #nullable enable +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] public class DeleteEntitiesWithId : IClientCommand { public string Command => "deleteewi"; diff --git a/Content.Server/Administration/Commands/OpenPermissionsCommand.cs b/Content.Server/Administration/Commands/OpenPermissionsCommand.cs new file mode 100644 index 0000000000..093a10733a --- /dev/null +++ b/Content.Server/Administration/Commands/OpenPermissionsCommand.cs @@ -0,0 +1,31 @@ +using Content.Server.Eui; +using Content.Shared.Administration; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Server.Administration.Commands +{ + [AdminCommand(AdminFlags.Permissions)] + public sealed class OpenPermissionsCommand : IClientCommand + { + public string Command => "permissions"; + public string Description => "Opens the admin permissions panel."; + public string Help => "Usage: permissions"; + + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (player == null) + { + shell.SendText(player, "This does not work from the server console."); + return; + } + + var eui = IoCManager.Resolve(); + var ui = new PermissionsEui(); + eui.OpenEui(ui, player); + } + } +} diff --git a/Content.Server/Administration/Commands/ReAdminCommand.cs b/Content.Server/Administration/Commands/ReAdminCommand.cs new file mode 100644 index 0000000000..8f78442689 --- /dev/null +++ b/Content.Server/Administration/Commands/ReAdminCommand.cs @@ -0,0 +1,35 @@ +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Server.Administration.Commands +{ + [AnyCommand] + public class ReAdminCommand : IClientCommand + { + public string Command => "readmin"; + public string Description => "Re-admins you if you previously de-adminned."; + public string Help => "Usage: readmin"; + + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (player == null) + { + shell.SendText(player, "You cannot use this command from the server console."); + return; + } + + var mgr = IoCManager.Resolve(); + + if (mgr.GetAdminData(player, includeDeAdmin: true) == null) + { + shell.SendText(player, "You're not an admin."); + return; + } + + mgr.ReAdmin(player); + } + } +} diff --git a/Content.Server/Administration/ReadyAll.cs b/Content.Server/Administration/Commands/ReadyAll.cs similarity index 90% rename from Content.Server/Administration/ReadyAll.cs rename to Content.Server/Administration/Commands/ReadyAll.cs index 05d0b48fc8..ec703bb5b7 100644 --- a/Content.Server/Administration/ReadyAll.cs +++ b/Content.Server/Administration/Commands/ReadyAll.cs @@ -1,12 +1,14 @@ #nullable enable using Content.Server.GameTicking; using Content.Server.Interfaces.GameTicking; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.IoC; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Server)] public class ReadyAll : IClientCommand { public string Command => "readyall"; diff --git a/Content.Server/Administration/Rejuvenate.cs b/Content.Server/Administration/Commands/Rejuvenate.cs similarity index 93% rename from Content.Server/Administration/Rejuvenate.cs rename to Content.Server/Administration/Commands/Rejuvenate.cs index deb59d4260..69c79a36b8 100644 --- a/Content.Server/Administration/Rejuvenate.cs +++ b/Content.Server/Administration/Commands/Rejuvenate.cs @@ -1,4 +1,5 @@ using Content.Server.GlobalVerbs; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; @@ -6,8 +7,9 @@ using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] class Rejuvenate : IClientCommand { public string Command => "rejuvenate"; diff --git a/Content.Server/Administration/WarpCommand.cs b/Content.Server/Administration/Commands/WarpCommand.cs similarity index 97% rename from Content.Server/Administration/WarpCommand.cs rename to Content.Server/Administration/Commands/WarpCommand.cs index 45fb041a40..6e48b9637d 100644 --- a/Content.Server/Administration/WarpCommand.cs +++ b/Content.Server/Administration/Commands/WarpCommand.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Content.Server.GameObjects.Components.Markers; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.Enums; @@ -10,8 +11,9 @@ using Robust.Shared.Interfaces.Map; using Robust.Shared.IoC; using Robust.Shared.Map; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] public class WarpCommand : IClientCommand { public string Command => "warp"; diff --git a/Content.Server/Administration/IAdminManager.cs b/Content.Server/Administration/IAdminManager.cs new file mode 100644 index 0000000000..ba2088f0b6 --- /dev/null +++ b/Content.Server/Administration/IAdminManager.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Administration; +using Robust.Server.Interfaces.Player; + +#nullable enable + +namespace Content.Server.Administration +{ + /// + /// Manages server administrators and their permission flags. + /// + public interface IAdminManager + { + /// + /// Fired when the permissions of an admin on the server changed. + /// + event Action OnPermsChanged; + + /// + /// Gets all active admins currently on the server. + /// + /// + /// This does not include admins that are de-adminned. + /// + IEnumerable ActiveAdmins { get; } + + /// + /// Gets the admin data for a player, if they are an admin. + /// + /// The player to get admin data for. + /// + /// Whether to return admin data for admins that are current de-adminned. + /// + /// if the player is not an admin. + AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false); + + /// + /// See if a player has an admin flag. + /// + /// True if the player is and admin and has the specified flags. + bool HasAdminFlag(IPlayerSession player, AdminFlags flag) + { + var data = GetAdminData(player); + return data != null && data.HasFlag(flag); + } + + /// + /// De-admins an admin temporarily so they are effectively a normal player. + /// + /// + /// De-adminned admins are able to re-admin at any time if they so desire. + /// + void DeAdmin(IPlayerSession session); + + /// + /// Re-admins a de-adminned admin. + /// + void ReAdmin(IPlayerSession session); + + /// + /// Re-loads the permissions of an player in case their admin data changed DB-side. + /// + /// + void ReloadAdmin(IPlayerSession player); + + /// + /// Reloads admin permissions for all admins with a certain rank. + /// + /// The database ID of the rank. + /// + void ReloadAdminsWithRank(int rankId); + + void Initialize(); + } +} diff --git a/Content.Server/Administration/PermissionsEui.cs b/Content.Server/Administration/PermissionsEui.cs new file mode 100644 index 0000000000..2c5ba53b08 --- /dev/null +++ b/Content.Server/Administration/PermissionsEui.cs @@ -0,0 +1,460 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Content.Server.Database; +using Content.Server.Eui; +using Content.Shared.Administration; +using Content.Shared.Eui; +using Robust.Server.Interfaces.Player; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Network; +using DbAdminRank = Content.Server.Database.AdminRank; +using static Content.Shared.Administration.PermissionsEuiMsg; + +#nullable enable + +namespace Content.Server.Administration +{ + public sealed class PermissionsEui : BaseEui + { + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IServerDbManager _db = default!; + [Dependency] private readonly IAdminManager _adminManager = default!; + + private bool _isLoading; + + private readonly List<(Admin a, string? lastUserName)> _admins = new List<(Admin, string? lastUserName)>(); + private readonly List _adminRanks = new List(); + + public PermissionsEui() + { + IoCManager.InjectDependencies(this); + } + + public override void Opened() + { + base.Opened(); + + StateDirty(); + LoadFromDb(); + _adminManager.OnPermsChanged += AdminManagerOnPermsChanged; + } + + public override void Closed() + { + base.Closed(); + + _adminManager.OnPermsChanged -= AdminManagerOnPermsChanged; + } + + private void AdminManagerOnPermsChanged(AdminPermsChangedEventArgs obj) + { + // Close UI if user loses +PERMISSIONS. + if (obj.Player == Player && !UserAdminFlagCheck(AdminFlags.Permissions)) + { + Close(); + } + } + + public override EuiStateBase GetNewState() + { + if (_isLoading) + { + return new PermissionsEuiState + { + IsLoading = true + }; + } + + return new PermissionsEuiState + { + Admins = _admins.Select(p => new PermissionsEuiState.AdminData + { + PosFlags = AdminFlagsHelper.NamesToFlags(p.a.Flags.Where(f => !f.Negative).Select(f => f.Flag)), + NegFlags = AdminFlagsHelper.NamesToFlags(p.a.Flags.Where(f => f.Negative).Select(f => f.Flag)), + Title = p.a.Title, + RankId = p.a.AdminRankId, + UserId = new NetUserId(p.a.UserId), + UserName = p.lastUserName + }).ToArray(), + + AdminRanks = _adminRanks.ToDictionary(a => a.Id, a => new PermissionsEuiState.AdminRankData + { + Flags = AdminFlagsHelper.NamesToFlags(a.Flags.Select(p => p.Flag)), + Name = a.Name + }) + }; + } + + public override async void HandleMessage(EuiMessageBase msg) + { + switch (msg) + { + case Close _: + { + Close(); + break; + } + + case AddAdmin ca: + { + await HandleCreateAdmin(ca); + break; + } + + case UpdateAdmin ua: + { + await HandleUpdateAdmin(ua); + break; + } + + case RemoveAdmin ra: + { + await HandleRemoveAdmin(ra); + break; + } + + case AddAdminRank ar: + { + await HandleAddAdminRank(ar); + break; + } + + case UpdateAdminRank ur: + { + await HandleUpdateAdminRank(ur); + break; + } + + case RemoveAdminRank ra: + { + await HandleRemoveAdminRank(ra); + break; + } + } + + if (!IsShutDown) + { + LoadFromDb(); + } + } + + private async Task HandleRemoveAdminRank(RemoveAdminRank rr) + { + var rank = await _db.GetAdminRankAsync(rr.Id); + if (rank == null) + { + return; + } + + if (!CanTouchRank(rank)) + { + Logger.WarningS("admin.perms", $"{Player} tried to remove higher-ranked admin rank {rank.Name}"); + return; + } + + await _db.RemoveAdminRankAsync(rr.Id); + + _adminManager.ReloadAdminsWithRank(rr.Id); + } + + private async Task HandleUpdateAdminRank(UpdateAdminRank ur) + { + var rank = await _db.GetAdminRankAsync(ur.Id); + if (rank == null) + { + return; + } + + if (!CanTouchRank(rank)) + { + Logger.WarningS("admin.perms", $"{Player} tried to update higher-ranked admin rank {rank.Name}"); + return; + } + + if (!UserAdminFlagCheck(ur.Flags)) + { + Logger.WarningS("admin.perms", $"{Player} tried to give a rank permissions above their authorization."); + return; + } + + rank.Flags = GenRankFlagList(ur.Flags); + rank.Name = ur.Name; + + await _db.UpdateAdminRankAsync(rank); + + var flagText = string.Join(' ', AdminFlagsHelper.FlagsToNames(ur.Flags).Select(f => $"+{f}")); + Logger.InfoS("admin.perms", $"{Player} updated admin rank {rank.Name}/{flagText}."); + + _adminManager.ReloadAdminsWithRank(ur.Id); + } + + private async Task HandleAddAdminRank(AddAdminRank ar) + { + if (!UserAdminFlagCheck(ar.Flags)) + { + Logger.WarningS("admin.perms", $"{Player} tried to give a rank permissions above their authorization."); + return; + } + + var rank = new DbAdminRank + { + Name = ar.Name, + Flags = GenRankFlagList(ar.Flags) + }; + + await _db.AddAdminRankAsync(rank); + + var flagText = string.Join(' ', AdminFlagsHelper.FlagsToNames(ar.Flags).Select(f => $"+{f}")); + Logger.InfoS("admin.perms", $"{Player} added admin rank {rank.Name}/{flagText}."); + } + + private async Task HandleRemoveAdmin(RemoveAdmin ra) + { + var admin = await _db.GetAdminDataForAsync(ra.UserId); + if (admin == null) + { + // Doesn't exist. + return; + } + + if (!CanTouchAdmin(admin)) + { + Logger.WarningS("admin.perms", $"{Player} tried to remove higher-ranked admin {ra.UserId.ToString()}"); + return; + } + + await _db.RemoveAdminAsync(ra.UserId); + + var record = await _db.GetPlayerRecordByUserId(ra.UserId); + Logger.InfoS("admin.perms", $"{Player} removed admin {record?.LastSeenUserName ?? ra.UserId.ToString()}"); + + if (_playerManager.TryGetSessionById(ra.UserId, out var player)) + { + _adminManager.ReloadAdmin(player); + } + } + + private async Task HandleUpdateAdmin(UpdateAdmin ua) + { + if (!CheckCreatePerms(ua.PosFlags, ua.NegFlags)) + { + return; + } + + var admin = await _db.GetAdminDataForAsync(ua.UserId); + if (admin == null) + { + // Was removed in the mean time I guess? + return; + } + + if (!CanTouchAdmin(admin)) + { + Logger.WarningS("admin.perms", $"{Player} tried to modify higher-ranked admin {ua.UserId.ToString()}"); + return; + } + + admin.Title = ua.Title; + admin.AdminRankId = ua.RankId; + admin.Flags = GenAdminFlagList(ua.PosFlags, ua.NegFlags); + + await _db.UpdateAdminAsync(admin); + + var playerRecord = await _db.GetPlayerRecordByUserId(ua.UserId); + var (bad, rankName) = await FetchAndCheckRank(ua.RankId); + if (bad) + { + return; + } + + var name = playerRecord?.LastSeenUserName ?? ua.UserId.ToString(); + var title = ua.Title ?? ""; + var flags = AdminFlagsHelper.PosNegFlagsText(ua.PosFlags, ua.NegFlags); + + Logger.InfoS("admin.perms", $"{Player} updated admin {name} to {title}/{rankName}/{flags}"); + + if (_playerManager.TryGetSessionById(ua.UserId, out var player)) + { + _adminManager.ReloadAdmin(player); + } + } + + private async Task HandleCreateAdmin(AddAdmin ca) + { + if (!CheckCreatePerms(ca.PosFlags, ca.NegFlags)) + { + return; + } + + string name; + NetUserId userId; + if (Guid.TryParse(ca.UserNameOrId, out var guid)) + { + userId = new NetUserId(guid); + var playerRecord = await _db.GetPlayerRecordByUserId(userId); + if (playerRecord == null) + { + name = userId.ToString(); + } + else + { + name = playerRecord.LastSeenUserName; + } + } + else + { + // Username entered, resolve user ID from DB. + var dbPlayer = await _db.GetPlayerRecordByUserName(ca.UserNameOrId); + if (dbPlayer == null) + { + // username not in DB. + // TODO: Notify user. + Logger.WarningS("admin.perms", + $"{Player} tried to add admin with unknown username {ca.UserNameOrId}."); + return; + } + + userId = dbPlayer.UserId; + name = ca.UserNameOrId; + } + + var existing = await _db.GetAdminDataForAsync(userId); + if (existing != null) + { + // Already exists. + return; + } + + var (bad, rankName) = await FetchAndCheckRank(ca.RankId); + if (bad) + { + return; + } + + rankName ??= ""; + + var admin = new Admin + { + Flags = GenAdminFlagList(ca.PosFlags, ca.NegFlags), + AdminRankId = ca.RankId, + UserId = userId.UserId, + Title = ca.Title + }; + + await _db.AddAdminAsync(admin); + + var title = ca.Title ?? ""; + var flags = AdminFlagsHelper.PosNegFlagsText(ca.PosFlags, ca.NegFlags); + + Logger.InfoS("admin.perms", $"{Player} added admin {name} as {title}/{rankName}/{flags}"); + + if (_playerManager.TryGetSessionById(userId, out var player)) + { + _adminManager.ReloadAdmin(player); + } + } + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + private bool CheckCreatePerms(AdminFlags posFlags, AdminFlags negFlags) + { + if ((posFlags & negFlags) != 0) + { + // Can't have overlapping pos and neg flags. + // Just deny the entire message. + return false; + } + + if (!UserAdminFlagCheck(posFlags)) + { + // Can't create an admin with higher perms than yourself, obviously. + Logger.WarningS("admin.perms", $"{Player} tried to grant admin powers above their authorization."); + return false; + } + + return true; + } + + private async Task<(bool bad, string?)> FetchAndCheckRank(int? rankId) + { + string? ret = null; + if (rankId is { } r) + { + var rank = await _db.GetAdminRankAsync(r); + if (rank == null) + { + // Tried to set to nonexistent rank. + Logger.WarningS("admin.perms", $"{Player} tried to assign nonexistent admin rank."); + return (true, null); + } + + ret = rank.Name; + + var rankFlags = AdminFlagsHelper.NamesToFlags(rank.Flags.Select(p => p.Flag)); + if (!UserAdminFlagCheck(rankFlags)) + { + // Can't assign a rank with flags you don't have yourself. + Logger.WarningS("admin.perms", $"{Player} tried to assign admin rank above their authorization."); + return (true, null); + } + } + + return (false, ret); + } + + private async void LoadFromDb() + { + StateDirty(); + _isLoading = true; + var (admins, ranks) = await _db.GetAllAdminAndRanksAsync(); + + _admins.Clear(); + _admins.AddRange(admins); + _adminRanks.Clear(); + _adminRanks.AddRange(ranks); + + _isLoading = false; + StateDirty(); + } + + private static List GenAdminFlagList(AdminFlags posFlags, AdminFlags negFlags) + { + var posFlagList = AdminFlagsHelper.FlagsToNames(posFlags); + var negFlagList = AdminFlagsHelper.FlagsToNames(negFlags); + + return posFlagList + .Select(f => new AdminFlag {Negative = false, Flag = f}) + .Concat(negFlagList.Select(f => new AdminFlag {Negative = true, Flag = f})) + .ToList(); + } + + private static List GenRankFlagList(AdminFlags flags) + { + return AdminFlagsHelper.FlagsToNames(flags).Select(f => new AdminRankFlag {Flag = f}).ToList(); + } + + private bool UserAdminFlagCheck(AdminFlags flags) + { + return _adminManager.HasAdminFlag(Player, flags); + } + + private bool CanTouchAdmin(Admin admin) + { + var posFlags = AdminFlagsHelper.NamesToFlags(admin.Flags.Where(f => !f.Negative).Select(f => f.Flag)); + var rankFlags = AdminFlagsHelper.NamesToFlags( + admin.AdminRank?.Flags.Select(f => f.Flag) ?? Array.Empty()); + + var totalFlags = posFlags | rankFlags; + return UserAdminFlagCheck(totalFlags); + } + + private bool CanTouchRank(DbAdminRank rank) + { + var rankFlags = AdminFlagsHelper.NamesToFlags(rank.Flags.Select(f => f.Flag)); + + return UserAdminFlagCheck(rankFlags); + } + } +} diff --git a/Content.Server/Atmos/AtmosCommands.cs b/Content.Server/Atmos/AtmosCommands.cs index 3785e7fe85..476849febf 100644 --- a/Content.Server/Atmos/AtmosCommands.cs +++ b/Content.Server/Atmos/AtmosCommands.cs @@ -1,8 +1,10 @@ #nullable enable using System; +using Content.Server.Administration; using Content.Server.GameObjects.Components.Atmos; using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems.Atmos; +using Content.Shared.Administration; using Content.Shared.Atmos; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; @@ -15,6 +17,7 @@ using Robust.Shared.Maths; namespace Content.Server.Atmos { + [AdminCommand(AdminFlags.Debug)] public class AddAtmos : IClientCommand { public string Command => "addatmos"; diff --git a/Content.Server/Chat/ChatCommands.cs b/Content.Server/Chat/ChatCommands.cs index 94aa2ae25c..e2d32aa25c 100644 --- a/Content.Server/Chat/ChatCommands.cs +++ b/Content.Server/Chat/ChatCommands.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Content.Server.Administration; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Observer; @@ -8,6 +9,7 @@ using Content.Server.Interfaces.GameObjects; using Content.Server.Observer; using Content.Server.Players; using Content.Server.Utility; +using Content.Shared.Administration; using Content.Shared.Damage; using Content.Shared.GameObjects.Components.Damage; using Content.Shared.Interfaces; @@ -20,6 +22,7 @@ using Robust.Shared.Localization; namespace Content.Server.Chat { + [AnyCommand] internal class SayCommand : IClientCommand { public string Command => "say"; @@ -51,6 +54,7 @@ namespace Content.Server.Chat } } + [AnyCommand] internal class MeCommand : IClientCommand { public string Command => "me"; @@ -76,6 +80,7 @@ namespace Content.Server.Chat } } + [AnyCommand] internal class OOCCommand : IClientCommand { public string Command => "ooc"; @@ -96,6 +101,7 @@ namespace Content.Server.Chat } } + [AdminCommand(AdminFlags.Admin)] internal class AdminChatCommand : IClientCommand { public string Command => "asay"; @@ -116,6 +122,7 @@ namespace Content.Server.Chat } } + [AnyCommand] internal class SuicideCommand : IClientCommand { public string Command => "suicide"; diff --git a/Content.Server/Chat/ChatManager.cs b/Content.Server/Chat/ChatManager.cs index 6550d58fb5..afeb0c11b2 100644 --- a/Content.Server/Chat/ChatManager.cs +++ b/Content.Server/Chat/ChatManager.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Content.Server.GameObjects.Components; +using Content.Server.Administration; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Headset; using Content.Server.GameObjects.Components.Items.Storage; @@ -12,7 +12,6 @@ using Content.Shared.Chat; using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces; -using Robust.Server.Console; using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects.Systems; @@ -47,7 +46,7 @@ namespace Content.Server.Chat [Dependency] private readonly IServerNetManager _netManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IMoMMILink _mommiLink = default!; - [Dependency] private readonly IConGroupController _conGroupController = default!; + [Dependency] private readonly IAdminManager _adminManager = default!; public void Initialize() { @@ -125,7 +124,7 @@ namespace Content.Server.Chat // Capitalize first letter message = message[0].ToString().ToUpper() + - message.Remove(0,1); + message.Remove(0, 1); if (source.TryGetComponent(out InventoryComponent inventory) && inventory.TryGetSlotItem(EquipmentSlotDefines.Slots.EARS, out ItemComponent item) && @@ -142,7 +141,7 @@ namespace Content.Server.Chat { // Capitalize first letter message = message[0].ToString().ToUpper() + - message.Remove(0,1); + message.Remove(0, 1); } var listeners = EntitySystem.Get(); @@ -212,7 +211,9 @@ namespace Content.Server.Chat return; } - var clients = _playerManager.GetPlayersBy(x => x.AttachedEntity != null && x.AttachedEntity.HasComponent()).Select(p => p.ConnectedClient);; + var clients = _playerManager + .GetPlayersBy(x => x.AttachedEntity != null && x.AttachedEntity.HasComponent()) + .Select(p => p.ConnectedClient); var msg = _netManager.CreateNetMessage(); msg.Channel = ChatChannel.Dead; @@ -231,12 +232,7 @@ namespace Content.Server.Chat return; } - if (!_conGroupController.CanCommand(player, "asay")) - { - SendOOC(player, message); - return; - } - var clients = _playerManager.GetPlayersBy(x => _conGroupController.CanCommand(x, "asay")).Select(p => p.ConnectedClient);; + var clients = _adminManager.ActiveAdmins.Select(p => p.ConnectedClient); var msg = _netManager.CreateNetMessage(); @@ -246,6 +242,19 @@ namespace Content.Server.Chat _netManager.ServerSendToMany(msg, clients.ToList()); } + public void SendAdminAnnouncement(string message) + { + var clients = _adminManager.ActiveAdmins.Select(p => p.ConnectedClient); + + var msg = _netManager.CreateNetMessage(); + + msg.Channel = ChatChannel.AdminChat; + msg.Message = message; + msg.MessageWrap = $"{Loc.GetString("ADMIN")}: {{0}}"; + + _netManager.ServerSendToMany(msg, clients.ToList()); + } + public void SendHookOOC(string sender, string message) { var msg = _netManager.CreateNetMessage(); diff --git a/Content.Server/Commands/AttachBodyPartCommand.cs b/Content.Server/Commands/AttachBodyPartCommand.cs index b19c829611..72128390b5 100644 --- a/Content.Server/Commands/AttachBodyPartCommand.cs +++ b/Content.Server/Commands/AttachBodyPartCommand.cs @@ -1,5 +1,7 @@ #nullable enable +using Content.Server.Administration; using Content.Server.GameObjects.Components.Body.Part; +using Content.Shared.Administration; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body.Part; using Robust.Server.Interfaces.Console; @@ -10,6 +12,7 @@ using Robust.Shared.IoC; namespace Content.Server.Commands { + [AdminCommand(AdminFlags.Fun)] public class AttachBodyPartCommand : IClientCommand { public string Command => "attachbodypart"; diff --git a/Content.Server/Commands/HideContainedContextCommand.cs b/Content.Server/Commands/HideContainedContextCommand.cs index a4c7393146..ad975313a1 100644 --- a/Content.Server/Commands/HideContainedContextCommand.cs +++ b/Content.Server/Commands/HideContainedContextCommand.cs @@ -1,11 +1,14 @@ #nullable enable +using Content.Server.Administration; using Content.Server.GameObjects.EntitySystems; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects.Systems; namespace Content.Server.Commands { + [AdminCommand(AdminFlags.Debug)] public class HideContainedContextCommand : IClientCommand { public string Command => "hidecontainedcontext"; diff --git a/Content.Server/Commands/Hungry.cs b/Content.Server/Commands/Hungry.cs index b3a56c0171..39daf0caf9 100644 --- a/Content.Server/Commands/Hungry.cs +++ b/Content.Server/Commands/Hungry.cs @@ -1,11 +1,14 @@ #nullable enable +using Content.Server.Administration; using Content.Server.GameObjects.Components.Nutrition; +using Content.Shared.Administration; using Content.Shared.GameObjects.Components.Nutrition; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; namespace Content.Server.Commands { + [AdminCommand(AdminFlags.Debug)] public class Hungry : IClientCommand { public string Command => "hungry"; diff --git a/Content.Server/Commands/ShowContainedContextCommand.cs b/Content.Server/Commands/ShowContainedContextCommand.cs index 3b37d6375e..8afcc5501b 100644 --- a/Content.Server/Commands/ShowContainedContextCommand.cs +++ b/Content.Server/Commands/ShowContainedContextCommand.cs @@ -1,11 +1,14 @@ #nullable enable +using Content.Server.Administration; using Content.Server.GameObjects.EntitySystems; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects.Systems; namespace Content.Server.Commands { + [AdminCommand(AdminFlags.Debug)] public class ShowContainedContextCommand : IClientCommand { public const string CommandName = "showcontainedcontext"; diff --git a/Content.Server/Database/PlayerRecord.cs b/Content.Server/Database/PlayerRecord.cs new file mode 100644 index 0000000000..19e05f833c --- /dev/null +++ b/Content.Server/Database/PlayerRecord.cs @@ -0,0 +1,29 @@ +using System; +using System.Net; +using Robust.Shared.Network; + +namespace Content.Server.Database +{ + public sealed class PlayerRecord + { + public NetUserId UserId { get; } + public DateTimeOffset FirstSeenTime { get; } + public string LastSeenUserName { get; } + public DateTimeOffset LastSeenTime { get; } + public IPAddress LastSeenAddress { get; } + + public PlayerRecord( + NetUserId userId, + DateTimeOffset firstSeenTime, + string lastSeenUserName, + DateTimeOffset lastSeenTime, + IPAddress lastSeenAddress) + { + UserId = userId; + FirstSeenTime = firstSeenTime; + LastSeenUserName = lastSeenUserName; + LastSeenTime = lastSeenTime; + LastSeenAddress = lastSeenAddress; + } + } +} diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index dbc1cc1231..c91f4ffd71 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Threading; using System.Threading.Tasks; using Content.Shared.Preferences; using Microsoft.EntityFrameworkCore; @@ -211,12 +212,95 @@ namespace Content.Server.Database * PLAYER RECORDS */ public abstract Task UpdatePlayerRecord(NetUserId userId, string userName, IPAddress address); + public abstract Task GetPlayerRecordByUserName(string userName, CancellationToken cancel); + public abstract Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel); /* * CONNECTION LOG */ public abstract Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address); + /* + * ADMIN STUFF + */ + public async Task GetAdminDataForAsync(NetUserId userId, CancellationToken cancel) + { + await using var db = await GetDb(); + + return await db.DbContext.Admin + .Include(p => p.Flags) + .Include(p => p.AdminRank) + .ThenInclude(p => p!.Flags) + .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel); + } + + public abstract Task<((Admin, string? lastUserName)[] admins, AdminRank[])> + GetAllAdminAndRanksAsync(CancellationToken cancel); + + public async Task GetAdminRankDataForAsync(int id, CancellationToken cancel = default) + { + await using var db = await GetDb(); + + return await db.DbContext.AdminRank + .Include(r => r.Flags) + .SingleOrDefaultAsync(r => r.Id == id, cancel); + } + + public async Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel) + { + await using var db = await GetDb(); + + var admin = await db.DbContext.Admin.SingleAsync(a => a.UserId == userId.UserId, cancel); + db.DbContext.Admin.Remove(admin); + + await db.DbContext.SaveChangesAsync(cancel); + } + + public async Task AddAdminAsync(Admin admin, CancellationToken cancel) + { + await using var db = await GetDb(); + + db.DbContext.Admin.Add(admin); + + await db.DbContext.SaveChangesAsync(cancel); + } + + public async Task UpdateAdminAsync(Admin admin, CancellationToken cancel) + { + await using var db = await GetDb(); + + db.DbContext.Admin.Update(admin); + + await db.DbContext.SaveChangesAsync(cancel); + } + + public async Task RemoveAdminRankAsync(int rankId, CancellationToken cancel) + { + await using var db = await GetDb(); + + var admin = await db.DbContext.AdminRank.SingleAsync(a => a.Id == rankId, cancel); + db.DbContext.AdminRank.Remove(admin); + + await db.DbContext.SaveChangesAsync(cancel); + } + + public async Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel) + { + await using var db = await GetDb(); + + db.DbContext.AdminRank.Add(rank); + + await db.DbContext.SaveChangesAsync(cancel); + } + + public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel) + { + await using var db = await GetDb(); + + db.DbContext.AdminRank.Update(rank); + + await db.DbContext.SaveChangesAsync(cancel); + } protected abstract Task GetDb(); diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs index 8d7dc816c4..e5651f12d5 100644 --- a/Content.Server/Database/ServerDbManager.cs +++ b/Content.Server/Database/ServerDbManager.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.IO; using System.Net; +using System.Threading; using System.Threading.Tasks; using Content.Shared; using Content.Shared.Preferences; @@ -27,7 +28,9 @@ namespace Content.Server.Database // Preferences Task InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile); Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index); + Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot); + // Single method for two operations for transaction. Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot); Task GetPlayerPreferencesAsync(NetUserId userId); @@ -42,9 +45,26 @@ namespace Content.Server.Database // Player records Task UpdatePlayerRecordAsync(NetUserId userId, string userName, IPAddress address); + Task GetPlayerRecordByUserName(string userName, CancellationToken cancel = default); + Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default); // Connection log Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address); + + // Admins + Task GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default); + Task GetAdminRankAsync(int id, CancellationToken cancel = default); + + Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync( + CancellationToken cancel = default); + + Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default); + Task AddAdminAsync(Admin admin, CancellationToken cancel = default); + Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default); + + Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default); + Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default); + Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default); } public sealed class ServerDbManager : IServerDbManager @@ -132,11 +152,67 @@ namespace Content.Server.Database return _db.UpdatePlayerRecord(userId, userName, address); } + public Task GetPlayerRecordByUserName(string userName, CancellationToken cancel = default) + { + return _db.GetPlayerRecordByUserName(userName, cancel); + } + + public Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default) + { + return _db.GetPlayerRecordByUserId(userId, cancel); + } + public Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address) { return _db.AddConnectionLogAsync(userId, userName, address); } + public Task GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default) + { + return _db.GetAdminDataForAsync(userId, cancel); + } + + public Task GetAdminRankAsync(int id, CancellationToken cancel = default) + { + return _db.GetAdminRankDataForAsync(id, cancel); + } + + public Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync( + CancellationToken cancel = default) + { + return _db.GetAllAdminAndRanksAsync(cancel); + } + + public Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default) + { + return _db.RemoveAdminAsync(userId, cancel); + } + + public Task AddAdminAsync(Admin admin, CancellationToken cancel = default) + { + return _db.AddAdminAsync(admin, cancel); + } + + public Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default) + { + return _db.UpdateAdminAsync(admin, cancel); + } + + public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default) + { + return _db.RemoveAdminRankAsync(rankId, cancel); + } + + public Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default) + { + return _db.AddAdminRankAsync(rank, cancel); + } + + public Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default) + { + return _db.UpdateAdminRankAsync(rank, cancel); + } + private DbContextOptions CreatePostgresOptions() { var host = _cfg.GetCVar(CCVars.DatabasePgHost); diff --git a/Content.Server/Database/ServerDbPostgres.cs b/Content.Server/Database/ServerDbPostgres.cs index b3ac0388cb..b57e557832 100644 --- a/Content.Server/Database/ServerDbPostgres.cs +++ b/Content.Server/Database/ServerDbPostgres.cs @@ -1,6 +1,8 @@ using System; +using System.Data; using System.Linq; using System.Net; +using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Robust.Shared.Network; @@ -138,6 +140,45 @@ namespace Content.Server.Database await db.PgDbContext.SaveChangesAsync(); } + public override async Task GetPlayerRecordByUserName(string userName, CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + // Sort by descending last seen time. + // So if, due to account renames, we have two people with the same username in the DB, + // the most recent one is picked. + var record = await db.PgDbContext.Player + .OrderByDescending(p => p.LastSeenTime) + .FirstOrDefaultAsync(p => p.LastSeenUserName == userName, cancel); + + return MakePlayerRecord(record); + } + + public override async Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + var record = await db.PgDbContext.Player + .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel); + + return MakePlayerRecord(record); + } + + private static PlayerRecord? MakePlayerRecord(PostgresPlayer? record) + { + if (record == null) + { + return null; + } + + return new PlayerRecord( + new NetUserId(record.UserId), + new DateTimeOffset(record.FirstSeenTime, TimeSpan.Zero), + record.LastSeenUserName, + new DateTimeOffset(record.LastSeenTime, TimeSpan.Zero), + record.LastSeenAddress); + } + public override async Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address) { await using var db = await GetDbImpl(); @@ -153,6 +194,27 @@ namespace Content.Server.Database await db.PgDbContext.SaveChangesAsync(); } + public override async Task<((Admin, string? lastUserName)[] admins, AdminRank[])> + GetAllAdminAndRanksAsync(CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + // Honestly this probably doesn't even matter but whatever. + await using var tx = + await db.DbContext.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancel); + + // Join with the player table to find their last seen username, if they have one. + var admins = await db.PgDbContext.Admin + .Include(a => a.Flags) + .GroupJoin(db.PgDbContext.Player, a => a.UserId, p => p.UserId, (a, grouping) => new {a, grouping}) + .SelectMany(t => t.grouping.DefaultIfEmpty(), (t, p) => new {t.a, p.LastSeenUserName}) + .ToArrayAsync(cancel); + + var adminRanks = await db.DbContext.AdminRank.Include(a => a.Flags).ToArrayAsync(cancel); + + return (admins.Select(p => (p.a, p.LastSeenUserName)).ToArray(), adminRanks)!; + } + private async Task GetDbImpl() { await _dbReadyTask; diff --git a/Content.Server/Database/ServerDbSqlite.cs b/Content.Server/Database/ServerDbSqlite.cs index 9ecefb0fc2..f8c654c95a 100644 --- a/Content.Server/Database/ServerDbSqlite.cs +++ b/Content.Server/Database/ServerDbSqlite.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; @@ -105,6 +106,44 @@ namespace Content.Server.Database await db.SqliteDbContext.SaveChangesAsync(); } + public override async Task GetPlayerRecordByUserName(string userName, CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + // Sort by descending last seen time. + // So if due to account renames we have two people with the same username in the DB, + // the most recent one is picked. + var record = await db.SqliteDbContext.Player + .OrderByDescending(p => p.LastSeenTime) + .FirstOrDefaultAsync(p => p.LastSeenUserName == userName, cancel); + + return MakePlayerRecord(record); + } + + public override async Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + var record = await db.SqliteDbContext.Player + .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel); + + return MakePlayerRecord(record); + } + + private static PlayerRecord? MakePlayerRecord(SqlitePlayer? record) + { + if (record == null) + { + return null; + } + + return new PlayerRecord( + new NetUserId(record.UserId), + new DateTimeOffset(record.FirstSeenTime, TimeSpan.Zero), + record.LastSeenUserName, + new DateTimeOffset(record.LastSeenTime, TimeSpan.Zero), + IPAddress.Parse(record.LastSeenAddress)); + } private static ServerBanDef? ConvertBan(SqliteServerBan? ban) { if (ban == null) @@ -156,6 +195,21 @@ namespace Content.Server.Database await db.SqliteDbContext.SaveChangesAsync(); } + public override async Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync( + CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + var admins = await db.SqliteDbContext.Admin + .Include(a => a.Flags) + .GroupJoin(db.SqliteDbContext.Player, a => a.UserId, p => p.UserId, (a, grouping) => new {a, grouping}) + .SelectMany(t => t.grouping.DefaultIfEmpty(), (t, p) => new {t.a, p.LastSeenUserName}) + .ToArrayAsync(cancel); + + var adminRanks = await db.DbContext.AdminRank.Include(a => a.Flags).ToArrayAsync(cancel); + + return (admins.Select(p => (p.a, p.LastSeenUserName)).ToArray(), adminRanks)!; + } private async Task GetDbImpl() { diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index ca684623f4..8afeb571d8 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -1,6 +1,8 @@ -using Content.Server.AI.Utility.Considerations; +using Content.Server.Administration; +using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; using Content.Server.Database; +using Content.Server.Eui; using Content.Server.GameObjects.Components.Mobs.Speech; using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; using Content.Server.Interfaces; @@ -23,6 +25,7 @@ namespace Content.Server public class EntryPoint : GameServer { private IGameTicker _gameTicker; + private EuiManager _euiManager; private StatusShell _statusShell; /// @@ -50,6 +53,7 @@ namespace Content.Server IoCManager.BuildGraph(); _gameTicker = IoCManager.Resolve(); + _euiManager = IoCManager.Resolve(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); @@ -79,6 +83,8 @@ namespace Content.Server IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); + _euiManager.Initialize(); } public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs) @@ -92,6 +98,11 @@ namespace Content.Server _gameTicker.Update(frameEventArgs); break; } + case ModUpdateLevel.PostEngine: + { + _euiManager.SendUpdates(); + break; + } } } } diff --git a/Content.Server/Eui/BaseEui.cs b/Content.Server/Eui/BaseEui.cs new file mode 100644 index 0000000000..cd2dc4c52f --- /dev/null +++ b/Content.Server/Eui/BaseEui.cs @@ -0,0 +1,131 @@ +using System; +using Content.Shared.Eui; +using Content.Shared.Network.NetMessages; +using Robust.Server.Interfaces.Player; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Server.Eui +{ + /// + /// Base class to implement server-side for an EUI. + /// + /// + /// An EUI is a system for making a relatively-easy connection between client and server + /// for the purposes of UIs. + /// + /// + /// An equivalently named class much exist server side for an EUI to work. + /// It will be instantiated, opened and closed automatically. + /// + public abstract class BaseEui + { + private bool _isStateDirty = false; + + /// + /// The player that this EUI is open for. + /// + public IPlayerSession Player { get; private set; } = default!; + public bool IsShutDown { get; private set; } + public EuiManager Manager { get; private set; } = default!; + public uint Id { get; private set; } + + /// + /// Called when the UI has been opened. Do initializing logic here. + /// + public virtual void Opened() + { + + } + + /// + /// Called when the UI has been closed. + /// + public virtual void Closed() + { + + } + + /// + /// Called when a message comes in from the client. + /// + public virtual void HandleMessage(EuiMessageBase msg) + { + } + + /// + /// Mark the current UI state as dirty and queue for an update. + /// + /// + public void StateDirty() + { + if (_isStateDirty) + { + return; + } + + _isStateDirty = true; + Manager.QueueStateUpdate(this); + } + + /// + /// Called some time after has been called + /// to get a new UI state that can be sent to the client. + /// + public virtual EuiStateBase GetNewState() + { + throw new NotSupportedException(); + } + + /// + /// Send a message to the client-side EUI. + /// + public void SendMessage(EuiMessageBase message) + { + var netMgr = IoCManager.Resolve(); + var msg = netMgr.CreateNetMessage(); + msg.Id = Id; + msg.Message = message; + + netMgr.ServerSendMessage(msg, Player.ConnectedClient); + } + + /// + /// Close the EUI, breaking the connection between client and server. + /// + public void Close() + { + Manager.CloseEui(this); + } + + internal void Shutdown() + { + Closed(); + IsShutDown = true; + } + + internal void DoStateUpdate() + { + _isStateDirty = false; + + var state = GetNewState(); + + var netMgr = IoCManager.Resolve(); + var msg = netMgr.CreateNetMessage(); + msg.Id = Id; + msg.State = state; + + netMgr.ServerSendMessage(msg, Player.ConnectedClient); + } + + internal void Initialize(EuiManager manager, IPlayerSession player, uint id) + { + Manager = manager; + Player = player; + Id = id; + Opened(); + } + } +} diff --git a/Content.Server/Eui/EuiManager.cs b/Content.Server/Eui/EuiManager.cs new file mode 100644 index 0000000000..b5a8ae2a87 --- /dev/null +++ b/Content.Server/Eui/EuiManager.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Network.NetMessages; +using Robust.Server.Interfaces.Player; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Utility; + +#nullable enable + +namespace Content.Server.Eui +{ + public sealed class EuiManager : IPostInjectInit + { + [Dependency] private readonly IPlayerManager _players = default!; + [Dependency] private readonly IServerNetManager _net = default!; + + private readonly Dictionary _playerData = + new Dictionary(); + + private readonly Queue<(IPlayerSession player, uint id)> _stateUpdateQueue = + new Queue<(IPlayerSession, uint id)>(); + + private sealed class PlayerEuiData + { + public uint NextId = 1; + public readonly Dictionary OpenUIs = new Dictionary(); + } + + void IPostInjectInit.PostInject() + { + _players.PlayerStatusChanged += PlayerStatusChanged; + } + + public void Initialize() + { + _net.RegisterNetMessage(MsgEuiCtl.NAME); + _net.RegisterNetMessage(MsgEuiState.NAME); + _net.RegisterNetMessage(MsgEuiMessage.NAME, RxMsgMessage); + } + + public void SendUpdates() + { + while (_stateUpdateQueue.TryDequeue(out var tuple)) + { + var (player, id) = tuple; + + // Check that UI and player still exist. + // COULD have been removed in the mean time. + if (!_playerData.TryGetValue(player, out var plyDat) || !plyDat.OpenUIs.TryGetValue(id, out var ui)) + { + continue; + } + + ui.DoStateUpdate(); + } + } + + public void OpenEui(BaseEui eui, IPlayerSession player) + { + if (eui.Id != 0) + { + throw new ArgumentException("That EUI is already open!"); + } + + var data = _playerData[player]; + var newId = data.NextId++; + eui.Initialize(this, player, newId); + + data.OpenUIs.Add(newId, eui); + + var msg = _net.CreateNetMessage(); + msg.Id = newId; + msg.Type = MsgEuiCtl.CtlType.Open; + msg.OpenType = eui.GetType().Name; + + _net.ServerSendMessage(msg, player.ConnectedClient); + } + + public void CloseEui(BaseEui eui) + { + eui.Shutdown(); + _playerData[eui.Player].OpenUIs.Remove(eui.Id); + + var msg = _net.CreateNetMessage(); + msg.Id = eui.Id; + msg.Type = MsgEuiCtl.CtlType.Close; + _net.ServerSendMessage(msg, eui.Player.ConnectedClient); + } + + private void RxMsgMessage(MsgEuiMessage message) + { + if (!_players.TryGetSessionByChannel(message.MsgChannel, out var ply)) + { + return; + } + + if (!_playerData.TryGetValue(ply, out var dat)) + { + return; + } + + if (!dat.OpenUIs.TryGetValue(message.Id, out var eui)) + { + Logger.WarningS("eui", $"Got EUI message from player {ply} for non-existing UI {message.Id}"); + return; + } + + eui.HandleMessage(message.Message); + } + + private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus == SessionStatus.Connected) + { + _playerData.Add(e.Session, new PlayerEuiData()); + } + else if (e.NewStatus == SessionStatus.Disconnected) + { + if (_playerData.TryGetValue(e.Session, out var plyDat)) + { + // Gracefully close all open UIs. + foreach (var ui in plyDat.OpenUIs.Values) + { + ui.Closed(); + } + + _playerData.Remove(e.Session); + } + } + } + + public void QueueStateUpdate(BaseEui eui) + { + DebugTools.Assert(eui.Id != 0, "EUI has not been opened yet."); + DebugTools.Assert(!eui.IsShutDown, "EUI has been closed."); + + _stateUpdateQueue.Enqueue((eui.Player, eui.Id)); + } + } +} diff --git a/Content.Server/GameObjects/Components/Body/BodyCommands.cs b/Content.Server/GameObjects/Components/Body/BodyCommands.cs index 867ad77f9f..d0129b7f1a 100644 --- a/Content.Server/GameObjects/Components/Body/BodyCommands.cs +++ b/Content.Server/GameObjects/Components/Body/BodyCommands.cs @@ -1,6 +1,8 @@ #nullable enable using System; using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; using Content.Shared.Damage; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body.Part; @@ -16,6 +18,7 @@ using Robust.Shared.Random; namespace Content.Server.GameObjects.Components.Body { + [AdminCommand(AdminFlags.Fun)] class AddHandCommand : IClientCommand { public const string DefaultHandPrototype = "LeftHandHuman"; @@ -149,6 +152,7 @@ namespace Content.Server.GameObjects.Components.Body } } + [AdminCommand(AdminFlags.Fun)] class RemoveHandCommand : IClientCommand { public string Command => "removehand"; @@ -190,6 +194,7 @@ namespace Content.Server.GameObjects.Components.Body } } + [AdminCommand(AdminFlags.Fun)] class DestroyMechanismCommand : IClientCommand { public string Command => "destroymechanism"; @@ -242,6 +247,7 @@ namespace Content.Server.GameObjects.Components.Body } } + [AdminCommand(AdminFlags.Fun)] class HurtCommand : IClientCommand { public string Command => "hurt"; diff --git a/Content.Server/GameObjects/Components/Damage/DamageCommands.cs b/Content.Server/GameObjects/Components/Damage/DamageCommands.cs index d691892293..e90fc5d294 100644 --- a/Content.Server/GameObjects/Components/Damage/DamageCommands.cs +++ b/Content.Server/GameObjects/Components/Damage/DamageCommands.cs @@ -1,7 +1,9 @@ #nullable enable using System; using System.Diagnostics.CodeAnalysis; +using Content.Server.Administration; using Content.Server.GameObjects.Components.Atmos; +using Content.Shared.Administration; using Content.Shared.GameObjects.Components.Damage; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; @@ -115,6 +117,7 @@ namespace Content.Server.GameObjects.Components.Damage } } + [AdminCommand(AdminFlags.Fun)] public class AddDamageFlagCommand : DamageFlagCommand { public override string Command => "adddamageflag"; @@ -133,6 +136,7 @@ namespace Content.Server.GameObjects.Components.Damage } } + [AdminCommand(AdminFlags.Fun)] public class RemoveDamageFlagCommand : DamageFlagCommand { public override string Command => "removedamageflag"; @@ -151,6 +155,7 @@ namespace Content.Server.GameObjects.Components.Damage } } + [AdminCommand(AdminFlags.Admin)] public class GodModeCommand : IClientCommand { public string Command => "godmode"; diff --git a/Content.Server/GameObjects/Components/Disposal/DisposalCommands.cs b/Content.Server/GameObjects/Components/Disposal/DisposalCommands.cs index 238b31c737..2f549a8037 100644 --- a/Content.Server/GameObjects/Components/Disposal/DisposalCommands.cs +++ b/Content.Server/GameObjects/Components/Disposal/DisposalCommands.cs @@ -1,4 +1,6 @@ #nullable enable +using Content.Server.Administration; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; @@ -8,6 +10,7 @@ using Robust.Shared.Localization; namespace Content.Server.GameObjects.Components.Disposal { + [AdminCommand(AdminFlags.Debug)] public class TubeConnectionsCommand : IClientCommand { public string Command => "tubeconnections"; diff --git a/Content.Server/GameObjects/Components/Interactable/ToolCommands.cs b/Content.Server/GameObjects/Components/Interactable/ToolCommands.cs index c4ec4a2a05..18faf899b6 100644 --- a/Content.Server/GameObjects/Components/Interactable/ToolCommands.cs +++ b/Content.Server/GameObjects/Components/Interactable/ToolCommands.cs @@ -1,5 +1,7 @@ #nullable enable using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; using Content.Shared.Maps; using JetBrains.Annotations; using Robust.Server.Interfaces.Console; @@ -14,7 +16,7 @@ namespace Content.Server.GameObjects.Components.Interactable /// /// /// - [UsedImplicitly] + [AdminCommand(AdminFlags.Debug)] class TilePryCommand : IClientCommand { public string Command => "tilepry"; @@ -69,7 +71,7 @@ namespace Content.Server.GameObjects.Components.Interactable } } - [UsedImplicitly] + [AdminCommand(AdminFlags.Debug)] class AnchorCommand : IClientCommand { public string Command => "anchor"; @@ -114,7 +116,7 @@ namespace Content.Server.GameObjects.Components.Interactable } } - [UsedImplicitly] + [AdminCommand(AdminFlags.Debug)] class UnAnchorCommand : IClientCommand { public string Command => "unanchor"; diff --git a/Content.Server/GameObjects/Components/Mobs/ServerAlertsComponent.cs b/Content.Server/GameObjects/Components/Mobs/ServerAlertsComponent.cs index 2587b491e4..37b1465891 100644 --- a/Content.Server/GameObjects/Components/Mobs/ServerAlertsComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/ServerAlertsComponent.cs @@ -1,6 +1,8 @@ using System; +using Content.Server.Administration; using Content.Server.Commands; using Content.Server.GameObjects.EntitySystems; +using Content.Shared.Administration; using Content.Shared.Alert; using Content.Shared.GameObjects.Components.Mobs; using Robust.Server.Interfaces.Console; @@ -88,6 +90,7 @@ namespace Content.Server.GameObjects.Components.Mobs } } + [AdminCommand(AdminFlags.Debug)] public sealed class ShowAlert : IClientCommand { public string Command => "showalert"; @@ -129,6 +132,7 @@ namespace Content.Server.GameObjects.Components.Mobs } } + [AdminCommand(AdminFlags.Debug)] public sealed class ClearAlert : IClientCommand { public string Command => "clearalert"; diff --git a/Content.Server/GameObjects/Components/Mobs/Speech/SpeechComponent.cs b/Content.Server/GameObjects/Components/Mobs/Speech/SpeechComponent.cs index 1140770e9d..3af74dbaa0 100644 --- a/Content.Server/GameObjects/Components/Mobs/Speech/SpeechComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/Speech/SpeechComponent.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.Interfaces.GameObjects; @@ -17,6 +19,7 @@ namespace Content.Server.GameObjects.Components.Mobs.Speech public string Accentuate(string message); } + [AdminCommand(AdminFlags.Fun)] public class AddAccent : IClientCommand { public string Command => "addaccent"; @@ -38,12 +41,12 @@ namespace Content.Server.GameObjects.Components.Mobs.Speech shell.SendText(player, "You don't have a player!"); return; } - + var compFactory = IoCManager.Resolve(); - + if (args[0] == "?") { - // Get all components that implement the ISpeechComponent except + // Get all components that implement the ISpeechComponent except var speeches = compFactory.GetAllRefTypes() .Where(c => typeof(IAccentComponent).IsAssignableFrom(c) && c.IsClass); var msg = ""; diff --git a/Content.Server/GameObjects/EntitySystems/AI/AiFactionTagSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/AiFactionTagSystem.cs index e12fc1633e..ea35dceb99 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/AiFactionTagSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/AiFactionTagSystem.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Text; +using Content.Server.Administration; using Content.Server.GameObjects.Components.AI; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects.Systems; @@ -19,12 +21,12 @@ namespace Content.Server.GameObjects.EntitySystems.AI * Currently factions are implicitly friendly if they are not hostile. * This may change where specified friendly factions are listed. (e.g. to get number of friendlies in area). */ - + public Faction GetHostileFactions(Faction faction) => _hostileFactions.TryGetValue(faction, out var hostiles) ? hostiles : Faction.None; - + private Dictionary _hostileFactions = new Dictionary { - {Faction.NanoTransen, + {Faction.NanoTransen, Faction.SimpleHostile | Faction.Syndicate | Faction.Xeno}, {Faction.SimpleHostile, Faction.NanoTransen | Faction.Syndicate @@ -35,11 +37,11 @@ namespace Content.Server.GameObjects.EntitySystems.AI }, {Faction.Syndicate, Faction.NanoTransen | Faction.SimpleHostile | Faction.Xeno}, - {Faction.Xeno, + {Faction.Xeno, Faction.NanoTransen | Faction.Syndicate}, }; - public Faction GetFactions(IEntity entity) => + public Faction GetFactions(IEntity entity) => entity.TryGetComponent(out AiFactionTagComponent factionTags) ? factionTags.Factions : Faction.None; @@ -76,7 +78,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI hostileFactions &= ~target; _hostileFactions[source] = hostileFactions; } - + public void MakeHostile(Faction source, Faction target) { if (!_hostileFactions.TryGetValue(source, out var hostileFactions)) @@ -89,12 +91,13 @@ namespace Content.Server.GameObjects.EntitySystems.AI _hostileFactions[source] = hostileFactions; } } - + + [AdminCommand(AdminFlags.Fun)] public sealed class FactionCommand : IClientCommand { public string Command => "factions"; public string Description => "Update / list factional relationships for NPCs."; - public string Help => "faction target\n" + + public string Help => "faction target\n" + "faction list: hostile factions"; public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) @@ -108,11 +111,11 @@ namespace Content.Server.GameObjects.EntitySystems.AI continue; result.Append(value + "\n"); } - + shell.SendText(player, result.ToString()); return; } - + if (args.Length < 2) { shell.SendText(player, Loc.GetString("Need more args")); @@ -141,7 +144,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI shell.SendText(player, Loc.GetString("Invalid target faction")); return; } - + EntitySystem.Get().MakeFriendly(faction, targetFaction); shell.SendText(player, Loc.GetString("Command successful")); break; @@ -157,7 +160,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI shell.SendText(player, Loc.GetString("Invalid target faction")); return; } - + EntitySystem.Get().MakeHostile(faction, targetFaction); shell.SendText(player, Loc.GetString("Command successful")); break; @@ -172,4 +175,4 @@ namespace Content.Server.GameObjects.EntitySystems.AI return; } } -} \ No newline at end of file +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs index cf99ce1086..1e4c995799 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs @@ -1,9 +1,10 @@ #nullable enable using System; using System.Collections.Generic; -using System.Linq; +using Content.Server.Administration; using Content.Server.GameObjects.Components.Movement; using Content.Shared; +using Content.Shared.Administration; using Content.Shared.GameObjects.Components.Movement; using JetBrains.Annotations; using Robust.Server.AI; @@ -145,6 +146,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI public bool ProcessorTypeExists(string name) => _processorTypes.ContainsKey(name); + [AdminCommand(AdminFlags.Fun)] private class AddAiCommand : IClientCommand { public string Command => "addai"; diff --git a/Content.Server/GameObjects/EntitySystems/SignalLinkerSystem.cs b/Content.Server/GameObjects/EntitySystems/SignalLinkerSystem.cs index 30979a3e5d..e812d3dd23 100644 --- a/Content.Server/GameObjects/EntitySystems/SignalLinkerSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/SignalLinkerSystem.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using Content.Server.Administration; using Content.Server.GameObjects.Components.MachineLinking; using Content.Server.GameObjects.EntitySystems.Click; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; @@ -98,6 +100,7 @@ namespace Content.Server.GameObjects.EntitySystems } + [AdminCommand(AdminFlags.Debug)] public class SignalLinkerCommand : IClientCommand { public string Command => "signallink"; diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 9e2a23b00b..114ab2023d 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using Content.Server.Administration; using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Items.Storage; diff --git a/Content.Server/GameTicking/GameTickerCommands.cs b/Content.Server/GameTicking/GameTickerCommands.cs index c84e448785..6fa0f2641a 100644 --- a/Content.Server/GameTicking/GameTickerCommands.cs +++ b/Content.Server/GameTicking/GameTickerCommands.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using Content.Server.Administration; using Content.Server.Interfaces.GameTicking; using Content.Server.Players; +using Content.Shared.Administration; using Content.Shared.Maps; using Content.Shared.Roles; using Robust.Server.Interfaces.Console; @@ -18,6 +20,7 @@ using Robust.Shared.Utility; namespace Content.Server.GameTicking { + [AdminCommand(AdminFlags.Server)] class DelayStartCommand : IClientCommand { public string Command => "delaystart"; @@ -60,6 +63,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] class StartRoundCommand : IClientCommand { public string Command => "startround"; @@ -80,6 +84,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] class EndRoundCommand : IClientCommand { public string Command => "endround"; @@ -100,6 +105,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] public class NewRoundCommand : IClientCommand { public string Command => "restartround"; @@ -165,6 +171,7 @@ namespace Content.Server.GameTicking } } + [AnyCommand] class ObserveCommand : IClientCommand { public string Command => "observe"; @@ -183,6 +190,7 @@ namespace Content.Server.GameTicking } } + [AnyCommand] class JoinGameCommand : IClientCommand { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -228,6 +236,7 @@ namespace Content.Server.GameTicking } } + [AnyCommand] class ToggleReadyCommand : IClientCommand { public string Command => "toggleready"; @@ -246,6 +255,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] class ToggleDisallowLateJoinCommand: IClientCommand { public string Command => "toggledisallowlatejoin"; @@ -274,6 +284,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] class SetGamePresetCommand : IClientCommand { public string Command => "setgamepreset"; @@ -294,6 +305,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] class ForcePresetCommand : IClientCommand { public string Command => "forcepreset"; @@ -327,6 +339,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server | AdminFlags.Mapping)] class MappingCommand : IClientCommand { public string Command => "mapping"; @@ -383,6 +396,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Mapping)] class TileWallsCommand : IClientCommand { // ReSharper disable once StringLiteralTypo diff --git a/Content.Server/Interfaces/Chat/IChatManager.cs b/Content.Server/Interfaces/Chat/IChatManager.cs index 9faee496eb..5dfcdbc458 100644 --- a/Content.Server/Interfaces/Chat/IChatManager.cs +++ b/Content.Server/Interfaces/Chat/IChatManager.cs @@ -31,5 +31,6 @@ namespace Content.Server.Interfaces.Chat delegate string TransformChat(IEntity speaker, string message); void RegisterChatTransform(TransformChat handler); + void SendAdminAnnouncement(string message); } } diff --git a/Content.Server/Mobs/Commands.cs b/Content.Server/Mobs/Commands.cs index 6d64578411..45af6e11b6 100644 --- a/Content.Server/Mobs/Commands.cs +++ b/Content.Server/Mobs/Commands.cs @@ -1,7 +1,9 @@ using System.Text; +using Content.Server.Administration; using Content.Server.GameObjects.Components.Mobs; using Content.Server.Mobs.Roles; using Content.Server.Players; +using Content.Shared.Administration; using Content.Shared.Roles; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; @@ -12,6 +14,7 @@ using Robust.Shared.Prototypes; namespace Content.Server.Mobs { + [AdminCommand(AdminFlags.Admin)] public class MindInfoCommand : IClientCommand { public string Command => "mindinfo"; @@ -49,6 +52,7 @@ namespace Content.Server.Mobs } } + [AdminCommand(AdminFlags.Fun)] public class AddRoleCommand : IClientCommand { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -81,6 +85,7 @@ namespace Content.Server.Mobs } } + [AdminCommand(AdminFlags.Fun)] public class RemoveRoleCommand : IClientCommand { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -113,6 +118,7 @@ namespace Content.Server.Mobs } } + [AdminCommand(AdminFlags.Debug)] public class AddOverlayCommand : IClientCommand { public string Command => "addoverlay"; @@ -137,6 +143,7 @@ namespace Content.Server.Mobs } } + [AdminCommand(AdminFlags.Debug)] public class RemoveOverlayCommand : IClientCommand { public string Command => "rmoverlay"; diff --git a/Content.Server/Observer/Ghost.cs b/Content.Server/Observer/Ghost.cs index 8e97025f66..6aca5cff89 100644 --- a/Content.Server/Observer/Ghost.cs +++ b/Content.Server/Observer/Ghost.cs @@ -1,3 +1,4 @@ +using Content.Server.Administration; using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Observer; using Content.Server.Interfaces.GameTicking; @@ -12,6 +13,7 @@ using Robust.Shared.IoC; namespace Content.Server.Observer { + [AnyCommand] public class Ghost : IClientCommand { public string Command => "ghost"; diff --git a/Content.Server/Players/PlayerData.cs b/Content.Server/Players/PlayerData.cs index c66b6f7024..d398d87747 100644 --- a/Content.Server/Players/PlayerData.cs +++ b/Content.Server/Players/PlayerData.cs @@ -25,6 +25,12 @@ namespace Content.Server.Players [ViewVariables] public Mind? Mind { get; set; } + /// + /// If true, the player is an admin and they explicitly de-adminned mid-game, + /// so they should not regain admin if they reconnect. + /// + public bool ExplicitlyDeadminned { get; set; } + public void WipeMind() { Mind?.ChangeOwningPlayer(null); diff --git a/Content.Server/ServerContentIoC.cs b/Content.Server/ServerContentIoC.cs index 362b7ebe56..ad69775abf 100644 --- a/Content.Server/ServerContentIoC.cs +++ b/Content.Server/ServerContentIoC.cs @@ -1,8 +1,10 @@ -using Content.Server.AI.Utility.Considerations; +using Content.Server.Administration; +using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; using Content.Server.Cargo; using Content.Server.Chat; using Content.Server.Database; +using Content.Server.Eui; using Content.Server.GameObjects.Components.Mobs.Speech; using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; using Content.Server.GameObjects.Components.Power.PowerNetComponents; @@ -47,7 +49,9 @@ namespace Content.Server IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Server/ServerNotifyManager.cs b/Content.Server/ServerNotifyManager.cs index 7dd9984245..aac886c0aa 100644 --- a/Content.Server/ServerNotifyManager.cs +++ b/Content.Server/ServerNotifyManager.cs @@ -1,5 +1,7 @@ +using Content.Server.Administration; using Content.Server.Interfaces; using Content.Shared; +using Content.Shared.Administration; using Content.Shared.Interfaces; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.GameObjects; @@ -71,6 +73,7 @@ namespace Content.Server _netManager.ServerSendMessage(netMessage, actor.playerSession.ConnectedClient); } + [AdminCommand(AdminFlags.Debug)] public class PopupMsgCommand : IClientCommand { public string Command => "srvpopupmsg"; diff --git a/Content.Server/StationEvents/StationEventCommand.cs b/Content.Server/StationEvents/StationEventCommand.cs index 1c6f1f6928..2549acded0 100644 --- a/Content.Server/StationEvents/StationEventCommand.cs +++ b/Content.Server/StationEvents/StationEventCommand.cs @@ -1,6 +1,7 @@ #nullable enable +using Content.Server.Administration; using Content.Server.GameObjects.EntitySystems.StationEvents; -using JetBrains.Annotations; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects.Systems; @@ -8,7 +9,7 @@ using Robust.Shared.Localization; namespace Content.Server.StationEvents { - [UsedImplicitly] + [AdminCommand(AdminFlags.Server)] public sealed class StationEventCommand : IClientCommand { public string Command => "events"; diff --git a/Content.Shared/Administration/AdminData.cs b/Content.Shared/Administration/AdminData.cs new file mode 100644 index 0000000000..8c56f901ed --- /dev/null +++ b/Content.Shared/Administration/AdminData.cs @@ -0,0 +1,68 @@ +#nullable enable + +namespace Content.Shared.Administration +{ + /// + /// Represents data for a single server admin. + /// + public sealed class AdminData + { + // Can be false if they're de-adminned with the ability to re-admin. + /// + /// Whether the admin is currently active. This can be false if they have de-adminned mid-round. + /// + public bool Active; + + /// + /// The admin's title. + /// + public string? Title; + + /// + /// The admin's permission flags. + /// + public AdminFlags Flags; + + /// + /// Checks whether this admin has an admin flag. + /// + /// The flags to check. Multiple flags can be specified, they must all be held. + /// False if this admin is not or does not have all the flags specified. + public bool HasFlag(AdminFlags flag) + { + return Active && (Flags & flag) == flag; + } + + /// + /// Check if this admin can open the VV menu. + /// + public bool CanViewVar() + { + return HasFlag(AdminFlags.VarEdit); + } + + /// + /// Check if this admin can spawn stuff in with the entity/tile spawn panel. + /// + public bool CanAdminPlace() + { + return HasFlag(AdminFlags.Spawn); + } + + /// + /// Check if this admin can execute server-side C# scripts. + /// + public bool CanScript() + { + return HasFlag(AdminFlags.Host); + } + + /// + /// Check if this admin can open the admin menu. + /// + public bool CanAdminMenu() + { + return HasFlag(AdminFlags.Admin); + } + } +} diff --git a/Content.Shared/Administration/AdminFlags.cs b/Content.Shared/Administration/AdminFlags.cs new file mode 100644 index 0000000000..2f6b336fe6 --- /dev/null +++ b/Content.Shared/Administration/AdminFlags.cs @@ -0,0 +1,68 @@ +using System; + +namespace Content.Shared.Administration +{ + /// + /// Permissions that admins can have. + /// + [Flags] + public enum AdminFlags : uint + { + None = 0, + + /// + /// Basic admin verbs. + /// + Admin = 1 << 0, + + /// + /// Ability to ban people. + /// + Ban = 1 << 1, + + /// + /// Debug commands for coders. + /// + Debug = 1 << 2, + + /// + /// !!FUN!! + /// + Fun = 1 << 3, + + /// + /// Ability to edit permissions for other administrators. + /// + Permissions = 1 << 4, + + /// + /// Ability to control teh server like restart it or change the round type. + /// + Server = 1 << 5, + + /// + /// Ability to spawn stuff in. + /// + Spawn = 1 << 6, + + /// + /// Ability to use VV. + /// + VarEdit = 1 << 7, + + /// + /// Large mapping operations. + /// + Mapping = 1 << 8, + + /// + /// Makes you british. + /// + //Piss = 1 << 9, + + /// + /// Dangerous host permissions like scsi. + /// + Host = 1u << 31, + } +} diff --git a/Content.Shared/Administration/AdminFlagsHelper.cs b/Content.Shared/Administration/AdminFlagsHelper.cs new file mode 100644 index 0000000000..9d5bba3baa --- /dev/null +++ b/Content.Shared/Administration/AdminFlagsHelper.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Content.Shared.Administration +{ + /// + /// Contains various helper methods for working with admin flags. + /// + public static class AdminFlagsHelper + { + // As you can tell from the boatload of bitwise ops, + // writing this class was genuinely fun. + + private static readonly Dictionary NameFlagsMap = new Dictionary(); + private static readonly string[] FlagsNameMap = new string[32]; + + /// + /// Every admin flag in the game, at once! + /// + public static readonly AdminFlags Everything; + + /// + /// A list of all individual admin flags. + /// + public static readonly IReadOnlyList AllFlags; + + static AdminFlagsHelper() + { + var t = typeof(AdminFlags); + var flags = (AdminFlags[]) Enum.GetValues(t); + var allFlags = new List(); + + foreach (var value in flags) + { + var name = value.ToString().ToUpper(); + + // If, in the future, somebody adds a combined admin flag or something for convenience, + // ignore it. + if (BitOperations.PopCount((uint) value) != 1) + { + continue; + } + + allFlags.Add(value); + Everything |= value; + NameFlagsMap.Add(name, value); + FlagsNameMap[BitOperations.Log2((uint) value)] = name; + } + + AllFlags = allFlags.ToArray(); + } + + /// + /// Converts an enumerable of admin flag names to a bitfield. + /// + /// + /// The flags must all be uppercase. + /// + /// + /// Thrown if a string that is not a valid admin flag is contained in . + /// + public static AdminFlags NamesToFlags(IEnumerable names) + { + var flags = AdminFlags.None; + foreach (var name in names) + { + if (!NameFlagsMap.TryGetValue(name, out var value)) + { + throw new ArgumentException($"Invalid admin flag name: {name}"); + } + + flags |= value; + } + + return flags; + } + + /// + /// Gets the flag bit for an admin flag name. + /// + /// + /// The flag name must be all uppercase. + /// + /// + /// Thrown if is not a valid admin flag name. + /// + public static AdminFlags NameToFlag(string name) + { + return NameFlagsMap[name]; + } + + /// + /// Converts a bitfield of admin flags to an array of all the flag names set. + /// + public static string[] FlagsToNames(AdminFlags flags) + { + var array = new string[BitOperations.PopCount((uint) flags)]; + var highest = BitOperations.LeadingZeroCount((uint) flags); + + var ai = 0; + for (var i = 0; i < 32 - highest; i++) + { + var flagValue = (AdminFlags) (1u << i); + if ((flags & flagValue) != 0) + { + array[ai++] = FlagsNameMap[i]; + } + } + + return array; + } + + public static string PosNegFlagsText(AdminFlags posFlags, AdminFlags negFlags) + { + var posFlagNames = FlagsToNames(posFlags).Select(f => (flag: f, fText: $"+{f}")); + var negFlagNames = FlagsToNames(negFlags).Select(f => (flag: f, fText: $"-{f}")); + + var flagsText = string.Join(' ', posFlagNames.Concat(negFlagNames).OrderBy(f => f.flag).Select(p => p.fText)); + return flagsText; + } + } +} diff --git a/Content.Shared/Administration/PermissionsEuiState.cs b/Content.Shared/Administration/PermissionsEuiState.cs new file mode 100644 index 0000000000..f49778ab60 --- /dev/null +++ b/Content.Shared/Administration/PermissionsEuiState.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Eui; +using Robust.Shared.Network; +using Robust.Shared.Serialization; + +namespace Content.Shared.Administration +{ + [Serializable, NetSerializable] + public sealed class PermissionsEuiState : EuiStateBase + { + public bool IsLoading; + + public AdminData[] Admins; + public Dictionary AdminRanks; + + [Serializable, NetSerializable] + public struct AdminData + { + public NetUserId UserId; + public string UserName; + public string Title; + public AdminFlags PosFlags; + public AdminFlags NegFlags; + public int? RankId; + } + + [Serializable, NetSerializable] + public struct AdminRankData + { + public string Name; + public AdminFlags Flags; + } + } + + public static class PermissionsEuiMsg + { + [Serializable, NetSerializable] + public sealed class Close : EuiMessageBase + { + } + + [Serializable, NetSerializable] + public sealed class AddAdmin : EuiMessageBase + { + public string UserNameOrId; + public string Title; + public AdminFlags PosFlags; + public AdminFlags NegFlags; + public int? RankId; + } + + [Serializable, NetSerializable] + public sealed class RemoveAdmin : EuiMessageBase + { + public NetUserId UserId; + } + + [Serializable, NetSerializable] + public sealed class UpdateAdmin : EuiMessageBase + { + public NetUserId UserId; + public string Title; + public AdminFlags PosFlags; + public AdminFlags NegFlags; + public int? RankId; + } + + + [Serializable, NetSerializable] + public sealed class AddAdminRank : EuiMessageBase + { + public string Name; + public AdminFlags Flags; + } + + [Serializable, NetSerializable] + public sealed class RemoveAdminRank : EuiMessageBase + { + public int Id; + } + + [Serializable, NetSerializable] + public sealed class UpdateAdminRank : EuiMessageBase + { + public int Id; + + public string Name; + public AdminFlags Flags; + } + } +} diff --git a/Content.Shared/CCVars.cs b/Content.Shared/CCVars.cs index ae6fde26bf..faf2c627d1 100644 --- a/Content.Shared/CCVars.cs +++ b/Content.Shared/CCVars.cs @@ -67,6 +67,13 @@ namespace Content.Shared public static readonly CVarDef GameDiagonalMovement = CVarDef.Create("game.diagonalmovement", true, CVar.ARCHIVE); + /* + * Console + */ + + public static readonly CVarDef + ConsoleLoginLocal = CVarDef.Create("console.loginlocal", true, CVar.ARCHIVE | CVar.SERVERONLY); + /* * Database stuff @@ -130,5 +137,15 @@ namespace Content.Shared public static readonly CVarDef NetGasOverlayTickRate = CVarDef.Create("net.gasoverlaytickrate", 3.0f); + + /* + * Admin stuff + */ + + public static readonly CVarDef AdminAnnounceLogin = + CVarDef.Create("admin.announce_login", true, CVar.SERVERONLY); + + public static readonly CVarDef AdminAnnounceLogout = + CVarDef.Create("admin.announce_logout", true, CVar.SERVERONLY); } } diff --git a/Content.Shared/Eui/EuiMessageBase.cs b/Content.Shared/Eui/EuiMessageBase.cs new file mode 100644 index 0000000000..5a252572d5 --- /dev/null +++ b/Content.Shared/Eui/EuiMessageBase.cs @@ -0,0 +1,10 @@ +using System; + +namespace Content.Shared.Eui +{ + [Serializable] + public abstract class EuiMessageBase + { + + } +} diff --git a/Content.Shared/Eui/EuiStateBase.cs b/Content.Shared/Eui/EuiStateBase.cs new file mode 100644 index 0000000000..c1fe469c35 --- /dev/null +++ b/Content.Shared/Eui/EuiStateBase.cs @@ -0,0 +1,11 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.Eui +{ + [Serializable, NetSerializable] + public abstract class EuiStateBase + { + + } +} diff --git a/Content.Shared/Network/NetMessages/MsgEuiCtl.cs b/Content.Shared/Network/NetMessages/MsgEuiCtl.cs new file mode 100644 index 0000000000..d1b553d18c --- /dev/null +++ b/Content.Shared/Network/NetMessages/MsgEuiCtl.cs @@ -0,0 +1,55 @@ +using Lidgren.Network; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Network; + +namespace Content.Shared.Network.NetMessages +{ + /// + /// Sent server -> client to signal that the client should open an EUI. + /// + public sealed class MsgEuiCtl : NetMessage + { + #region REQUIRED + + public const MsgGroups GROUP = MsgGroups.Command; + public const string NAME = nameof(MsgEuiCtl); + + public MsgEuiCtl(INetChannel channel) : base(NAME, GROUP) { } + + #endregion + + public CtlType Type; + public string OpenType; + public uint Id; + + public override void ReadFromBuffer(NetIncomingMessage buffer) + { + Id = buffer.ReadUInt32(); + Type = (CtlType) buffer.ReadByte(); + switch (Type) + { + case CtlType.Open: + OpenType = buffer.ReadString(); + break; + } + } + + public override void WriteToBuffer(NetOutgoingMessage buffer) + { + buffer.Write(Id); + buffer.Write((byte) Type); + switch (Type) + { + case CtlType.Open: + buffer.Write(OpenType); + break; + } + } + + public enum CtlType : byte + { + Open, + Close + } + } +} diff --git a/Content.Shared/Network/NetMessages/MsgEuiMessage.cs b/Content.Shared/Network/NetMessages/MsgEuiMessage.cs new file mode 100644 index 0000000000..7fe5912d20 --- /dev/null +++ b/Content.Shared/Network/NetMessages/MsgEuiMessage.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using Content.Shared.Eui; +using Lidgren.Network; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.IoC; +using Robust.Shared.Network; + +namespace Content.Shared.Network.NetMessages +{ + public sealed class MsgEuiMessage : NetMessage + { + #region REQUIRED + + public const MsgGroups GROUP = MsgGroups.Command; + public const string NAME = nameof(MsgEuiMessage); + + public MsgEuiMessage(INetChannel channel) : base(NAME, GROUP) { } + + #endregion + + public uint Id; + public EuiMessageBase Message; + + public override void ReadFromBuffer(NetIncomingMessage buffer) + { + Id = buffer.ReadUInt32(); + + var ser = IoCManager.Resolve(); + var len = buffer.ReadVariableInt32(); + var stream = buffer.ReadAlignedMemory(len); + Message = ser.Deserialize(stream); + } + + public override void WriteToBuffer(NetOutgoingMessage buffer) + { + buffer.Write(Id); + var stream = new MemoryStream(); + + var ser = IoCManager.Resolve(); + ser.Serialize(stream, Message); + var length = (int)stream.Length; + buffer.WriteVariableInt32(length); + buffer.Write(stream.GetBuffer().AsSpan(0, length)); + } + } +} diff --git a/Content.Shared/Network/NetMessages/MsgEuiState.cs b/Content.Shared/Network/NetMessages/MsgEuiState.cs new file mode 100644 index 0000000000..044c4c17b3 --- /dev/null +++ b/Content.Shared/Network/NetMessages/MsgEuiState.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using Content.Shared.Eui; +using Lidgren.Network; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.IoC; +using Robust.Shared.Network; + +namespace Content.Shared.Network.NetMessages +{ + public sealed class MsgEuiState : NetMessage + { + #region REQUIRED + + public const MsgGroups GROUP = MsgGroups.Command; + public const string NAME = nameof(MsgEuiState); + + public MsgEuiState(INetChannel channel) : base(NAME, GROUP) { } + + #endregion + + public uint Id; + public EuiStateBase State; + + public override void ReadFromBuffer(NetIncomingMessage buffer) + { + Id = buffer.ReadUInt32(); + + var ser = IoCManager.Resolve(); + var len = buffer.ReadVariableInt32(); + var stream = buffer.ReadAlignedMemory(len); + State = ser.Deserialize(stream); + } + + public override void WriteToBuffer(NetOutgoingMessage buffer) + { + buffer.Write(Id); + var stream = new MemoryStream(); + + var ser = IoCManager.Resolve(); + ser.Serialize(stream, State); + var length = (int)stream.Length; + buffer.WriteVariableInt32(length); + buffer.Write(stream.GetBuffer().AsSpan(0, length)); + } + } +} diff --git a/Content.Shared/Network/NetMessages/MsgUpdateAdminStatus.cs b/Content.Shared/Network/NetMessages/MsgUpdateAdminStatus.cs new file mode 100644 index 0000000000..bdeb4be645 --- /dev/null +++ b/Content.Shared/Network/NetMessages/MsgUpdateAdminStatus.cs @@ -0,0 +1,73 @@ +using Content.Shared.Administration; +using Lidgren.Network; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Network; + +namespace Content.Shared.Network.NetMessages +{ + public sealed class MsgUpdateAdminStatus : NetMessage + { + #region REQUIRED + + public const MsgGroups GROUP = MsgGroups.Command; + public const string NAME = nameof(MsgUpdateAdminStatus); + + public MsgUpdateAdminStatus(INetChannel channel) : base(NAME, GROUP) { } + + #endregion + + public AdminData Admin; + public string[] AvailableCommands; + + public override void ReadFromBuffer(NetIncomingMessage buffer) + { + var count = buffer.ReadVariableInt32(); + + AvailableCommands = new string[count]; + + for (var i = 0; i < count; i++) + { + AvailableCommands[i] = buffer.ReadString(); + } + + if (buffer.ReadBoolean()) + { + var active = buffer.ReadBoolean(); + buffer.ReadPadBits(); + var flags = (AdminFlags) buffer.ReadUInt32(); + var title = buffer.ReadString(); + + Admin = new AdminData + { + Active = active, + Title = title, + Flags = flags, + }; + } + + } + + public override void WriteToBuffer(NetOutgoingMessage buffer) + { + buffer.WriteVariableInt32(AvailableCommands.Length); + + foreach (var cmd in AvailableCommands) + { + buffer.Write(cmd); + } + + var isAdmin = Admin != null; + buffer.Write(isAdmin); + + if (isAdmin) + { + buffer.Write(Admin.Active); + buffer.WritePadBits(); + buffer.Write((uint) Admin.Flags); + buffer.Write(Admin.Title); + } + } + + public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableOrdered; + } +} diff --git a/Content.Tests/Shared/Administration/AdminFlagsExtTest.cs b/Content.Tests/Shared/Administration/AdminFlagsExtTest.cs new file mode 100644 index 0000000000..e2bc95a8f7 --- /dev/null +++ b/Content.Tests/Shared/Administration/AdminFlagsExtTest.cs @@ -0,0 +1,35 @@ +using System; +using Content.Shared.Administration; +using NUnit.Framework; + +namespace Content.Tests.Shared.Administration +{ + [TestFixture] + [Parallelizable(ParallelScope.All)] + public class AdminFlagsExtTest + { + [Test] + [TestCase("ADMIN", AdminFlags.Admin)] + [TestCase("ADMIN,DEBUG", AdminFlags.Admin | AdminFlags.Debug)] + [TestCase("ADMIN,DEBUG,HOST", AdminFlags.Admin | AdminFlags.Debug | AdminFlags.Host)] + [TestCase("", AdminFlags.None)] + public void TestNamesToFlags(string namesConcat, AdminFlags flags) + { + var names = namesConcat.Split(",", StringSplitOptions.RemoveEmptyEntries); + + Assert.That(AdminFlagsHelper.NamesToFlags(names), Is.EqualTo(flags)); + } + + [Test] + [TestCase("ADMIN", AdminFlags.Admin)] + [TestCase("ADMIN,DEBUG", AdminFlags.Admin | AdminFlags.Debug)] + [TestCase("ADMIN,DEBUG,HOST", AdminFlags.Admin | AdminFlags.Debug | AdminFlags.Host)] + [TestCase("", AdminFlags.None)] + public void TestFlagsToNames(string namesConcat, AdminFlags flags) + { + var names = namesConcat.Split(",", StringSplitOptions.RemoveEmptyEntries); + + Assert.That(AdminFlagsHelper.FlagsToNames(flags), Is.EquivalentTo(names)); + } + } +} diff --git a/Resources/Groups/groups.yml b/Resources/Groups/groups.yml deleted file mode 100644 index 640345c72d..0000000000 --- a/Resources/Groups/groups.yml +++ /dev/null @@ -1,241 +0,0 @@ -- Index: 1 - Name: Player - Commands: - - login - - joingame - - help - - list - - say - - whisper - - me - - ooc - - observe - - toggleready - - ghost - - suicide - - hostlogin - -- Index: 50 - Name: Moderator - Commands: - - login - - logout - - joingame - - help - - list - - say - - whisper - - me - - ooc - - showtime - - observe - - toggleready - - ghost - - suicide - - kick - - listplayers - - loc - - hostlogin - - events - - factions - CanAdminMenu: true - -- Index: 100 - Name: Administrator - Commands: - - logout - - joingame - - help - - list - - say - - whisper - - me - - ooc - - showtime - - aghost - - observe - - toggleready - - ghost - - suicide - - spawn - - delete - - tp - - tpto - - tpgrid - - setgamepreset - - forcepreset - - delaystart - - startround - - endround - - restartround - - respawn - - rejuvenate - - addcomp - - rmcomp - - controlmob - - kick - - listplayers - - loc - - lsmap - - lsgrid - - mindinfo - - addrole - - rmrole - - addoverlay - - rmoverlay - - showtime - - group - - addai - - warp - - hostlogin - - deleteewc - - asay - - mapping - - addhand - - removehand - - tilepry - - anchor - - unanchor - - tubeconnections - - tilewalls - - events - - destroymechanism - - addaccent - - readyall - - factions - - signallink - - adddamageflag - - removedamageflag - - godmode - - deleteewi - - hurt - - toggledisallowlatejoin - - showcontainedcontext - - hidecontainedcontext - - showmechanisms - - hidemechanisms - - attachbodypart - - attachtoself - - attachtogrid - - attachtograndparent - - inrangeunoccluded - - hungry - CanViewVar: true - CanAdminPlace: true - CanAdminMenu: true - -- Index: 200 - Name: Host - Commands: - - logout - - joingame - - help - - list - - say - - whisper - - me - - ooc - - showtime - - aghost - - observe - - toggleready - - ghost - - suicide - - spawn - - delete - - tp - - tpto - - tpgrid - - setgamepreset - - forcepreset - - delaystart - - startround - - endround - - restartround - - respawn - - rejuvenate - - addcomp - - controlmob - - kick - - listplayers - - loc - - lsmap - - lsgrid - - mindinfo - - addrole - - rmrole - - addoverlay - - rmoverlay - - srvpopupmsg - - group - - showtime - - restart - - cvar - - netaudit - - szr_stats - - mem - - addai - - loglevel - - testlog - - addmap - - rmmap - - savebp - - loadbp - - savemap - - loadmap - - pausemap - - unpausemap - - querymappaused - - mapinit - - saveconfig - - gc - - gc_mode - - warp - - deleteewc - - sudo - - asay - - mapping - - addhand - - removehand - - tilepry - - anchor - - unanchor - - tubeconnections - - addatmos - - addgas - - fillgas - - listgases - - removegas - - settemp - - setatmostemp - - deletegas - - showatmos - - tilewalls - - events - - destroymechanism - - addaccent - - readyall - - factions - - signallink - - adddamageflag - - removedamageflag - - godmode - - deleteewi - - hurt - - toggledisallowlatejoin - - showcontainedcontext - - hidecontainedcontext - - showmechanisms - - hidemechanisms - - attachbodypart - - attachtoself - - attachtogrid - - attachtograndparent - - inrangeunoccluded - - showalert - - clearalert - - hungry - CanViewVar: true - CanAdminPlace: true - CanScript: true - CanAdminMenu: true diff --git a/Resources/engineCommandPerms.yml b/Resources/engineCommandPerms.yml new file mode 100644 index 0000000000..4a97e25a55 --- /dev/null +++ b/Resources/engineCommandPerms.yml @@ -0,0 +1,65 @@ +# Available to everybody +- Commands: + - help + - list + +- Flags: FUN + Commands: + - addcomp + - rmcomp + +- Flags: DEBUG + Commands: + - delete + - lsgrid + - lsmap + - listplayers + - loc + - mem + - netaudit + - querymappaused + - showtime + - inrangeunoccluded + +- Flags: MAPPING + Commands: + - addmap + - loadbp + - loadmap + - pausemap + - querymappaused + - rmgrid + - rmmap + - mapinit + - savebp + - savemap + - tpgrid + +- Flags: ADMIN + Commands: + - delete + - kick + - listplayers + - tp + - tpto + +- Flags: SERVER + Commands: + - delete + - pausemap + - unpausemap + - restart + - shutdown + +- Flags: SPAWN + Commands: + - spawn + +- Flags: HOST + Commands: + - gc_mode + - gc + - loglevel + - saveconfig + - testlog + - sudo diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings index 77823baaed..e9b9cc859f 100644 --- a/SpaceStation14.sln.DotSettings +++ b/SpaceStation14.sln.DotSettings @@ -64,6 +64,7 @@ <data /> <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="*.UnitTesting" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> True + True True True True