Permissions panel.

This commit is contained in:
Pieter-Jan Briers
2020-11-10 16:50:28 +01:00
parent c9236d88ac
commit e39ddd4802
41 changed files with 3355 additions and 35 deletions

View File

@@ -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;

View File

@@ -7,6 +7,7 @@ namespace Content.Client.Administration
{
public event Action AdminStatusUpdated;
AdminFlags? Flags { get; }
bool HasFlag(AdminFlags flag);
bool CanCommand(string cmdName);

View File

@@ -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<IStationEventManager, StationEventManager>();
IoCManager.Register<IAdminMenuManager, AdminMenuManager>();
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
IoCManager.Register<EuiManager, EuiManager>();
}
}
}

View File

@@ -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<IClientPreferencesManager>().Initialize();
IoCManager.Resolve<IStationEventManager>().Initialize();
IoCManager.Resolve<IAdminMenuManager>().Initialize();
IoCManager.Resolve<EuiManager>().Initialize();
_baseClient.RunLevelChanged += (sender, args) =>
{

View File

@@ -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<MsgEuiMessage>();
netMsg.Id = Id;
netMsg.Message = msg;
_netManager.ClientSendMessage(netMsg);
}
}
}

View File

@@ -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<uint, EuiData> _openUis = new Dictionary<uint, EuiData>();
public void Initialize()
{
_net.RegisterNetMessage<MsgEuiCtl>(MsgEuiCtl.NAME, RxMsgCtl);
_net.RegisterNetMessage<MsgEuiState>(MsgEuiState.NAME, RxMsgState);
_net.RegisterNetMessage<MsgEuiMessage>(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<BaseEui>(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;
}
}
}
}

View File

@@ -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<string, LineEdit>(_inputs, "Text");
Owner.SendConfiguration(config);
Close();
}

View File

@@ -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<SS14Window> _commandWindows;
public void Initialize()
public void Initialize()
{
_commandWindows = new List<SS14Window>();
// Reset the AdminMenu Window on disconnect

View File

@@ -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<SS14Window> _subWindows = new List<SS14Window>();
private Dictionary<int, PermissionsEuiState.AdminRankData> _ranks =
new Dictionary<int, PermissionsEuiState.AdminRankData>();
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<int, PermissionsEuiState.AdminRankData>? 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<int, PermissionsEuiState.AdminRankData> 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<AdminFlags, (Button inherit, Button sub, Button plus)> FlagButtons
= new Dictionary<AdminFlags, (Button, Button, Button)>();
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<AdminFlags, CheckBox> FlagCheckBoxes = new Dictionary<AdminFlags, CheckBox>();
public EditAdminRankWindow(PermissionsEui ui, KeyValuePair<int, PermissionsEuiState.AdminRankData>? 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);
}
}
}

View File

@@ -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),
}),
};
}
}

View File

