Admin notes (#7259)

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
DrSmugleaf
2022-04-16 20:57:50 +02:00
committed by GitHub
parent 0041b9d933
commit 5227d1a023
38 changed files with 4009 additions and 23 deletions

View File

@@ -10,6 +10,7 @@
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2">
<BoxContainer Access="Public" Name="BwoinkArea" VerticalExpand="True" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Right">
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" />
<Button Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" />
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" />
<Button Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" />

View File

@@ -72,6 +72,12 @@ namespace Content.Client.Administration.UI
return bch!.LastMessage.CompareTo(ach!.LastMessage);
};
Notes.OnPressed += _ =>
{
if (_currentPlayer is not null)
_console.ExecuteCommand($"adminnotes \"{_currentPlayer.SessionId}\"");
};
// ew
Ban.OnPressed += _ =>
{
@@ -138,6 +144,9 @@ namespace Content.Client.Administration.UI
private void FixButtons()
{
Notes.Visible = _adminManager.HasFlag(AdminFlags.ViewNotes);
Notes.Disabled = !Notes.Visible;
Ban.Visible = _adminManager.HasFlag(AdminFlags.Ban);
Ban.Disabled = !Ban.Visible;

View File

@@ -5,7 +5,7 @@
MinHeight="400">
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#25252add"/>
<gfx:StyleBoxFlat BackgroundColor="#25252A"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Vertical">

View File

@@ -1,14 +1,12 @@
using System.Linq;
using Content.Client.Eui;
using Content.Shared.Administration;
using Content.Shared.Administration.Logs;
using Content.Shared.Eui;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.IoC;
using static Content.Shared.Administration.AdminLogsEuiMsg;
using static Content.Shared.Administration.Logs.AdminLogsEuiMsg;
namespace Content.Client.Administration.UI.Logs;

View File

@@ -0,0 +1,17 @@
<Control xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
MinWidth="400"
MinHeight="400">
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#25252A"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical">
<ScrollContainer VerticalExpand="True" HorizontalExpand="True" HScrollEnabled="False">
<BoxContainer Orientation="Vertical" Name="Notes" Access="Public" VerticalExpand="True"/>
</ScrollContainer>
<Label Name="NewNoteLabel" Text="{Loc admin-notes-new-note}" />
<HistoryLineEdit Name="NewNote"/>
</BoxContainer>
</PanelContainer>
</Control>

View File

@@ -0,0 +1,146 @@
using System.Linq;
using Content.Shared.Administration.Notes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using static Robust.Client.UserInterface.Controls.LineEdit;
namespace Content.Client.Administration.UI.Notes;
[GenerateTypedNameReferences]
public sealed partial class AdminNotesControl : Control
{
[Dependency] private readonly IUserInterfaceManager _ui = default!;
public event Action<int, string>? OnNoteChanged;
public event Action<string>? OnNewNoteEntered;
public event Action<int>? OnNoteDeleted;
private AdminNotesLinePopup? _popup;
public AdminNotesControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
NewNote.OnTextEntered += NewNoteEntered;
}
private Dictionary<int, AdminNotesLine> Inputs { get; } = new();
private bool CanCreate { get; set; }
private bool CanDelete { get; set; }
private bool CanEdit { get; set; }
private void NewNoteEntered(LineEditEventArgs args)
{
if (string.IsNullOrWhiteSpace(args.Text))
{
return;
}
NewNote.Clear();
OnNewNoteEntered?.Invoke(args.Text);
}
private void NoteSubmitted(AdminNotesLine input)
{
var text = input.EditText.Trim();
if (input.OriginalMessage == text)
{
return;
}
OnNoteChanged?.Invoke(input.Id, text);
}
private bool NoteRightClicked(AdminNotesLine line)
{
ClosePopup();
_popup = new AdminNotesLinePopup(line.Note, CanDelete, CanEdit);
_popup.OnEditPressed += noteId =>
{
if (!Inputs.TryGetValue(noteId, out var input))
{
return;
}
input.SetEditable(true);
};
_popup.OnDeletePressed += noteId => OnNoteDeleted?.Invoke(noteId);
var box = UIBox2.FromDimensions(_ui.MousePositionScaled.Position, (1, 1));
_popup.Open(box);
return true;
}
private void ClosePopup()
{
_popup?.Close();
_popup = null;
}
public void SetNotes(Dictionary<int, SharedAdminNote> notes)
{
foreach (var (id, input) in Inputs)
{
if (!notes.ContainsKey(id))
{
Notes.RemoveChild(input);
Inputs.Remove(id);
}
}
foreach (var note in notes.Values.OrderBy(note => note.Id))
{
if (Inputs.TryGetValue(note.Id, out var input))
{
input.UpdateNote(note);
continue;
}
input = new AdminNotesLine(note);
input.OnSubmitted += NoteSubmitted;
input.OnRightClicked += NoteRightClicked;
Notes.AddChild(input);
Inputs[note.Id] = input;
}
}
public void SetPermissions(bool create, bool delete, bool edit)
{
CanCreate = create;
CanDelete = delete;
CanEdit = edit;
NewNoteLabel.Visible = create;
NewNote.Visible = create;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
{
return;
}
foreach (var input in Inputs.Values)
{
input.OnSubmitted -= NoteSubmitted;
}
Inputs.Clear();
NewNote.OnTextEntered -= NewNoteEntered;
if (_popup != null)
{
_ui.PopupRoot.RemoveChild(_popup);
}
OnNoteChanged = null;
OnNewNoteEntered = null;
OnNoteDeleted = null;
}
}

View File

@@ -0,0 +1,42 @@
using Content.Client.Eui;
using Content.Shared.Administration.Notes;
using Content.Shared.Eui;
using JetBrains.Annotations;
using static Content.Shared.Administration.Notes.AdminNoteEuiMsg;
namespace Content.Client.Administration.UI.Notes;
[UsedImplicitly]
public sealed class AdminNotesEui : BaseEui
{
public AdminNotesEui()
{
NoteWindow = new AdminNotesWindow();
NoteControl = NoteWindow.Notes;
NoteControl.OnNoteChanged += (id, text) => SendMessage(new EditNoteRequest(id, text));
NoteControl.OnNewNoteEntered += text => SendMessage(new CreateNoteRequest(text));
NoteControl.OnNoteDeleted += id => SendMessage(new DeleteNoteRequest(id));
}
private AdminNotesWindow NoteWindow { get; }
private AdminNotesControl NoteControl { get; }
public override void HandleState(EuiStateBase state)
{
if (state is not AdminNotesEuiState s)
{
return;
}
NoteWindow.SetTitlePlayer(s.NotedPlayerName);
NoteControl.SetNotes(s.Notes);
NoteControl.SetPermissions(s.CanCreate, s.CanDelete, s.CanEdit);
}
public override void Opened()
{
NoteWindow.OpenCentered();
}
}

View File

@@ -0,0 +1,5 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
Orientation="Vertical">
<cc:HSeparator Name="Separator"/>
</BoxContainer>

View File

@@ -0,0 +1,142 @@
using Content.Shared.Administration.Notes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
using static Robust.Client.UserInterface.Controls.LineEdit;
namespace Content.Client.Administration.UI.Notes;
[GenerateTypedNameReferences]
public sealed partial class AdminNotesLine : BoxContainer
{
private RichTextLabel? _label;
private LineEdit? _edit;
public AdminNotesLine(SharedAdminNote note)
{
RobustXamlLoader.Load(this);
Note = note;
MouseFilter = MouseFilterMode.Pass;
AddLabel();
}
public SharedAdminNote Note { get; private set; }
public int Id => Note.Id;
public string OriginalMessage => Note.Message;
public string EditText => _edit?.Text ?? OriginalMessage;
public event Action<AdminNotesLine>? OnSubmitted;
public event Func<AdminNotesLine, bool>? OnRightClicked;
private void AddLabel()
{
if (_edit != null)
{
_edit.OnTextEntered -= Submitted;
_edit.OnFocusExit -= Submitted;
RemoveChild(_edit);
_edit = null;
}
_label = new RichTextLabel();
_label.SetMessage(Note.Message);
AddChild(_label);
_label.SetPositionFirst();
Separator.Visible = true;
}
private void AddLineEdit()
{
if (_label != null)
{
RemoveChild(_label);
_label = null;
}
_edit = new LineEdit {Text = Note.Message};
_edit.OnTextEntered += Submitted;
_edit.OnFocusExit += Submitted;
AddChild(_edit);
_edit.SetPositionFirst();
_edit.GrabKeyboardFocus();
_edit.CursorPosition = _edit.Text.Length;
Separator.Visible = false;
}
private void Submitted(LineEditEventArgs args)
{
OnSubmitted?.Invoke(this);
AddLabel();
var note = Note with {Message = args.Text};
UpdateNote(note);
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
{
base.KeyBindDown(args);
if (args.Function != EngineKeyFunctions.UIRightClick &&
args.Function != EngineKeyFunctions.UIClick)
{
return;
}
if (OnRightClicked?.Invoke(this) == true)
{
args.Handle();
}
}
public void UpdateNote(SharedAdminNote note)
{
Note = note;
_label?.SetMessage(note.Message);
if (_edit != null && _edit.Text != note.Message)
{
_edit.Text = note.Message;
}
}
public void SetEditable(bool editable)
{
if (editable)
{
AddLineEdit();
}
else
{
AddLabel();
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
{
return;
}
if (_edit != null)
{
_edit.OnTextEntered -= Submitted;
_edit.OnFocusExit -= Submitted;
}
OnSubmitted = null;
OnRightClicked = null;
}
}

View File

@@ -0,0 +1,21 @@
<Popup xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#25252A"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical">
<Label Name="IdLabel"/>
<Label Name="RoundIdLabel"/>
<Label Name="CreatedByLabel"/>
<Label Name="CreatedAtLabel"/>
<Label Name="EditedByLabel"/>
<Label Name="EditedAtLabel"/>
<BoxContainer Orientation="Horizontal">
<Button Name="EditButton" Text="{Loc admin-notes-edit}"/>
<Control HorizontalExpand="True"/>
<Button Name="DeleteButton" Text="{Loc admin-notes-delete}" HorizontalAlignment="Right"/>
</BoxContainer>
</BoxContainer>
</PanelContainer>
</Popup>

View File

@@ -0,0 +1,78 @@
using Content.Shared.Administration.Notes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Administration.UI.Notes;
[GenerateTypedNameReferences]
public sealed partial class AdminNotesLinePopup : Popup
{
public event Action<int>? OnEditPressed;
public event Action<int>? OnDeletePressed;
public AdminNotesLinePopup(SharedAdminNote note, bool showDelete, bool showEdit)
{
RobustXamlLoader.Load(this);
NoteId = note.Id;
DeleteButton.Visible = showDelete;
EditButton.Visible = showEdit;
UserInterfaceManager.ModalRoot.AddChild(this);
IdLabel.Text = Loc.GetString("admin-notes-id", ("id", note.Id));
RoundIdLabel.Text = note.Round == null
? Loc.GetString("admin-notes-round-id-unknown")
: Loc.GetString("admin-notes-round-id", ("id", note.Round));
CreatedByLabel.Text = Loc.GetString("admin-notes-created-by", ("author", note.CreatedByName));
CreatedAtLabel.Text = Loc.GetString("admin-notes-created-at", ("date", note.CreatedAt.ToString("dd MMM yyyy HH:mm:ss")));
EditedByLabel.Text = Loc.GetString("admin-notes-last-edited-by", ("author", note.EditedByName));
EditedAtLabel.Text = Loc.GetString("admin-notes-last-edited-at", ("date", note.LastEditedAt.ToString("dd MMM yyyy HH:mm:ss")));
EditButton.OnPressed += EditPressed;
DeleteButton.OnPressed += DeletePressed;
}
private int NoteId { get; }
private bool ConfirmingDelete { get; set; }
private void EditPressed(ButtonEventArgs args)
{
OnEditPressed?.Invoke(NoteId);
Close();
}
private void DeletePressed(ButtonEventArgs args)
{
if (!ConfirmingDelete)
{
ConfirmingDelete = true;
DeleteButton.Text = Loc.GetString("admin-notes-delete-confirm");
DeleteButton.ModulateSelfOverride = Color.Red;
return;
}
ConfirmingDelete = false;
DeleteButton.ModulateSelfOverride = null;
OnDeletePressed?.Invoke(NoteId);
Close();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
{
return;
}
EditButton.OnPressed -= EditPressed;
DeleteButton.OnPressed -= DeletePressed;
OnEditPressed = null;
OnDeletePressed = null;
}
}

View File

@@ -0,0 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:notes="clr-namespace:Content.Client.Administration.UI.Notes"
MinWidth="400"
MinHeight="400">
<notes:AdminNotesControl Name="Notes" Access="Public"/>
</DefaultWindow>

View File

@@ -0,0 +1,19 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.Notes;
[GenerateTypedNameReferences]
public sealed partial class AdminNotesWindow : DefaultWindow
{
public AdminNotesWindow()
{
RobustXamlLoader.Load(this);
}
public void SetTitlePlayer(string playerName)
{
Title = Loc.GetString("admin-notes-title", ("player", playerName));
}
}

View File

@@ -15,6 +15,7 @@
<cc:CommandButton Command="announceui" Text="{Loc Announce}"/>
<cc:UICommandButton Command="callshuttle" Text="{Loc (Re)call Shuttle}" WindowType="{x:Type at:AdminShuttleWindow}"/>
<cc:CommandButton Command="adminlogs" Text="{Loc Admin Logs}"/>
<cc:CommandButton Command="adminnotes" Text="{Loc Admin Notes}"/>
</GridContainer>
</BoxContainer>
</Control>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
#nullable disable
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Content.Server.Database.Migrations.Postgres
{
public partial class AdminNotes : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "admin_notes",
columns: table => new
{
admin_notes_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
round_id = table.Column<int>(type: "integer", nullable: true),
player_user_id = table.Column<Guid>(type: "uuid", nullable: false),
message = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
created_by_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
last_edited_by_id = table.Column<Guid>(type: "uuid", nullable: false),
last_edited_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
deleted = table.Column<bool>(type: "boolean", nullable: false),
deleted_by_id = table.Column<Guid>(type: "uuid", nullable: true),
deleted_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
shown_to_player = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_admin_notes", x => x.admin_notes_id);
table.ForeignKey(
name: "FK_admin_notes_player_created_by_id",
column: x => x.created_by_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_admin_notes_player_deleted_by_id",
column: x => x.deleted_by_id,
principalTable: "player",
principalColumn: "user_id");
table.ForeignKey(
name: "FK_admin_notes_player_last_edited_by_id",
column: x => x.last_edited_by_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_admin_notes_player_player_user_id",
column: x => x.player_user_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_admin_notes_round_round_id",
column: x => x.round_id,
principalTable: "round",
principalColumn: "round_id");
});
migrationBuilder.CreateIndex(
name: "IX_admin_notes_created_by_id",
table: "admin_notes",
column: "created_by_id");
migrationBuilder.CreateIndex(
name: "IX_admin_notes_deleted_by_id",
table: "admin_notes",
column: "deleted_by_id");
migrationBuilder.CreateIndex(
name: "IX_admin_notes_last_edited_by_id",
table: "admin_notes",
column: "last_edited_by_id");
migrationBuilder.CreateIndex(
name: "IX_admin_notes_player_user_id",
table: "admin_notes",
column: "player_user_id");
migrationBuilder.CreateIndex(
name: "IX_admin_notes_round_id",
table: "admin_notes",
column: "round_id");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "admin_notes");
}
}
}

