]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add webhook for votekicks (#32513)
authorSlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Sun, 29 Sep 2024 13:23:53 +0000 (15:23 +0200)
committerGitHub <noreply@github.com>
Sun, 29 Sep 2024 13:23:53 +0000 (15:23 +0200)
* Initial commit

* Localization

Content.Server/Discord/WebhookMessages/VoteWebhooks.cs [new file with mode: 0644]
Content.Server/IoC/ServerContentIoC.cs
Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs
Content.Server/Voting/VoteCommands.cs
Content.Shared/CCVar/CCVars.cs
Resources/Locale/en-US/administration/commands/custom-vote-command.ftl [deleted file]
Resources/Locale/en-US/discord/vote-notifications.ftl [new file with mode: 0644]

diff --git a/Content.Server/Discord/WebhookMessages/VoteWebhooks.cs b/Content.Server/Discord/WebhookMessages/VoteWebhooks.cs
new file mode 100644 (file)
index 0000000..74953d1
--- /dev/null
@@ -0,0 +1,183 @@
+using Content.Server.GameTicking;
+using Content.Server.Voting;
+using Robust.Server;
+using Robust.Shared.Configuration;
+using Robust.Shared.Utility;
+using System.Diagnostics;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace Content.Server.Discord.WebhookMessages;
+
+public sealed class VoteWebhooks : IPostInjectInit
+{
+    [Dependency] private readonly IConfigurationManager _cfg = default!;
+    [Dependency] private readonly IEntitySystemManager _entSys = default!;
+    [Dependency] private readonly DiscordWebhook _discord = default!;
+    [Dependency] private readonly IBaseServer _baseServer = default!;
+
+    private ISawmill _sawmill = default!;
+
+    public WebhookState? CreateWebhookIfConfigured(VoteOptions voteOptions, string? webhookUrl = null, string? customVoteName = null, string? customVoteMessage = null)
+    {
+        // All this webhook code is complete garbage.
+        // I tried to clean it up somewhat, at least to fix the glaring bugs in it.
+        // Jesus christ man what is with our code review process.
+
+        if (string.IsNullOrEmpty(webhookUrl))
+            return null;
+
+        // Set up the webhook payload
+        var serverName = _baseServer.ServerName;
+
+        var fields = new List<WebhookEmbedField>();
+
+        foreach (var voteOption in voteOptions.Options)
+        {
+            var newVote = new WebhookEmbedField
+            {
+                Name = voteOption.text,
+                Value = Loc.GetString("custom-vote-webhook-option-pending")
+            };
+            fields.Add(newVote);
+        }
+
+        var gameTicker = _entSys.GetEntitySystemOrNull<GameTicker>();
+        _sawmill = Logger.GetSawmill("discord");
+
+        var runLevel = gameTicker != null ? Loc.GetString($"game-run-level-{gameTicker.RunLevel}") : "";
+        var runId = gameTicker != null ? gameTicker.RoundId : 0;
+
+        var voteName = customVoteName ?? Loc.GetString("custom-vote-webhook-name");
+        var description = customVoteMessage ?? voteOptions.Title;
+
+        var payload = new WebhookPayload()
+        {
+            Username = voteName,
+            Embeds = new List<WebhookEmbed>
+                {
+                    new()
+                    {
+                        Title = voteOptions.InitiatorText,
+                        Color = 13438992, // #CD1010
+                        Description = description,
+                        Footer = new WebhookEmbedFooter
+                        {
+                            Text = Loc.GetString(
+                                "custom-vote-webhook-footer",
+                                ("serverName", serverName),
+                                ("roundId", runId),
+                                ("runLevel", runLevel)),
+                        },
+
+                        Fields = fields,
+                    },
+                },
+        };
+
+        var state = new WebhookState
+        {
+            WebhookUrl = webhookUrl,
+            Payload = payload,
+        };
+
+        CreateWebhookMessage(state, payload);
+
+        return state;
+    }
+
+    public void UpdateWebhookIfConfigured(WebhookState? state, VoteFinishedEventArgs finished)
+    {
+        if (state == null)
+            return;
+
+        var embed = state.Payload.Embeds![0];
+        embed.Color = 2353993; // #23EB49
+
+        for (var i = 0; i < finished.Votes.Count; i++)
+        {
+            var oldName = embed.Fields[i].Name;
+            var newValue = finished.Votes[i].ToString();
+            embed.Fields[i] = new WebhookEmbedField { Name = oldName, Value = newValue, Inline = true };
+        }
+
+        state.Payload.Embeds[0] = embed;
+
+        UpdateWebhookMessage(state, state.Payload, state.MessageId);
+    }
+
+    public void UpdateCancelledWebhookIfConfigured(WebhookState? state, string? customCancelReason = null)
+    {
+        if (state == null)
+            return;
+
+        var embed = state.Payload.Embeds![0];
+        embed.Color = 13356304; // #CBCD10
+        if (customCancelReason == null)
+            embed.Description += "\n\n" + Loc.GetString("custom-vote-webhook-cancelled");
+        else
+            embed.Description += "\n\n" + customCancelReason;
+
+        for (var i = 0; i < embed.Fields.Count; i++)
+        {
+            var oldName = embed.Fields[i].Name;
+            embed.Fields[i] = new WebhookEmbedField { Name = oldName, Value = Loc.GetString("custom-vote-webhook-option-cancelled"), Inline = true };
+        }
+
+        state.Payload.Embeds[0] = embed;
+
+        UpdateWebhookMessage(state, state.Payload, state.MessageId);
+    }
+
+    // Sends the payload's message.
+    public async void CreateWebhookMessage(WebhookState state, WebhookPayload payload)
+    {
+        try
+        {
+            if (await _discord.GetWebhook(state.WebhookUrl) is not { } identifier)
+                return;
+
+            state.Identifier = identifier.ToIdentifier();
+            _sawmill.Debug(JsonSerializer.Serialize(payload));
+
+            var request = await _discord.CreateMessage(identifier.ToIdentifier(), payload);
+            var content = await request.Content.ReadAsStringAsync();
+            state.MessageId = ulong.Parse(JsonNode.Parse(content)?["id"]!.GetValue<string>()!);
+        }
+        catch (Exception e)
+        {
+            _sawmill.Error($"Error while sending vote webhook to Discord: {e}");
+        }
+    }
+
+    // Edits a pre-existing payload message, given an ID
+    public async void UpdateWebhookMessage(WebhookState state, WebhookPayload payload, ulong id)
+    {
+        if (state.MessageId == 0)
+        {
+            _sawmill.Warning("Failed to deliver update to custom vote webhook: message ID was zero. This likely indicates a previous connection error sending the original message.");
+            return;
+        }
+
+        DebugTools.Assert(state.Identifier != default);
+
+        try
+        {
+            await _discord.EditMessage(state.Identifier, id, payload);
+        }
+        catch (Exception e)
+        {
+            _sawmill.Error($"Error while updating vote webhook on Discord: {e}");
+        }
+    }
+
+    public sealed class WebhookState
+    {
+        public required string WebhookUrl;
+        public required WebhookPayload Payload;
+        public WebhookIdentifier Identifier;
+        public ulong MessageId;
+    }
+
+    void IPostInjectInit.PostInject() { }
+}
index d7f6b85eb6000a61b823d17dda4ba261168c19dc..902e16d531f0737c72d88b33db1c9dc5d6c7acaa 100644 (file)
@@ -7,6 +7,7 @@ using Content.Server.Chat.Managers;
 using Content.Server.Connection;
 using Content.Server.Database;
 using Content.Server.Discord;
+using Content.Server.Discord.WebhookMessages;
 using Content.Server.EUI;
 using Content.Server.GhostKick;
 using Content.Server.Info;
@@ -64,6 +65,7 @@ namespace Content.Server.IoC
             IoCManager.Register<ServerInfoManager>();
             IoCManager.Register<PoissonDiskSampler>();
             IoCManager.Register<DiscordWebhook>();
+            IoCManager.Register<VoteWebhooks>();
             IoCManager.Register<ServerDbEntryManager>();
             IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
             IoCManager.Register<ServerApi>();
index 0f7d238518809520f3a4097bc9995e22b5fe01e5..736ff48817eed8a08fab77915c27bcc96400c4e0 100644 (file)
@@ -2,6 +2,7 @@ using System.Linq;
 using Content.Server.Administration;
 using Content.Server.Administration.Managers;
 using Content.Server.Database;
+using Content.Server.Discord.WebhookMessages;
 using Content.Server.GameTicking;
 using Content.Server.GameTicking.Presets;
 using Content.Server.Maps;
@@ -27,6 +28,7 @@ namespace Content.Server.Voting.Managers
         [Dependency] private readonly ILogManager _logManager = default!;
         [Dependency] private readonly IBanManager _bans = default!;
         [Dependency] private readonly IServerDbManager _dbManager = default!;
+        [Dependency] private readonly VoteWebhooks _voteWebhooks = default!;
 
         private VotingSystem? _votingSystem;
         private RoleSystem? _roleSystem;
@@ -449,10 +451,13 @@ namespace Content.Server.Voting.Managers
             var vote = CreateVote(options);
             _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} ({targetEntityName}) due to {reason} started, initiated by {initiator}.");
 
