diff --git a/Content.Client/Administration/ClientAdminManager.cs b/Content.Client/Administration/ClientAdminManager.cs index be0c18c974..96b644579c 100644 --- a/Content.Client/Administration/ClientAdminManager.cs +++ b/Content.Client/Administration/ClientAdminManager.cs @@ -21,6 +21,8 @@ namespace Content.Client.Administration public event Action? AdminStatusUpdated; + public AdminFlags? Flags => _adminData?.Flags; + public bool HasFlag(AdminFlags flag) { return _adminData?.HasFlag(flag) ?? false; @@ -74,6 +76,7 @@ namespace Content.Client.Administration } AdminStatusUpdated?.Invoke(); + ConGroupUpdated?.Invoke(); } public event Action? ConGroupUpdated; diff --git a/Content.Client/Administration/IClientAdminManager.cs b/Content.Client/Administration/IClientAdminManager.cs index 533850dfaa..1bdf0cb029 100644 --- a/Content.Client/Administration/IClientAdminManager.cs +++ b/Content.Client/Administration/IClientAdminManager.cs @@ -7,6 +7,7 @@ namespace Content.Client.Administration { public event Action AdminStatusUpdated; + AdminFlags? Flags { get; } bool HasFlag(AdminFlags flag); bool CanCommand(string cmdName); diff --git a/Content.Client/ClientContentIoC.cs b/Content.Client/ClientContentIoC.cs index 9e353cee87..841ed7d44a 100644 --- a/Content.Client/ClientContentIoC.cs +++ b/Content.Client/ClientContentIoC.cs @@ -1,5 +1,6 @@ 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 +38,7 @@ namespace Content.Client IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 8512a2231a..d3fa1057f8 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -1,5 +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; @@ -154,6 +155,7 @@ namespace Content.Client 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..65ebe3d213 --- /dev/null +++ b/Content.Client/Eui/BaseEui.cs @@ -0,0 +1,53 @@ +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); + } + + public void Initialize(EuiManager mgr, uint id) + { + Manager = mgr; + Id = id; + } + + public virtual void Opened() + { + } + + public virtual void Closed() + { + } + + public virtual void HandleState(EuiStateBase state) + { + } + + public virtual void HandleMessage(EuiMessageBase msg) + { + } + + 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/GameObjects/Components/Configuration/ConfigurationMenu.cs b/Content.Client/GameObjects/Components/Configuration/ConfigurationMenu.cs index d236ae0202..f3a2141d60 100644 --- a/Content.Client/GameObjects/Components/Configuration/ConfigurationMenu.cs +++ b/Content.Client/GameObjects/Components/Configuration/ConfigurationMenu.cs @@ -101,7 +101,7 @@ namespace Content.Client.GameObjects.Components.Wires { _column.Children.Clear(); _inputs.Clear(); - + foreach (var field in state.Config) { var margin = new MarginContainer @@ -143,7 +143,7 @@ namespace Content.Client.GameObjects.Components.Wires private void OnConfirm(ButtonEventArgs args) { var config = GenerateDictionary(_inputs, "Text"); - + Owner.SendConfiguration(config); Close(); } 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..4801472f3e --- /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 = AdminFlagsExt.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(' ', AdminFlagsExt.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 AdminFlagsExt.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 = "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 AdminFlagsExt.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.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 916c5843f2..16f84e3e27 100644 --- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs @@ -67,6 +67,9 @@ namespace Content.Server.Database.Migrations.Postgres b.HasIndex("AdminId"); + b.HasIndex("Flag", "AdminId") + .IsUnique(); + b.ToTable("admin_flag"); }); @@ -109,6 +112,9 @@ namespace Content.Server.Database.Migrations.Postgres b.HasIndex("AdminRankId"); + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + b.ToTable("admin_rank_flag"); }); @@ -260,6 +266,8 @@ namespace Content.Server.Database.Migrations.Postgres b.HasKey("Id"); + b.HasIndex("LastSeenUserName"); + b.HasIndex("UserId") .IsUnique(); 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 67f92bea49..bf24a18637 100644 --- a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs @@ -62,6 +62,9 @@ namespace Content.Server.Database.Migrations.Sqlite b.HasIndex("AdminId"); + b.HasIndex("Flag", "AdminId") + .IsUnique(); + b.ToTable("admin_flag"); }); @@ -102,6 +105,9 @@ namespace Content.Server.Database.Migrations.Sqlite b.HasIndex("AdminRankId"); + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + b.ToTable("admin_rank_flag"); }); @@ -340,6 +346,8 @@ namespace Content.Server.Database.Migrations.Sqlite b.HasKey("Id"); + b.HasIndex("LastSeenUserName"); + b.ToTable("player"); }); diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index d701c5cfcc..c52a41fc0c 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -57,6 +57,14 @@ namespace Content.Server.Database .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(); } } 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/AdminManager.cs b/Content.Server/Administration/AdminManager.cs index 28c5f93c30..8048346941 100644 --- a/Content.Server/Administration/AdminManager.cs +++ b/Content.Server/Administration/AdminManager.cs @@ -4,6 +4,7 @@ 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; @@ -40,6 +41,8 @@ namespace Content.Server.Administration private readonly Dictionary _admins = new Dictionary(); + public event Action? OnPermsChanged; + public IEnumerable ActiveAdmins => _admins .Where(p => p.Value.Data.Active) .Select(p => p.Key); @@ -78,6 +81,7 @@ namespace Content.Server.Administration plyData.ExplicitlyDeadminned = true; reg.Data.Active = false; + SendPermsChangedEvent(session); UpdateAdminStatus(session); } @@ -96,9 +100,70 @@ namespace Content.Server.Administration _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); @@ -143,7 +208,7 @@ namespace Content.Server.Administration { if (!_adminCommands.TryGetValue(cmd, out var exFlags)) { - _adminCommands.Add(cmd, new []{flags}); + _adminCommands.Add(cmd, new[] {flags}); } else { @@ -213,7 +278,39 @@ namespace Content.Server.Administration private async void LoginAdminMaybe(IPlayerSession session) { - AdminReg reg; + 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 @@ -222,10 +319,7 @@ namespace Content.Server.Administration Flags = AdminFlagsExt.Everything, }; - reg = new AdminReg(session, data) - { - IsSpecialLogin = true, - }; + return (data, null, true); } else { @@ -234,7 +328,7 @@ namespace Content.Server.Administration if (dbData == null) { // Not an admin! - return; + return null; } var flags = AdminFlags.None; @@ -271,22 +365,8 @@ namespace Content.Server.Administration data.Title = dbData.AdminRank.Name; } - reg = new AdminReg(session, data); + return (data, dbData.AdminRankId, false); } - - _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)); - } - } - - UpdateAdminStatus(session); } private static bool IsLocal(IPlayerSession player) @@ -372,14 +452,20 @@ namespace Content.Server.Administration 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 - // Means that stuff like permissions editing is blocked. public bool IsSpecialLogin; public AdminReg(IPlayerSession session, AdminData data) 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/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/IAdminManager.cs b/Content.Server/Administration/IAdminManager.cs index e475ebeec8..ba2088f0b6 100644 --- a/Content.Server/Administration/IAdminManager.cs +++ b/Content.Server/Administration/IAdminManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Content.Shared.Administration; using Robust.Server.Interfaces.Player; @@ -11,6 +12,11 @@ namespace Content.Server.Administration /// 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. /// @@ -29,6 +35,16 @@ namespace Content.Server.Administration /// 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. /// @@ -42,6 +58,19 @@ namespace Content.Server.Administration /// 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..018b3a83ab --- /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 += AdminManagerOnOnPermsChanged; + } + + public override void Closed() + { + base.Closed(); + + _adminManager.OnPermsChanged -= AdminManagerOnOnPermsChanged; + } + + private void AdminManagerOnOnPermsChanged(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 = AdminFlagsExt.NamesToFlags(p.a.Flags.Where(f => !f.Negative).Select(f => f.Flag)), + NegFlags = AdminFlagsExt.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 = AdminFlagsExt.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(' ', AdminFlagsExt.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(' ', AdminFlagsExt.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 = AdminFlagsExt.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 = AdminFlagsExt.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 = AdminFlagsExt.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 = AdminFlagsExt.FlagsToNames(posFlags); + var negFlagList = AdminFlagsExt.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 AdminFlagsExt.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 = AdminFlagsExt.NamesToFlags(admin.Flags.Where(f => !f.Negative).Select(f => f.Flag)); + var rankFlags = AdminFlagsExt.NamesToFlags( + admin.AdminRank?.Flags.Select(f => f.Flag) ?? Array.Empty()); + + var totalFlags = posFlags | rankFlags; + return UserAdminFlagCheck(totalFlags); + } + + private bool CanTouchRank(DbAdminRank rank) + { + var rankFlags = AdminFlagsExt.NamesToFlags(rank.Flags.Select(f => f.Flag)); + + return UserAdminFlagCheck(rankFlags); + } + } +} 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 e09ebc014e..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,6 +212,8 @@ 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 @@ -220,7 +223,7 @@ namespace Content.Server.Database /* * ADMIN STUFF */ - public async Task GetAdminDataForAsync(NetUserId userId) + public async Task GetAdminDataForAsync(NetUserId userId, CancellationToken cancel) { await using var db = await GetDb(); @@ -228,7 +231,75 @@ namespace Content.Server.Database .Include(p => p.Flags) .Include(p => p.AdminRank) .ThenInclude(p => p!.Flags) - .SingleOrDefaultAsync(p => p.UserId == userId.UserId); + .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 f7eb67ee09..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,12 +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); + 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 @@ -135,14 +152,65 @@ 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) + public Task GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default) { - return _db.GetAdminDataForAsync(userId); + 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() 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 3bc16dee3a..6c8e46bf6d 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -2,6 +2,7 @@ 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 +24,7 @@ namespace Content.Server public class EntryPoint : GameServer { private IGameTicker _gameTicker; + private EuiManager _euiManager; private StatusShell _statusShell; /// @@ -50,6 +52,7 @@ namespace Content.Server IoCManager.BuildGraph(); _gameTicker = IoCManager.Resolve(); + _euiManager = IoCManager.Resolve(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); @@ -79,6 +82,7 @@ namespace Content.Server IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + _euiManager.Initialize(); } public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs) @@ -92,6 +96,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..5b58cc4a4a --- /dev/null +++ b/Content.Server/Eui/BaseEui.cs @@ -0,0 +1,97 @@ +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 +{ + public abstract class BaseEui + { + private bool _isStateDirty = false; + + public bool IsShutDown { get; private set; } + public EuiManager Manager { get; private set; } = default!; + public IPlayerSession Player { get; private set; } = default!; + public uint Id { get; private set; } + + public void Initialize(EuiManager manager, IPlayerSession player, uint id) + { + Manager = manager; + Player = player; + Id = id; + Opened(); + } + + public virtual void Opened() + { + + } + + public virtual void Closed() + { + + } + + public virtual void HandleMessage(EuiMessageBase msg) + { + } + + public void Shutdown() + { + Closed(); + IsShutDown = true; + } + + /// + /// Mark the current UI state as dirty and queue for an update. + /// + public void StateDirty() + { + if (_isStateDirty) + { + return; + } + + _isStateDirty = true; + Manager.QueueStateUpdate(this); + } + + public virtual EuiStateBase GetNewState() + { + throw new NotSupportedException(); + } + + public void Close() + { + Manager.CloseEui(this); + } + + public 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); + } + + public void SendMessage(EuiMessageBase message) + { + var netMgr = IoCManager.Resolve(); + var msg = netMgr.CreateNetMessage(); + msg.Id = Id; + msg.Message = message; + + netMgr.ServerSendMessage(msg, Player.ConnectedClient); + } + } +} diff --git a/Content.Server/Eui/EuiManager.cs b/Content.Server/Eui/EuiManager.cs new file mode 100644 index 0000000000..80e0246d08 --- /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.Closed(); + _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/ServerContentIoC.cs b/Content.Server/ServerContentIoC.cs index fca5cef686..6d9d94949b 100644 --- a/Content.Server/ServerContentIoC.cs +++ b/Content.Server/ServerContentIoC.cs @@ -4,6 +4,7 @@ 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; @@ -48,6 +49,7 @@ namespace Content.Server IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Shared/Administration/AdminFlags.cs b/Content.Shared/Administration/AdminFlags.cs index 4b3a856c55..2f6b336fe6 100644 --- a/Content.Shared/Administration/AdminFlags.cs +++ b/Content.Shared/Administration/AdminFlags.cs @@ -58,7 +58,7 @@ namespace Content.Shared.Administration /// /// Makes you british. /// - Piss = 1 << 9, + //Piss = 1 << 9, /// /// Dangerous host permissions like scsi. diff --git a/Content.Shared/Administration/AdminFlagsExt.cs b/Content.Shared/Administration/AdminFlagsExt.cs index abe198a1f0..0c018e8c3a 100644 --- a/Content.Shared/Administration/AdminFlagsExt.cs +++ b/Content.Shared/Administration/AdminFlagsExt.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; namespace Content.Shared.Administration @@ -11,10 +12,13 @@ namespace Content.Shared.Administration public static readonly AdminFlags Everything; + public static readonly IReadOnlyList AllFlags; + static AdminFlagsExt() { var t = typeof(AdminFlags); var flags = (AdminFlags[]) Enum.GetValues(t); + var allFlags = new List(); foreach (var value in flags) { @@ -25,10 +29,13 @@ namespace Content.Shared.Administration continue; } + allFlags.Add(value); Everything |= value; NameFlagsMap.Add(name, value); FlagsNameMap[BitOperations.Log2((uint) value)] = name; } + + AllFlags = allFlags.ToArray(); } public static AdminFlags NamesToFlags(IEnumerable names) @@ -69,5 +76,14 @@ namespace Content.Shared.Administration 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/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)); + } + } +}