From 4a7c941a44144d32d65dcb1ad744a6779b9eb5ee Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 9 Apr 2024 21:02:26 +0900 Subject: [PATCH 1/5] feature: webhook request wrapper --- Modules/Webhook/WebhookRequest.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Modules/Webhook/WebhookRequest.cs diff --git a/Modules/Webhook/WebhookRequest.cs b/Modules/Webhook/WebhookRequest.cs new file mode 100644 index 000000000..29ddaff1f --- /dev/null +++ b/Modules/Webhook/WebhookRequest.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace TownOfHost.Modules.Webhook; + +public readonly record struct WebhookRequest( + [property: JsonPropertyName("content")] + string Content, + [property: JsonPropertyName("username")] + string UserName, + [property: JsonPropertyName("avatar_url")] + string AvatarUrl); From 5d2677feb2bd1c0003b4e87019bed19e46742326 Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 9 Apr 2024 21:03:17 +0900 Subject: [PATCH 2/5] feature: webhook builder --- Modules/Webhook/WebhookMessageBuilder.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Modules/Webhook/WebhookMessageBuilder.cs diff --git a/Modules/Webhook/WebhookMessageBuilder.cs b/Modules/Webhook/WebhookMessageBuilder.cs new file mode 100644 index 000000000..b3ee5bfa9 --- /dev/null +++ b/Modules/Webhook/WebhookMessageBuilder.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace TownOfHost.Modules.Webhook; + +public sealed class WebhookMessageBuilder +{ + public StringBuilder ContentBuilder { get; } = new(); + public string UserName { get; init; } = DefaultUserName; + public string AvatarUrl { get; init; } = DefaultAvatarUrl; + + private const string DefaultUserName = "TownOfHost-H"; + private const string DefaultAvatarUrl = "https://raw.githubusercontent.com/Hyz-sui/TownOfHost-H/images-H/Images/discord-avatar.png"; +} From 261731bf40bd49f4cace09c0588b19c83b57c1bc Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 9 Apr 2024 21:16:58 +0900 Subject: [PATCH 3/5] feature: manager --- Modules/Webhook/WebhookManager.cs | 127 ++++++++++++++++++++++++++++++ main.cs | 9 ++- 2 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 Modules/Webhook/WebhookManager.cs diff --git a/Modules/Webhook/WebhookManager.cs b/Modules/Webhook/WebhookManager.cs new file mode 100644 index 000000000..b7e8485cb --- /dev/null +++ b/Modules/Webhook/WebhookManager.cs @@ -0,0 +1,127 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace TownOfHost.Modules.Webhook; + +public sealed class WebhookManager : IDisposable +{ + public static WebhookManager Instance { get; } = new(); + + // see https://discord.com/developers/docs/resources/webhook#execute-webhook + private HttpClient httpClient = new() + { + Timeout = TimeSpan.FromSeconds(4), + }; + + private static readonly ILogHandler logger = Logger.Handler(nameof(WebhookManager)); + private bool disposedValue; + + public void StartSend(WebhookMessageBuilder builder) + { + if (!TryReadUrl(out var url)) + { + logger.Warn("URL設定が正しくありません"); + return; + } + var message = builder.ContentBuilder.ToString(); + var content = new WebhookRequest(message, builder.UserName, builder.AvatarUrl); + var sendTask = SendAsync(content, url); + sendTask.ContinueWith(task => + { + if (task.Exception is { } aggregateException) + { + logger.Warn("送信中に例外が発生しました"); + logger.Exception(aggregateException.InnerException); + } + }); + } + private bool TryReadUrl(out string url) + { + if (CreateConfigFileIfNecessary()) + { + url = null; + return false; + } + using var stream = WebhookUrlFile.OpenRead(); + using var reader = new StreamReader(stream, Encoding.UTF8); + var text = reader.ReadLine(); + if (ValidateUrl(text)) + { + url = text; + return true; + } + else + { + url = null; + return false; + } + } + public bool CreateConfigFileIfNecessary() + { + if (WebhookUrlFile.Exists) + { + return false; + } + using var stream = WebhookUrlFile.Create(); + using var writer = new StreamWriter(stream, Encoding.UTF8); + writer.WriteLine("この文章をすべて削除してウェブフックのURLを入力し,上書き保存してください"); + return true; + } + private bool ValidateUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return false; + } + return webhookUrlRegex.IsMatch(url); + } + public async Task SendAsync(WebhookRequest webhookRequest, string url, CancellationToken cancellationToken = default) + { + try + { + var response = await httpClient.PostAsJsonAsync(url, webhookRequest, cancellationToken); + logger.Info($"{(int)response.StatusCode} {response.ReasonPhrase}"); + if (!response.IsSuccessStatusCode) + { + logger.Warn("送信に失敗"); + } + } + catch (TaskCanceledException taskCanceledException) + { + logger.Warn("送信はキャンセルされました"); + logger.Exception(taskCanceledException); + } + } + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + httpClient.Dispose(); + } + disposedValue = true; + } + } + public void Dispose() + { + // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public FileInfo WebhookUrlFile { get; } = +#if DEBUG + new("DebugWebhookUrl.txt"); +#else + new("WebhookUrl.txt"); +#endif + private readonly Regex webhookUrlRegex = new("^(https://(ptb.|canary.)?discord(app)?.com/api/webhooks/)"); +} diff --git a/main.cs b/main.cs index bf1f1e62a..86a005dbb 100644 --- a/main.cs +++ b/main.cs @@ -16,6 +16,7 @@ using TownOfHost.Attributes; using TownOfHost.Modules; using TownOfHost.Roles.Core; +using TownOfHost.Modules.Webhook; [assembly: AssemblyFileVersionAttribute(TownOfHost.Main.PluginVersion)] [assembly: AssemblyInformationalVersionAttribute(TownOfHost.Main.PluginVersion)] @@ -241,13 +242,17 @@ public override void Load() ClassInjector.RegisterTypeInIl2Cpp(); - if (!File.Exists("WebhookUrl.txt")) - Utils.MakeWebhookUrlFile(); + WebhookManager.Instance.CreateConfigFileIfNecessary(); SystemEnvironment.SetEnvironmentVariables(); Harmony.PatchAll(); } + public override bool Unload() + { + WebhookManager.Instance.Dispose(); + return false; + } } public enum CustomDeathReason { From 62a2d583c22ce3add9e0fd15d0d4da67d14c68c7 Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 9 Apr 2024 21:18:50 +0900 Subject: [PATCH 4/5] migrate to new webhook impl --- Patches/OutroPatch.cs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/Patches/OutroPatch.cs b/Patches/OutroPatch.cs index ac4ae2987..e118c83f5 100644 --- a/Patches/OutroPatch.cs +++ b/Patches/OutroPatch.cs @@ -12,6 +12,7 @@ using static TownOfHost.Translator; using TownOfHost.Modules.GameEventHistory; using TownOfHost.Modules.GameEventHistory.Events; +using TownOfHost.Modules.Webhook; namespace TownOfHost { @@ -257,26 +258,42 @@ public static void Postfix(EndGameManager __instance) if (PlayerControl.LocalPlayer.PlayerId == 0) { if (CustomWinnerHolder.WinnerTeam == CustomWinner.Draw) + { Logger.Info("廃村のため試合結果の送信をキャンセル", "Webhook"); + } else { + var resultMessageBuilder = new WebhookMessageBuilder() + { + UserName = "試合結果", + }; if (Main.SendResultToDiscord.Value) { - var resultMessage = ""; + resultMessageBuilder.ContentBuilder.AppendLine("### 各プレイヤーの最終結果"); foreach (var id in Main.winnerList) { - resultMessage += Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead) + ":star:" + EndGamePatch.SummaryText[id].RemoveHtmlTags() + "\n"; + resultMessageBuilder.ContentBuilder.Append(Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead)); + resultMessageBuilder.ContentBuilder.Append(":star:"); + resultMessageBuilder.ContentBuilder.Append(EndGamePatch.SummaryText[id].RemoveHtmlTags()); + resultMessageBuilder.ContentBuilder.AppendLine(); } foreach (var id in cloneRoles) { - resultMessage += Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead) + "\u3000" + EndGamePatch.SummaryText[id].RemoveHtmlTags() + "\n"; + resultMessageBuilder.ContentBuilder.Append(Utils.ColorIdToDiscordEmoji(Palette.PlayerColors.IndexOf(Main.PlayerColors[id]), !PlayerState.GetByPlayerId(id).IsDead)); + resultMessageBuilder.ContentBuilder.Append('\u3000'); + resultMessageBuilder.ContentBuilder.Append(EndGamePatch.SummaryText[id].RemoveHtmlTags()); + resultMessageBuilder.ContentBuilder.AppendLine(); } - Utils.SendWebhook(resultMessage, GetString("LastResult")); } if (Main.SendHistoryToDiscord.Value) { - var historyMessage = EventHistory.CurrentInstance.ToDiscordString(); - Utils.SendWebhook(historyMessage, "ゲーム記録"); + resultMessageBuilder.ContentBuilder.AppendLine("### 記録"); + EventHistory.CurrentInstance.AppendDiscordString(resultMessageBuilder.ContentBuilder); + } + + if (resultMessageBuilder.ContentBuilder.Length > 0) + { + WebhookManager.Instance.StartSend(resultMessageBuilder); } } } From 72b63b02f791b90c91730bd774af4d3b351abcea Mon Sep 17 00:00:00 2001 From: Hyz-sui <86903430+Hyz-sui@users.noreply.github.com> Date: Tue, 9 Apr 2024 21:19:47 +0900 Subject: [PATCH 5/5] remove legacy methods --- Modules/Utils.cs | 44 -------------------------------------------- 1 file changed, 44 deletions(-) diff --git a/Modules/Utils.cs b/Modules/Utils.cs index 4e3a6f30f..4c3018da3 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -1130,50 +1130,6 @@ public static void FlashColor(Color color, float duration = 1f) obj.GetComponent().color = new(color.r, color.g, color.b, Mathf.Clamp01((-2f * Mathf.Abs(t - 0.5f) + 1) * color.a)); //アルファ値を0→目標→0に変化させる }))); } - public static void MakeWebhookUrlFile() - { - Logger.Info("WebhookUrl.txtを作成", "Webhook"); - try - { - File.WriteAllText("WebhookUrl.txt", "この文章を削除してウェブフックのURLを記述/Remove this text and enter Webhook url"); - } - catch (Exception ex) - { - Logger.Error(ex.ToString(), "Webhook"); - } - } - public static void SendWebhook(string text, string userName = "Town Of Host") - { - if (!File.Exists("WebhookUrl.txt")) - MakeWebhookUrlFile(); - HttpClient client = new(); - Dictionary message = new() - { - { "content", text }, - { "username", userName }, - { "avatar_url", "https://raw.githubusercontent.com/Hyz-sui/TownOfHost-H/images-H/Images/discord-avatar.png" } - }; - using StreamReader sr = new("WebhookUrl.txt", Encoding.UTF8); - string webhookUrl = sr.ReadLine(); - if (!Regex.IsMatch(webhookUrl, "^(https://(ptb.|canary.)?discord(app)?.com/api/webhooks/)")) // ptbとcanaryとappはあってもなくてもいい - { - Logger.Info("WebhookUrl.txtの内容がdiscordのウェブフックurlではなかったためウェブフックの送信をキャンセル", "Webhook"); - return; - } - try - { - TaskAwaiter awaiter = client.PostAsync(webhookUrl, new FormUrlEncodedContent(message)).GetAwaiter(); - var response = awaiter.GetResult(); - Logger.Info("ウェブフックを送信しました", "Webhook"); - if (!response.IsSuccessStatusCode) - Logger.Warn("応答が異常です", "Webhook"); - Logger.Info($"{(int)response.StatusCode} {response.ReasonPhrase}", "Webhook"); // 正常な応答: 204 No Content - } - catch (Exception ex) - { - Logger.Error(ex.ToString(), "Webhook"); - } - } public static string ColorIdToDiscordEmoji(int colorId, bool alive) { if (alive)