+            // Create Discord webhook
+            var webhookState = _voteWebhooks.CreateWebhookIfConfigured(options, _cfg.GetCVar(CCVars.DiscordVotekickWebhook), Loc.GetString("votekick-webhook-name"), options.Title + "\n" + Loc.GetString("votekick-webhook-description", ("initiator", initiatorName), ("target", targetSession)));
+
             // Time out the vote now that we know it will happen
             TimeoutStandardVote(StandardVoteType.Votekick);
 
-            vote.OnFinished += (_, _) =>
+            vote.OnFinished += (_, eventArgs) =>
             {
 
                 var votesYes = vote.VotesPerOption["yes"];
@@ -487,6 +492,7 @@ namespace Content.Server.Voting.Managers
                     {
                         _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} attempted to pass, but an admin was online. Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
                         AnnounceCancelledVotekickForVoters(targetEntityName);
+                        _voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-admin-online"));
                         return;
                     }
                     // Check if the target is an antag and the vote reason is raiding (this is to prevent false positives)
@@ -494,6 +500,7 @@ namespace Content.Server.Voting.Managers
                     {
                         _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} due to {reason} finished, created by {initiator}, but was cancelled due to the target being an antagonist.");
                         AnnounceCancelledVotekickForVoters(targetEntityName);
