Relay custom votes to a webhook (#18561)
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com> Co-authored-by: DrSmugleaf <drsmugleaf@gmail.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Content.Server.Discord;
|
namespace Content.Server.Discord;
|
||||||
@@ -66,7 +68,7 @@ public sealed class DiscordWebhook : IPostInjectInit
|
|||||||
public async Task<HttpResponseMessage> CreateMessage(WebhookIdentifier identifier, WebhookPayload payload)
|
public async Task<HttpResponseMessage> CreateMessage(WebhookIdentifier identifier, WebhookPayload payload)
|
||||||
{
|
{
|
||||||
var url = $"{GetUrl(identifier)}?wait=true";
|
var url = $"{GetUrl(identifier)}?wait=true";
|
||||||
return await _http.PostAsJsonAsync(url, payload);
|
return await _http.PostAsJsonAsync(url, payload, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -91,7 +93,7 @@ public sealed class DiscordWebhook : IPostInjectInit
|
|||||||
public async Task<HttpResponseMessage> EditMessage(WebhookIdentifier identifier, ulong messageId, WebhookPayload payload)
|
public async Task<HttpResponseMessage> EditMessage(WebhookIdentifier identifier, ulong messageId, WebhookPayload payload)
|
||||||
{
|
{
|
||||||
var url = $"{GetUrl(identifier)}/messages/{messageId}";
|
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()
|
void IPostInjectInit.PostInject()
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Content.Server.Discord;
|
namespace Content.Server.Discord;
|
||||||
|
|
||||||
// https://discord.com/developers/docs/resources/channel#embed-object-embed-structure
|
// https://discord.com/developers/docs/resources/channel#embed-object-embed-structure
|
||||||
public struct WebhookEmbed
|
public struct WebhookEmbed
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
|
||||||
[JsonPropertyName("description")]
|
[JsonPropertyName("description")]
|
||||||
public string Description { get; set; } = "";
|
public string Description { get; set; } = "";
|
||||||
|
|
||||||
@@ -14,6 +17,10 @@ public struct WebhookEmbed
|
|||||||
[JsonPropertyName("footer")]
|
[JsonPropertyName("footer")]
|
||||||
public WebhookEmbedFooter? Footer { get; set; } = null;
|
public WebhookEmbedFooter? Footer { get; set; } = null;
|
||||||
|
|
||||||
|
|
||||||
|
[JsonPropertyName("fields")]
|
||||||
|
public List<WebhookEmbedField> Fields { get; set; } = default!;
|
||||||
|
|
||||||
public WebhookEmbed()
|
public WebhookEmbed()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
20
Content.Server/Discord/WebhookEmbedField.cs
Normal file
20
Content.Server/Discord/WebhookEmbedField.cs
Normal file
@@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Content.Server.Discord;
|
namespace Content.Server.Discord;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Content.Server.Discord;
|
namespace Content.Server.Discord;
|
||||||
|
|
||||||
@@ -9,13 +9,13 @@ public struct WebhookPayload
|
|||||||
/// The message to send in the webhook. Maximum of 2000 characters.
|
/// The message to send in the webhook. Maximum of 2000 characters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("content")]
|
[JsonPropertyName("content")]
|
||||||
public string Content { get; set; } = "";
|
public string? Content { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("username")]
|
[JsonPropertyName("username")]
|
||||||
public string? Username { get; set; }
|
public string? Username { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("avatar_url")]
|
[JsonPropertyName("avatar_url")]
|
||||||
public string? AvatarUrl { get; set; } = "";
|
public string? AvatarUrl { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("embeds")]
|
[JsonPropertyName("embeds")]
|
||||||
public List<WebhookEmbed>? Embeds { get; set; } = null;
|
public List<WebhookEmbed>? Embeds { get; set; } = null;
|
||||||
|
|||||||
@@ -373,10 +373,16 @@ namespace Content.Server.Voting.Managers
|
|||||||
.First()
|
.First()
|
||||||
.Select(e => e.Data)
|
.Select(e => e.Data)
|
||||||
.ToImmutableArray();
|
.ToImmutableArray();
|
||||||
|
// Store all votes in order for webhooks
|
||||||
|
var voteTally = new List<int>();
|
||||||
|
foreach(var entry in v.Entries)
|
||||||
|
{
|
||||||
|
voteTally.Add(entry.Votes);
|
||||||
|
}
|
||||||
|
|
||||||
v.Finished = true;
|
v.Finished = true;
|
||||||
v.Dirty = 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);
|
v.OnFinished?.Invoke(_voteHandles[v.Id], args);
|
||||||
DirtyCanCallVoteAll();
|
DirtyCanCallVoteAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using Content.Server.Administration;
|
using Content.Server.Administration;
|
||||||
using Content.Server.Administration.Logs;
|
using Content.Server.Administration.Logs;
|
||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
|
using Content.Server.Discord;
|
||||||
|
using Content.Server.GameTicking;
|
||||||
using Content.Server.Voting.Managers;
|
using Content.Server.Voting.Managers;
|
||||||
using Content.Shared.Administration;
|
using Content.Shared.Administration;
|
||||||
|
using Content.Shared.CCVar;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.Voting;
|
using Content.Shared.Voting;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
|
|
||||||
namespace Content.Server.Voting
|
namespace Content.Server.Voting
|
||||||
@@ -61,6 +70,11 @@ namespace Content.Server.Voting
|
|||||||
public sealed class CreateCustomCommand : IConsoleCommand
|
public sealed class CreateCustomCommand : IConsoleCommand
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
[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;
|
private const int MaxArgCount = 10;
|
||||||
|
|
||||||
@@ -68,8 +82,15 @@ namespace Content.Server.Voting
|
|||||||
public string Description => Loc.GetString("cmd-customvote-desc");
|
public string Description => Loc.GetString("cmd-customvote-desc");
|
||||||
public string Help => Loc.GetString("cmd-customvote-help");
|
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)
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
{
|
{
|
||||||
|
_sawmill = Logger.GetSawmill("vote");
|
||||||
|
|
||||||
if (args.Length < 3 || args.Length > MaxArgCount)
|
if (args.Length < 3 || args.Length > MaxArgCount)
|
||||||
{
|
{
|
||||||
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 3), ("upper", 10)));
|
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));
|
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<GameTicker>();
|
||||||
|
|
||||||
|
var payload = new WebhookPayload()
|
||||||
|
{
|
||||||
|
Username = Loc.GetString("custom-vote-webhook-name"),
|
||||||
|
Embeds = new List<WebhookEmbed>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Title = $"{shell.Player}",
|
||||||
|
Color = 13438992,
|
||||||
|
Description = options.Title,
|
||||||
|
Footer = new WebhookEmbedFooter
|
||||||
|
{
|
||||||
|
Text = $"{_serverName} {_gameTicker.RoundId} {_gameTicker.RunLevel}",
|
||||||
|
},
|
||||||
|
|
||||||
|
Fields = new List<WebhookEmbedField> {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
options.SetInitiatorOrServer(shell.Player);
|
||||||
|
|
||||||
if (shell.Player != null)
|
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]}");
|
_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])));
|
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;
|
var n = args.Length - 1;
|
||||||
return CompletionResult.FromHint(Loc.GetString("cmd-customvote-arg-option-n", ("n", n)));
|
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<string>()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,16 @@ namespace Content.Server.Voting
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly ImmutableArray<object> Winners;
|
public readonly ImmutableArray<object> Winners;
|
||||||
|
|
||||||
public VoteFinishedEventArgs(object? winner, ImmutableArray<object> winners)
|
/// <summary>
|
||||||
|
/// Stores all the votes in a string, for webhooks.
|
||||||
|
/// </summary>
|
||||||
|
public readonly List<int> Votes;
|
||||||
|
|
||||||
|
public VoteFinishedEventArgs(object? winner, ImmutableArray<object> winners, List<int> votes)
|
||||||
{
|
{
|
||||||
Winner = winner;
|
Winner = winner;
|
||||||
Winners = winners;
|
Winners = winners;
|
||||||
|
Votes = votes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,6 +349,11 @@ namespace Content.Shared.CCVar
|
|||||||
CVarDef.Create("discord.ahelp_avatar", string.Empty, CVar.SERVERONLY);
|
CVarDef.Create("discord.ahelp_avatar", string.Empty, CVar.SERVERONLY);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// URL of the Discord webhook which will relay all custom votes. If left empty, disables the webhook.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly CVarDef<string> DiscordVoteWebhook =
|
||||||
|
CVarDef.Create("discord.vote_webhook", string.Empty, CVar.SERVERONLY);
|
||||||
|
|
||||||
/// URL of the Discord webhook which will relay round restart messages.
|
/// URL of the Discord webhook which will relay round restart messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly CVarDef<string> DiscordRoundUpdateWebhook =
|
public static readonly CVarDef<string> DiscordRoundUpdateWebhook =
|
||||||
@@ -360,6 +365,7 @@ namespace Content.Shared.CCVar
|
|||||||
public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
|
public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
|
||||||
CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY);
|
CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY);
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Suspicion
|
* Suspicion
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
custom-vote-webhook-name = Custom Vote Held
|
||||||
Reference in New Issue
Block a user