--- /dev/null
+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() { }
+}
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;
IoCManager.Register<ServerInfoManager>();
IoCManager.Register<PoissonDiskSampler>();
IoCManager.Register<DiscordWebhook>();
+ IoCManager.Register<VoteWebhooks>();
IoCManager.Register<ServerDbEntryManager>();
IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
IoCManager.Register<ServerApi>();
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;
[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;
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"];
{
_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)
{
_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
{
_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
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);
}
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)));
}
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;
{
[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!;
var vote = _voteManager.CreateVote(options);
- var webhookState = CreateWebhookIfConfigured(options);
+ var webhookState = _voteWebhooks.CreateWebhookIfConfigured(options, _cfg.GetCVar(CCVars.DiscordVoteWebhook));
vote.OnFinished += (_, eventArgs) =>
{
_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);
};
}
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]
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 =
+++ /dev/null
-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
--- /dev/null
+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**