View File

@@ -184,6 +184,79 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("admin_log_player", (string)null);
});
modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("admin_notes_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Guid>("CreatedById")
.HasColumnType("uuid")
.HasColumnName("created_by_id");
b.Property<bool>("Deleted")
.HasColumnType("boolean")
.HasColumnName("deleted");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Guid?>("DeletedById")
.HasColumnType("uuid")
.HasColumnName("deleted_by_id");
b.Property<DateTime>("LastEditedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_edited_at");
b.Property<Guid>("LastEditedById")
.HasColumnType("uuid")
.HasColumnName("last_edited_by_id");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("message");
b.Property<Guid>("PlayerUserId")
.HasColumnType("uuid")
.HasColumnName("player_user_id");
b.Property<int?>("RoundId")
.HasColumnType("integer")
.HasColumnName("round_id");
b.Property<bool>("ShownToPlayer")
.HasColumnType("boolean")
.HasColumnName("shown_to_player");
b.HasKey("Id")
.HasName("PK_admin_notes");
b.HasIndex("CreatedById");
b.HasIndex("DeletedById");
b.HasIndex("LastEditedById");
b.HasIndex("PlayerUserId")
.HasDatabaseName("IX_admin_notes_player_user_id");
b.HasIndex("RoundId")
.HasDatabaseName("IX_admin_notes_round_id");
b.ToTable("admin_notes", (string)null);
});
modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
{
b.Property<int>("Id")
@@ -927,6 +1000,54 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
{
b.HasOne("Content.Server.Database.Player", "CreatedBy")
.WithMany("AdminNotesCreated")
.HasForeignKey("CreatedById")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_admin_notes_player_created_by_id");
b.HasOne("Content.Server.Database.Player", "DeletedBy")
.WithMany("AdminNotesDeleted")
.HasForeignKey("DeletedById")
.HasPrincipalKey("UserId")
.HasConstraintName("FK_admin_notes_player_deleted_by_id");
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
.WithMany("AdminNotesLastEdited")
.HasForeignKey("LastEditedById")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_admin_notes_player_last_edited_by_id");
b.HasOne("Content.Server.Database.Player", "Player")
.WithMany("AdminNotesReceived")
.HasForeignKey("PlayerUserId")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_admin_notes_player_player_user_id");
b.HasOne("Content.Server.Database.Round", "Round")
.WithMany()
.HasForeignKey("RoundId")
.HasConstraintName("FK_admin_notes_round_round_id");
b.Navigation("CreatedBy");
b.Navigation("DeletedBy");
b.Navigation("LastEditedBy");
b.Navigation("Player");
b.Navigation("Round");
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "Rank")
@@ -1076,6 +1197,14 @@ namespace Content.Server.Database.Migrations.Postgres
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.Navigation("AdminLogs");
b.Navigation("AdminNotesCreated");
b.Navigation("AdminNotesDeleted");
b.Navigation("AdminNotesLastEdited");
b.Navigation("AdminNotesReceived");
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
#nullable disable
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Sqlite
{
public partial class AdminNotes : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "admin_notes",
columns: table => new
{
admin_notes_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
round_id = table.Column<int>(type: "INTEGER", nullable: true),
player_user_id = table.Column<Guid>(type: "TEXT", nullable: false),
message = table.Column<string>(type: "TEXT", maxLength: 4096, nullable: false),
created_by_id = table.Column<Guid>(type: "TEXT", nullable: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false),
last_edited_by_id = table.Column<Guid>(type: "TEXT", nullable: false),
last_edited_at = table.Column<DateTime>(type: "TEXT", nullable: false),
deleted = table.Column<bool>(type: "INTEGER", nullable: false),
deleted_by_id = table.Column<Guid>(type: "TEXT", nullable: true),
deleted_at = table.Column<DateTime>(type: "TEXT", nullable: true),
shown_to_player = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_admin_notes", x => x.admin_notes_id);
table.ForeignKey(
name: "FK_admin_notes_player_created_by_id",
column: x => x.created_by_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_admin_notes_player_deleted_by_id",
column: x => x.deleted_by_id,
principalTable: "player",
principalColumn: "user_id");
table.ForeignKey(
name: "FK_admin_notes_player_last_edited_by_id",
column: x => x.last_edited_by_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_admin_notes_player_player_user_id",
column: x => x.player_user_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_admin_notes_round_round_id",
column: x => x.round_id,
principalTable: "round",
principalColumn: "round_id");
});
migrationBuilder.CreateIndex(
name: "IX_admin_notes_created_by_id",
table: "admin_notes",
column: "created_by_id");
migrationBuilder.CreateIndex(
name: "IX_admin_notes_deleted_by_id",
table: "admin_notes",
column: "deleted_by_id");
migrationBuilder.CreateIndex(
name: "IX_admin_notes_last_edited_by_id",
table: "admin_notes",
column: "last_edited_by_id");
migrationBuilder.CreateIndex(
name: "IX_admin_notes_player_user_id",
table: "admin_notes",
column: "player_user_id");
migrationBuilder.CreateIndex(
name: "IX_admin_notes_round_id",
table: "admin_notes",
column: "round_id");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "admin_notes");
}
}
}

