0.2.0: keep all chat, @ staff chat and SimpleAdmin's say commands too

wh_messages now holds every message with a channel (all, team, pm, staff,
to_staff, css_say/asay/psay/csay/hsay/cssay) and NULL sender columns for
the console. Seen by a catch-all listener added in Load, which CSS runs
before CS2-SimpleAdmin's (added in OnAllPluginsLoaded), so gagged lines and
@ chat are kept before SimpleAdmin stops or rewrites them. Replaces
001_CreateTables in place: drop wh_messages and wh_migrations first.
This commit is contained in:
Astra 2026-10-05 14:57:19 +01:00
parent c414e8a770
commit 06823c2d47
5 changed files with 146 additions and 29 deletions

View file

@ -15,8 +15,15 @@ using Microsoft.Extensions.Logging;
namespace Whisper;
// Private messages between players: /pm <target> <message>, /r to reply, and /block, /blocklist,
// /unblock. Lines use CS2-Tags' rank prefixes and colours from tags.json. Each message is kept in
// CS2-SimpleAdmin's database (wh_messages) while History is on, and blocks in wh_blocks.
// /unblock. Lines use CS2-Tags' rank prefixes and colours from tags.json. While History is on, every
// message is kept in CS2-SimpleAdmin's database (wh_messages): private ones, all and team chat, @
// staff chat and SimpleAdmin's say commands. Blocks are in wh_blocks.
//
// Chat is seen by a catch-all command listener added in Load. CSS runs catch-alls before any
// per-command listener, in the order they were added, and stops at the first that returns Stop.
// CS2-SimpleAdmin adds its own (which stops gagged players' chat and handles @) in
// OnAllPluginsLoaded, after every plugin's Load, so this one always sees a line first. It only
// looks, never blocks.
//
// Chat commands are caught in a say listener rather than left to CSS's chat triggers: for "!pm" the
// game prints the whole line, message and all, to everyone before CSS runs the command. Stopping
@ -27,7 +34,7 @@ namespace Whisper;
public class Whisper : BasePlugin, IPluginConfig<WhisperConfig>
{
public override string ModuleName => "Whisper";
public override string ModuleVersion => "0.1.0";
public override string ModuleVersion => "0.2.0";
public override string ModuleAuthor => "astra";
public override string ModuleDescription => "Private messages between players";
@ -37,6 +44,10 @@ public class Whisper : BasePlugin, IPluginConfig<WhisperConfig>
private static readonly HashSet<string> ChatCommands = new(StringComparer.OrdinalIgnoreCase)
{ "pm", "w", "r", "block", "blocklist", "unblock" };
// SimpleAdmin's commands that send a message, logged under their own name as the channel.
private static readonly HashSet<string> StaffCommands = new(StringComparer.OrdinalIgnoreCase)
{ "css_say", "css_asay", "css_psay", "css_csay", "css_hsay", "css_cssay" };
// Targets that mean more than one player. A PM goes to one.
private static readonly HashSet<string> GroupTargets = new(StringComparer.OrdinalIgnoreCase)
{ "@all", "@t", "@ct", "@spec", "@alive", "@dead", "@bot", "@bots", "@human", "@humans", "@!me" };
@ -69,6 +80,8 @@ public class Whisper : BasePlugin, IPluginConfig<WhisperConfig>
_tags = new Tags(Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp", Config.TagsFile),
message => Logger.LogWarning("{Message}", message));
// In Load, not OnAllPluginsLoaded: see the top of this file.
AddCommandListener(null, OnAnyCommand, HookMode.Pre);
AddCommandListener("say", OnSay, HookMode.Pre);
AddCommandListener("say_team", OnSay, HookMode.Pre);
RegisterEventHandler<EventPlayerConnectFull>(OnPlayerConnectFull);
@ -200,13 +213,73 @@ public class Whisper : BasePlugin, IPluginConfig<WhisperConfig>
// --- Chat -----------------------------------------------------------------------
// Logs chat and staff messages as they're sent. Never blocks anything.
private HookResult OnAnyCommand(CCSPlayerController? player, CommandInfo info)
{
if (!Config.History || _writer == null)
return HookResult.Continue;
var command = info.GetArg(0).ToLowerInvariant();
if (StaffCommands.Contains(command))
{
// Typed in console, RCON (the web panel too), or "!asay" in chat, which CSS runs as css_asay.
var sender = player is { IsValid: true, IsBot: false } ? StateOf(player) : player == null ? PlayerState.Console : null;
if (sender == null)
return HookResult.Continue;
var rest = info.ArgString;
var target = command == "css_psay" ? NextToken(ref rest) : null;
var message = rest.Trim();
if (message.Length > 0)
Log(command, sender, null, target, message, Result.Sent);
return HookResult.Continue;
}
if (command is not ("say" or "say_team") || player is not { IsValid: true, IsBot: false })
return HookResult.Continue;
var text = info.GetArg(1);
if (text.Length == 0)
return HookResult.Continue;
var team = command == "say_team";
var channel = team ? ChatChannel.Team : ChatChannel.All;
var trigger = ChatTrigger(text);
if (trigger != null)
{
var rest = text[trigger.Length..];
var word = NextToken(ref rest).ToLowerInvariant();
// Logged on their own: PMs where they're sent, "!asay" and the like as the command.
if (!ChatCommands.Contains(word) && !StaffCommands.Contains("css_" + word))
Log(channel, StateOf(player), null, null, text, Result.Command);
return HookResult.Continue;
}
if (text.StartsWith('@'))
{
var chatStaff = AdminManager.PlayerHasPermissions(player, "@css/chat");
// SimpleAdmin runs this as css_say, which is logged then.
if (!team && chatStaff)
return HookResult.Continue;
// @ team chat goes to staff, and gets through a gag.
if (team)
{
Log(chatStaff ? ChatChannel.Staff : ChatChannel.ToStaff, StateOf(player), null, null, text[1..].Trim(), Result.Sent);
return HookResult.Continue;
}
}
Log(channel, StateOf(player), null, null, text, IsGagged(player, out _) ? Result.Gagged : Result.Sent);
return HookResult.Continue;
}
private static string? ChatTrigger(string text) =>
CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger).FirstOrDefault(t => t.Length > 0 && text.StartsWith(t));
private HookResult OnSay(CCSPlayerController? player, CommandInfo info)
{
if (player is not { IsValid: true, IsBot: false })
return HookResult.Continue;
var text = info.GetArg(1);
var trigger = CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger)
.FirstOrDefault(t => t.Length > 0 && text.StartsWith(t));
var trigger = ChatTrigger(text);
if (trigger == null)
return HookResult.Continue;
@ -602,7 +675,10 @@ public class Whisper : BasePlugin, IPluginConfig<WhisperConfig>
// --- History --------------------------------------------------------------------
private void Log(CCSPlayerController sender, CCSPlayerController? recipient, string target, string message, string result)
private void Log(CCSPlayerController sender, CCSPlayerController? recipient, string target, string message, string result) =>
Log(ChatChannel.Pm, StateOf(sender), recipient, target, message, result);
private void Log(string channel, PlayerState sender, CCSPlayerController? recipient, string? target, string message, string result)
{
if (!Config.History || _writer == null)
return;
@ -615,7 +691,7 @@ public class Whisper : BasePlugin, IPluginConfig<WhisperConfig>
{
}
_writer.Enqueue(new MessageRecord(Guid.NewGuid().ToString("N"), DateTime.UtcNow, serverId, Server.MapName ?? "",
StateOf(sender), recipient == null ? null : StateOf(recipient), target, message, result));
channel, sender, recipient == null ? null : StateOf(recipient), target, message, result));
}
[ConsoleCommand("css_whisper_history", "Turns keeping message history on or off")]