diff --git a/Content.Client/Administration/BwoinkSystem.cs b/Content.Client/Administration/BwoinkSystem.cs index 554ebf3b2c..8ac9549165 100644 --- a/Content.Client/Administration/BwoinkSystem.cs +++ b/Content.Client/Administration/BwoinkSystem.cs @@ -1,19 +1,15 @@ #nullable enable using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Content.Client.Administration.UI; using Content.Shared.Administration; using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Client.Player; -using Robust.Shared.Localization; using Robust.Shared.GameObjects; -using Robust.Shared.GameObjects.Components; -using Robust.Shared.Network; -using Robust.Shared.Players; using Robust.Shared.Player; using Robust.Shared.Audio; using Robust.Shared.IoC; +using Robust.Shared.Network; namespace Content.Client.Administration { @@ -43,24 +39,16 @@ namespace Content.Client.Administration public BwoinkWindow EnsureWindow(NetUserId channelId) { - if (_activeWindowMap.TryGetValue(channelId, out var existingWindow)) + if (!_activeWindowMap.TryGetValue(channelId, out var existingWindow)) { - existingWindow.Open(); - return existingWindow; + _activeWindowMap[channelId] = existingWindow = new BwoinkWindow(channelId, + _playerManager.SessionsDict.TryGetValue(channelId, out var otherSession) + ? otherSession.Name + : channelId.ToString()); } - string title; - if (_playerManager.SessionsDict.TryGetValue(channelId, out var otherSession)) - { - title = otherSession.Name; - } - else - { - title = channelId.ToString(); - } - var window = new BwoinkWindow(channelId, title); - _activeWindowMap[channelId] = window; - window.Open(); - return window; + + existingWindow.Open(); + return existingWindow; } public void EnsureWindowForLocalPlayer() diff --git a/Content.Client/Administration/UI/BwoinkWindow.xaml.cs b/Content.Client/Administration/UI/BwoinkWindow.xaml.cs index c3f000f6ca..59bee427ff 100644 --- a/Content.Client/Administration/UI/BwoinkWindow.xaml.cs +++ b/Content.Client/Administration/UI/BwoinkWindow.xaml.cs @@ -1,24 +1,13 @@ #nullable enable -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Content.Client.UserInterface; -using Content.Client.Administration; -using Content.Shared; -using Robust.Client.Credits; using Robust.Client.AutoGenerated; using Robust.Client.Player; -using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Maths; -using Robust.Shared.Utility; -using Robust.Shared.Network; using Robust.Shared.GameObjects; -using YamlDotNet.RepresentationModel; +using Robust.Shared.Network; +using Robust.Shared.Utility; namespace Content.Client.Administration.UI { @@ -33,14 +22,13 @@ namespace Content.Client.Administration.UI private readonly NetUserId _channelId; - public BwoinkWindow(NetUserId channelId, string title) + public BwoinkWindow(NetUserId userId, string channelName) { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - - _channelId = channelId; - Title = (_playerManager.LocalPlayer?.UserId == _channelId) ? "Admin Message" : title; + _channelId = userId; + Title = (_playerManager.LocalPlayer?.UserId == _channelId) ? "Admin Message" : channelName; SenderLineEdit.OnTextEntered += Input_OnTextEntered; } diff --git a/Content.Client/Commands/OpenAHelpCommand.cs b/Content.Client/Commands/OpenAHelpCommand.cs index cbe61a3b9a..05e818efbb 100644 --- a/Content.Client/Commands/OpenAHelpCommand.cs +++ b/Content.Client/Commands/OpenAHelpCommand.cs @@ -1,13 +1,9 @@ using System; - using Content.Client.Administration; +using Content.Client.Administration; using Content.Shared.Administration; -using Robust.Client.Console; -using Robust.Client.GameObjects; using Robust.Shared.Console; -using Robust.Shared.Network; -using Robust.Shared.Containers; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; +using Robust.Shared.Network; namespace Content.Client.Commands { @@ -38,7 +34,6 @@ namespace Content.Client.Commands else { shell.WriteLine("Bad GUID!"); - return; } } } diff --git a/Content.Server/Administration/BwoinkSystem.cs b/Content.Server/Administration/BwoinkSystem.cs index 4fb29c33ee..36094fb85d 100644 --- a/Content.Server/Administration/BwoinkSystem.cs +++ b/Content.Server/Administration/BwoinkSystem.cs @@ -1,12 +1,21 @@ #nullable enable +using System; +using System.Collections.Generic; using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; using Content.Server.Administration.Managers; using Content.Shared.Administration; +using Content.Shared.CCVar; using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.Localization; using Robust.Server.Player; +using Robust.Shared; +using Robust.Shared.Configuration; using Robust.Shared.IoC; +using Robust.Shared.Log; using Robust.Shared.Utility; namespace Content.Server.Administration @@ -16,6 +25,37 @@ namespace Content.Server.Administration { [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IAdminManager _adminManager = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly IPlayerLocator _playerLocator = default!; + + private ISawmill? _sawmill; + private readonly HttpClient _httpClient = new(); + private string _webhookUrl = string.Empty; + private string _serverName = string.Empty; + + public override void Initialize() + { + base.Initialize(); + _config.OnValueChanged(CCVars.DiscordAHelpWebhook, OnWebhookChanged, true); + _config.OnValueChanged(CVars.GameHostName, OnServerNameChanged, true); + } + + private void OnServerNameChanged(string obj) + { + _serverName = obj; + } + + public override void Shutdown() + { + base.Shutdown(); + _config.UnsubValueChanged(CCVars.DiscordAHelpWebhook, OnWebhookChanged); + _config.UnsubValueChanged(CVars.GameHostName, OnServerNameChanged); + } + + private void OnWebhookChanged(string obj) + { + _webhookUrl = obj; + } protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySessionEventArgs eventArgs) { @@ -24,9 +64,9 @@ namespace Content.Server.Administration // TODO: Sanitize text? // Confirm that this person is actually allowed to send a message here. - var senderPersonalChannel = senderSession.UserId == message.ChannelId; + var personalChannel = senderSession.UserId == message.ChannelId; var senderAdmin = _adminManager.GetAdminData(senderSession); - var authorized = senderPersonalChannel || senderAdmin != null; + var authorized = personalChannel || senderAdmin != null; if (!authorized) { // Unauthorized bwoink (log?) @@ -60,15 +100,65 @@ namespace Content.Server.Administration foreach (var channel in targets) RaiseNetworkEvent(msg, channel); + var sendsWebhook = _webhookUrl != string.Empty; + if (sendsWebhook) + { + async void SendWebhook() + { + _sawmill ??= IoCManager.Resolve().GetSawmill("AHELP"); + + var lookup = await _playerLocator.LookupIdAsync(message.ChannelId); + + if (lookup == null) + { + _sawmill.Log(LogLevel.Error, $"Unable to find player for netuserid {msg.ChannelId} when sending discord webhook."); + return; + } + + var payload = new WebhookPayload() + { + username = _serverName, + content = $"`[{lookup.Username}]` {senderSession.Name}: \"{message.Text}\"" + }; + + var request = await _httpClient.PostAsync(_webhookUrl, + new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")); + + if (!request.IsSuccessStatusCode) + { + var content = await request.Content.ReadAsStringAsync(); + _sawmill.Log(LogLevel.Error, $"Discord returned bad status code: {request.StatusCode}\nResponse: {content}"); + } + } + + SendWebhook(); + } + if (targets.Count == 1) { - var systemText = senderPersonalChannel ? - Loc.GetString("bwoink-system-starmute-message-no-other-users-primary") : - Loc.GetString("bwoink-system-starmute-message-no-other-users-secondary"); + var systemText = sendsWebhook ? + Loc.GetString("bwoink-system-starmute-message-no-other-users-webhook") : + Loc.GetString("bwoink-system-starmute-message-no-other-users"); var starMuteMsg = new BwoinkTextMessage(message.ChannelId, SystemUserId, systemText); RaiseNetworkEvent(starMuteMsg, senderSession.ConnectedClient); } } + + private struct WebhookPayload + { + // ReSharper disable once InconsistentNaming + public string username { get; set; } = ""; + + // ReSharper disable once InconsistentNaming + public string content { get; set; } = ""; + + // ReSharper disable once InconsistentNaming + public Dictionary allowed_mentions { get; set; } = + new() + { + { "parse", Array.Empty() } + }; + } } } diff --git a/Content.Server/Administration/PlayerLocator.cs b/Content.Server/Administration/PlayerLocator.cs index 9f180548c3..ddf6d46140 100644 --- a/Content.Server/Administration/PlayerLocator.cs +++ b/Content.Server/Administration/PlayerLocator.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Content.Server.Database; using JetBrains.Annotations; +using Newtonsoft.Json; using Robust.Server.Player; using Robust.Shared; using Robust.Shared.Configuration; @@ -16,7 +17,7 @@ using Robust.Shared.Network; namespace Content.Server.Administration { - public sealed record LocatedPlayerData(NetUserId UserId, IPAddress? LastAddress, ImmutableArray? LastHWId); + public sealed record LocatedPlayerData(NetUserId UserId, IPAddress? LastAddress, ImmutableArray? LastHWId, string Username); /// /// Utilities for finding user IDs that extend to more than the server database. @@ -38,6 +39,12 @@ namespace Content.Server.Administration /// If passed a player name, returns . /// Task LookupIdByNameOrIdAsync(string playerName, CancellationToken cancel = default); + + /// + /// Look up a user by globally. + /// + /// Null if the player does not exist. + Task LookupIdAsync(NetUserId userId, CancellationToken cancel = default); } internal sealed class PlayerLocator : IPlayerLocator @@ -54,13 +61,13 @@ namespace Content.Server.Administration var userId = session.UserId; var address = session.ConnectedClient.RemoteEndPoint.Address; var hwId = session.ConnectedClient.UserData.HWId; - return new LocatedPlayerData(userId, address, hwId); + return new LocatedPlayerData(userId, address, hwId, session.Name); } // Check database for past players. var record = await _db.GetPlayerRecordByUserName(playerName, cancel); if (record != null) - return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId); + return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName); // If all else fails, ask the auth server. var client = new HttpClient(); @@ -85,7 +92,7 @@ namespace Content.Server.Administration return null; } - return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null); + return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName); } public async Task LookupIdAsync(NetUserId userId, CancellationToken cancel = default) @@ -95,19 +102,19 @@ namespace Content.Server.Administration { var address = session.ConnectedClient.RemoteEndPoint.Address; var hwId = session.ConnectedClient.UserData.HWId; - return new LocatedPlayerData(userId, address, hwId); + return new LocatedPlayerData(userId, address, hwId, session.Name); } // Check database for past players. var record = await _db.GetPlayerRecordByUserId(userId, cancel); if (record != null) - return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId); + return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName); // If all else fails, ask the auth server. var client = new HttpClient(); var authServer = _configurationManager.GetCVar(CVars.AuthServer); var requestUri = $"{authServer}api/query/userid?userid={WebUtility.UrlEncode(userId.UserId.ToString())}"; - using var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, requestUri), cancel); + using var resp = await client.GetAsync(requestUri, cancel); if (resp.StatusCode == HttpStatusCode.NotFound) return null; @@ -118,7 +125,15 @@ namespace Content.Server.Administration return null; } - return new LocatedPlayerData(userId, null, null); + var responseData = await resp.Content.ReadFromJsonAsync(cancellationToken: cancel); + + if (responseData == null) + { + Logger.ErrorS("PlayerLocate", "Auth server returned null response!"); + return null; + } + + return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName); } public async Task LookupIdByNameOrIdAsync(string playerName, CancellationToken cancel = default) diff --git a/Content.Shared/Administration/SharedBwoinkSystem.cs b/Content.Shared/Administration/SharedBwoinkSystem.cs index 347bea7702..5125e69722 100644 --- a/Content.Shared/Administration/SharedBwoinkSystem.cs +++ b/Content.Shared/Administration/SharedBwoinkSystem.cs @@ -1,12 +1,8 @@ #nullable enable using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Robust.Shared.Network; using Robust.Shared.Serialization; -using Robust.Shared.Utility; using Robust.Shared.GameObjects; -using Robust.Shared.GameObjects.Components; using Robust.Shared.Log; namespace Content.Shared.Administration diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 3bc05bb0bc..cafb1afde8 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -154,6 +154,13 @@ namespace Content.Shared.CCVar public static readonly CVarDef SoftMaxPlayers = CVarDef.Create("game.soft_max_players", 30, CVar.SERVERONLY | CVar.ARCHIVE); + /* + * Discord + */ + + public static readonly CVarDef DiscordAHelpWebhook = + CVarDef.Create("discord.ahelp_webhook", string.Empty, CVar.SERVERONLY); + /* * Suspicion */ diff --git a/Resources/Locale/en-US/administration/bwoink.ftl b/Resources/Locale/en-US/administration/bwoink.ftl index 4ff6e246fc..a91b88627d 100644 --- a/Resources/Locale/en-US/administration/bwoink.ftl +++ b/Resources/Locale/en-US/administration/bwoink.ftl @@ -1,4 +1,4 @@ -bwoink-system-starmute-message-no-other-users-primary = *System: Nobody is available to receive your message. Try pinging Game Admins on Discord. +bwoink-system-starmute-message-no-other-users = *System: Nobody is available to receive your message. Try pinging Game Admins on Discord. -bwoink-system-starmute-message-no-other-users-secondary = *System: Nobody is available to receive your message. +bwoink-system-starmute-message-no-other-users-webhook = *System: Your message has been relayed to the admins via discord.