View File

@@ -168,6 +168,77 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("admin_log_player", (string)null);
});
modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("admin_notes_id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<Guid>("CreatedById")
.HasColumnType("TEXT")
.HasColumnName("created_by_id");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER")
.HasColumnName("deleted");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("TEXT")
.HasColumnName("deleted_at");
b.Property<Guid?>("DeletedById")
.HasColumnType("TEXT")
.HasColumnName("deleted_by_id");
b.Property<DateTime>("LastEditedAt")
.HasColumnType("TEXT")
.HasColumnName("last_edited_at");
b.Property<Guid>("LastEditedById")
.HasColumnType("TEXT")
.HasColumnName("last_edited_by_id");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("TEXT")
.HasColumnName("message");
b.Property<Guid>("PlayerUserId")
.HasColumnType("TEXT")
.HasColumnName("player_user_id");
b.Property<int?>("RoundId")
.HasColumnType("INTEGER")
.HasColumnName("round_id");
b.Property<bool>("ShownToPlayer")
.HasColumnType("INTEGER")
.HasColumnName("shown_to_player");
b.HasKey("Id")
.HasName("PK_admin_notes");
b.HasIndex("CreatedById");
b.HasIndex("DeletedById");
b.HasIndex("LastEditedById");
b.HasIndex("PlayerUserId")
.HasDatabaseName("IX_admin_notes_player_user_id");
b.HasIndex("RoundId")
.HasDatabaseName("IX_admin_notes_round_id");
b.ToTable("admin_notes", (string)null);
});
modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
{
b.Property<int>("Id")
@@ -869,6 +940,54 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
{
b.HasOne("Content.Server.Database.Player", "CreatedBy")
.WithMany("AdminNotesCreated")
.HasForeignKey("CreatedById")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_admin_notes_player_created_by_id");
b.HasOne("Content.Server.Database.Player", "DeletedBy")
.WithMany("AdminNotesDeleted")
.HasForeignKey("DeletedById")
.HasPrincipalKey("UserId")
.HasConstraintName("FK_admin_notes_player_deleted_by_id");
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
.WithMany("AdminNotesLastEdited")
.HasForeignKey("LastEditedById")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_admin_notes_player_last_edited_by_id");
b.HasOne("Content.Server.Database.Player", "Player")
.WithMany("AdminNotesReceived")
.HasForeignKey("PlayerUserId")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_admin_notes_player_player_user_id");
b.HasOne("Content.Server.Database.Round", "Round")
.WithMany()
.HasForeignKey("RoundId")
.HasConstraintName("FK_admin_notes_round_round_id");
b.Navigation("CreatedBy");
b.Navigation("DeletedBy");
b.Navigation("LastEditedBy");
b.Navigation("Player");
b.Navigation("Round");
});
modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
{
b.HasOne("Content.Server.Database.AdminRank", "Rank")
@@ -1018,6 +1137,14 @@ namespace Content.Server.Database.Migrations.Sqlite
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.Navigation("AdminLogs");
b.Navigation("AdminNotesCreated");
b.Navigation("AdminNotesDeleted");
b.Navigation("AdminNotesLastEdited");
b.Navigation("AdminNotesReceived");
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>

View File

@@ -34,6 +34,7 @@ namespace Content.Server.Database
public DbSet<ServerRoleBan> RoleBan { get; set; } = default!;
public DbSet<ServerRoleUnban> RoleUnban { get; set; } = default!;
public DbSet<UploadedResourceLog> UploadedResourceLog { get; set; } = default!;
public DbSet<AdminNote> AdminNotes { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -140,6 +141,30 @@ namespace Content.Server.Database
modelBuilder.Entity<ConnectionLog>()
.HasIndex(p => p.UserId);
modelBuilder.Entity<AdminNote>()
.HasOne(note => note.Player)
.WithMany(player => player.AdminNotesReceived)
.HasForeignKey(note => note.PlayerUserId)
.HasPrincipalKey(player => player.UserId);
modelBuilder.Entity<AdminNote>()
.HasOne(version => version.CreatedBy)
.WithMany(author => author.AdminNotesCreated)
.HasForeignKey(note => note.CreatedById)
.HasPrincipalKey(author => author.UserId);
modelBuilder.Entity<AdminNote>()
.HasOne(version => version.LastEditedBy)
.WithMany(author => author.AdminNotesLastEdited)
.HasForeignKey(note => note.LastEditedById)
.HasPrincipalKey(author => author.UserId);
modelBuilder.Entity<AdminNote>()
.HasOne(version => version.DeletedBy)
.WithMany(author => author.AdminNotesDeleted)
.HasForeignKey(note => note.DeletedById)
.HasPrincipalKey(author => author.UserId);
}
public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
@@ -251,6 +276,11 @@ namespace Content.Server.Database
public List<AdminLogPlayer> AdminLogs { get; set; } = null!;
public DateTime? LastReadRules { get; set; }
public List<AdminNote> AdminNotesReceived { get; set; } = null!;
public List<AdminNote> AdminNotesCreated { get; set; } = null!;
public List<AdminNote> AdminNotesLastEdited { get; set; } = null!;
public List<AdminNote> AdminNotesDeleted { get; set; } = null!;
}
[Table("whitelist")]
@@ -477,4 +507,35 @@ namespace Content.Server.Database
public byte[] Data { get; set; } = default!;
}
[Index(nameof(PlayerUserId))]
public class AdminNote
{
[Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
[ForeignKey("Round")] public int? RoundId { get; set; }
public Round? Round { get; set; }
[Required, ForeignKey("Player")] public Guid PlayerUserId { get; set; }
public Player Player { get; set; } = default!;
[Required, MaxLength(4096)] public string Message { get; set; } = string.Empty;
[Required, ForeignKey("CreatedBy")] public Guid CreatedById { get; set; }
[Required] public Player CreatedBy { get; set; } = default!;
[Required] public DateTime CreatedAt { get; set; }
[Required, ForeignKey("LastEditedBy")] public Guid LastEditedById { get; set; }
[Required] public Player LastEditedBy { get; set; } = default!;
[Required] public DateTime LastEditedAt { get; set; }
public bool Deleted { get; set; }
[ForeignKey("DeletedBy")] public Guid? DeletedById { get; set; }
public Player? DeletedBy { get; set; }
public DateTime? DeletedAt { get; set; }
public bool ShownToPlayer { get; set; }
}
}