@@ -0,0 +1,517 @@
// <auto-generated />
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<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnName("user_id")
.HasColumnType("uuid");
b.Property<int?>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("integer");
b.Property<string>("Title")
.HasColumnName("title")
.HasColumnType("text");
b.HasKey("UserId");
b.HasIndex("AdminRankId");
b.ToTable("admin");
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_flag_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<Guid>("AdminId")
.HasColumnName("admin_id")
.HasColumnType("uuid");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("text");
b.Property<bool>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("Name")
.IsRequired()
.HasColumnName("name")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("admin_rank");
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_flag_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("integer");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("antag_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("AntagName")
.IsRequired()
.HasColumnName("antag_name")
.HasColumnType("text");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("assigned_user_id_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("job_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("JobName")
.IsRequired()
.HasColumnName("job_name")
.HasColumnType("text");
b.Property<int>("Priority")
.HasColumnName("priority")
.HasColumnType("integer");
b.Property<int>("ProfileId")
.HasColumnName("profile_id")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ProfileId");
b.ToTable("job");
});
modelBuilder.Entity("Content.Server.Database.PostgresConnectionLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("connection_log_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<IPAddress>("Address")
.IsRequired()
.HasColumnName("address")
.HasColumnType("inet");
b.Property<DateTime>("Time")
.HasColumnName("time")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("uuid");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("player_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<DateTime>("FirstSeenTime")
.HasColumnName("first_seen_time")
.HasColumnType("timestamp with time zone");
b.Property<IPAddress>("LastSeenAddress")
.IsRequired()
.HasColumnName("last_seen_address")
.HasColumnType("inet");
b.Property<DateTime>("LastSeenTime")
.HasColumnName("last_seen_time")
.HasColumnType("timestamp with time zone");
b.Property<string>("LastSeenUserName")
.IsRequired()
.HasColumnName("last_seen_user_name")
.HasColumnType("text");
b.Property<Guid>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("server_ban_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<ValueTuple<IPAddress, int>?>("Address")
.HasColumnName("address")
.HasColumnType("inet");
b.Property<DateTime>("BanTime")
.HasColumnName("ban_time")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("BanningAdmin")
.HasColumnName("banning_admin")
.HasColumnType("uuid");
b.Property<DateTime?>("ExpirationTime")
.HasColumnName("expiration_time")
.HasColumnType("timestamp with time zone");
b.Property<string>("Reason")
.IsRequired()
.HasColumnName("reason")
.HasColumnType("text");
b.Property<Guid?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("unban_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("BanId")
.HasColumnName("ban_id")
.HasColumnType("integer");
b.Property<DateTime>("UnbanTime")
.HasColumnName("unban_time")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("preference_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("SelectedCharacterSlot")
.HasColumnName("selected_character_slot")
.HasColumnType("integer");
b.Property<Guid>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("profile_id")
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("Age")
.HasColumnName("age")
.HasColumnType("integer");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnName("char_name")
.HasColumnType("text");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnName("eye_color")
.HasColumnType("text");
b.Property<string>("FacialHairColor")
.IsRequired()
.HasColumnName("facial_hair_color")
.HasColumnType("text");
b.Property<string>("FacialHairName")
.IsRequired()
.HasColumnName("facial_hair_name")
.HasColumnType("text");
b.Property<string>("HairColor")
.IsRequired()
.HasColumnName("hair_color")
.HasColumnType("text");
b.Property<string>("HairName")
.IsRequired()
.HasColumnName("hair_name")
.HasColumnType("text");
b.Property<int>("PreferenceId")
.HasColumnName("preference_id")
.HasColumnType("integer");
b.Property<int>("PreferenceUnavailable")
.HasColumnName("pref_unavailable")
.HasColumnType("integer");
b.Property<string>("Sex")
.IsRequired()
.HasColumnName("sex")
.HasColumnType("text");
b.Property<string>("SkinColor")
.IsRequired()
.HasColumnName("skin_color")
.HasColumnType("text");
b.Property<int>("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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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();

View File

@@ -0,0 +1,484 @@
// <auto-generated />
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<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.Property<int?>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnName("title")
.HasColumnType("TEXT");
b.HasKey("UserId");
b.HasIndex("AdminRankId");
b.ToTable("admin");
});
modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_flag_id")
.HasColumnType("INTEGER");
b.Property<Guid>("AdminId")
.HasColumnName("admin_id")
.HasColumnType("TEXT");
b.Property<string>("Flag")
.IsRequired()
.HasColumnName("flag")
.HasColumnType("TEXT");
b.Property<bool>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnName("name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("admin_rank");
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("admin_rank_flag_id")
.HasColumnType("INTEGER");
b.Property<int>("AdminRankId")
.HasColumnName("admin_rank_id")
.HasColumnType("INTEGER");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("antag_id")
.HasColumnType("INTEGER");
b.Property<string>("AntagName")
.IsRequired()
.HasColumnName("antag_name")
.HasColumnType("TEXT");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("assigned_user_id_id")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("job_id")
.HasColumnType("INTEGER");
b.Property<string>("JobName")
.IsRequired()
.HasColumnName("job_name")
.HasColumnType("TEXT");
b.Property<int>("Priority")
.HasColumnName("priority")
.HasColumnType("INTEGER");
b.Property<int>("ProfileId")
.HasColumnName("profile_id")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ProfileId");
b.ToTable("job");
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("preference_id")
.HasColumnType("INTEGER");
b.Property<int>("SelectedCharacterSlot")
.HasColumnName("selected_character_slot")
.HasColumnType("INTEGER");
b.Property<Guid>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("profile_id")
.HasColumnType("INTEGER");
b.Property<int>("Age")
.HasColumnName("age")
.HasColumnType("INTEGER");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnName("char_name")
.HasColumnType("TEXT");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnName("eye_color")
.HasColumnType("TEXT");
b.Property<string>("FacialHairColor")
.IsRequired()
.HasColumnName("facial_hair_color")
.HasColumnType("TEXT");
b.Property<string>("FacialHairName")
.IsRequired()
.HasColumnName("facial_hair_name")
.HasColumnType("TEXT");
b.Property<string>("HairColor")
.IsRequired()
.HasColumnName("hair_color")
.HasColumnType("TEXT");
b.Property<string>("HairName")
.IsRequired()
.HasColumnName("hair_name")
.HasColumnType("TEXT");
b.Property<int>("PreferenceId")
.HasColumnName("preference_id")
.HasColumnType("INTEGER");
b.Property<int>("PreferenceUnavailable")
.HasColumnName("pref_unavailable")
.HasColumnType("INTEGER");
b.Property<string>("Sex")
.IsRequired()
.HasColumnName("sex")
.HasColumnType("TEXT");
b.Property<string>("SkinColor")
.IsRequired()
.HasColumnName("skin_color")
.HasColumnType("TEXT");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("connection_log_id")
.HasColumnType("INTEGER");
b.Property<string>("Address")
.IsRequired()
.HasColumnName("address")
.HasColumnType("TEXT");
b.Property<DateTime>("Time")
.HasColumnName("time")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.Property<string>("UserName")
.IsRequired()
.HasColumnName("user_name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("connection_log");
});
modelBuilder.Entity("Content.Server.Database.SqlitePlayer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("player_id")
.HasColumnType("INTEGER");
b.Property<DateTime>("FirstSeenTime")
.HasColumnName("first_seen_time")
.HasColumnType("TEXT");
b.Property<string>("LastSeenAddress")
.IsRequired()
.HasColumnName("last_seen_address")
.HasColumnType("TEXT");
b.Property<DateTime>("LastSeenTime")
.HasColumnName("last_seen_time")
.HasColumnType("TEXT");
b.Property<string>("LastSeenUserName")
.IsRequired()
.HasColumnName("last_seen_user_name")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LastSeenUserName");
b.ToTable("player");
});
modelBuilder.Entity("Content.Server.Database.SqliteServerBan", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("ban_id")
.HasColumnType("INTEGER");
b.Property<string>("Address")
.HasColumnName("address")
.HasColumnType("TEXT");
b.Property<DateTime>("BanTime")
.HasColumnName("ban_time")
.HasColumnType("TEXT");
b.Property<Guid?>("BanningAdmin")
.HasColumnName("banning_admin")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpirationTime")
.HasColumnName("expiration_time")
.HasColumnType("TEXT");
b.Property<string>("Reason")
.IsRequired()
.HasColumnName("reason")
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnName("user_id")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ban");
});
modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("unban_id")
.HasColumnType("INTEGER");
b.Property<int>("BanId")
.HasColumnName("ban_id")
.HasColumnType("INTEGER");
b.Property<DateTime>("UnbanTime")
.HasColumnName("unban_time")
.HasColumnType("TEXT");
b.Property<Guid?>("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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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");
});

