Add Discord webhook on watchlist connection (#33483)

This commit is contained in:
Palladinium
2025-01-15 11:32:24 +11:00
committed by GitHub
parent bb1a3c42ed
commit 87779250ee
6 changed files with 200 additions and 1 deletions

View File

@@ -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();
}

View File

@@ -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;
}
}
}

View 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;
}

View 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>();
}
}

View 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);
}

View File

@@ -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
}