From 68ae3bc668a57039e37e42ae01bf11d9140dfa36 Mon Sep 17 00:00:00 2001 From: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:00:22 -0400 Subject: [PATCH 1/2] Discord Ahelp Reply System (#2283) (#1004) Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com> Co-authored-by: Whatstone Co-authored-by: Myzumi <34660019+Myzumi@users.noreply.github.com> Co-authored-by: Aiden --- Content.Server/Administration/ServerApi.cs | 49 +++++++- .../Administration/Systems/BwoinkSystem.cs | 106 +++++++++++++----- Content.Server/Discord/WebhookPayload.cs | 2 + 3 files changed, 127 insertions(+), 30 deletions(-) diff --git a/Content.Server/Administration/ServerApi.cs b/Content.Server/Administration/ServerApi.cs index f1f09d4b50..b592b9322d 100644 --- a/Content.Server/Administration/ServerApi.cs +++ b/Content.Server/Administration/ServerApi.cs @@ -7,12 +7,14 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; using Content.Server.Administration.Systems; +using Content.Server.Administration.Managers; using Content.Server.GameTicking; using Content.Server.GameTicking.Presets; using Content.Server.GameTicking.Rules.Components; using Content.Server.Maps; using Content.Server.RoundEnd; using Content.Shared.Administration.Managers; +using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.GameTicking.Components; using Content.Shared.Prototypes; @@ -48,7 +50,7 @@ public sealed partial class ServerApi : IPostInjectInit [Dependency] private readonly IStatusHost _statusHost = default!; [Dependency] private readonly IConfigurationManager _config = default!; [Dependency] private readonly ISharedPlayerManager _playerManager = default!; - [Dependency] private readonly ISharedAdminManager _adminManager = default!; + [Dependency] private readonly IAdminManager _adminManager = default!; // Frontier: ISharedAdminManager [Dependency] private readonly IGameMapManager _gameMapManager = default!; [Dependency] private readonly IServerNetManager _netManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -81,6 +83,8 @@ void IPostInjectInit.PostInject() RegisterActorHandler(HttpMethod.Post, "/admin/actions/force_preset", ActionForcePreset); RegisterActorHandler(HttpMethod.Post, "/admin/actions/set_motd", ActionForceMotd); RegisterActorHandler(HttpMethod.Patch, "/admin/actions/panic_bunker", ActionPanicPunker); + + RegisterHandler(HttpMethod.Post, "/admin/actions/send_bwoink", ActionSendBwoink); // Frontier - Discord Ahelp Reply } public void Initialize() @@ -393,6 +397,40 @@ await RunOnMainThread(async () => _sawmill.Info($"Forced instant round restart by {FormatLogActor(actor)}"); await RespondOk(context); }); + } + #endregion + + #region Frontier + // Creating a region here incase more actions are added in the future + + private async Task ActionSendBwoink(IStatusHandlerContext context) + { + var body = await ReadJson(context); + if (body == null) + return; + + await RunOnMainThread(async () => + { + // Player not online or wrong Guid + if (!_playerManager.TryGetSessionById(new NetUserId(body.Guid), out var player)) + { + await RespondError( + context, + ErrorCode.PlayerNotFound, + HttpStatusCode.UnprocessableContent, + "Player not found"); + return; + } + + var serverBwoinkSystem = _entitySystemManager.GetEntitySystem(); + var message = new SharedBwoinkSystem.BwoinkTextMessage(player.UserId, SharedBwoinkSystem.SystemUserId, body.Text); + serverBwoinkSystem.OnWebhookBwoinkTextMessage(message, body); + + // Respond with OK + await RespondOk(context); + }); + + } #endregion @@ -631,6 +669,15 @@ private sealed class MotdActionBody public required string Motd { get; init; } } + public sealed class BwoinkActionBody + { + public required string Text { get; init; } + public required string Username { get; init; } + public required Guid Guid { get; init; } + public bool UserOnly { get; init; } + public required bool WebhookUpdate { get; init; } + } + #endregion #region Responses diff --git a/Content.Server/Administration/Systems/BwoinkSystem.cs b/Content.Server/Administration/Systems/BwoinkSystem.cs index 4358b7e387..11e1fd8e74 100644 --- a/Content.Server/Administration/Systems/BwoinkSystem.cs +++ b/Content.Server/Administration/Systems/BwoinkSystem.cs @@ -44,7 +44,7 @@ public sealed partial class BwoinkSystem : SharedBwoinkSystem [Dependency] private readonly IServerDbManager _dbManager = default!; [Dependency] private readonly PlayerRateLimitManager _rateLimit = default!; - [GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")] + [GeneratedRegex(@"^https://(?:(?:canary|ptb)\.)?discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")] private static partial Regex DiscordRegex(); private string _webhookUrl = string.Empty; @@ -142,7 +142,7 @@ private async void OnCallChanged(string url) var webhookId = match.Groups[1].Value; var webhookToken = match.Groups[2].Value; - _onCallData = await GetWebhookData(webhookId, webhookToken); + _onCallData = await GetWebhookData(url); } private void PlayerRateLimitedAction(ICommonSession obj) @@ -351,6 +351,7 @@ private async void OnWebhookChanged(string url) { // TODO: Ideally, CVar validation during setting should be better integrated Log.Warning("Webhook URL does not appear to be valid. Using anyways..."); + await GetWebhookData(url); // Frontier - Support for Custom URLS, we still want to see if theres Webhook data available return; } @@ -360,22 +361,19 @@ private async void OnWebhookChanged(string url) return; } - var webhookId = match.Groups[1].Value; - var webhookToken = match.Groups[2].Value; - // Fire and forget - _webhookData = await GetWebhookData(webhookId, webhookToken); + await GetWebhookData(url); // Frontier - Support for Custom URLS } - private async Task GetWebhookData(string id, string token) + private async Task GetWebhookData(string url) { - var response = await _httpClient.GetAsync($"https://discord.com/api/v10/webhooks/{id}/{token}"); + var response = await _httpClient.GetAsync(url); var content = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { _sawmill.Log(LogLevel.Error, - $"Discord returned bad status code when trying to get webhook data (perhaps the webhook URL is invalid?): {response.StatusCode}\nResponse: {content}"); + $"Webhook returned bad status code when trying to get webhook data (perhaps the webhook URL is invalid?): {response.StatusCode}\nResponse: {content}"); return null; } @@ -480,6 +478,7 @@ private async void ProcessQueue(NetUserId userId, Queue mess var payload = GeneratePayload(existingEmbed.Description, existingEmbed.Username, + userId.UserId, // Frontier, this is used to identify the players in the webhook existingEmbed.CharacterName); // If there is no existing embed, create a new one @@ -546,7 +545,7 @@ private async void ProcessQueue(NetUserId userId, Queue mess $"**[Go to ahelp](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.Id})**"); } - payload = GeneratePayload(message.ToString(), existingEmbed.Username, existingEmbed.CharacterName); + payload = GeneratePayload(message.ToString(), existingEmbed.Username, userId, existingEmbed.CharacterName); var request = await _httpClient.PostAsync($"{_onCallUrl}?wait=true", new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")); @@ -566,7 +565,7 @@ private async void ProcessQueue(NetUserId userId, Queue mess _processingChannels.Remove(userId); } - private WebhookPayload GeneratePayload(string messages, string username, string? characterName = null) + private WebhookPayload GeneratePayload(string messages, string username, Guid userId, string? characterName = null) // Frontier: added Guid { // Add character name if (characterName != null) @@ -592,6 +591,7 @@ private WebhookPayload GeneratePayload(string messages, string username, string? return new WebhookPayload { Username = username, + UserID = userId, // Frontier, this is used to identify the players in the webhook AvatarUrl = string.IsNullOrWhiteSpace(_avatarUrl) ? null : _avatarUrl, Embeds = new List { @@ -629,10 +629,20 @@ public override void Update(float frameTime) } } + // Frontier: webhook text messages + public void OnWebhookBwoinkTextMessage(BwoinkTextMessage message, ServerApi.BwoinkActionBody body) + { + // Note for forks: + AdminData webhookAdminData = new(); + + // TODO: fix args + OnBwoinkInternal(message, SystemUserId, webhookAdminData, body.Username, null, body.UserOnly, body.WebhookUpdate, true); + } + protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySessionEventArgs eventArgs) { base.OnBwoinkTextMessage(message, eventArgs); - _activeConversations[message.UserId] = DateTime.Now; + var senderSession = eventArgs.SenderSession; // TODO: Sanitize text? @@ -650,6 +660,23 @@ protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySes if (_rateLimit.CountAction(eventArgs.SenderSession, RateLimitKey) != RateLimitStatus.Allowed) return; + OnBwoinkInternal(message, eventArgs.SenderSession.UserId, senderAdmin, eventArgs.SenderSession.Name, eventArgs.SenderSession.Channel, false, true, false); + } + + /// + /// Sends a bwoink. Common to both internal messages (sent via the ahelp or admin interface) and webhook messages (sent through the webhook, e.g. via Discord) + /// + /// The message being sent. + /// The network GUID of the person sending the message. + /// The admin privileges of the person sending the message. + /// The name of the person sending the message. + /// The channel to send a message to, e.g. in case of failure to send + /// If true, message should be sent off through the webhook if possible + /// Message originated from a webhook (e.g. Discord) + private void OnBwoinkInternal(BwoinkTextMessage message, NetUserId senderId, AdminData? senderAdmin, string senderName, INetChannel? senderChannel, bool userOnly, bool sendWebhook, bool fromWebhook) + { + _activeConversations[message.UserId] = DateTime.Now; + var escapedText = FormattedMessage.EscapeText(message.Text); string bwoinkText; @@ -665,31 +692,37 @@ protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySes senderAdmin.Flags == AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently. { - bwoinkText = $"[color=purple]{adminPrefix}{senderSession.Name}[/color]"; + bwoinkText = $"[color=purple]{adminPrefix}{senderName}[/color]"; } - else if (senderAdmin is not null && senderAdmin.HasFlag(AdminFlags.Adminhelp)) + else if (fromWebhook || senderAdmin is not null && senderAdmin.HasFlag(AdminFlags.Adminhelp)) // Frontier: anything sent via webhooks are from an admin. { - bwoinkText = $"[color=red]{adminPrefix}{senderSession.Name}[/color]"; + bwoinkText = $"[color=red]{adminPrefix}{senderName}[/color]"; } else { - bwoinkText = $"{senderSession.Name}"; + bwoinkText = $"{senderName}"; } + if (fromWebhook) + bwoinkText = $"(DISCORD) {bwoinkText}"; + bwoinkText = $"{(message.PlaySound ? "" : "(S) ")}{bwoinkText}: {escapedText}"; // If it's not an admin / admin chooses to keep the sound then play it. - var playSound = !senderAHelpAdmin || message.PlaySound; - var msg = new BwoinkTextMessage(message.UserId, senderSession.UserId, bwoinkText, playSound: playSound); + var playSound = senderAdmin == null || message.PlaySound; + var msg = new BwoinkTextMessage(message.UserId, senderId, bwoinkText, playSound: playSound); LogBwoink(msg); var admins = GetTargetAdmins(); // Notify all admins - foreach (var channel in admins) + if (!userOnly) { - RaiseNetworkEvent(msg, channel); + foreach (var channel in admins) + { + RaiseNetworkEvent(msg, channel); + } } string adminPrefixWebhook = ""; @@ -721,13 +754,16 @@ protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySes } else { - overrideMsgText = $"{senderSession.Name}"; // Not an admin, name is not overridden. + overrideMsgText = $"{senderName}"; // Not an admin, name is not overridden. } + if (fromWebhook) + overrideMsgText = $"(DC) {overrideMsgText}"; + overrideMsgText = $"{(message.PlaySound ? "" : "(S) ")}{overrideMsgText}: {escapedText}"; RaiseNetworkEvent(new BwoinkTextMessage(message.UserId, - senderSession.UserId, + senderId, overrideMsgText, playSound: playSound), session.Channel); @@ -738,13 +774,13 @@ protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySes } var sendsWebhook = _webhookUrl != string.Empty; - if (sendsWebhook) + if (sendsWebhook && sendWebhook) { if (!_messageQueues.ContainsKey(msg.UserId)) _messageQueues[msg.UserId] = new Queue(); var str = message.Text; - var unameLength = senderSession.Name.Length; + var unameLength = senderName.Length; if (unameLength + str.Length + _maxAdditionalChars > DescriptionMax) { @@ -753,12 +789,13 @@ protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySes var nonAfkAdmins = GetNonAfkAdmins(); var messageParams = new AHelpMessageParams( - senderSession.Name, + senderName, str, - !personalChannel, + senderId != message.UserId, _gameTicker.RoundDuration().ToString("hh\\:mm\\:ss"), _gameTicker.RunLevel, playedSound: playSound, + isDiscord: fromWebhook, noReceivers: nonAfkAdmins.Count == 0 ); _messageQueues[msg.UserId].Enqueue(GenerateAHelpMessage(messageParams)); @@ -768,10 +805,14 @@ protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySes return; // No admin online, let the player know - var systemText = Loc.GetString("bwoink-system-starmute-message-no-other-users"); - var starMuteMsg = new BwoinkTextMessage(message.UserId, SystemUserId, systemText); - RaiseNetworkEvent(starMuteMsg, senderSession.Channel); + if (senderChannel != null) + { + var systemText = Loc.GetString("bwoink-system-starmute-message-no-other-users"); + var starMuteMsg = new BwoinkTextMessage(message.UserId, SystemUserId, systemText); + RaiseNetworkEvent(starMuteMsg, senderChannel); + } } + // End Frontier: private IList GetNonAfkAdmins() { @@ -807,6 +848,10 @@ private static DiscordRelayedData GenerateAHelpMessage(AHelpMessageParams parame stringbuilder.Append($" **{parameters.RoundTime}**"); if (!parameters.PlayedSound) stringbuilder.Append(" **(S)**"); + + if (parameters.IsDiscord) // Frontier - Discord Indicator + stringbuilder.Append(" **(DC)**"); + if (parameters.Icon == null) stringbuilder.Append($" **{parameters.Username}:** "); else @@ -870,6 +915,7 @@ public sealed class AHelpMessageParams public GameRunLevel RoundState { get; set; } public bool PlayedSound { get; set; } public bool NoReceivers { get; set; } + public bool IsDiscord { get; set; } // Frontier public string? Icon { get; set; } public AHelpMessageParams( @@ -879,6 +925,7 @@ public AHelpMessageParams( string roundTime, GameRunLevel roundState, bool playedSound, + bool isDiscord = false, // Frontier bool noReceivers = false, string? icon = null) { @@ -887,6 +934,7 @@ public AHelpMessageParams( IsAdmin = isAdmin; RoundTime = roundTime; RoundState = roundState; + IsDiscord = isDiscord; // Frontier PlayedSound = playedSound; NoReceivers = noReceivers; Icon = icon; diff --git a/Content.Server/Discord/WebhookPayload.cs b/Content.Server/Discord/WebhookPayload.cs index fdf5f48444..8d587e0bd1 100644 --- a/Content.Server/Discord/WebhookPayload.cs +++ b/Content.Server/Discord/WebhookPayload.cs @@ -5,6 +5,8 @@ namespace Content.Server.Discord; // https://discord.com/developers/docs/resources/channel#message-object-message-structure public struct WebhookPayload { + [JsonPropertyName("UserID")] // Frontier, this is used to identify the players in the webhook + public Guid? UserID { get; set; } /// /// The message to send in the webhook. Maximum of 2000 characters. /// From 1eac257e6a1457365101a6d8c7974df6048d7565 Mon Sep 17 00:00:00 2001 From: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:17:53 -0400 Subject: [PATCH 2/2] AHelps: The Configuration Update (#1022) Co-authored-by: Aiden --- Content.Server/Administration/ServerApi.cs | 2 + .../Administration/Systems/BwoinkSystem.cs | 185 ++++++++++++------ Content.Shared/CCVar/CCVars.Admin.Ahelp.cs | 48 +++++ 3 files changed, 173 insertions(+), 62 deletions(-) diff --git a/Content.Server/Administration/ServerApi.cs b/Content.Server/Administration/ServerApi.cs index b592b9322d..1a73a52434 100644 --- a/Content.Server/Administration/ServerApi.cs +++ b/Content.Server/Administration/ServerApi.cs @@ -676,6 +676,8 @@ public sealed class BwoinkActionBody public required Guid Guid { get; init; } public bool UserOnly { get; init; } public required bool WebhookUpdate { get; init; } + public required string RoleName { get; init; } + public required string RoleColor { get; init; } } #endregion diff --git a/Content.Server/Administration/Systems/BwoinkSystem.cs b/Content.Server/Administration/Systems/BwoinkSystem.cs index 11e1fd8e74..be58a94288 100644 --- a/Content.Server/Administration/Systems/BwoinkSystem.cs +++ b/Content.Server/Administration/Systems/BwoinkSystem.cs @@ -11,12 +11,14 @@ using Content.Server.Discord; using Content.Server.GameTicking; using Content.Server.Players.RateLimiting; +using Content.Server.Preferences.Managers; using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.GameTicking; using Content.Shared.Mind; using Content.Shared.Players.RateLimiting; using JetBrains.Annotations; +using Pidgin.Configuration; using Robust.Server.Player; using Robust.Shared; using Robust.Shared.Configuration; @@ -43,6 +45,7 @@ public sealed partial class BwoinkSystem : SharedBwoinkSystem [Dependency] private readonly IAfkManager _afkManager = default!; [Dependency] private readonly IServerDbManager _dbManager = default!; [Dependency] private readonly PlayerRateLimitManager _rateLimit = default!; + [Dependency] private readonly IServerPreferencesManager _preferencesManager = default!; [GeneratedRegex(@"^https://(?:(?:canary|ptb)\.)?discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")] private static partial Regex DiscordRegex(); @@ -635,8 +638,18 @@ public void OnWebhookBwoinkTextMessage(BwoinkTextMessage message, ServerApi.Bwoi // Note for forks: AdminData webhookAdminData = new(); - // TODO: fix args - OnBwoinkInternal(message, SystemUserId, webhookAdminData, body.Username, null, body.UserOnly, body.WebhookUpdate, true); + var bwoinkParams = new BwoinkParams( + message, + SystemUserId, + webhookAdminData, + body.Username, + null, + body.UserOnly, + body.WebhookUpdate, + true, + body.RoleName, + body.RoleColor); + OnBwoinkInternal(bwoinkParams); } protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySessionEventArgs eventArgs) @@ -660,64 +673,79 @@ protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySes if (_rateLimit.CountAction(eventArgs.SenderSession, RateLimitKey) != RateLimitStatus.Allowed) return; - OnBwoinkInternal(message, eventArgs.SenderSession.UserId, senderAdmin, eventArgs.SenderSession.Name, eventArgs.SenderSession.Channel, false, true, false); + var bwoinkParams = new BwoinkParams(message, + eventArgs.SenderSession.UserId, + senderAdmin, + eventArgs.SenderSession.Name, + eventArgs.SenderSession.Channel, + false, + true, + false); + OnBwoinkInternal(bwoinkParams); } /// /// Sends a bwoink. Common to both internal messages (sent via the ahelp or admin interface) and webhook messages (sent through the webhook, e.g. via Discord) /// - /// The message being sent. - /// The network GUID of the person sending the message. - /// The admin privileges of the person sending the message. - /// The name of the person sending the message. - /// The channel to send a message to, e.g. in case of failure to send - /// If true, message should be sent off through the webhook if possible - /// Message originated from a webhook (e.g. Discord) - private void OnBwoinkInternal(BwoinkTextMessage message, NetUserId senderId, AdminData? senderAdmin, string senderName, INetChannel? senderChannel, bool userOnly, bool sendWebhook, bool fromWebhook) + /// The parameters of the message being sent. + private void OnBwoinkInternal(BwoinkParams bwoinkParams) { - _activeConversations[message.UserId] = DateTime.Now; + _activeConversations[bwoinkParams.Message.UserId] = DateTime.Now; - var escapedText = FormattedMessage.EscapeText(message.Text); - - string bwoinkText; - string adminPrefix = ""; + var escapedText = FormattedMessage.EscapeText(bwoinkParams.Message.Text); + var adminColor = _config.GetCVar(CCVars.AdminBwoinkColor); + var adminPrefix = ""; + var bwoinkText = $"{bwoinkParams.SenderName}"; //Getting an administrator position - if (_config.GetCVar(CCVars.AhelpAdminPrefix) && senderAdmin is not null && senderAdmin.Title is not null) + if (_config.GetCVar(CCVars.AhelpAdminPrefix)) { - adminPrefix = $"[bold]\\[{senderAdmin.Title}\\][/bold] "; - } + if (bwoinkParams.SenderAdmin is not null && bwoinkParams.SenderAdmin.Title is not null) + adminPrefix = $"[bold]\\[{bwoinkParams.SenderAdmin.Title}\\][/bold] "; - if (senderAdmin is not null && - senderAdmin.Flags == - AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently. - { - bwoinkText = $"[color=purple]{adminPrefix}{senderName}[/color]"; + if (_config.GetCVar(CCVars.UseDiscordRoleName) && bwoinkParams.RoleName is not null) + adminPrefix = $"[bold]\\[{bwoinkParams.RoleName}\\][/bold] "; } - else if (fromWebhook || senderAdmin is not null && senderAdmin.HasFlag(AdminFlags.Adminhelp)) // Frontier: anything sent via webhooks are from an admin. + + // If role color is enabled and exists, use it, otherwise use the discord reply color + if (_config.GetCVar(CCVars.DiscordReplyColor) != string.Empty && bwoinkParams.FromWebhook) + adminColor = _config.GetCVar(CCVars.DiscordReplyColor); + + if (_config.GetCVar(CCVars.UseDiscordRoleColor) && bwoinkParams.RoleColor is not null) + adminColor = bwoinkParams.RoleColor; + + if (!bwoinkParams.FromWebhook + && _config.GetCVar(CCVars.UseAdminOOCColorInBwoinks) + && bwoinkParams.SenderAdmin is not null) { - bwoinkText = $"[color=red]{adminPrefix}{senderName}[/color]"; + var prefs = _preferencesManager.GetPreferences(bwoinkParams.SenderId); + adminColor = prefs.AdminOOCColor.ToHex(); } - else + + if (bwoinkParams.SenderAdmin is not null) { - bwoinkText = $"{senderName}"; + if (bwoinkParams.SenderAdmin.Flags == + AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently. + bwoinkText = $"[color=purple]{adminPrefix}{bwoinkParams.SenderName}[/color]"; + else if (bwoinkParams.FromWebhook || bwoinkParams.SenderAdmin.HasFlag(AdminFlags.Adminhelp)) // Frontier: anything sent via webhooks are from an admin. + bwoinkText = $"[color={adminColor}]{adminPrefix}{bwoinkParams.SenderName}[/color]"; } - if (fromWebhook) - bwoinkText = $"(DISCORD) {bwoinkText}"; + if (bwoinkParams.FromWebhook) + bwoinkText = $"{_config.GetCVar(CCVars.DiscordReplyPrefix)}{bwoinkText}"; - bwoinkText = $"{(message.PlaySound ? "" : "(S) ")}{bwoinkText}: {escapedText}"; + bwoinkText = $"{(bwoinkParams.Message.PlaySound ? "" : "(S) ")}{bwoinkText}: {escapedText}"; // If it's not an admin / admin chooses to keep the sound then play it. - var playSound = senderAdmin == null || message.PlaySound; - var msg = new BwoinkTextMessage(message.UserId, senderId, bwoinkText, playSound: playSound); + var playSound = bwoinkParams.SenderAdmin == null || bwoinkParams.Message.PlaySound; + var msg = new BwoinkTextMessage(bwoinkParams.Message.UserId, bwoinkParams.SenderId, bwoinkText, playSound: playSound); LogBwoink(msg); var admins = GetTargetAdmins(); // Notify all admins - if (!userOnly) + if (!bwoinkParams.UserOnly) { foreach (var channel in admins) { @@ -727,13 +755,13 @@ private void OnBwoinkInternal(BwoinkTextMessage message, NetUserId senderId, Adm string adminPrefixWebhook = ""; - if (_config.GetCVar(CCVars.AhelpAdminPrefixWebhook) && senderAdmin is not null && senderAdmin.Title is not null) + if (_config.GetCVar(CCVars.AhelpAdminPrefixWebhook) && bwoinkParams.SenderAdmin is not null && bwoinkParams.SenderAdmin.Title is not null) { - adminPrefixWebhook = $"[bold]\\[{senderAdmin.Title}\\][/bold] "; + adminPrefixWebhook = $"[bold]\\[{bwoinkParams.SenderAdmin.Title}\\][/bold] "; } // Notify player - if (_playerManager.TryGetSessionById(message.UserId, out var session)) + if (_playerManager.TryGetSessionById(bwoinkParams.Message.UserId, out var session)) { if (!admins.Contains(session.Channel)) { @@ -742,28 +770,22 @@ private void OnBwoinkInternal(BwoinkTextMessage message, NetUserId senderId, Adm { string overrideMsgText; // Doing the same thing as above, but with the override name. Theres probably a better way to do this. - if (senderAdmin is not null && - senderAdmin.Flags == + if (bwoinkParams.SenderAdmin is not null && + bwoinkParams.SenderAdmin.Flags == AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently. - { overrideMsgText = $"[color=purple]{adminPrefixWebhook}{_overrideClientName}[/color]"; - } - else if (senderAdmin is not null && senderAdmin.HasFlag(AdminFlags.Adminhelp)) - { + else if (bwoinkParams.SenderAdmin is not null && bwoinkParams.SenderAdmin.HasFlag(AdminFlags.Adminhelp)) overrideMsgText = $"[color=red]{adminPrefixWebhook}{_overrideClientName}[/color]"; - } else - { - overrideMsgText = $"{senderName}"; // Not an admin, name is not overridden. - } + overrideMsgText = $"{bwoinkParams.SenderName}"; // Not an admin, name is not overridden. - if (fromWebhook) - overrideMsgText = $"(DC) {overrideMsgText}"; + if (bwoinkParams.FromWebhook) + overrideMsgText = $"{_config.GetCVar(CCVars.DiscordReplyPrefix)}{overrideMsgText}"; - overrideMsgText = $"{(message.PlaySound ? "" : "(S) ")}{overrideMsgText}: {escapedText}"; + overrideMsgText = $"{(bwoinkParams.Message.PlaySound ? "" : "(S) ")}{overrideMsgText}: {escapedText}"; - RaiseNetworkEvent(new BwoinkTextMessage(message.UserId, - senderId, + RaiseNetworkEvent(new BwoinkTextMessage(bwoinkParams.Message.UserId, + bwoinkParams.SenderId, overrideMsgText, playSound: playSound), session.Channel); @@ -774,13 +796,13 @@ private void OnBwoinkInternal(BwoinkTextMessage message, NetUserId senderId, Adm } var sendsWebhook = _webhookUrl != string.Empty; - if (sendsWebhook && sendWebhook) + if (sendsWebhook && bwoinkParams.SendWebhook) { if (!_messageQueues.ContainsKey(msg.UserId)) _messageQueues[msg.UserId] = new Queue(); - var str = message.Text; - var unameLength = senderName.Length; + var str = bwoinkParams.Message.Text; + var unameLength = bwoinkParams.SenderName.Length; if (unameLength + str.Length + _maxAdditionalChars > DescriptionMax) { @@ -789,13 +811,13 @@ private void OnBwoinkInternal(BwoinkTextMessage message, NetUserId senderId, Adm var nonAfkAdmins = GetNonAfkAdmins(); var messageParams = new AHelpMessageParams( - senderName, + bwoinkParams.SenderName, str, - senderId != message.UserId, + bwoinkParams.SenderId != bwoinkParams.Message.UserId, _gameTicker.RoundDuration().ToString("hh\\:mm\\:ss"), _gameTicker.RunLevel, playedSound: playSound, - isDiscord: fromWebhook, + isDiscord: bwoinkParams.FromWebhook, noReceivers: nonAfkAdmins.Count == 0 ); _messageQueues[msg.UserId].Enqueue(GenerateAHelpMessage(messageParams)); @@ -805,11 +827,11 @@ private void OnBwoinkInternal(BwoinkTextMessage message, NetUserId senderId, Adm return; // No admin online, let the player know - if (senderChannel != null) + if (bwoinkParams.SenderChannel != null) { var systemText = Loc.GetString("bwoink-system-starmute-message-no-other-users"); - var starMuteMsg = new BwoinkTextMessage(message.UserId, SystemUserId, systemText); - RaiseNetworkEvent(starMuteMsg, senderChannel); + var starMuteMsg = new BwoinkTextMessage(bwoinkParams.Message.UserId, SystemUserId, systemText); + RaiseNetworkEvent(starMuteMsg, bwoinkParams.SenderChannel); } } // End Frontier: @@ -833,6 +855,7 @@ private IList GetTargetAdmins() private static DiscordRelayedData GenerateAHelpMessage(AHelpMessageParams parameters) { + var config = IoCManager.Resolve(); var stringbuilder = new StringBuilder(); if (parameters.Icon != null) @@ -850,7 +873,7 @@ private static DiscordRelayedData GenerateAHelpMessage(AHelpMessageParams parame stringbuilder.Append(" **(S)**"); if (parameters.IsDiscord) // Frontier - Discord Indicator - stringbuilder.Append(" **(DC)**"); + stringbuilder.Append($" **{config.GetCVar(CCVars.DiscordReplyPrefix)}**"); if (parameters.Icon == null) stringbuilder.Append($" **{parameters.Username}:** "); @@ -941,6 +964,44 @@ public AHelpMessageParams( } } + public sealed class BwoinkParams + { + public SharedBwoinkSystem.BwoinkTextMessage Message { get; set; } + public NetUserId SenderId { get; set; } + public AdminData? SenderAdmin { get; set; } + public string SenderName { get; set; } + public INetChannel? SenderChannel { get; set; } + public bool UserOnly { get; set; } + public bool SendWebhook { get; set; } + public bool FromWebhook { get; set; } + public string? RoleName { get; set; } + public string? RoleColor { get; set; } + + public BwoinkParams( + SharedBwoinkSystem.BwoinkTextMessage message, + NetUserId senderId, + AdminData? senderAdmin, + string senderName, + INetChannel? senderChannel, + bool userOnly, + bool sendWebhook, + bool fromWebhook, + string? roleName = null, + string? roleColor = null) + { + Message = message; + SenderId = senderId; + SenderAdmin = senderAdmin; + SenderName = senderName; + SenderChannel = senderChannel; + UserOnly = userOnly; + SendWebhook = sendWebhook; + FromWebhook = fromWebhook; + RoleName = roleName; + RoleColor = roleColor; + } + } + public enum PlayerStatusType { Connected, diff --git a/Content.Shared/CCVar/CCVars.Admin.Ahelp.cs b/Content.Shared/CCVar/CCVars.Admin.Ahelp.cs index 48f3965bb5..d9d22aed63 100644 --- a/Content.Shared/CCVar/CCVars.Admin.Ahelp.cs +++ b/Content.Shared/CCVar/CCVars.Admin.Ahelp.cs @@ -36,4 +36,52 @@ public sealed partial class CCVars /// public static readonly CVarDef AhelpAdminPrefixWebhook = CVarDef.Create("ahelp.admin_prefix_webhook", false, CVar.SERVERONLY); + + /// + /// If an admin replies to users from discord, should it use their discord role color? (if applicable) + /// Overrides DiscordReplyColor and AdminBwoinkColor. + /// + /// + /// + public static readonly CVarDef UseDiscordRoleColor = + CVarDef.Create("ahelp.use_discord_role_color", true, CVar.SERVERONLY); + + /// + /// If an admin replies to users from discord, should it use their discord role name? (if applicable) + /// + public static readonly CVarDef UseDiscordRoleName = + CVarDef.Create("ahelp.use_discord_role_name", true, CVar.SERVERONLY); + + /// + /// The text before an admin's name when replying from discord to indicate they're speaking from discord. + /// + public static readonly CVarDef DiscordReplyPrefix = + CVarDef.Create("ahelp.discord_reply_prefix", "(DISCORD) ", CVar.SERVERONLY); + + /// + /// The color of the names of admins. This is the fallback color for admins. + /// + /// + /// + /// + public static readonly CVarDef AdminBwoinkColor = + CVarDef.Create("ahelp.admin_bwoink_color", "red", CVar.SERVERONLY); + + /// + /// The color of the names of admins who reply from discord. Leave empty to disable. + /// Unused if UseDiscordRoleColor is true. + /// Overrides AdminBwoinkColor. + /// + /// + /// + public static readonly CVarDef DiscordReplyColor = + CVarDef.Create("ahelp.discord_reply_color", string.Empty, CVar.SERVERONLY); + + /// + /// Use the admin's Admin OOC color in bwoinks. + /// If either the ooc color or this is not set, uses the admin.admin_bwoink_color value. + /// + /// + public static readonly CVarDef UseAdminOOCColorInBwoinks = + CVarDef.Create("ahelp.bwoink_use_admin_ooc_color", true, CVar.SERVERONLY); }