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:
parent
c414e8a770
commit
06823c2d47
5 changed files with 146 additions and 29 deletions
|
|
@ -73,11 +73,12 @@ public sealed class Database(string connectionString)
|
|||
var r = o.Recipient;
|
||||
await Exec(connection, token, """
|
||||
INSERT IGNORE INTO wh_messages
|
||||
(message_key, sent_at, server_id, map, sender_steamid64, sender_name, sender_team, sender_alive,
|
||||
(message_key, sent_at, server_id, map, channel, sender_steamid64, sender_name, sender_team, sender_alive,
|
||||
recipient_steamid64, recipient_name, recipient_team, recipient_alive, target, message, result)
|
||||
VALUES (@key, @at, @server, @map, @sid, @sname, @steam, @salive, @rid, @rname, @rteam, @ralive, @target, @message, @result)
|
||||
""", ("@key", o.Key), ("@at", o.At), ("@server", o.ServerId), ("@map", Clip(o.Map, 64)),
|
||||
("@sid", o.Sender.SteamId), ("@sname", Clip(o.Sender.Name, 128)), ("@steam", o.Sender.Team), ("@salive", o.Sender.Alive),
|
||||
VALUES (@key, @at, @server, @map, @channel, @sid, @sname, @steam, @salive, @rid, @rname, @rteam, @ralive, @target, @message, @result)
|
||||
""", ("@key", o.Key), ("@at", o.At), ("@server", o.ServerId), ("@map", Clip(o.Map, 64)), ("@channel", Clip(o.Channel, 16)),
|
||||
("@sid", o.Sender.SteamId == 0 ? null : o.Sender.SteamId), ("@sname", Clip(o.Sender.Name, 128)),
|
||||
("@steam", o.Sender.Team), ("@salive", o.Sender.Alive),
|
||||
("@rid", r?.SteamId), ("@rname", Clip(r?.Name, 128)), ("@rteam", r?.Team), ("@ralive", r?.Alive),
|
||||
("@target", Clip(o.Target, 128)), ("@message", Clip(o.Message, 512)), ("@result", o.Result));
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,38 @@
|
|||
-- Whisper's tables, in CS2-SimpleAdmin's database. Applied once at load, tracked in wh_migrations.
|
||||
-- All times are UTC.
|
||||
|
||||
-- One row per private message attempt, sent or not (while History is on in Whisper.json).
|
||||
-- result: sent, blocked, gagged, cooldown, no_target, ambiguous, group_target, too_long.
|
||||
-- The recipient columns are NULL when no single player was found. team: 0 none, 1 spectator,
|
||||
-- 2 T, 3 CT. alive: 1 when on T/CT with a living pawn. target is what the sender typed for it.
|
||||
-- Every chat message, private message and staff message (while History is on in Whisper.json).
|
||||
-- channel: all, team (normal chat), pm (/pm and /r), staff (@ team chat from staff), to_staff
|
||||
-- (@ team chat from a player), or the SimpleAdmin command used: css_say, css_asay, css_psay,
|
||||
-- css_csay, css_hsay, css_cssay.
|
||||
-- result: sent; gagged (blocked by a gag or silence); command (a ! or / chat command); and for pm,
|
||||
-- blocked, cooldown, too_long, no_target, ambiguous or group_target. Staff commands are logged
|
||||
-- as typed, so one the sender wasn't allowed to use is still "sent".
|
||||
-- The sender columns are NULL for the server console (RCON, the web panel). The recipient columns
|
||||
-- are for pm only, NULL when no single player was found. team: 0 none, 1 spectator, 2 T, 3 CT.
|
||||
-- alive: 1 when on T/CT with a living pawn. target is what the sender typed for it (pm, css_psay).
|
||||
CREATE TABLE IF NOT EXISTS `wh_messages` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`message_key` char(32) NOT NULL,
|
||||
`sent_at` datetime NOT NULL,
|
||||
`server_id` int NULL,
|
||||
`map` varchar(64) NOT NULL,
|
||||
`sender_steamid64` bigint unsigned NOT NULL,
|
||||
`channel` varchar(16) NOT NULL,
|
||||
`sender_steamid64` bigint unsigned NULL,
|
||||
`sender_name` varchar(128) NOT NULL,
|
||||
`sender_team` tinyint NOT NULL,
|
||||
`sender_alive` tinyint NOT NULL,
|
||||
`sender_team` tinyint NULL,
|
||||
`sender_alive` tinyint NULL,
|
||||
`recipient_steamid64` bigint unsigned NULL,
|
||||
`recipient_name` varchar(128) NULL,
|
||||
`recipient_team` tinyint NULL,
|
||||
`recipient_alive` tinyint NULL,
|
||||
`target` varchar(128) NOT NULL,
|
||||
`target` varchar(128) NULL,
|
||||
`message` varchar(512) NOT NULL,
|
||||
`result` varchar(16) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_message_key` (`message_key`),
|
||||
KEY `ix_sent_at` (`sent_at`),
|
||||
KEY `ix_channel` (`channel`, `sent_at`),
|
||||
KEY `ix_sender` (`sender_steamid64`, `sent_at`),
|
||||
KEY `ix_recipient` (`recipient_steamid64`, `sent_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
|
|
|||
21
Ops.cs
21
Ops.cs
|
|
@ -11,20 +11,35 @@ namespace Whisper;
|
|||
[JsonDerivedType(typeof(BlockRemove), "block_remove")]
|
||||
public abstract record Op;
|
||||
|
||||
public sealed record PlayerState(ulong SteamId, string Name, int Team, bool Alive);
|
||||
// A player as they were when the message was sent. Console: SteamId 0, no team.
|
||||
public sealed record PlayerState(ulong SteamId, string Name, int? Team, bool? Alive)
|
||||
{
|
||||
public static readonly PlayerState Console = new(0, "Console", null, null);
|
||||
}
|
||||
|
||||
public sealed record MessageRecord(
|
||||
string Key, DateTime At, int? ServerId, string Map, PlayerState Sender, PlayerState? Recipient,
|
||||
string Target, string Message, string Result) : Op;
|
||||
string Key, DateTime At, int? ServerId, string Map, string Channel, PlayerState Sender, PlayerState? Recipient,
|
||||
string? Target, string Message, string Result) : Op;
|
||||
|
||||
public sealed record BlockAdd(ulong Blocker, ulong Blocked, string BlockedName, DateTime At) : Op;
|
||||
|
||||
public sealed record BlockRemove(ulong Blocker, ulong Blocked) : Op;
|
||||
|
||||
// Where a message went (wh_messages.channel). SimpleAdmin's commands are logged by their own name.
|
||||
public static class ChatChannel
|
||||
{
|
||||
public const string All = "all";
|
||||
public const string Team = "team";
|
||||
public const string Pm = "pm";
|
||||
public const string Staff = "staff";
|
||||
public const string ToStaff = "to_staff";
|
||||
}
|
||||
|
||||
// What the message log calls each outcome (wh_messages.result).
|
||||
public static class Result
|
||||
{
|
||||
public const string Sent = "sent";
|
||||
public const string Command = "command";
|
||||
public const string Blocked = "blocked";
|
||||
public const string Gagged = "gagged";
|
||||
public const string Cooldown = "cooldown";
|
||||
|
|
|
|||
31
README.md
31
README.md
|
|
@ -69,19 +69,36 @@ tables created on first load and tracked in `wh_migrations`. All times are UTC.
|
|||
|
||||
### History
|
||||
|
||||
With `History` on, each `/pm` and `/r`, sent or not, is a row in `wh_messages`: when (`sent_at`),
|
||||
`server_id` (SimpleAdmin's), `map`, the sender's and recipient's SteamID64, name, team (0 none,
|
||||
1 spectator, 2 T, 3 CT) and whether alive, the `target` as typed, the `message`, and the `result`:
|
||||
`sent`, `blocked`, `gagged`, `cooldown`, `too_long`, `no_target`, `ambiguous` or `group_target`. The
|
||||
recipient columns are `NULL` when no single player was found.
|
||||
With `History` on, every message is a row in `wh_messages`, sent or not:
|
||||
|
||||
| `channel` | What |
|
||||
|---|---|
|
||||
| `all`, `team` | Normal chat, including `!`/`/` commands (`result` `command`) other than the ones below |
|
||||
| `pm` | `/pm`, `/w` and `/r` |
|
||||
| `staff`, `to_staff` | `@` team chat, from staff (`@css/chat`) or from a player to staff |
|
||||
| `css_say`, `css_asay`, `css_psay`, `css_csay`, `css_hsay`, `css_cssay` | SimpleAdmin's say commands, typed in console, sent over RCON (the web panel too), or as `!asay` and the like in chat. `@` in all chat from staff is `css_say`, as SimpleAdmin runs it |
|
||||
|
||||
Each row has when (`sent_at`), `server_id` (SimpleAdmin's), `map`, the sender's SteamID64, name,
|
||||
team (0 none, 1 spectator, 2 T, 3 CT) and whether alive (all `NULL` and "Console" for the server
|
||||
console), the `message`, and the `result`: `sent`, `gagged` (stopped by a gag or silence; `@` team
|
||||
chat gets through one), `command`, or for `pm` `blocked`, `cooldown`, `too_long`, `no_target`,
|
||||
`ambiguous` or `group_target`. PMs also have the recipient's SteamID64, name, team and whether alive,
|
||||
and `target` as typed (`css_psay` has `target` too). Staff commands are logged as typed, so one the
|
||||
sender wasn't allowed to use still says `sent`.
|
||||
|
||||
Chat is seen by a catch-all command listener added in `Load`. CSS runs catch-alls before any
|
||||
per-command listener (CS2-Tags' `say`, say), in the order they were added, and stops at the first
|
||||
that returns `Stop`. CS2-SimpleAdmin adds its catch-all (which stops gagged players and handles `@`)
|
||||
in `OnAllPluginsLoaded`, after every plugin's `Load`, so Whisper always sees a line before it's
|
||||
stopped or rewritten. Keep that in mind if SimpleAdmin ever moves its listener into `Load`.
|
||||
|
||||
`css_whisper_history off` stops it at once and saves the setting to `Whisper.json`; rows already kept
|
||||
stay. Writes go in order on a background task and spool to `plugins/Whisper/spool.jsonl` while the
|
||||
database is down.
|
||||
|
||||
```sql
|
||||
SELECT sent_at, map, sender_name, sender_steamid64, sender_team, sender_alive,
|
||||
recipient_name, recipient_steamid64, recipient_team, recipient_alive, message, result
|
||||
SELECT sent_at, map, channel, sender_name, sender_steamid64, sender_team, sender_alive,
|
||||
recipient_name, message, result
|
||||
FROM wh_messages ORDER BY sent_at DESC LIMIT 50;
|
||||
```
|
||||
|
||||
|
|
|
|||
90
Whisper.cs
90
Whisper.cs
|
|
@ -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")]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue