]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add Discord webhook on watchlist connection (#33483)
authorPalladinium <patrick.chieppe@hotmail.com>
Wed, 15 Jan 2025 00:32:24 +0000 (11:32 +1100)
committerGitHub <noreply@github.com>
Wed, 15 Jan 2025 00:32:24 +0000 (01:32 +0100)
Content.Server/Administration/Managers/IWatchlistWebhookManager.cs [new file with mode: 0644]
Content.Server/Administration/Managers/WatchlistWebhookManager.cs [new file with mode: 0644]
Content.Server/Entry/EntryPoint.cs
Content.Server/IoC/ServerContentIoC.cs
Content.Shared/CCVar/CCVars.Discord.cs
Resources/Locale/en-US/discord/watchlist-connections.ftl [new file with mode: 0644]

diff --git a/Content.Server/Administration/Managers/IWatchlistWebhookManager.cs b/Content.Server/Administration/Managers/IWatchlistWebhookManager.cs
new file mode 100644 (file)
index 0000000..6be4805
--- /dev/null
@@ -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;
+
+/// <summary>
+///     This manager sends a webhook notification whenever a player with an active
+///     watchlist joins the server.
+/// </summary>
+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 (file)
index 0000000..054d45b
--- /dev/null
@@ -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;
+
+/// <summary>
+///     This manager sends a Discord webhook notification whenever a player with an active
+///     watchlist joins the server.
+/// </summary>
+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<WatchlistConnection> 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<AdminWatchlistRecord> Watchlists;
+
+        public WatchlistConnection(string playerName, List<AdminWatchlistRecord> watchlists)
+        {
+            PlayerName = playerName;
+            Watchlists = watchlists;
+        }
+    }
+}
index a02cf5dcedd2f80e045c5219889a254babd53757..3d4ea922dc0e4e63f9cf0cf27280253a951c3974 100644 (file)
@@ -47,6 +47,7 @@ namespace Content.Server.Entry
         private PlayTimeTrackingManager? _playTimeTracking;
         private IEntitySystemManager? _sysMan;
         private IServerDbManager? _dbManager;
+        private IWatchlistWebhookManager _watchlistWebhookManager = default!;
         private IConnectionManager? _connectionManager;
 
         /// <inheritdoc />
@@ -95,6 +96,7 @@ namespace Content.Server.Entry
                 _connectionManager = IoCManager.Resolve<IConnectionManager>();
                 _sysMan = IoCManager.Resolve<IEntitySystemManager>();
                 _dbManager = IoCManager.Resolve<IServerDbManager>();
+                _watchlistWebhookManager = IoCManager.Resolve<IWatchlistWebhookManager>();
 
                 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<JobWhitelistManager>().Initialize();
                 IoCManager.Resolve<PlayerRateLimitManager>().Initialize();
             }
@@ -168,6 +171,7 @@ namespace Content.Server.Entry
                 case ModUpdateLevel.FramePostEngine:
                     _updateManager.Update();
                     _playTimeTracking?.Update();
+                    _watchlistWebhookManager.Update();
                     _connectionManager?.Update();
                     break;
             }
index d91d59e741e4faba157f38607e66d5247b2c21d1..777e134246994b7b2b653679a7c7c7a8bd5c5b3a 100644 (file)
@@ -73,6 +73,7 @@ namespace Content.Server.IoC
             IoCManager.Register<PlayerRateLimitManager>();
             IoCManager.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
             IoCManager.Register<MappingManager>();
+            IoCManager.Register<IWatchlistWebhookManager, WatchlistWebhookManager>();
             IoCManager.Register<ConnectionManager>();
         }
     }
index a6c4ada745411989d279473edfbcfd5c9c2105ef..6e4ef532cdc7145851baf8ca23653242d277c218 100644 (file)
@@ -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
     /// </summary>
     public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
         CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY);
+
+    /// <summary>
+    ///     URL of the Discord webhook which will relay watchlist connection notifications. If left empty, disables the webhook.
+    /// </summary>
+    public static readonly CVarDef<string> DiscordWatchlistConnectionWebhook =
+        CVarDef.Create("discord.watchlist_connection_webhook", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
+
+    /// <summary>
+    ///     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).
+    /// </summary>
+    public static readonly CVarDef<float> 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 (file)
index 0000000..72dc971
--- /dev/null
@@ -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 <t:{$expiry}:R>)
+    }{ $otherWatchlists ->
+        [0] {""}
+        [one] {" "}and {$otherWatchlists} other watchlist
+        *[other] {" "}and {$otherWatchlists} other watchlists
+    }