View File

@@ -0,0 +1,38 @@
using Content.Server.Administration.Notes;
using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.ViewNotes)]
public sealed class OpenAdminNotesCommand : IConsoleCommand
{
public const string CommandName = "adminnotes";
public string Command => CommandName;
public string Description => "Opens the admin notes panel.";
public string Help => $"Usage: {Command} <notedPlayerUserId>";
public async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not IPlayerSession player)
{
shell.WriteError("This does not work from the server console.");
return;
}
Guid notedPlayer;
switch (args.Length)
{
case 1 when Guid.TryParse(args[0], out notedPlayer):
break;
default:
shell.WriteError($"Invalid arguments.\n{Help}");
return;
}
await IoCManager.Resolve<IAdminNotesManager>().OpenEui(player, notedPlayer);
}
}

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Managers;
@@ -11,11 +9,8 @@ using Content.Shared.Administration.Logs;
using Content.Shared.CCVar;
using Content.Shared.Eui;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using static Content.Shared.Administration.AdminLogsEuiMsg;
using static Content.Shared.Administration.Logs.AdminLogsEuiMsg;
namespace Content.Server.Administration.Logs;

View File

@@ -0,0 +1,156 @@
using System.Threading.Tasks;
using Content.Server.Administration.Managers;
using Content.Server.EUI;
using Content.Shared.Administration.Notes;
using Content.Shared.Eui;
using static Content.Shared.Administration.Notes.AdminNoteEuiMsg;
namespace Content.Server.Administration.Notes;
public sealed class AdminNotesEui : BaseEui
{
[Dependency] private readonly IAdminManager _admins = default!;
[Dependency] private readonly IAdminNotesManager _notesMan = default!;
public AdminNotesEui()
{
IoCManager.InjectDependencies(this);
}
private Guid NotedPlayer { get; set; }
private string NotedPlayerName { get; set; } = string.Empty;
private Dictionary<int, SharedAdminNote> Notes { get; set; } = new();
public override async void Opened()
{
base.Opened();
_admins.OnPermsChanged += OnPermsChanged;
_notesMan.NoteAdded += NoteModified;
_notesMan.NoteModified += NoteModified;
_notesMan.NoteDeleted += NoteDeleted;
}
public override void Closed()
{
base.Closed();
_admins.OnPermsChanged -= OnPermsChanged;
_notesMan.NoteAdded -= NoteModified;
_notesMan.NoteModified -= NoteModified;
_notesMan.NoteDeleted -= NoteDeleted;
}
public override EuiStateBase GetNewState()
{
return new AdminNotesEuiState(
NotedPlayerName,
Notes,
_notesMan.CanCreate(Player),
_notesMan.CanDelete(Player),
_notesMan.CanEdit(Player)
);
}
public override async void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
switch (msg)
{
case Close _:
{
Close();
break;
}
case CreateNoteRequest {Message: var message}:
{
if (!_notesMan.CanCreate(Player))
{
Close();
break;
}
if (string.IsNullOrWhiteSpace(message))
{
break;
}
await _notesMan.AddNote(Player, NotedPlayer, message);
break;
}
case DeleteNoteRequest request:
{
if (!_notesMan.CanDelete(Player))
{
Close();
break;
}
await _notesMan.DeleteNote(request.Id, Player);
break;
}
case EditNoteRequest request:
{
if (!_notesMan.CanEdit(Player))
{
Close();
break;
}
if (string.IsNullOrWhiteSpace(request.Message))
{
break;
}
await _notesMan.ModifyNote(request.Id, Player, request.Message);
break;
}
}
}
public async Task ChangeNotedPlayer(Guid notedPlayer)
{
NotedPlayer = notedPlayer;
await LoadFromDb();
}
private void NoteModified(SharedAdminNote note)
{
Notes[note.Id] = note;
StateDirty();
}
private void NoteDeleted(int id)
{
Notes.Remove(id);
StateDirty();
}
private async Task LoadFromDb()
{
NotedPlayerName = await _notesMan.GetPlayerName(NotedPlayer);
var notes = new Dictionary<int, SharedAdminNote>();
foreach (var note in await _notesMan.GetNotes(NotedPlayer))
{
notes.Add(note.Id, note.ToShared());
}
Notes = notes;
StateDirty();
}
private void OnPermsChanged(AdminPermsChangedEventArgs args)
{
if (args.Player == Player && !_notesMan.CanView(Player))
{
Close();
}
else
{
StateDirty();
}
}
}

