diff --git a/Projects/UOContent/Engines/Help/PageDiscord.cs b/Projects/UOContent/Engines/Help/PageDiscord.cs new file mode 100644 index 000000000..d483cd1c9 --- /dev/null +++ b/Projects/UOContent/Engines/Help/PageDiscord.cs @@ -0,0 +1,222 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Server.Logging; +using System.Collections.Generic; +using System.Net.Http.Json; + +namespace Server.Engines.Help; + +public static class PageDiscord +{ + private static HttpClient _httpClient; + private static readonly ILogger _logger = LogFactory.GetLogger(typeof(PageDiscord)); + private static string _webhookUrl; + + public static void Configure() + { + _webhookUrl = ServerConfiguration.GetOrUpdateSetting("pages.discordWebhookUrl", null); + + if (IsEnabled) + { + _logger.Information("Relaying pages to discord enabled."); + } + } + + public static bool IsEnabled => !string.IsNullOrEmpty(_webhookUrl); + + public static async ValueTask SendPageNotificationAsync(PageEntry entry) + { + if (!IsEnabled) + { + return; + } + + var pageTypeName = PageQueue.GetPageTypeName(entry.Type); + var locationInfo = $"{entry.PageLocation} ({entry.PageMap?.Name ?? "Unknown"})"; + var fieldsList = new List + { + new { name = "Player", value = entry.Sender?.Name ?? "Unknown", inline = true }, + new { name = "Account", value = entry.Sender?.Account?.Username ?? "Unknown", inline = true }, + new { name = "Page Type", value = pageTypeName, inline = true }, + new { name = "Location", value = locationInfo, inline = true }, + new { name = "Time Sent", value = FormatDiscordTimestamp(entry.Sent), inline = true }, + new { name = "Queue Position", value = $"#{PageQueue.List.Count}", inline = true }, + new { name = "Message", value = TruncateMessage(entry.Message), inline = false } + }; + + if (entry.SpeechLog != null && entry.SpeechLog.Count > 0) + { + fieldsList.Add(new + { + name = "Speech Log Available", + value = $"Contains {entry.SpeechLog.Count} speech entries", + inline = false + }); + } + + var embed = new + { + title = $"New {pageTypeName} Page", + color = GetPageTypeColor(entry.Type), + fields = fieldsList.ToArray(), + footer = new { text = "UO Page System" }, + timestamp = entry.Sent.ToString("o") + }; + + var payload = new + { + username = "UO Pages", + embeds = new[] { embed } + }; + + try + { + _httpClient ??= new HttpClient(); + await _httpClient.PostAsJsonAsync(_webhookUrl, payload); + + _logger.Debug("Discord page notification sent for {Player} - {PageType}", entry.Sender?.Name, pageTypeName); + } + catch (Exception ex) + { + _logger.Error(ex, "Discord page webhook failed for {Player} - {PageType}", + entry.Sender?.Name, entry.Type); + } + } + + public static async ValueTask SendPageHandlerUpdateAsync(PageEntry entry, Mobile oldHandler, Mobile newHandler) + { + if (!IsEnabled || oldHandler != null && newHandler == null && PageQueue.IndexOf(entry) == -1) + { + return; + } + + var pageTypeName = PageQueue.GetPageTypeName(entry.Type); + string actionText; + int color; + + if (oldHandler == null && newHandler != null) + { + actionText = $"🛠️ **{newHandler.Name}** is now handling the page"; + color = 0x00FF00; // green + } + else if (oldHandler != null && newHandler == null) + { + actionText = $"❌ **{oldHandler.Name}** stopped handling the page"; + color = 0xFF0000; // red + } + else + { + return; + } + + var embed = new + { + title = $"{pageTypeName} Page Update", + description = actionText, + color, + fields = new[] + { + new { name = "Player", value = entry.Sender?.Name ?? "Unknown", inline = true }, + new { name = "Page Type", value = pageTypeName, inline = true }, + new { name = "Queue Position", value = $"#{PageQueue.IndexOf(entry) + 1}", inline = true } + }, + footer = new { text = "UO Page System" }, + timestamp = Core.Now.ToString("o") + }; + + var payload = new + { + username = "UO Pages", + embeds = new[] { embed } + }; + + try + { + _httpClient ??= new HttpClient(); + await _httpClient.PostAsJsonAsync(_webhookUrl, payload); + + _logger.Debug("Discord page handler update sent for {Player} - {PageType}", entry.Sender?.Name, pageTypeName); + } + catch (Exception ex) + { + _logger.Error(ex, "Discord page handler update webhook failed for {Player} - {PageType}", + entry.Sender?.Name, entry.Type); + } + } + + public static async ValueTask SendPageCompletedAsync(PageEntry entry) + { + if (!IsEnabled) + { + return; + } + + var pageTypeName = PageQueue.GetPageTypeName(entry.Type); + var handlerName = entry.Handler?.Name ?? "Staff"; + var elapsed = Core.Now - entry.Sent; + var elapsedText = elapsed.TotalMinutes < 1 + ? "< 1 minute" + : $"{elapsed.TotalMinutes:F0} minutes"; + + var embed = new + { + title = $"✅ {pageTypeName} Page Completed", + color = 0x00AA00, // dark green + fields = new[] + { + new { name = "Player", value = entry.Sender?.Name ?? "Unknown", inline = true }, + new { name = "Handled By", value = handlerName, inline = true }, + new { name = "Response Time", value = elapsedText, inline = true } + }, + footer = new { text = "UO Page System" }, + timestamp = Core.Now.ToString("yyyy-MM-ddTHH:mm:ssZ") + }; + + var payload = new + { + username = "UO Pages", + embeds = new[] { embed } + }; + + try + { + _httpClient ??= new HttpClient(); + await _httpClient.PostAsJsonAsync(_webhookUrl, payload); + + _logger.Debug("Discord page completed notification sent for {Player} - {PageType}", + entry.Sender?.Name, pageTypeName); + } + catch (Exception ex) + { + _logger.Error(ex, "Discord page completed webhook failed for {Player} - {PageType}", + entry.Sender?.Name, entry.Type); + } + } + + private static int GetPageTypeColor(PageType type) => type switch + { + PageType.Bug => 0xFF0000, // red + PageType.Stuck => 0xFFFF00, // yellow + PageType.Account => 0x0000FF, // blue + PageType.Question => 0x00FFFF, // cyan + PageType.Suggestion => 0x00FF00, // green + PageType.VerbalHarassment => 0xFF00FF, // magenta + PageType.PhysicalHarassment => 0x800080, // purple + _ => 0x888888 // gray for other + }; + + private static string FormatDiscordTimestamp(DateTime utcTime, char style = 'f') => + $""; + + private static string TruncateMessage(string message) + { + if (string.IsNullOrEmpty(message)) + { + return "*(no message)*"; + } + + const int maxLength = 1000; // apparently the discord field limit is 1024 + return message.Length <= maxLength ? message : $"{message[..(maxLength - 3)]}..."; + } +} diff --git a/Projects/UOContent/Engines/Help/PageQueue.cs b/Projects/UOContent/Engines/Help/PageQueue.cs index ea0c418cd..eda4bb00d 100644 --- a/Projects/UOContent/Engines/Help/PageQueue.cs +++ b/Projects/UOContent/Engines/Help/PageQueue.cs @@ -127,6 +127,7 @@ namespace Server.Engines.Help public static void Configure() { CommandSystem.Register("Pages", AccessLevel.Counselor, Pages_OnCommand); + PageDiscord.Configure(); } public static bool CheckAllowedToPage(Mobile from) @@ -174,6 +175,8 @@ namespace Server.Engines.Help m_KeyedByHandler[value] = entry; } + _ = PageDiscord.SendPageHandlerUpdateAsync(entry, old, value); + if (old == null || value == null) { return; @@ -214,6 +217,11 @@ namespace Server.Engines.Help e.Stop(); + if (e.Handler != null) + { + _ = PageDiscord.SendPageCompletedAsync(e); + } + List.Remove(e); m_KeyedBySender.Remove(e.Sender); @@ -261,7 +269,7 @@ namespace Server.Engines.Help if (!isStaffOnline) { entry.Sender.SendMessage( - "We are sorry, but no staff members are currently available to assist you. Your page will remain in the queue until one becomes available, or until you cancel it manually." + "We are sorry, but no staff members are currently available to assist you. Your page will remain in the queue until one becomes available, or until you cancel it manually." ); } @@ -270,7 +278,9 @@ namespace Server.Engines.Help Email.SendQueueEmail(entry, GetPageTypeName(entry.Type)); } + _ = PageDiscord.SendPageNotificationAsync(entry); + HelpEvents.InvokePageEnqueued(entry); } } -} +} \ No newline at end of file