From: Palladinium Date: Wed, 15 Jan 2025 00:32:24 +0000 (+1100) Subject: Add Discord webhook on watchlist connection (#33483) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=87779250ee6d8973822a20e66211fcae52588464;p=space-station-14.git Add Discord webhook on watchlist connection (#33483) --- diff --git a/Content.Server/Administration/Managers/IWatchlistWebhookManager.cs b/Content.Server/Administration/Managers/IWatchlistWebhookManager.cs new file mode 100644 index 0000000000..6be4805365 --- /dev/null +++ b/Content.Server/Administration/Managers/IWatchlistWebhookManager.cs @@ -0,0 +1,23 @@ +using Content.Server.Administration.Notes; +using Content.Server.Database; +using Content.Server.Discord; +using Content.Shared.CCVar; +using Robust.Server; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.Configuration; +using Robust.Shared.Network; +using Robust.Shared.Player; +using System.Linq; + +namespace Content.Server.Administration.Managers; + +/// +/// This manager sends a webhook notification whenever a player with an active +/// watchlist joins the server. +/// +public interface IWatchlistWebhookManager +{ + void Initialize(); + void Update(); +} diff --git a/Content.Server/Administration/Managers/WatchlistWebhookManager.cs b/Content.Server/Administration/Managers/WatchlistWebhookManager.cs new file mode 100644 index 0000000000..054d45bfd0 --- /dev/null +++ b/Content.Server/Administration/Managers/WatchlistWebhookManager.cs @@ -0,0 +1,143 @@ +using Content.Server.Administration.Notes; +using Content.Server.Database; +using Content.Server.Discord; +using Content.Shared.CCVar; +using Robust.Server; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.Configuration; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Timing; +using System.Linq; +using System.Text; + +namespace Content.Server.Administration.Managers; + +/// +/// This manager sends a Discord webhook notification whenever a player with an active +/// watchlist joins the server. +/// +public sealed class WatchlistWebhookManager : IWatchlistWebhookManager +{ + [Dependency] private readonly IAdminNotesManager _adminNotes = default!; + [Dependency] private readonly IBaseServer _baseServer = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly DiscordWebhook _discord = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + private ISawmill _sawmill = default!; + + private string _webhookUrl = default!; + private TimeSpan _bufferTime; + + private List watchlistConnections = new(); + private TimeSpan? _bufferStartTime; + + public void Initialize() + { + _sawmill = Logger.GetSawmill("discord"); + _cfg.OnValueChanged(CCVars.DiscordWatchlistConnectionBufferTime, SetBufferTime, true); + _cfg.OnValueChanged(CCVars.DiscordWatchlistConnectionWebhook, SetWebhookUrl, true); + _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; + } + + private void SetBufferTime(float bufferTimeSeconds) + { + _bufferTime = TimeSpan.FromSeconds(bufferTimeSeconds); + } + + private void SetWebhookUrl(string webhookUrl) + { + _webhookUrl = webhookUrl; + } + + private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus != SessionStatus.Connected) + return; + + var watchlists = await _adminNotes.GetActiveWatchlists(e.Session.UserId); + + if (watchlists.Count == 0) + return; + + watchlistConnections.Add(new WatchlistConnection(e.Session.Name, watchlists)); + + if (_bufferTime > TimeSpan.Zero) + { + if (_bufferStartTime == null) + _bufferStartTime = _gameTiming.RealTime; + } + else + { + SendDiscordMessage(); + } + } + + public void Update() + { + if (_bufferStartTime != null && _gameTiming.RealTime > (_bufferStartTime + _bufferTime)) + { + SendDiscordMessage(); + _bufferStartTime = null; + } + } + + private async void SendDiscordMessage() + { + try + { + if (string.IsNullOrWhiteSpace(_webhookUrl)) + return; + + var webhookData = await _discord.GetWebhook(_webhookUrl); + if (webhookData == null) + return; + + var webhookIdentifier = webhookData.Value.ToIdentifier(); + + var messageBuilder = new StringBuilder(Loc.GetString("discord-watchlist-connection-header", + ("players", watchlistConnections.Count), + ("serverName", _baseServer.ServerName))); + + foreach (var connection in watchlistConnections) + { + messageBuilder.Append('\n'); + + var watchlist = connection.Watchlists.First(); + var expiry = watchlist.ExpirationTime?.ToUnixTimeSeconds(); + messageBuilder.Append(Loc.GetString("discord-watchlist-connection-entry", + ("playerName", connection.PlayerName), + ("message", watchlist.Message), + ("expiry", expiry ?? 0), + ("otherWatchlists", connection.Watchlists.Count - 1))); + } + + var payload = new WebhookPayload { Content = messageBuilder.ToString() }; + + await _discord.CreateMessage(webhookIdentifier, payload); + } + catch (Exception e) + { + _sawmill.Error($"Error while sending discord watchlist connection message:\n{e}"); + } + + // Clear the buffered list regardless of whether the message is sent successfully + // This prevents infinitely buffering connections if we fail to send a message + watchlistConnections.Clear(); + } + + private sealed class WatchlistConnection + { + public string PlayerName; + public List Watchlists; + + public WatchlistConnection(string playerName, List watchlists) + { + PlayerName = playerName; + Watchlists = watchlists; + } + } +} diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index a02cf5dced..3d4ea922dc 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -47,6 +47,7 @@ namespace Content.Server.Entry private PlayTimeTrackingManager? _playTimeTracking; private IEntitySystemManager? _sysMan; private IServerDbManager? _dbManager; + private IWatchlistWebhookManager _watchlistWebhookManager = default!; private IConnectionManager? _connectionManager; /// @@ -95,6 +96,7 @@ namespace Content.Server.Entry _connectionManager = IoCManager.Resolve(); _sysMan = IoCManager.Resolve(); _dbManager = IoCManager.Resolve(); + _watchlistWebhookManager = IoCManager.Resolve(); logManager.GetSawmill("Storage").Level = LogLevel.Info; logManager.GetSawmill("db.ef").Level = LogLevel.Info; @@ -112,6 +114,7 @@ namespace Content.Server.Entry _voteManager.Initialize(); _updateManager.Initialize(); _playTimeTracking.Initialize(); + _watchlistWebhookManager.Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); } @@ -168,6 +171,7 @@ namespace Content.Server.Entry case ModUpdateLevel.FramePostEngine: _updateManager.Update(); _playTimeTracking?.Update(); + _watchlistWebhookManager.Update(); _connectionManager?.Update(); break; } diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index d91d59e741..777e134246 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -73,6 +73,7 @@ namespace Content.Server.IoC IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); IoCManager.Register(); } } diff --git a/Content.Shared/CCVar/CCVars.Discord.cs b/Content.Shared/CCVar/CCVars.Discord.cs index a6c4ada745..6e4ef532cd 100644 --- a/Content.Shared/CCVar/CCVars.Discord.cs +++ b/Content.Shared/CCVar/CCVars.Discord.cs @@ -1,4 +1,4 @@ -using Robust.Shared.Configuration; +using Robust.Shared.Configuration; namespace Content.Shared.CCVar; @@ -58,4 +58,18 @@ public sealed partial class CCVars /// public static readonly CVarDef DiscordRoundEndRoleWebhook = CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY); + + /// + /// URL of the Discord webhook which will relay watchlist connection notifications. If left empty, disables the webhook. + /// + public static readonly CVarDef DiscordWatchlistConnectionWebhook = + CVarDef.Create("discord.watchlist_connection_webhook", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL); + + /// + /// How long to buffer watchlist connections for, in seconds. + /// All connections within this amount of time from the first one will be batched and sent as a single + /// Discord notification. If zero, always sends a separate notification for each connection (not recommended). + /// + public static readonly CVarDef DiscordWatchlistConnectionBufferTime = + CVarDef.Create("discord.watchlist_connection_buffer_time", 5f, CVar.SERVERONLY); } diff --git a/Resources/Locale/en-US/discord/watchlist-connections.ftl b/Resources/Locale/en-US/discord/watchlist-connections.ftl new file mode 100644 index 0000000000..72dc971c0a --- /dev/null +++ b/Resources/Locale/en-US/discord/watchlist-connections.ftl @@ -0,0 +1,14 @@ +discord-watchlist-connection-header = + { $players -> + [one] {$players} player on a watchlist has + *[other] {$players} players on a watchlist have + } connected to {$serverName} + +discord-watchlist-connection-entry = - {$playerName} with message "{$message}"{ $expiry -> + [0] {""} + *[other] {" "}(expires ) + }{ $otherWatchlists -> + [0] {""} + [one] {" "}and {$otherWatchlists} other watchlist + *[other] {" "}and {$otherWatchlists} other watchlists + }