View File

@@ -0,0 +1,20 @@
using Content.Server.Database;
using Content.Shared.Administration.Notes;
namespace Content.Server.Administration.Notes;
public static class AdminNotesExtensions
{
public static SharedAdminNote ToShared(this AdminNote note)
{
return new SharedAdminNote(
note.Id,
note.RoundId,
note.Message,
note.CreatedBy.LastSeenUserName,
note.LastEditedBy.LastSeenUserName,
note.CreatedAt,
note.LastEditedAt
);
}
}

View File

@@ -0,0 +1,136 @@
using System.Threading.Tasks;
using Content.Server.Administration.Managers;
using Content.Server.Database;
using Content.Server.EUI;
using Content.Server.GameTicking;
using Content.Shared.Administration;
using Content.Shared.Administration.Notes;
using Robust.Server.Player;
using Robust.Shared.Network;
namespace Content.Server.Administration.Notes;
public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
{
[Dependency] private readonly IAdminManager _admins = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly EuiManager _euis = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!;
public const string SawmillId = "admin.notes";
public event Action<SharedAdminNote>? NoteAdded;
public event Action<SharedAdminNote>? NoteModified;
public event Action<int>? NoteDeleted;
private ISawmill _sawmill = default!;
public bool CanCreate(IPlayerSession admin)
{
return CanEdit(admin);
}
public bool CanDelete(IPlayerSession admin)
{
return CanEdit(admin);
}
public bool CanEdit(IPlayerSession admin)
{
return _admins.HasAdminFlag(admin, AdminFlags.EditNotes);
}
public bool CanView(IPlayerSession admin)
{
return _admins.HasAdminFlag(admin, AdminFlags.ViewNotes);
}
public async Task OpenEui(IPlayerSession admin, Guid notedPlayer)
{
var ui = new AdminNotesEui();
_euis.OpenEui(ui, admin);
await ui.ChangeNotedPlayer(notedPlayer);
}
public async Task AddNote(IPlayerSession createdBy, Guid player, string message)
{
_sawmill.Info($"Player {createdBy.Name} added note with message {message}");
_systems.TryGetEntitySystem(out GameTicker? ticker);
int? round = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId;
var createdAt = DateTime.UtcNow;
var noteId = await _db.AddAdminNote(round, player, message, createdBy.UserId, createdAt);
var note = new SharedAdminNote(
noteId,
round,
message,
createdBy.Name,
createdBy.Name,
createdAt,
createdAt
);
NoteAdded?.Invoke(note);
}
public async Task DeleteNote(int noteId, IPlayerSession deletedBy)
{
var note = await _db.GetAdminNote(noteId);
if (note == null)
{
_sawmill.Info($"Player {deletedBy.Name} tried to delete non-existent note {noteId}");
return;
}
_sawmill.Info($"Player {deletedBy.Name} deleted note {noteId}");
var deletedAt = DateTime.UtcNow;
await _db.DeleteAdminNote(noteId, deletedBy.UserId, deletedAt);
NoteDeleted?.Invoke(noteId);
}
public async Task ModifyNote(int noteId, IPlayerSession editedBy, string message)
{
message = message.Trim();
var note = await _db.GetAdminNote(noteId);
if (note == null || note.Message == message)
{
return;
}
_sawmill.Info($"Player {editedBy.Name} modified note {noteId} with message {message}");
var editedAt = DateTime.UtcNow;
await _db.EditAdminNote(noteId, message, editedBy.UserId, editedAt);
var sharedNote = new SharedAdminNote(
noteId,
note.RoundId,
message,
note.CreatedBy.LastSeenUserName,
editedBy.Name,
note.CreatedAt,
note.LastEditedAt
);
NoteModified?.Invoke(sharedNote);
}
public async Task<List<AdminNote>> GetNotes(Guid player)
{
return await _db.GetAdminNotes(player);
}
public async Task<string> GetPlayerName(Guid player)
{
return (await _db.GetPlayerRecordByUserId(new NetUserId(player)))?.LastSeenUserName ?? string.Empty;
}
public void PostInject()
{
_sawmill = _logManager.GetSawmill(SawmillId);
}
}

