Permissions panel.
This commit is contained in:
@@ -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)
|
||||
|
||||
33
Content.Server/Administration/AdminPermsChangedEventArgs.cs
Normal file
33
Content.Server/Administration/AdminPermsChangedEventArgs.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
460
Content.Server/Administration/PermissionsEui.cs
Normal file
460
Content.Server/Administration/PermissionsEui.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Content.Server/Database/PlayerRecord.cs
Normal file
29
Content.Server/Database/PlayerRecord.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
97
Content.Server/Eui/BaseEui.cs
Normal file
97
Content.Server/Eui/BaseEui.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
144
Content.Server/Eui/EuiManager.cs
Normal file
144
Content.Server/Eui/EuiManager.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user