+                        _voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-antag-target"));
                         return;
                     }
                     // Check if the target is an admin/de-admined admin
@@ -501,6 +508,7 @@ namespace Content.Server.Voting.Managers
                     {
                         _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} due to {reason} finished, created by {initiator}, but was cancelled due to the target being a de-admined admin.");
                         AnnounceCancelledVotekickForVoters(targetEntityName);
+                        _voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState, Loc.GetString("votekick-webhook-cancelled-admin-target"));
                         return;
                     }
                     else
@@ -515,6 +523,9 @@ namespace Content.Server.Voting.Managers
                             severity = NoteSeverity.High;
                         }
 
+                        // Discord webhook, success
+                        _voteWebhooks.UpdateWebhookIfConfigured(webhookState, eventArgs);
+
                         uint minutes = (uint)_cfg.GetCVar(CCVars.VotekickBanDuration);
 
                         _bans.CreateServerBan(targetUid, target, null, null, targetHWid, minutes, severity, reason);
@@ -522,6 +533,10 @@ namespace Content.Server.Voting.Managers
                 }
                 else
                 {
+
+                    // Discord webhook, failure
+                    _voteWebhooks.UpdateWebhookIfConfigured(webhookState, eventArgs);
+
                     _adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick failed: Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
                     _chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-votekick-failure", ("target", targetEntityName), ("reason", reason)));
                 }
index 7dbb41b50fefb3ef2feb52074fcc2e6484ea06f4..d4c236f3945e6df326364a53bb78e1c61f128235 100644 (file)
@@ -1,11 +1,8 @@
 using System.Linq;
-using System.Text.Json;
-using System.Text.Json.Nodes;
 using Content.Server.Administration;
 using Content.Server.Administration.Logs;
 using Content.Server.Chat.Managers;
-using Content.Server.Discord;
-using Content.Server.GameTicking;
+using Content.Server.Discord.WebhookMessages;
 using Content.Server.Voting.Managers;
 using Content.Shared.Administration;
 using Content.Shared.CCVar;
