From 94628d6ab18302504f2f998f3972fab23a8fce1c Mon Sep 17 00:00:00 2001 From: LankLTE <135308300+LankLTE@users.noreply.github.com> Date: Thu, 14 Dec 2023 20:03:32 -0800 Subject: [PATCH] Relay custom votes to a webhook (#18561) Co-authored-by: DrSmugleaf Co-authored-by: DrSmugleaf --- Content.Server/Discord/DiscordWebhook.cs | 8 +- Content.Server/Discord/WebhookEmbed.cs | 9 +- Content.Server/Discord/WebhookEmbedField.cs | 20 ++++ Content.Server/Discord/WebhookMentions.cs | 2 +- Content.Server/Discord/WebhookPayload.cs | 6 +- Content.Server/Voting/Managers/VoteManager.cs | 8 +- Content.Server/Voting/VoteCommands.cs | 100 ++++++++++++++++++ .../Voting/VoteFinishedEventArgs.cs | 8 +- Content.Shared/CCVar/CCVars.cs | 6 ++ .../commands/custom-vote-command.ftl | 1 + 10 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 Content.Server/Discord/WebhookEmbedField.cs create mode 100644 Resources/Locale/en-US/administration/commands/custom-vote-command.ftl diff --git a/Content.Server/Discord/DiscordWebhook.cs b/Content.Server/Discord/DiscordWebhook.cs index d8a9318444..ace5766764 100644 --- a/Content.Server/Discord/DiscordWebhook.cs +++ b/Content.Server/Discord/DiscordWebhook.cs @@ -1,5 +1,7 @@ -using System.Net.Http; +using System.Net.Http; using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace Content.Server.Discord; @@ -66,7 +68,7 @@ public sealed class DiscordWebhook : IPostInjectInit public async Task CreateMessage(WebhookIdentifier identifier, WebhookPayload payload) { var url = $"{GetUrl(identifier)}?wait=true"; - return await _http.PostAsJsonAsync(url, payload); + return await _http.PostAsJsonAsync(url, payload, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); } /// @@ -91,7 +93,7 @@ public sealed class DiscordWebhook : IPostInjectInit public async Task EditMessage(WebhookIdentifier identifier, ulong messageId, WebhookPayload payload) { var url = $"{GetUrl(identifier)}/messages/{messageId}"; - return await _http.PatchAsJsonAsync(url, payload); + return await _http.PatchAsJsonAsync(url, payload, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); } void IPostInjectInit.PostInject() diff --git a/Content.Server/Discord/WebhookEmbed.cs b/Content.Server/Discord/WebhookEmbed.cs index 8e0db0722c..25083e4895 100644 --- a/Content.Server/Discord/WebhookEmbed.cs +++ b/Content.Server/Discord/WebhookEmbed.cs @@ -1,10 +1,13 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Content.Server.Discord; // https://discord.com/developers/docs/resources/channel#embed-object-embed-structure public struct WebhookEmbed { + [JsonPropertyName("title")] + public string Title { get; set; } = ""; + [JsonPropertyName("description")] public string Description { get; set; } = ""; @@ -14,6 +17,10 @@ public struct WebhookEmbed [JsonPropertyName("footer")] public WebhookEmbedFooter? Footer { get; set; } = null; + + [JsonPropertyName("fields")] + public List Fields { get; set; } = default!; + public WebhookEmbed() { } diff --git a/Content.Server/Discord/WebhookEmbedField.cs b/Content.Server/Discord/WebhookEmbedField.cs new file mode 100644 index 0000000000..5c03c84899 --- /dev/null +++ b/Content.Server/Discord/WebhookEmbedField.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Content.Server.Discord; + +// https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure +public struct WebhookEmbedField +{ + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("value")] + public string Value { get; set; } = ""; + + [JsonPropertyName("inline")] + public bool Inline { get; set; } = true; + + public WebhookEmbedField() + { + } +} diff --git a/Content.Server/Discord/WebhookMentions.cs b/Content.Server/Discord/WebhookMentions.cs index da945b363e..e28726990f 100644 --- a/Content.Server/Discord/WebhookMentions.cs +++ b/Content.Server/Discord/WebhookMentions.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Content.Server.Discord; diff --git a/Content.Server/Discord/WebhookPayload.cs b/Content.Server/Discord/WebhookPayload.cs index 6300f86ad5..fdf5f48444 100644 --- a/Content.Server/Discord/WebhookPayload.cs +++ b/Content.Server/Discord/WebhookPayload.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Content.Server.Discord; @@ -9,13 +9,13 @@ public struct WebhookPayload /// The message to send in the webhook. Maximum of 2000 characters. /// [JsonPropertyName("content")] - public string Content { get; set; } = ""; + public string? Content { get; set; } [JsonPropertyName("username")] public string? Username { get; set; } [JsonPropertyName("avatar_url")] - public string? AvatarUrl { get; set; } = ""; + public string? AvatarUrl { get; set; } [JsonPropertyName("embeds")] public List? Embeds { get; set; } = null; diff --git a/Content.Server/Voting/Managers/VoteManager.cs b/Content.Server/Voting/Managers/VoteManager.cs index 90089afb54..4fb022fad0 100644 --- a/Content.Server/Voting/Managers/VoteManager.cs +++ b/Content.Server/Voting/Managers/VoteManager.cs @@ -373,10 +373,16 @@ namespace Content.Server.Voting.Managers .First() .Select(e => e.Data) .ToImmutableArray(); + // Store all votes in order for webhooks + var voteTally = new List(); + foreach(var entry in v.Entries) + { + voteTally.Add(entry.Votes); + } v.Finished = true; v.Dirty = true; - var args = new VoteFinishedEventArgs(winners.Length == 1 ? winners[0] : null, winners); + var args = new VoteFinishedEventArgs(winners.Length == 1 ? winners[0] : null, winners, voteTally); v.OnFinished?.Invoke(_voteHandles[v.Id], args); DirtyCanCallVoteAll(); } diff --git a/Content.Server/Voting/VoteCommands.cs b/Content.Server/Voting/VoteCommands.cs index 498c9d0494..aad0ee43d7 100644 --- a/Content.Server/Voting/VoteCommands.cs +++ b/Content.Server/Voting/VoteCommands.cs @@ -1,11 +1,20 @@ using System.Linq; +using System.Net.Http; +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.Voting.Managers; using Content.Shared.Administration; +using Content.Shared.CCVar; using Content.Shared.Database; using Content.Shared.Voting; +using Robust.Server.Player; +using Robust.Shared; +using Robust.Shared.Configuration; using Robust.Shared.Console; namespace Content.Server.Voting @@ -61,6 +70,11 @@ namespace Content.Server.Voting public sealed class CreateCustomCommand : IConsoleCommand { [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly IEntitySystemManager _entitySystem = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly DiscordWebhook _discord = default!; + + private ISawmill _sawmill = default!; private const int MaxArgCount = 10; @@ -68,8 +82,15 @@ namespace Content.Server.Voting public string Description => Loc.GetString("cmd-customvote-desc"); public string Help => Loc.GetString("cmd-customvote-help"); + // Webhook stuff + private string _webhookUrl = string.Empty; + private ulong _webhookId; + private WebhookIdentifier? _webhookIdentifier; + public void Execute(IConsoleShell shell, string argStr, string[] args) { + _sawmill = Logger.GetSawmill("vote"); + if (args.Length < 3 || args.Length > MaxArgCount) { shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 3), ("upper", 10))); @@ -91,6 +112,41 @@ namespace Content.Server.Voting options.Options.Add((args[i], i)); } + // Set up the webhook payload + string _serverName = _cfg.GetCVar(CVars.GameHostName); + _webhookUrl = _cfg.GetCVar(CCVars.DiscordVoteWebhook); + + + var _gameTicker = _entitySystem.GetEntitySystem(); + + var payload = new WebhookPayload() + { + Username = Loc.GetString("custom-vote-webhook-name"), + Embeds = new List + { + new() + { + Title = $"{shell.Player}", + Color = 13438992, + Description = options.Title, + Footer = new WebhookEmbedFooter + { + Text = $"{_serverName} {_gameTicker.RoundId} {_gameTicker.RunLevel}", + }, + + Fields = new List {}, + }, + }, + }; + + foreach (var voteOption in options.Options) + { + var NewVote = new WebhookEmbedField() { Name = voteOption.text, Value = "0"}; + payload.Embeds[0].Fields.Add(NewVote); + } + + WebhookMessage(payload); + options.SetInitiatorOrServer(shell.Player); if (shell.Player != null) @@ -114,6 +170,18 @@ namespace Content.Server.Voting _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Custom vote {options.Title} finished: {args[(int) eventArgs.Winner]}"); chatMgr.DispatchServerAnnouncement(Loc.GetString("cmd-customvote-on-finished-win",("winner", args[(int) eventArgs.Winner]))); } + + for (int i = 0; i < eventArgs.Votes.Count - 1; i++) + { + var oldName = payload.Embeds[0].Fields[i].Name; + var newValue = eventArgs.Votes[i].ToString(); + var newEmbed = payload.Embeds[0]; + newEmbed.Color = 2353993; + payload.Embeds[0] = newEmbed; + payload.Embeds[0].Fields[i] = new WebhookEmbedField() { Name = oldName, Value = newValue, Inline = true}; + } + + WebhookMessage(payload, _webhookId); }; } @@ -128,6 +196,38 @@ namespace Content.Server.Voting var n = args.Length - 1; return CompletionResult.FromHint(Loc.GetString("cmd-customvote-arg-option-n", ("n", n))); } + + // Sends the payload's message. + private async void WebhookMessage(WebhookPayload payload) + { + if (string.IsNullOrEmpty(_webhookUrl)) + return; + + if (await _discord.GetWebhook(_webhookUrl) is not { } identifier) + return; + + _webhookIdentifier = identifier.ToIdentifier(); + + _sawmill.Debug(JsonSerializer.Serialize(payload)); + + var request = await _discord.CreateMessage(_webhookIdentifier.Value, payload); + var content = await request.Content.ReadAsStringAsync(); + _webhookId = ulong.Parse(JsonNode.Parse(content)?["id"]!.GetValue()!); + } + + // Edits a pre-existing payload message, given an ID + private async void WebhookMessage(WebhookPayload payload, ulong id) + { + if (string.IsNullOrEmpty(_webhookUrl)) + return; + + if (await _discord.GetWebhook(_webhookUrl) is not { } identifier) + return; + + _webhookIdentifier = identifier.ToIdentifier(); + + var request = await _discord.EditMessage(_webhookIdentifier.Value, id, payload); + } } diff --git a/Content.Server/Voting/VoteFinishedEventArgs.cs b/Content.Server/Voting/VoteFinishedEventArgs.cs index 344abd4db4..2a641272b0 100644 --- a/Content.Server/Voting/VoteFinishedEventArgs.cs +++ b/Content.Server/Voting/VoteFinishedEventArgs.cs @@ -15,10 +15,16 @@ namespace Content.Server.Voting /// public readonly ImmutableArray Winners; - public VoteFinishedEventArgs(object? winner, ImmutableArray winners) + /// + /// Stores all the votes in a string, for webhooks. + /// + public readonly List Votes; + + public VoteFinishedEventArgs(object? winner, ImmutableArray winners, List votes) { Winner = winner; Winners = winners; + Votes = votes; } } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index a8b2960728..ad570ffd3f 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -349,6 +349,11 @@ namespace Content.Shared.CCVar CVarDef.Create("discord.ahelp_avatar", string.Empty, CVar.SERVERONLY); /// + /// URL of the Discord webhook which will relay all custom votes. If left empty, disables the webhook. + /// + public static readonly CVarDef DiscordVoteWebhook = + CVarDef.Create("discord.vote_webhook", string.Empty, CVar.SERVERONLY); + /// URL of the Discord webhook which will relay round restart messages. /// public static readonly CVarDef DiscordRoundUpdateWebhook = @@ -360,6 +365,7 @@ namespace Content.Shared.CCVar public static readonly CVarDef DiscordRoundEndRoleWebhook = CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY); + /* * Suspicion */ diff --git a/Resources/Locale/en-US/administration/commands/custom-vote-command.ftl b/Resources/Locale/en-US/administration/commands/custom-vote-command.ftl new file mode 100644 index 0000000000..b8d64d2634 --- /dev/null +++ b/Resources/Locale/en-US/administration/commands/custom-vote-command.ftl @@ -0,0 +1 @@ +custom-vote-webhook-name = Custom Vote Held \ No newline at end of file