View File

@@ -57,6 +57,14 @@ namespace Content.Server.Database
.HasOne(p => p.AdminRank)
.WithMany(p => p!.Admins)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<AdminFlag>()
.HasIndex(f => new {f.Flag, f.AdminId})
.IsUnique();
modelBuilder.Entity<AdminRankFlag>()
.HasIndex(f => new {f.Flag, f.AdminRankId})
.IsUnique();
}
}

View File

@@ -65,6 +65,9 @@ namespace Content.Server.Database
.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4",
"NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
modelBuilder.Entity<PostgresPlayer>()
.HasIndex(p => p.LastSeenUserName);
modelBuilder.Entity<PostgresConnectionLog>()
.HasIndex(p => p.UserId);

View File

@@ -21,6 +21,14 @@ namespace Content.Server.Database
options.UseSqlite("dummy connection string");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<SqlitePlayer>()
.HasIndex(p => p.LastSeenUserName);
}
public SqliteServerDbContext(DbContextOptions<ServerDbContext> options) : base(options)
{
}

View File

@@ -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<IPlayerSession, AdminReg> _admins = new Dictionary<IPlayerSession, AdminReg>();
public event Action<AdminPermsChangedEventArgs>? OnPermsChanged;
public IEnumerable<IPlayerSession> 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>(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)

View File