@@ -76,11 +73,9 @@ namespace Content.Server.Voting
     {
         [Dependency] private readonly IVoteManager _voteManager = default!;
         [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-        [Dependency] private readonly IConfigurationManager _cfg = default!;
-        [Dependency] private readonly DiscordWebhook _discord = default!;
-        [Dependency] private readonly GameTicker _gameTicker = default!;
-        [Dependency] private readonly IBaseServer _baseServer = default!;
         [Dependency] private readonly IChatManager _chatManager = default!;
+        [Dependency] private readonly VoteWebhooks _voteWebhooks = default!;
+        [Dependency] private readonly IConfigurationManager _cfg = default!;
 
         private ISawmill _sawmill = default!;
 
@@ -120,7 +115,7 @@ namespace Content.Server.Voting
 
             var vote = _voteManager.CreateVote(options);
 
-            var webhookState = CreateWebhookIfConfigured(options);
+            var webhookState = _voteWebhooks.CreateWebhookIfConfigured(options, _cfg.GetCVar(CCVars.DiscordVoteWebhook));
 
             vote.OnFinished += (_, eventArgs) =>
             {
@@ -136,12 +131,12 @@ namespace Content.Server.Voting
                     _chatManager.DispatchServerAnnouncement(Loc.GetString("cmd-customvote-on-finished-win", ("winner", args[(int) eventArgs.Winner])));
                 }
 
-                UpdateWebhookIfConfigured(webhookState, eventArgs);
+                _voteWebhooks.UpdateWebhookIfConfigured(webhookState, eventArgs);
             };
 
             vote.OnCancelled += _ =>
             {
-                UpdateCancelledWebhookIfConfigured(webhookState);
+                _voteWebhooks.UpdateCancelledWebhookIfConfigured(webhookState);
             };
         }
 
@@ -156,159 +151,6 @@ namespace Content.Server.Voting
             var n = args.Length - 1;
             return CompletionResult.FromHint(Loc.GetString("cmd-customvote-arg-option-n", ("n", n)));
         }
-
-        private WebhookState? CreateWebhookIfConfigured(VoteOptions voteOptions)
-        {
-            // All this webhook code is complete garbage.
-            // I tried to clean it up somewhat, at least to fix the glaring bugs in it.
-            // Jesus christ man what is with our code review process.
-
-            var webhookUrl = _cfg.GetCVar(CCVars.DiscordVoteWebhook);
-            if (string.IsNullOrEmpty(webhookUrl))
-                return null;
-
-            // Set up the webhook payload
-            var serverName = _baseServer.ServerName;
-
-            var fields = new List<WebhookEmbedField>();
-
-            foreach (var voteOption in voteOptions.Options)
-            {
-                var newVote = new WebhookEmbedField
-                {
-                    Name = voteOption.text,
-                    Value = Loc.GetString("custom-vote-webhook-option-pending")
-                };
-                fields.Add(newVote);
-            }
-
-            var runLevel = Loc.GetString($"game-run-level-{_gameTicker.RunLevel}");
-
-            var payload = new WebhookPayload()
-            {
-                Username = Loc.GetString("custom-vote-webhook-name"),
-                Embeds = new List<WebhookEmbed>
-                {
-                    new()
-                    {
-                        Title = voteOptions.InitiatorText,
-                        Color = 13438992, // #CD1010
-                        Description = voteOptions.Title,
-                        Footer = new WebhookEmbedFooter
-                        {
-                            Text = Loc.GetString(
-                                "custom-vote-webhook-footer",
-                                ("serverName", serverName),
-                                ("roundId", _gameTicker.RoundId),
-                                ("runLevel", runLevel)),
-                        },
-
-                        Fields = fields,
-                    },
-                },
-            };
-
-            var state = new WebhookState
-            {
-                WebhookUrl = webhookUrl,
-                Payload = payload,
-            };
-
-            CreateWebhookMessage(state, payload);
-
-            return state;
-        }
-
-        private void UpdateWebhookIfConfigured(WebhookState? state, VoteFinishedEventArgs finished)
-        {
-            if (state == null)
-                return;
-
-            var embed = state.Payload.Embeds![0];
-            embed.Color = 2353993; // #23EB49
-
-            for (var i = 0; i < finished.Votes.Count; i++)
-            {
-                var oldName = embed.Fields[i].Name;
-                var newValue = finished.Votes[i].ToString();
-                embed.Fields[i] = new WebhookEmbedField { Name = oldName, Value = newValue, Inline =  true};
-            }
-
-            state.Payload.Embeds[0] = embed;
-
-            UpdateWebhookMessage(state, state.Payload, state.MessageId);
-        }
-
-        private void UpdateCancelledWebhookIfConfigured(WebhookState? state)
-        {
-            if (state == null)
-                return;
-
-            var embed = state.Payload.Embeds![0];
-            embed.Color = 13356304; // #CBCD10
-            embed.Description += "\n\n" + Loc.GetString("custom-vote-webhook-cancelled");
-
-            for (var i = 0; i < embed.Fields.Count; i++)
-            {
-                var oldName = embed.Fields[i].Name;
-                embed.Fields[i] = new WebhookEmbedField { Name = oldName, Value = Loc.GetString("custom-vote-webhook-option-cancelled"), Inline =  true};
-            }
-
-            state.Payload.Embeds[0] = embed;
-
-            UpdateWebhookMessage(state, state.Payload, state.MessageId);
-        }
-
-        // Sends the payload's message.
-        private async void CreateWebhookMessage(WebhookState state, WebhookPayload payload)
-        {
-            try
-            {
-                if (await _discord.GetWebhook(state.WebhookUrl) is not { } identifier)
-                    return;
-
-                state.Identifier = identifier.ToIdentifier();
-
-                _sawmill.Debug(JsonSerializer.Serialize(payload));
-
-                var request = await _discord.CreateMessage(identifier.ToIdentifier(), payload);
-                var content = await request.Content.ReadAsStringAsync();
-                state.MessageId = ulong.Parse(JsonNode.Parse(content)?["id"]!.GetValue<string>()!);
-            }
-            catch (Exception e)
-            {
-                _sawmill.Error($"Error while sending vote webhook to Discord: {e}");
-            }
-        }
-
-        // Edits a pre-existing payload message, given an ID
-        private async void UpdateWebhookMessage(WebhookState state, WebhookPayload payload, ulong id)
-        {
-            if (state.MessageId == 0)
-            {
-                _sawmill.Warning("Failed to deliver update to custom vote webhook: message ID was zero. This likely indicates a previous connection error sending the original message.");
-                return;
-            }
-
-            DebugTools.Assert(state.Identifier != default);
-
-            try
-            {
-                await _discord.EditMessage(state.Identifier, id, payload);
-            }
-            catch (Exception e)
-            {
-                _sawmill.Error($"Error while updating vote webhook on Discord: {e}");
-            }
-        }
-
-        private sealed class WebhookState
-        {
-            public required string WebhookUrl;
-            public required WebhookPayload Payload;
-            public WebhookIdentifier Identifier;
-            public ulong MessageId;
-        }
     }
 
     [AnyCommand]