View File

@@ -0,0 +1,43 @@
using Content.Server.Administration.Commands;
using Content.Shared.Database;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Console;
namespace Content.Server.Administration.Notes;
public sealed class AdminNotesSystem : EntitySystem
{
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly IAdminNotesManager _notes = default!;
public override void Initialize()
{
SubscribeLocalEvent<GetVerbsEvent<Verb>>(AddVerbs);
}
private void AddVerbs(GetVerbsEvent<Verb> ev)
{
if (EntityManager.GetComponentOrNull<ActorComponent>(ev.User) is not {PlayerSession: var user} ||
EntityManager.GetComponentOrNull<ActorComponent>(ev.Target) is not {PlayerSession: var target})
{
return;
}
if (!_notes.CanView(user))
{
return;
}
var verb = new Verb
{
Text = Loc.GetString("admin-notes-verb-text"),
Category = VerbCategory.Admin,
IconTexture = "/Textures/Interface/VerbIcons/examine.svg.192dpi.png",
Act = () => _console.RemoteExecuteCommand(user, $"{OpenAdminNotesCommand.CommandName} \"{target.UserId}\""),
Impact = LogImpact.Low
};
ev.Verbs.Add(verb);
}
}

View File

@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Shared.Administration.Notes;
using Robust.Server.Player;
namespace Content.Server.Administration.Notes;
public interface IAdminNotesManager
{
event Action<SharedAdminNote>? NoteAdded;
event Action<SharedAdminNote>? NoteModified;
event Action<int>? NoteDeleted;
bool CanCreate(IPlayerSession admin);
bool CanDelete(IPlayerSession admin);
bool CanEdit(IPlayerSession admin);
bool CanView(IPlayerSession admin);
Task OpenEui(IPlayerSession admin, Guid notedPlayer);
Task AddNote(IPlayerSession createdBy, Guid player, string message);
Task DeleteNote(int noteId, IPlayerSession deletedBy);
Task ModifyNote(int noteId, IPlayerSession editedBy, string message);
Task<List<AdminNote>> GetNotes(Guid player);
Task<string> GetPlayerName(Guid player);
}