@@ -0,0 +1,33 @@
using System;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Player;
namespace Content.Server.Administration
{
/// <summary>
/// Sealed when the permissions of an admin on the server change.
/// </summary>
public sealed class AdminPermsChangedEventArgs : EventArgs
{
public AdminPermsChangedEventArgs(IPlayerSession player, AdminFlags? flags)
{
Player = player;
Flags = flags;
}
/// <summary>
/// The player that had their admin permissions changed.
/// </summary>
public IPlayerSession Player { get; }
/// <summary>
/// The admin flags of the player. Null if the player is no longer an admin.
/// </summary>
public AdminFlags? Flags { get; }
/// <summary>
/// Whether the player is now an admin.
/// </summary>
public bool IsAdmin => Flags.HasValue;
}
}

View File

@@ -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<EuiManager>();
var ui = new PermissionsEui();
eui.OpenEui(ui, player);
}
}
}

View File

@@ -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
/// </summary>
public interface IAdminManager
{
/// <summary>
/// Fired when the permissions of an admin on the server changed.
/// </summary>
event Action<AdminPermsChangedEventArgs> OnPermsChanged;
/// <summary>
/// Gets all active admins currently on the server.
/// </summary>
@@ -29,6 +35,16 @@ namespace Content.Server.Administration
/// <returns><see langword="null" /> if the player is not an admin.</returns>
AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false);
/// <summary>
/// See if a player has an admin flag.
/// </summary>
/// <returns>True if the player is and admin and has the specified flags.</returns>
bool HasAdminFlag(IPlayerSession player, AdminFlags flag)
{
var data = GetAdminData(player);
return data != null && data.HasFlag(flag);
}
/// <summary>
/// De-admins an admin temporarily so they are effectively a normal player.
/// </summary>
@@ -42,6 +58,19 @@ namespace Content.Server.Administration
/// </summary>
void ReAdmin(IPlayerSession session);
/// <summary>
/// Re-loads the permissions of an player in case their admin data changed DB-side.
/// </summary>
/// <seealso cref="ReloadAdminsWithRank"/>
void ReloadAdmin(IPlayerSession player);
/// <summary>
/// Reloads admin permissions for all admins with a certain rank.
/// </summary>
/// <param name="rankId">The database ID of the rank.</param>
/// <seealso cref="ReloadAdmin"/>
void ReloadAdminsWithRank(int rankId);
void Initialize();
}
}

View File

@@ -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<DbAdminRank> _adminRanks = new List<DbAdminRank>();
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 ?? "<no 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 ??= "<no rank>";
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 ?? "<no 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<AdminFlag> 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<AdminRankFlag> 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<string>());
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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel);
public abstract Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel);
/*
* CONNECTION LOG
@@ -220,7 +223,7 @@ namespace Content.Server.Database
/*
* ADMIN STUFF
*/
public async Task<Admin?> GetAdminDataForAsync(NetUserId userId)
public async Task<Admin?> 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<AdminRank?> 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<DbGuard> GetDb();

View File

@@ -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<PlayerPreferences> 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<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId);
@@ -42,12 +45,26 @@ namespace Content.Server.Database
// Player records
Task UpdatePlayerRecordAsync(NetUserId userId, string userName, IPAddress address);
Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default);
Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default);
// Connection log
Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address);
// Admins
Task<Admin?> GetAdminDataForAsync(NetUserId userId);
Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default);
Task<AdminRank?> 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<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default)
{
return _db.GetPlayerRecordByUserName(userName, cancel);
}
public Task<PlayerRecord?> 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<Admin?> GetAdminDataForAsync(NetUserId userId)
public Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default)
{
return _db.GetAdminDataForAsync(userId);
return _db.GetAdminDataForAsync(userId, cancel);
}
public Task<AdminRank?> 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<ServerDbContext> CreatePostgresOptions()

View File

@@ -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<PlayerRecord?> 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<PlayerRecord?> 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<DbGuardImpl> GetDbImpl()
{
await _dbReadyTask;

View File

@@ -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<PlayerRecord?> 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<PlayerRecord?> 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<DbGuardImpl> GetDbImpl()
{

View File

@@ -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;
/// <inheritdoc />
@@ -50,6 +52,7 @@ namespace Content.Server
IoCManager.BuildGraph();
_gameTicker = IoCManager.Resolve<IGameTicker>();
_euiManager = IoCManager.Resolve<EuiManager>();
IoCManager.Resolve<IServerNotifyManager>().Initialize();
IoCManager.Resolve<IChatManager>().Initialize();
@@ -79,6 +82,7 @@ namespace Content.Server
IoCManager.Resolve<ConsiderationsManager>().Initialize();
IoCManager.Resolve<IPDAUplinkManager>().Initialize();
IoCManager.Resolve<IAdminManager>().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;
}
}
}
}