index 14bb760f409af6494e9331ebc1ad597999303c67..a4f315d62c567f60fd30d5b5576d964d2f98df4b 100644 (file)
@@ -465,6 +465,12 @@ namespace Content.Shared.CCVar
         public static readonly CVarDef<string> DiscordVoteWebhook =
             CVarDef.Create("discord.vote_webhook", string.Empty, CVar.SERVERONLY);
 
+        /// <summary>
+        /// URL of the Discord webhook which will relay all votekick votes. If left empty, disables the webhook.
+        /// </summary>
+        public static readonly CVarDef<string> DiscordVotekickWebhook =
+            CVarDef.Create("discord.votekick_webhook", string.Empty, CVar.SERVERONLY);
+
         /// URL of the Discord webhook which will relay round restart messages.
         /// </summary>
         public static readonly CVarDef<string> DiscordRoundUpdateWebhook =
diff --git a/Resources/Locale/en-US/administration/commands/custom-vote-command.ftl b/Resources/Locale/en-US/administration/commands/custom-vote-command.ftl
deleted file mode 100644 (file)
index 221f062..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-custom-vote-webhook-name = Custom Vote Held
-custom-vote-webhook-footer = server: { $serverName }, round: { $roundId } { $runLevel }
-custom-vote-webhook-cancelled = **Vote cancelled**
-custom-vote-webhook-option-pending = TBD
-custom-vote-webhook-option-cancelled = N/A
diff --git a/Resources/Locale/en-US/discord/vote-notifications.ftl b/Resources/Locale/en-US/discord/vote-notifications.ftl
new file mode 100644 (file)
index 0000000..f6779ca
--- /dev/null
@@ -0,0 +1,11 @@
+custom-vote-webhook-name = Custom Vote Held
+custom-vote-webhook-footer = server: { $serverName }, round: { $roundId } { $runLevel }
+custom-vote-webhook-cancelled = **Vote cancelled**
+custom-vote-webhook-option-pending = TBD
+custom-vote-webhook-option-cancelled = N/A
+
+votekick-webhook-name = Votekick Held
+votekick-webhook-description = Initiator: { $initiator }; Target: { $target }
+votekick-webhook-cancelled-admin-online = **Vote cancelled due to admins online**
+votekick-webhook-cancelled-admin-target = **Vote cancelled due to target being admin**
+votekick-webhook-cancelled-antag-target = **Vote cancelled due to target being antag**