View File

@@ -822,6 +822,69 @@ namespace Content.Server.Database
#endregion
#region Admin Notes
public virtual async Task<int> AddAdminNote(AdminNote note)
{
await using var db = await GetDb();
db.DbContext.AdminNotes.Add(note);
await db.DbContext.SaveChangesAsync();
return note.Id;
}
public async Task<AdminNote?> GetAdminNote(int id)
{
await using var db = await GetDb();
return await db.DbContext.AdminNotes
.Where(note => note.Id == id)
.Include(note => note.Round)
.Include(note => note.CreatedBy)
.Include(note => note.LastEditedBy)
.Include(note => note.DeletedBy)
.Include(note => note.Player)
.SingleOrDefaultAsync();
}
public async Task<List<AdminNote>> GetAdminNotes(Guid player)
{
await using var db = await GetDb();
return await db.DbContext.AdminNotes
.Where(note => note.PlayerUserId == player)
.Where(note => !note.Deleted)
.Include(note => note.Round)
.Include(note => note.CreatedBy)
.Include(note => note.LastEditedBy)
.Include(note => note.Player)
.ToListAsync();
}
public async Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt)
{
await using var db = await GetDb();
var note = await db.DbContext.AdminNotes.Where(note => note.Id == id).SingleAsync();
note.Deleted = true;
note.DeletedById = deletedBy;
note.DeletedAt = deletedAt;
await db.DbContext.SaveChangesAsync();
}
public async Task EditAdminNote(int id, string message, Guid editedBy, DateTime editedAt)
{
await using var db = await GetDb();
var note = await db.DbContext.AdminNotes.Where(note => note.Id == id).SingleAsync();
note.Message = message;
note.LastEditedById = editedBy;
note.LastEditedAt = editedAt;
await db.DbContext.SaveChangesAsync();
}
#endregion
protected abstract Task<DbGuard> GetDb();
protected abstract class DbGuard : IAsyncDisposable

View File