View File

@@ -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;
}
/// <summary>
/// Mark the current UI state as dirty and queue for an update.
/// </summary>
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<IServerNetManager>();
var msg = netMgr.CreateNetMessage<MsgEuiState>();
msg.Id = Id;
msg.State = state;
netMgr.ServerSendMessage(msg, Player.ConnectedClient);
}
public void SendMessage(EuiMessageBase message)
{
var netMgr = IoCManager.Resolve<IServerNetManager>();
var msg = netMgr.CreateNetMessage<MsgEuiMessage>();
msg.Id = Id;
msg.Message = message;
netMgr.ServerSendMessage(msg, Player.ConnectedClient);
}
}
}

View File

@@ -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<IPlayerSession, PlayerEuiData> _playerData =
new Dictionary<IPlayerSession, PlayerEuiData>();
private readonly Queue<(IPlayerSession player, uint id)> _stateUpdateQueue =
new Queue<(IPlayerSession, uint id)>();
private sealed class PlayerEuiData
{
public uint NextId = 1;
public readonly Dictionary<uint, BaseEui> OpenUIs = new Dictionary<uint, BaseEui>();
}
void IPostInjectInit.PostInject()
{
_players.PlayerStatusChanged += PlayerStatusChanged;
}
public void Initialize()
{
_net.RegisterNetMessage<MsgEuiCtl>(MsgEuiCtl.NAME);
_net.RegisterNetMessage<MsgEuiState>(MsgEuiState.NAME);
_net.RegisterNetMessage<MsgEuiMessage>(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<MsgEuiCtl>();
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<MsgEuiCtl>();
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));
}
}
}

View File

@@ -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<IConnectionManager, ConnectionManager>();
IoCManager.Register<IAdminManager, AdminManager>();
IoCManager.Register<IDeviceNetwork, DeviceNetwork>();
IoCManager.Register<EuiManager, EuiManager>();
}
}
}

View File

@@ -58,7 +58,7 @@ namespace Content.Shared.Administration
/// <summary>
/// Makes you british.
/// </summary>
Piss = 1 << 9,
//Piss = 1 << 9,
/// <summary>
/// Dangerous host permissions like scsi.

View File

@@ -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<AdminFlags> AllFlags;
static AdminFlagsExt()
{
var t = typeof(AdminFlags);
var flags = (AdminFlags[]) Enum.GetValues(t);
var allFlags = new List<AdminFlags>();
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<string> 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;
}
}
}

View File

@@ -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<int, AdminRankData> 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;
}
}
}

View File

@@ -0,0 +1,10 @@
using System;
namespace Content.Shared.Eui
{
[Serializable]
public abstract class EuiMessageBase
{
}
}

View File

@@ -0,0 +1,11 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.Eui
{
[Serializable, NetSerializable]
public abstract class EuiStateBase
{
}
}

View File

@@ -0,0 +1,55 @@
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Network;
namespace Content.Shared.Network.NetMessages
{
/// <summary>
/// Sent server -> client to signal that the client should open an EUI.
/// </summary>
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
}
}
}

View File

@@ -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<IRobustSerializer>();
var len = buffer.ReadVariableInt32();
var stream = buffer.ReadAlignedMemory(len);
Message = ser.Deserialize<EuiMessageBase>(stream);
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(Id);
var stream = new MemoryStream();
var ser = IoCManager.Resolve<IRobustSerializer>();
ser.Serialize(stream, Message);
var length = (int)stream.Length;
buffer.WriteVariableInt32(length);
buffer.Write(stream.GetBuffer().AsSpan(0, length));
}
}
}

View File

@@ -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<IRobustSerializer>();
var len = buffer.ReadVariableInt32();
var stream = buffer.ReadAlignedMemory(len);
State = ser.Deserialize<EuiStateBase>(stream);
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
{
buffer.Write(Id);
var stream = new MemoryStream();
var ser = IoCManager.Resolve<IRobustSerializer>();
ser.Serialize(stream, State);
var length = (int)stream.Length;
buffer.WriteVariableInt32(length);
buffer.Write(stream.GetBuffer().AsSpan(0, length));
}
}
}