@@ -195,6 +195,16 @@ namespace Content.Server.Database
Task SetLastReadRules(NetUserId player, DateTime time);
#endregion
#region Admin Notes
Task<int> AddAdminNote(int? roundId, Guid player, string message, Guid createdBy, DateTime createdAt);
Task<AdminNote?> GetAdminNote(int id);
Task<List<AdminNote>> GetAdminNotes(Guid player);
Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt);
Task EditAdminNote(int id, string message, Guid editedBy, DateTime editedAt);
#endregion
}
public sealed class ServerDbManager : IServerDbManager
@@ -485,6 +495,42 @@ namespace Content.Server.Database
return _db.SetLastReadRules(player, time);
}
public Task<int> AddAdminNote(int? roundId, Guid player, string message, Guid createdBy, DateTime createdAt)
{
var note = new AdminNote
{
RoundId = roundId,
CreatedById = createdBy,
LastEditedById = createdBy,
PlayerUserId = player,
Message = message,
CreatedAt = createdAt,
LastEditedAt = createdAt
};
return _db.AddAdminNote(note);
}
public Task<AdminNote?> GetAdminNote(int id)
{
return _db.GetAdminNote(id);
}
public Task<List<AdminNote>> GetAdminNotes(Guid player)
{
return _db.GetAdminNotes(player);
}
public Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt)
{
return _db.DeleteAdminNote(id, deletedBy, deletedAt);
}
public Task EditAdminNote(int id, string message, Guid editedBy, DateTime editedAt)
{
return _db.EditAdminNote(id, message, editedBy, editedAt);
}
private DbContextOptions<PostgresServerDbContext> CreatePostgresOptions()
{
var host = _cfg.GetCVar(CCVars.DatabasePgHost);

View File

@@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Net;
@@ -11,7 +9,6 @@ using Content.Server.Preferences.Managers;
using Content.Shared.CCVar;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Utility;
@@ -491,6 +488,22 @@ namespace Content.Server.Database
await db.DbContext.SaveChangesAsync();
}
public override async Task<int> AddAdminNote(AdminNote note)
{
await using (var db = await GetDb())
{
var nextId = 1;
if (await db.DbContext.AdminNotes.AnyAsync())
{
nextId = await db.DbContext.AdminNotes.MaxAsync(dbVersion => dbVersion.Id) + 1;
}
note.Id = nextId;
}
return await base.AddAdminNote(note);
}
private async Task<DbGuardImpl> GetDbImpl()
{
await _dbReadyTask;

View File

@@ -1,5 +1,6 @@
using Content.Server.Administration;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Notes;
using Content.Server.Afk;
using Content.Server.AI.Utility;
using Content.Server.AI.Utility.Considerations;
@@ -17,7 +18,6 @@ using Content.Server.Objectives;
using Content.Server.Objectives.Interfaces;
using Content.Server.Preferences.Managers;
using Content.Server.Voting.Managers;
using Content.Shared.Actions;
using Content.Shared.Administration;
using Content.Shared.Kitchen;
using Content.Shared.Module;
@@ -51,6 +51,7 @@ namespace Content.Server.IoC
IoCManager.Register<RulesManager, RulesManager>();
IoCManager.Register<RoleBanManager, RoleBanManager>();
IoCManager.Register<NetworkResourceManager>();
IoCManager.Register<IAdminNotesManager, AdminNotesManager>();
}
}
}

View File

@@ -1,6 +1,4 @@
using System;
namespace Content.Shared.Administration
namespace Content.Shared.Administration
{
/// <summary>
/// Permissions that admins can have.
@@ -80,6 +78,16 @@ namespace Content.Shared.Administration
/// </summary>
Adminhelp = 1 << 12,
/// <summary>
/// Lets you view admin notes.
/// </summary>
ViewNotes = 1 << 13,
/// <summary>
/// Lets you create, edit and delete admin notes.
/// </summary>
EditNotes = 1 << 14,
/// <summary>
/// Dangerous host permissions like scsi.
/// </summary>

View File

@@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Database;
using Content.Shared.Eui;
using Robust.Shared.Serialization;
namespace Content.Shared.Administration;
namespace Content.Shared.Administration.Logs;
[Serializable, NetSerializable]
public sealed class AdminLogsEuiState : EuiStateBase

View File

@@ -0,0 +1,66 @@
using Content.Shared.Eui;
using Robust.Shared.Serialization;
namespace Content.Shared.Administration.Notes;
[Serializable, NetSerializable]
public sealed class AdminNotesEuiState : EuiStateBase
{
public AdminNotesEuiState(string notedPlayerName, Dictionary<int, SharedAdminNote> notes, bool canCreate, bool canDelete, bool canEdit)
{
NotedPlayerName = notedPlayerName;
Notes = notes;
CanCreate = canCreate;
CanDelete = canDelete;
CanEdit = canEdit;
}
public string NotedPlayerName { get; }
public Dictionary<int, SharedAdminNote> Notes { get; }
public bool CanCreate { get; }
public bool CanDelete { get; }
public bool CanEdit { get; }
}
public static class AdminNoteEuiMsg
{
[Serializable, NetSerializable]
public sealed class Close : EuiMessageBase
{
}
[Serializable, NetSerializable]
public sealed class CreateNoteRequest : EuiMessageBase
{
public CreateNoteRequest(string message)
{
Message = message;
}
public string Message { get; set; }
}
[Serializable, NetSerializable]
public sealed class DeleteNoteRequest : EuiMessageBase
{
public DeleteNoteRequest(int id)
{
Id = id;
}
public int Id { get; set; }
}
[Serializable, NetSerializable]
public sealed class EditNoteRequest : EuiMessageBase
{
public EditNoteRequest(int id, string message)
{
Id = id;
Message = message;
}
public int Id { get; set; }
public string Message { get; set; }
}
}

View File

@@ -0,0 +1,6 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Administration.Notes;
[Serializable, NetSerializable]
public sealed record SharedAdminNote(int Id, int? Round, string Message, string CreatedByName, string EditedByName, DateTime CreatedAt, DateTime LastEditedAt);

View File

@@ -1,3 +1,4 @@
admin-player-actions-notes = Notes
admin-player-actions-kick = Kick
admin-player-actions-ban = Ban
admin-player-actions-ahelp = AHelp

View File

@@ -0,0 +1,16 @@
# UI
admin-notes-title = Notes for {$player}
admin-notes-new-note = New note:
admin-notes-id = Id: {$id}
admin-notes-round-id = Round Id: {$id}
admin-notes-round-id-unknown = Round Id: Unknown
admin-notes-created-by = Created by: {$author}
admin-notes-created-at = Created At: {$date}
admin-notes-last-edited-by = Last edited by: {$author}
admin-notes-last-edited-at = Last edited at: {$date}
admin-notes-edit = Edit
admin-notes-delete = Delete
admin-notes-delete-confirm = Are you sure?
# Verb
admin-notes-verb-text = Open Admin Notes