From b0ec868172ebb962cf36b438950dbad714fe5212 Mon Sep 17 00:00:00 2001 From: KMohZaid <68484509+KMohZaid@users.noreply.github.com> Date: Sun, 15 Jun 2025 18:20:01 -0700 Subject: [PATCH 1/5] add(topic support) && update(sql schema) 1. changes TelegramId datatype in `werewolf.sql` to `bigint` to fix issue 2. added `/setgrouptopic` command which admin can use to set group for werewolf bot 3. added new column in `dbo.Group` table : `GroupTopicId` to store topic id Note : I may have forget some places to update and make topic support TODOs : 1. maybe ignore command in other topics when topic id is set 2. add command to unset topic id, if group admin want to disable topics in group and use in normaly way 3. Remove redundant direct call to Telegram send API and make single method that send normal message or document based on flag parameter such as `msg_type: [TEXT,DOC,GIF]` 4. A database migrator system, so we can avoid manual database backup and restore --- Werewolf for Telegram/Database/Group.cs | 1 + .../Database/WerewolfModel.Designer.cs | 2 +- .../Database/WerewolfModel.edmx | 5 +++- .../Commands/AdminCommands.cs | 29 ++++++++++++++++++ .../Commands/GeneralCommands.cs | 6 ++-- .../Werewolf Control/Commands/GifCommands.cs | 22 +++++++------- .../Werewolf Control/Commands/Helpers.cs | 4 +-- .../Handlers/UpdateHandler.cs | 1 + .../Werewolf Control/Helpers/Bot.cs | 14 +++++++++ .../Helpers/LanguageHelper.cs | 2 +- .../Werewolf Node/Program.cs | 22 +++++++++++--- .../Werewolf Node/Werewolf.cs | 30 +++++++++++-------- werewolf.sql | 7 +++-- 13 files changed, 106 insertions(+), 39 deletions(-) diff --git a/Werewolf for Telegram/Database/Group.cs b/Werewolf for Telegram/Database/Group.cs index deaf32e80..0ad60f324 100644 --- a/Werewolf for Telegram/Database/Group.cs +++ b/Werewolf for Telegram/Database/Group.cs @@ -25,6 +25,7 @@ public Group() public int Id { get; set; } public string Name { get; set; } public long GroupId { get; set; } + public Nullable GroupTopicId { get; set; } public Nullable Preferred { get; set; } public string Language { get; set; } public Nullable DisableNotification { get; set; } diff --git a/Werewolf for Telegram/Database/WerewolfModel.Designer.cs b/Werewolf for Telegram/Database/WerewolfModel.Designer.cs index 31386a04d..6b6a0f4be 100644 --- a/Werewolf for Telegram/Database/WerewolfModel.Designer.cs +++ b/Werewolf for Telegram/Database/WerewolfModel.Designer.cs @@ -1,4 +1,4 @@ -// T4 code generation is enabled for model 'C:\Users\parab\Source\Repos\Werewolf\Werewolf for Telegram\Database\WerewolfModel.edmx'. +// T4 code generation is enabled for model 'C:\Users\waifu\Desktop\Werewolf\Werewolf for Telegram\Database\WerewolfModel.edmx'. // To enable legacy code generation, change the value of the 'Code Generation Strategy' designer // property to 'Legacy ObjectContext'. This property is available in the Properties Window when the model // is open in the designer. diff --git a/Werewolf for Telegram/Database/WerewolfModel.edmx b/Werewolf for Telegram/Database/WerewolfModel.edmx index 74ef243a4..d360ef315 100644 --- a/Werewolf for Telegram/Database/WerewolfModel.edmx +++ b/Werewolf for Telegram/Database/WerewolfModel.edmx @@ -178,6 +178,7 @@ + @@ -884,6 +885,7 @@ warning 6002: The table/view 'werewolf.dbo.v_IdleKill24HoursMain' does not have + @@ -1664,6 +1666,7 @@ warning 6002: The table/view 'werewolf.dbo.v_IdleKill24HoursMain' does not have + @@ -2176,4 +2179,4 @@ warning 6002: The table/view 'werewolf.dbo.v_IdleKill24HoursMain' does not have - \ No newline at end of file + diff --git a/Werewolf for Telegram/Werewolf Control/Commands/AdminCommands.cs b/Werewolf for Telegram/Werewolf Control/Commands/AdminCommands.cs index 2674466ad..9b2b90aa8 100644 --- a/Werewolf for Telegram/Werewolf Control/Commands/AdminCommands.cs +++ b/Werewolf for Telegram/Werewolf Control/Commands/AdminCommands.cs @@ -23,6 +23,35 @@ namespace Werewolf_Control { public static partial class Commands { + [Attributes.Command(Trigger = "setgrouptopic", GroupAdminOnly = true, InGroupOnly = true)] + public static void SetGroupTopic(Update update, string[] args) + { + var chatId = update.Message.Chat.Id; + var topicId = update.Message.MessageThreadId; + + if (topicId == null) + { + Bot.Api.SendTextMessageAsync(chatId, "This command must be used inside a topic/thread (not inside default general topic).", messageThreadId: topicId); + return; + } + + using (var db = new WWContext()) + { + var group = db.Groups.FirstOrDefault(g => g.GroupId == chatId); + + if (group == null) + { + group = MakeDefaultGroup(chatId, update.Message.Chat.Title, "setgrouptopic"); + db.Groups.Add(group); + } + + group.GroupTopicId = topicId; + db.SaveChanges(); + } + + Bot.Api.SendTextMessageAsync(chatId, "Group topic has been set successfully.", messageThreadId: topicId); + } + [Attributes.Command(Trigger = "smite", GroupAdminOnly = true, Blockable = true, InGroupOnly = true, AllowAnonymousAdmins = true)] public static void Smite(Update u, string[] args) { diff --git a/Werewolf for Telegram/Werewolf Control/Commands/GeneralCommands.cs b/Werewolf for Telegram/Werewolf Control/Commands/GeneralCommands.cs index c2f975894..6f40ebedf 100644 --- a/Werewolf for Telegram/Werewolf Control/Commands/GeneralCommands.cs +++ b/Werewolf for Telegram/Werewolf Control/Commands/GeneralCommands.cs @@ -179,9 +179,9 @@ public static void SetLang(Update update, string[] args) var curLangFileName = GetLanguage(update.Message.From.Id); var curLang = langs.First(x => x.FileName == curLangFileName); Bot.Api.SendTextMessageAsync(chatId: update.Message.From.Id, text: GetLocaleString("WhatLang", curLangFileName, curLang.Base), - replyMarkup: menu); + replyMarkup: menu, messageThreadId: update.Message.MessageThreadId); if (update.Message.Chat.Type != ChatType.Private) - Send(GetLocaleString("SentPrivate", GetLanguage(update.Message.From.Id)), update.Message.Chat.Id); + Send(GetLocaleString("SentPrivate", GetLanguage(update.Message.From.Id)), update.Message.Chat.Id, messageThreadId: update.Message.MessageThreadId); } [Command(Trigger = "start")] @@ -581,7 +581,7 @@ public static void MyIdles(Update update, string[] args) try { - var result = Bot.Api.SendTextMessageAsync(chatId: update.Message.From.Id, text: reply).Result; + var result = Bot.Api.SendTextMessageAsync(chatId: update.Message.From.Id, text: reply, messageThreadId: update.Message.MessageThreadId).Result; if (update.Message.Chat.Type != ChatType.Private) Send(GetLocaleString("SentPrivate", GetLanguage(update.Message.From.Id)), update.Message.Chat.Id); } diff --git a/Werewolf for Telegram/Werewolf Control/Commands/GifCommands.cs b/Werewolf for Telegram/Werewolf Control/Commands/GifCommands.cs index 0493f4fd4..a4a7ed403 100644 --- a/Werewolf for Telegram/Werewolf Control/Commands/GifCommands.cs +++ b/Werewolf for Telegram/Werewolf Control/Commands/GifCommands.cs @@ -28,7 +28,7 @@ public static void Donate(Update u, string[] args) { // Donations disabled as of 2024-06-08 var link = $"currently disabled"; - Bot.Api.SendTextMessageAsync(chatId: u.Message.Chat.Id, text: $"Donations are {link}, sorry!", parseMode: ParseMode.Html, disableWebPagePreview: true).Wait(); + Bot.Api.SendTextMessageAsync(chatId: u.Message.Chat.Id, text: $"Donations are {link}, sorry!", parseMode: ParseMode.Html, disableWebPagePreview: true, messageThreadId: u.Message.MessageThreadId).Wait(); return; //Bot.Api.SendTextMessageAsync(u.Message.Chat.Id, @@ -106,7 +106,7 @@ public static void SetCustomGifs(Update u, string[] args) "\n\n" + "PLEASE NOTE: Changing any gifs will automatically remove the approval for your pack, and an admin will need to approve it again\n" + "Let's begin! Select the situation you want to set a gif for", - replyMarkup: GetGifMenu(data)); + replyMarkup: GetGifMenu(data), messageThreadId: u.Message.MessageThreadId); var msg = "Current Approval Status:\n"; switch (data.Approved) @@ -123,7 +123,7 @@ public static void SetCustomGifs(Update u, string[] args) msg += "Disapproved By " + dby.Name + " for: " + data.DenyReason; break; } - Bot.Send(msg, u.Message.From.Id); + Bot.Send(msg, u.Message.From.Id, messageThreadId: u.Message.MessageThreadId); } } @@ -304,7 +304,7 @@ public static void RequestGif(CallbackQuery q) Bot.Api.SendTextMessageAsync(chatId: q.From.Id, text: q.Data.Split('|')[1] + "\nOk, send me the GIF you want to use for this situation, as a reply\n" + "#" + choice, - replyMarkup: new ForceReplyMarkup()); + replyMarkup: new ForceReplyMarkup(), messageThreadId: q.Message.MessageThreadId); } public static void AddGif(Message m) @@ -337,14 +337,14 @@ public static void AddGif(Message m) "users are unable to view them, we require you to use telegram's " + "[new GIFs in .mp4 format](https://telegram.org/blog/gif-revolution). " + "To fix this, try reuploading the GIF, your telegram app should then render it as .mp4. " + - "Please send me the GIF you want to use for this situation, as a reply\n#" + gifchoice, replyMarkup: new ForceReplyMarkup(), parseMode: ParseMode.Markdown); + "Please send me the GIF you want to use for this situation, as a reply\n#" + gifchoice, replyMarkup: new ForceReplyMarkup(), parseMode: ParseMode.Markdown, messageThreadId: m.MessageThreadId); return; } if (m.Animation.FileSize >= 1048576) // Maximum size is 1 MB { Bot.Api.SendTextMessageAsync(chatId: m.From.Id, text: "This GIF is too large, the maximum allowed size is 1MB.\n\n" + "Please send me the GIF you want to use for this situation, as a reply\n#" + gifchoice, - replyMarkup: new ForceReplyMarkup()); + replyMarkup: new ForceReplyMarkup(), messageThreadId: m.MessageThreadId); return; } @@ -431,7 +431,7 @@ public static void GetXsollaLink(CallbackQuery q = null, Message m = null) { // Donations disabled as of 2024-06-08 var link = $"currently disabled"; - Bot.Api.SendTextMessageAsync(chatId: (q?.Message ?? m).Chat.Id, text: $"Donations are {link}, sorry!", parseMode: ParseMode.Html, disableWebPagePreview: true).Wait(); + Bot.Api.SendTextMessageAsync(chatId: (q?.Message ?? m).Chat.Id, text: $"Donations are {link}, sorry!", parseMode: ParseMode.Html, disableWebPagePreview: true, messageThreadId: (q?.Message ?? m).MessageThreadId).Wait(); return; var from = q?.From ?? m?.From; @@ -465,13 +465,13 @@ public static void GetDonationInfo(CallbackQuery q = null, Message m = null) { // Donations disabled as of 2024-06-08 var link = $"currently disabled"; - Bot.Api.SendTextMessageAsync(chatId: q?.From.Id ?? m.From.Id, text: $"Donations are {link}, sorry!", parseMode: ParseMode.Html, disableWebPagePreview: true).Wait(); + Bot.Api.SendTextMessageAsync(chatId: q?.From.Id ?? m.From.Id, text: $"Donations are {link}, sorry!", parseMode: ParseMode.Html, disableWebPagePreview: true, messageThreadId: q.Message.MessageThreadId).Wait(); return; var menu = new Menu(); Bot.Api.SendTextMessageAsync(chatId: q?.From.Id ?? m.From.Id, text: "How much would you like to donate? Please enter a whole number, in US Dollars (USD), in reply to this message", - replyMarkup: new ForceReplyMarkup()); + replyMarkup: new ForceReplyMarkup(), messageThreadId: (q?.Message ?? m).MessageThreadId); } public static void ValidateDonationAmount(Message m) @@ -488,14 +488,14 @@ public static void ValidateDonationAmount(Message m) var api = RegHelper.GetRegValue("MainStripeProdAPI"); #endif Bot.Api.SendInvoiceAsync(chatId: m.From.Id, title: "Werewolf Donation", description: "Make a donation to Werewolf to help keep us online", payload: "somepayloadtest", providerToken: api, - currency: "USD", prices: new[] { new LabeledPrice("Donation", amt * 100) }, startParameter: "donatetg").Wait(); + currency: "USD", prices: new[] { new LabeledPrice("Donation", amt * 100) }, startParameter: "donatetg", messageThreadId: m.MessageThreadId).Wait(); } else { Bot.Api.SendTextMessageAsync(chatId: m.From.Id, text: "Invalid input.\n" + "How much would you like to donate? Please enter a whole number, in US Dollars (USD), in reply to this message", - replyMarkup: new ForceReplyMarkup()); + replyMarkup: new ForceReplyMarkup(), messageThreadId: m.MessageThreadId); } } diff --git a/Werewolf for Telegram/Werewolf Control/Commands/Helpers.cs b/Werewolf for Telegram/Werewolf Control/Commands/Helpers.cs index 2d1995379..7a98c31b6 100644 --- a/Werewolf for Telegram/Werewolf Control/Commands/Helpers.cs +++ b/Werewolf for Telegram/Werewolf Control/Commands/Helpers.cs @@ -170,9 +170,9 @@ private static void StartGame(GameMode gameMode, Update update) } } - internal static Task Send(string message, long id, bool clearKeyboard = false, InlineKeyboardMarkup customMenu = null) + internal static Task Send(string message, long id, bool clearKeyboard = false, InlineKeyboardMarkup customMenu = null, Nullable messageThreadId = null) { - return Bot.Send(message, id, clearKeyboard, customMenu); + return Bot.Send(message, id, clearKeyboard, customMenu, messageThreadId: messageThreadId); } diff --git a/Werewolf for Telegram/Werewolf Control/Handlers/UpdateHandler.cs b/Werewolf for Telegram/Werewolf Control/Handlers/UpdateHandler.cs index 82d4ed353..a54e17d10 100644 --- a/Werewolf for Telegram/Werewolf Control/Handlers/UpdateHandler.cs +++ b/Werewolf for Telegram/Werewolf Control/Handlers/UpdateHandler.cs @@ -824,6 +824,7 @@ internal static void HandleCallback(CallbackQuery query) var id = query.From.Id; Send($"Sending gifs for {pid}", id); Thread.Sleep(1000); + // INFO: dumping gifs, no need of topic send (this is mostly done in pm...) Bot.Api.SendDocumentAsync(chatId: id, document: new InputFileId(pack.CultWins), caption: "Cult Wins"); Bot.Api.SendDocumentAsync(chatId: id, document: new InputFileId(pack.LoversWin), caption: "Lovers Win"); Thread.Sleep(250); diff --git a/Werewolf for Telegram/Werewolf Control/Helpers/Bot.cs b/Werewolf for Telegram/Werewolf Control/Helpers/Bot.cs index 055ab05ca..716e551cf 100644 --- a/Werewolf for Telegram/Werewolf Control/Helpers/Bot.cs +++ b/Werewolf for Telegram/Werewolf Control/Helpers/Bot.cs @@ -513,6 +513,20 @@ internal static Task Send(string message, long id, bool clearKeyboard = { //MessagesSent++; //message = message.Replace("`",@"\`"); + + // Try to load GroupTopicId from the database if no thread ID is provided + if (messageThreadId == null) + { + using (var db = new WWContext()) + { + var group = db.Groups.FirstOrDefault(g => g.GroupId == id); + if (group?.GroupTopicId != null) + { + messageThreadId = group.GroupTopicId; + } + } + } + if (clearKeyboard) { //var menu = new ReplyKeyboardRemove() { RemoveKeyboard = true }; diff --git a/Werewolf for Telegram/Werewolf Control/Helpers/LanguageHelper.cs b/Werewolf for Telegram/Werewolf Control/Helpers/LanguageHelper.cs index b1a3f0d4f..ddb0f07a3 100644 --- a/Werewolf for Telegram/Werewolf Control/Helpers/LanguageHelper.cs +++ b/Werewolf for Telegram/Werewolf Control/Helpers/LanguageHelper.cs @@ -109,7 +109,7 @@ public static void ValidateFiles(long id, int msgId, int? messageThreadId, strin result += "\n"; } - Bot.Api.SendTextMessageAsync(chatId: id, text: result, parseMode: ParseMode.Markdown); + Bot.Api.SendTextMessageAsync(chatId: id, text: result, parseMode: ParseMode.Markdown, messageThreadId: messageThreadId); var sortedfiles = Directory.GetFiles(Bot.LanguageDirectory).Select(x => new LangFile(x)).Where(x => x.Base == (choice ?? x.Base)).OrderBy(x => x.LatestUpdate); result = $"*Validation complete*\nErrors: {errors.Count(x => x.Level == ErrorLevel.Error)}\nMissing strings: {errors.Count(x => x.Level == ErrorLevel.MissingString)}"; result += $"\nMost recently updated file: {sortedfiles.Last().FileName}.xml ({sortedfiles.Last().LatestUpdate.ToString("MMM dd")})\nLeast recently updated file: {sortedfiles.First().FileName}.xml ({sortedfiles.First().LatestUpdate.ToString("MMM dd")})"; diff --git a/Werewolf for Telegram/Werewolf Node/Program.cs b/Werewolf for Telegram/Werewolf Node/Program.cs index 9aec7ff3a..89d53773c 100644 --- a/Werewolf for Telegram/Werewolf Node/Program.cs +++ b/Werewolf for Telegram/Werewolf Node/Program.cs @@ -380,23 +380,37 @@ public static void RemoveGame(Werewolf werewolf) } } - internal static async Task Send(string message, long id, bool clearKeyboard = false, InlineKeyboardMarkup customMenu = null, Werewolf game = null, bool notify = false, bool preview = false) + internal static async Task Send(string message, long id, bool clearKeyboard = false, InlineKeyboardMarkup customMenu = null, Werewolf game = null, bool notify = false, bool preview = false, bool isPlayerDM = false) { //MessagesSent++; //message = message.FormatHTML(); //message = message.Replace("`",@"\`"); + + // Try to load GroupTopicId from the database, only if it is not player dm + int? messageThreadId = null; + if(!isPlayerDM) + using (var db = new WWContext()) + { + var group = db.Groups.FirstOrDefault(g => g.GroupId == id); + if (group?.GroupTopicId != null) + { + messageThreadId = group.GroupTopicId; + } + } + + if (clearKeyboard) { var menu = new ReplyKeyboardRemove(); - return await Bot.SendTextMessageAsync(chatId: id, text: message, replyMarkup: menu, disableWebPagePreview: !preview, parseMode: ParseMode.Html, disableNotification: notify); + return await Bot.SendTextMessageAsync(chatId: id, text: message, replyMarkup: menu, disableWebPagePreview: !preview, parseMode: ParseMode.Html, disableNotification: notify, messageThreadId: messageThreadId); } else if (customMenu != null) { - return await Bot.SendTextMessageAsync(chatId: id, text: message, replyMarkup: customMenu, disableWebPagePreview: !preview, parseMode: ParseMode.Html, disableNotification: notify); + return await Bot.SendTextMessageAsync(chatId: id, text: message, replyMarkup: customMenu, disableWebPagePreview: !preview, parseMode: ParseMode.Html, disableNotification: notify, messageThreadId: messageThreadId); } else { - return await Bot.SendTextMessageAsync(chatId: id, text: message, disableWebPagePreview: !preview, parseMode: ParseMode.Html, disableNotification: notify); + return await Bot.SendTextMessageAsync(chatId: id, text: message, disableWebPagePreview: !preview, parseMode: ParseMode.Html, disableNotification: notify, messageThreadId: messageThreadId); } } diff --git a/Werewolf for Telegram/Werewolf Node/Werewolf.cs b/Werewolf for Telegram/Werewolf Node/Werewolf.cs index bb9c57fcd..783330337 100644 --- a/Werewolf for Telegram/Werewolf Node/Werewolf.cs +++ b/Werewolf for Telegram/Werewolf Node/Werewolf.cs @@ -231,9 +231,9 @@ public Werewolf(long chatid, User u, string chatGroup, GameMode gameMode) case GameMode.Chaos: FirstMessage = GetLocaleString("PlayerStartedChaosGame", u.FirstName); #if RELEASE - _joinMsgId = Program.Bot.SendDocumentAsync(chatId: ChatId, document: new InputFileId(GetRandomImage(StartChaosGame)), caption: FirstMessage, replyMarkup: _joinButton).Result.MessageId; + _joinMsgId = Program.Bot.SendDocumentAsync(chatId: ChatId, document: new InputFileId(GetRandomImage(StartChaosGame)), caption: FirstMessage, replyMarkup: _joinButton, messageThreadId: DbGroup.GroupTopicId).Result.MessageId; #else - _joinMsgId = Program.Bot.SendTextMessageAsync(chatId: chatid, text: $"\u200C{FirstMessage.FormatHTML()}", replyMarkup: _joinButton, parseMode: ParseMode.Html).Result.MessageId; + _joinMsgId = Program.Bot.SendTextMessageAsync(chatId: chatid, text: $"\u200C{FirstMessage.FormatHTML()}", replyMarkup: _joinButton, parseMode: ParseMode.Html, messageThreadId: DbGroup.GroupTopicId).Result.MessageId; #endif break; @@ -241,9 +241,9 @@ public Werewolf(long chatid, User u, string chatGroup, GameMode gameMode) default: FirstMessage = GetLocaleString("PlayerStartedGame", u.FirstName); #if RELEASE - _joinMsgId = Program.Bot.SendDocumentAsync(chatId: ChatId, document: new InputFileId(GetRandomImage(StartGame)), caption: FirstMessage, replyMarkup: _joinButton).Result.MessageId; + _joinMsgId = Program.Bot.SendDocumentAsync(chatId: ChatId, document: new InputFileId(GetRandomImage(StartGame)), caption: FirstMessage, replyMarkup: _joinButton, messageThreadId: DbGroup.GroupTopicId).Result.MessageId; #else - _joinMsgId = Program.Bot.SendTextMessageAsync(chatId: chatid, text: $"\u200C{FirstMessage.FormatHTML()}", replyMarkup: _joinButton, parseMode: ParseMode.Html).Result.MessageId; + _joinMsgId = Program.Bot.SendTextMessageAsync(chatId: chatid, text: $"\u200C{FirstMessage.FormatHTML()}", replyMarkup: _joinButton, parseMode: ParseMode.Html, messageThreadId: DbGroup.GroupTopicId).Result.MessageId; #endif break; } @@ -469,7 +469,7 @@ private void GameTimer() if (i == Settings.GameJoinTime - s) { var str = s == 60 ? GetLocaleString("MinuteLeftToJoin") : GetLocaleString("SecondsLeftToJoin", s.ToString().ToBold()); - r = Program.Bot.SendTextMessageAsync(chatId: ChatId, text: str, parseMode: ParseMode.Html, replyMarkup: _joinButton).Result; + r = Program.Bot.SendTextMessageAsync(chatId: ChatId, text: str, parseMode: ParseMode.Html, replyMarkup: _joinButton, messageThreadId: DbGroup.GroupTopicId).Result; break; } } @@ -485,7 +485,7 @@ private void GameTimer() _secondsToAdd > 0 ? "SecondsAdded" : "SecondsRemoved", Math.Abs(_secondsToAdd).ToString().ToBold(), TimeSpan.FromSeconds(Settings.GameJoinTime - i).ToString(@"mm\:ss").ToBold() - ), parseMode: ParseMode.Html, replyMarkup: _joinButton + ), parseMode: ParseMode.Html, replyMarkup: _joinButton, messageThreadId: DbGroup.GroupTopicId ).Result; _secondsToAdd = 0; @@ -1162,28 +1162,32 @@ public void HandleReply(CallbackQuery query) } } - private Task Send(string message, long id = 0, bool clearKeyboard = false, InlineKeyboardMarkup menu = null, bool notify = false, bool preview = false) + private Task Send(string message, long id = 0, bool clearKeyboard = false, InlineKeyboardMarkup menu = null, bool notify = false, bool preview = false, bool isPlayerDM = false) { if (id == 0) id = ChatId; - return Program.Send(message, id, clearKeyboard, menu, game: this, notify: notify, preview: preview); + return Program.Send(message, id, clearKeyboard, menu, game: this, notify: notify, preview: preview, isPlayerDM: isPlayerDM); } private void SendGif(string text, string image, long id = 0) { //Program.MessagesSent++; + bool isPlayerDM = false; if (id == 0) id = ChatId; + else + isPlayerDM = true; + //Log.WriteLine($"{id} -> {image} {text}"); if (!String.IsNullOrWhiteSpace(image)) #if RELEASE - Program.Bot.SendDocumentAsync(chatId: id, document: new InputFileId(image), caption: text); + Program.Bot.SendDocumentAsync(chatId: id, document: new InputFileId(image), caption: text, messageThreadId: DbGroup.GroupTopicId); #else - Send($"\u200C{text}", id, preview: true); + Send($"\u200C{text}", id, preview: true, isPlayerDM: isPlayerDM ); #endif else - Send(text, id, preview: false); + Send(text, id, preview: false, isPlayerDM: isPlayerDM); } private void SendWithQueue(string text, string gif = null, bool requestPM = false) @@ -1421,7 +1425,7 @@ public void OutputPlayers() LastPlayersOutput = DateTime.Now; try { - Program.Bot.SendTextMessageAsync(chatId: ChatId, text: GetLocaleString(_playerListId != 0 ? "LatestList" : "UnableToGetList"), parseMode: ParseMode.Html, replyToMessageId: _playerListId); + Program.Bot.SendTextMessageAsync(chatId: ChatId, text: GetLocaleString(_playerListId != 0 ? "LatestList" : "UnableToGetList"), parseMode: ParseMode.Html, replyToMessageId: _playerListId, messageThreadId: DbGroup.GroupTopicId); } catch { } } @@ -1433,7 +1437,7 @@ public async void ShowJoinButton() LastJoinButtonShowed = DateTime.Now; try { - var r = await Program.Bot.SendTextMessageAsync(chatId: ChatId, text: GetLocaleString("JoinByButton"), parseMode: ParseMode.Html, replyMarkup: _joinButton); + var r = await Program.Bot.SendTextMessageAsync(chatId: ChatId, text: GetLocaleString("JoinByButton"), parseMode: ParseMode.Html, replyMarkup: _joinButton, messageThreadId: DbGroup.GroupTopicId); _joinButtons.Add(r.MessageId); } catch diff --git a/werewolf.sql b/werewolf.sql index c5a350413..d468682b8 100644 --- a/werewolf.sql +++ b/werewolf.sql @@ -123,7 +123,7 @@ GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [db_owner].[ContestTerms]( - [TelegramId] [int] NOT NULL, + [TelegramId] [bigint] NOT NULL, [AgreedTerms] [bit] NOT NULL, CONSTRAINT [PK_ContestTerms] PRIMARY KEY CLUSTERED ( @@ -138,7 +138,7 @@ SET QUOTED_IDENTIFIER ON GO CREATE TABLE [db_owner].[GlobalBan]( [Id] [int] IDENTITY(1,1) NOT NULL, - [TelegramId] [int] NOT NULL, + [TelegramId] [bigint] NOT NULL, [Reason] [nvarchar](max) NOT NULL, [Expires] [datetime] NOT NULL, [BannedBy] [nvarchar](max) NOT NULL, @@ -381,6 +381,7 @@ CREATE TABLE [dbo].[Group]( [Id] [int] IDENTITY(1,1) NOT NULL, [Name] [nvarchar](max) NOT NULL, [GroupId] [bigint] NOT NULL, + [GroupTopicId] [int] NULL, [Preferred] [bit] NULL, [Language] [nvarchar](max) NULL, [DisableNotification] [bit] NULL, @@ -536,7 +537,7 @@ SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[Player]( [Id] [int] IDENTITY(1,1) NOT NULL, - [TelegramId] [int] NOT NULL, + [TelegramId] [bigint] NOT NULL, [Name] [nvarchar](max) NOT NULL, [UserName] [nvarchar](max) NULL, [Banned] [bit] NULL, From 8abf556f22ed8e84bf24f006896b988bd260cc2c Mon Sep 17 00:00:00 2001 From: KMohZaid <68484509+KMohZaid@users.noreply.github.com> Date: Tue, 17 Jun 2025 18:06:03 -0700 Subject: [PATCH 2/5] fix(/setgrouptopic): complete TODO 1 and 2, used button for set and unset. TODO 1 and 2 are unset featue and restrict to specified topic --- .../Werewolf Control/Attributes/Command.cs | 5 + .../Commands/AdminCommands.cs | 103 ++++++++++++++++-- .../Werewolf Control/Commands/GameCommands.cs | 3 +- .../Handlers/UpdateHandler.cs | 31 ++++++ .../Werewolf Control/Helpers/Bot.cs | 5 +- .../Werewolf Control/Models/Command.cs | 1 + 6 files changed, 133 insertions(+), 15 deletions(-) diff --git a/Werewolf for Telegram/Werewolf Control/Attributes/Command.cs b/Werewolf for Telegram/Werewolf Control/Attributes/Command.cs index 9c897e3ef..dfb91e115 100644 --- a/Werewolf for Telegram/Werewolf Control/Attributes/Command.cs +++ b/Werewolf for Telegram/Werewolf Control/Attributes/Command.cs @@ -44,5 +44,10 @@ public class Command : Attribute /// Can this command be run by anonymous admins in groups /// public bool AllowAnonymousAdmins { get; set; } = false; + + /// + /// Allow commands to be run outside configured topic in group. + /// + public bool AllowOutsideConfiguredTopic {get; set; } = false; } } diff --git a/Werewolf for Telegram/Werewolf Control/Commands/AdminCommands.cs b/Werewolf for Telegram/Werewolf Control/Commands/AdminCommands.cs index 9b2b90aa8..0358843f1 100644 --- a/Werewolf for Telegram/Werewolf Control/Commands/AdminCommands.cs +++ b/Werewolf for Telegram/Werewolf Control/Commands/AdminCommands.cs @@ -1,4 +1,4 @@ -using Database; +using Database; using Newtonsoft.Json; using System; using System.Collections; @@ -26,32 +26,111 @@ public static partial class Commands [Attributes.Command(Trigger = "setgrouptopic", GroupAdminOnly = true, InGroupOnly = true)] public static void SetGroupTopic(Update update, string[] args) { - var chatId = update.Message.Chat.Id; - var topicId = update.Message.MessageThreadId; + long chatId = update.Message.Chat.Id; + int? topicId = update.Message.MessageThreadId; - if (topicId == null) + int? currentTopicId; + + using (var db = new WWContext()) { - Bot.Api.SendTextMessageAsync(chatId, "This command must be used inside a topic/thread (not inside default general topic).", messageThreadId: topicId); - return; + currentTopicId = db.Groups + .Where(g => g.GroupId == chatId) + .Select(g => g.GroupTopicId) + .FirstOrDefault(); + } + + bool inGeneralTopic = topicId == null || topicId == 0; + + string infoText = $"Current Topic ID: `{(currentTopicId.HasValue ? currentTopicId.Value.ToString() : "None")}`\n\n"; + + if (inGeneralTopic) + { + infoText += "This message is in the general topic (no thread ID).\n" + + "You cannot set this as the group topic. Only specific threads can be set."; + } + else + { + infoText += $"> You are currently inside topic ID: `{topicId}`.\n" + + "Choose what you'd like to do:"; } + var buttons = new List(); + + if (!inGeneralTopic) + { + buttons.Add(new[]{ + InlineKeyboardButton.WithCallbackData("Set Topic", "setgrouptopic_cmd|set"), + }); + } + + buttons.Add(new[]{ + InlineKeyboardButton.WithCallbackData("Unset Topic", "setgrouptopic_cmd|unset") + }); + + + var inlineKeyboard = new InlineKeyboardMarkup(buttons); + + Bot.Send( + infoText, + chatId, + customMenu: inlineKeyboard, + parseMode: ParseMode.Markdown, + messageThreadId: topicId, + forceTopic: false // TODO: use this param in other admin cmd, cmd which admin are allowed to use anywhere + ); + } + + internal static void SetGroupTopicCallback(CallbackQuery query) + { + var chatId = query.Message.Chat.Id; + var topicId = query.Message.MessageThreadId; + using (var db = new WWContext()) { var group = db.Groups.FirstOrDefault(g => g.GroupId == chatId); - if (group == null) { - group = MakeDefaultGroup(chatId, update.Message.Chat.Title, "setgrouptopic"); + group = MakeDefaultGroup(chatId, query.Message.Chat.Title, "setgrouptopic_callback"); db.Groups.Add(group); } - group.GroupTopicId = topicId; - db.SaveChanges(); - } + string[] args = query.Data.Split('|'); + string action = args.Length > 1 ? args[1] : ""; + + switch (action) + { + case "set": + if (topicId == null) + { + Bot.ReplyToCallback(query, "This must be used inside a topic/thread."); + return; + } + + group.GroupTopicId = topicId; + db.SaveChanges(); + + Bot.Api.EditMessageTextAsync(chatId, query.Message.MessageId, + "Group topic has been set successfully."); + break; + + case "unset": + group.GroupTopicId = null; + db.SaveChanges(); - Bot.Api.SendTextMessageAsync(chatId, "Group topic has been set successfully.", messageThreadId: topicId); + Bot.Api.EditMessageTextAsync(chatId, query.Message.MessageId, + "Group topic has been unset."); + break; + + default: + Bot.ReplyToCallback(query, "Invalid topic action."); + break; + } + + Bot.Api.AnswerCallbackQueryAsync(query.Id); + } } + [Attributes.Command(Trigger = "smite", GroupAdminOnly = true, Blockable = true, InGroupOnly = true, AllowAnonymousAdmins = true)] public static void Smite(Update u, string[] args) { diff --git a/Werewolf for Telegram/Werewolf Control/Commands/GameCommands.cs b/Werewolf for Telegram/Werewolf Control/Commands/GameCommands.cs index 6a2bea184..3d72e7e1e 100644 --- a/Werewolf for Telegram/Werewolf Control/Commands/GameCommands.cs +++ b/Werewolf for Telegram/Werewolf Control/Commands/GameCommands.cs @@ -186,7 +186,8 @@ public static void Extend(Update update, string[] args) } } - [Command(Trigger = "stopwaiting", Blockable = true)] + // allowing outside topic, as it doesn't response into topic command was ran. dm user that their next game notification is turned off + [Command(Trigger = "stopwaiting", Blockable = true, AllowOutsideConfiguredTopic = true)] public static void StopWaiting(Update update, string[] args) { long groupid = 0; diff --git a/Werewolf for Telegram/Werewolf Control/Handlers/UpdateHandler.cs b/Werewolf for Telegram/Werewolf Control/Handlers/UpdateHandler.cs index a54e17d10..8ca805da0 100644 --- a/Werewolf for Telegram/Werewolf Control/Handlers/UpdateHandler.cs +++ b/Werewolf for Telegram/Werewolf Control/Handlers/UpdateHandler.cs @@ -438,6 +438,31 @@ internal static void HandleUpdate(Update update) id); return; } + // If command is not allow outside topic, or it is not admin / dev command. Check topic id and restrict to topic. + // Kmoh COOKED : (Oops... i forgot another thing that i had to write down. It was in my mind a second ago...) + if (!(command.AllowOutsideConfiguredTopic || command.GlobalAdminOnly || command.LangAdminOnly || command.GroupAdminOnly || command.DevOnly) ) + { + // Only apply this restriction in groups (topics are only meaningful in groups) + if (update.Message.Chat.Type == ChatType.Group || update.Message.Chat.Type == ChatType.Supergroup) + { + int? currentTopicId; + using (var db = new WWContext()) + { + currentTopicId = db.Groups + .Where(g => g.GroupId == id) + .Select(g => g.GroupTopicId) + .FirstOrDefault(); + } + + // If a specific topic is set for the group and this message is NOT in that topic + if (currentTopicId == null || currentTopicId != update.Message.MessageThreadId) + { + Bot.Send($"This command can only be used in the configured topic/thread. (Topic id : {currentTopicId})", id,messageThreadId: update.Message.MessageThreadId, forceTopic : false); + return; + } + } + } + Bot.CommandsReceived++; command.Method.Invoke(update, args); } @@ -791,6 +816,12 @@ internal static void HandleCallback(CallbackQuery query) return; } + if (args[0] == "setgrouptopic_cmd") + { + Commands.SetGroupTopicCallback(query); + return; + } + //first off, if it's a game, send it to the node. if (args[0] == "vote") { diff --git a/Werewolf for Telegram/Werewolf Control/Helpers/Bot.cs b/Werewolf for Telegram/Werewolf Control/Helpers/Bot.cs index 716e551cf..264feeab7 100644 --- a/Werewolf for Telegram/Werewolf Control/Helpers/Bot.cs +++ b/Werewolf for Telegram/Werewolf Control/Helpers/Bot.cs @@ -102,6 +102,7 @@ public static void Initialize(string updateid = null) c.InGroupOnly = ca.InGroupOnly; c.LangAdminOnly = ca.LangAdminOnly; c.AllowAnonymousAdmins = ca.AllowAnonymousAdmins; + c.AllowOutsideConfiguredTopic = ca.AllowOutsideConfiguredTopic; Commands.Add(c); } } @@ -509,13 +510,13 @@ public static Node GetBestAvailableNode() } - internal static Task Send(string message, long id, bool clearKeyboard = false, InlineKeyboardMarkup customMenu = null, ParseMode parseMode = ParseMode.Html, int? messageThreadId = null) + internal static Task Send(string message, long id, bool clearKeyboard = false, InlineKeyboardMarkup customMenu = null, ParseMode parseMode = ParseMode.Html, int? messageThreadId = null, bool forceTopic = true) { //MessagesSent++; //message = message.Replace("`",@"\`"); // Try to load GroupTopicId from the database if no thread ID is provided - if (messageThreadId == null) + if (messageThreadId == null && forceTopic) { using (var db = new WWContext()) { diff --git a/Werewolf for Telegram/Werewolf Control/Models/Command.cs b/Werewolf for Telegram/Werewolf Control/Models/Command.cs index effbf14e1..6b78de584 100644 --- a/Werewolf for Telegram/Werewolf Control/Models/Command.cs +++ b/Werewolf for Telegram/Werewolf Control/Models/Command.cs @@ -19,5 +19,6 @@ class Command public bool InGroupOnly { get; set; } public bool LangAdminOnly { get; set; } public bool AllowAnonymousAdmins { get; set; } + public bool AllowOutsideConfiguredTopic { get; set; } } } From 97cfe9e5e693372f2f41c428fb0e5713acc95379 Mon Sep 17 00:00:00 2001 From: KMohZaid <68484509+KMohZaid@users.noreply.github.com> Date: Tue, 17 Jun 2025 18:28:51 -0700 Subject: [PATCH 3/5] feat(auto detects unreadable name): tell user if there name is unreadable, allow them to force join with warning --- .../Commands/GeneralCommands.cs | 44 ++++++++ .../Werewolf Control/Commands/Helpers.cs | 100 +++++++++++++++++- .../Werewolf Control/Program.cs | 5 + 3 files changed, 146 insertions(+), 3 deletions(-) diff --git a/Werewolf for Telegram/Werewolf Control/Commands/GeneralCommands.cs b/Werewolf for Telegram/Werewolf Control/Commands/GeneralCommands.cs index 6f40ebedf..89a89a8a6 100644 --- a/Werewolf for Telegram/Werewolf Control/Commands/GeneralCommands.cs +++ b/Werewolf for Telegram/Werewolf Control/Commands/GeneralCommands.cs @@ -239,8 +239,44 @@ public static void Start(Update u, string[] args) return; } + // check for force join + bool isForceJoin = false; + if (args[1].StartsWith("forcejoin")) + { + isForceJoin = true; + // remove "force" from starting, so below if statement work for force join also + args[1] = args[1].Substring("force".Length); + } + + // handle join command if (args[1].StartsWith("join") && args[1].Length == 48) // 4 "join" + 22 node id + 22 game id { + // check if user have validate name + bool isValid; + string msg; + (isValid, msg) = ValidatePlayerName(p.Name); + if(!isValid && !isForceJoin) + { + + + // TODO: move to string xml file for translation support + string warningMsg = "⚠️ If you join the game using the button below, you can join successfully, " + + "but you may receive a warning if your name is unreadable or invalid."; + + + string forceJoinURI = $"https://t.me/{Bot.Me.Username}/?start={"force" + args[1]}"; + + // send user messaage + Bot.Send(msg+"\n\n"+warningMsg, u.Message.Chat.Id, customMenu: new InlineKeyboardMarkup(new[] + { + new[] + { + InlineKeyboardButton.WithUrl("Force Join Game", forceJoinURI) + } + })); + return; + } + //okay, they are joining a game. string nodeid = ""; string gameid = ""; @@ -340,6 +376,14 @@ public static void Start(Update u, string[] args) } game.AddPlayer(u, gameid); + + // notify group about force join + if (isForceJoin) { + // TODO: move to strings xml for translation support + string forceJoinNotice = $"⚠️ Player with name '[{p.Name}](tg://user?id={p.TelegramId})' used force join button, their name is detected as unreadable by bot."; + + Bot.Send(forceJoinNotice, game.GroupId, parseMode: ParseMode.Markdown); + } return; } catch (AggregateException e) diff --git a/Werewolf for Telegram/Werewolf Control/Commands/Helpers.cs b/Werewolf for Telegram/Werewolf Control/Commands/Helpers.cs index 7a98c31b6..d8dd03cf0 100644 --- a/Werewolf for Telegram/Werewolf Control/Commands/Helpers.cs +++ b/Werewolf for Telegram/Werewolf Control/Commands/Helpers.cs @@ -1,20 +1,21 @@ -using Shared; +using Database; +using Shared; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; -using Database; +using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.ReplyMarkups; using Werewolf_Control.Handler; using Werewolf_Control.Helpers; using Werewolf_Control.Models; -using Telegram.Bot; #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed namespace Werewolf_Control { @@ -33,6 +34,99 @@ public static partial class Commands }; #endif + internal static void TestPlayerNameValidation() + { + // Fix: enable Unicode output for fancy letters and emoji + Console.OutputEncoding = System.Text.Encoding.UTF8; + + var testCases = new List<(string Name, bool Expected, string Description)> + { + ("John", true, "Latin letters"), + ("𝕁𝕠𝕙𝕟", true, "Stylized Latin (math bold)"), + ("ABC", true, "Full-width Latin"), + ("李", false, "Single CJK (non-Latin) character"), + ("😎😎", false, "Emoji only"), + (" ", false, "Whitespace only"), + ("A", false, "Only one Latin letter"), + ("AB", true, "Two Latin letters"), + ("ㅤㅤㅤ", false, "Hangul fillers (invisible)"), + ("⌘ KMohZaid ⌘", true, "Latin letters surrounded by symbols"), + ("AswatthamA", true, "Regular Latin name"), + ("Cuenta Eliminada", true, "Spanish Latin name"), + ("ဂွေးဂျိနက်မမ", false, "Burmese script (non-Latin)"), + ("Jasmine", true, "Regular Latin name"), + ("Van", true, "Short but valid Latin name"), + ("", false, "Empty string"), + ("ℛ𝒶𝓃𝒶𝒹 ℬℯ𝓃 ℛ𝒶𝓂𝒶𝒹𝒶𝓃", true, "Fancy Unicode Latin") + }; + + foreach (var (name, expected, description) in testCases) + { + bool actual; + string msg; + (actual, msg) = Commands.ValidatePlayerName(name); + + Console.WriteLine($"Name: \"{name}\""); + Console.WriteLine($"Description: {description}"); + Console.WriteLine($"Message: {msg}"); + Console.WriteLine($"Expected: {(expected ? "Valid" : "Invalid")}, Actual: {(actual ? "Valid" : "Invalid")}"); + + if (actual == expected) + { + Console.WriteLine("✅ TEST PASS"); + } + else + { + Console.WriteLine("❌ TEST FAIL"); + } + + Console.WriteLine(new string('-', 50)); + } + + Console.WriteLine(new string('-', 50)); + Console.WriteLine("PRESSING ENTER WILL START ACTUAL BOT RUN, AFTER WHICH YOU WILL SEE MESSED UP BOT STATUS MESSAGE."); + Console.WriteLine(new string('-', 50)); + Console.ReadLine(); // this is to stop program from overriding output screen with running bot status message + } + + internal static (bool, string) ValidatePlayerName(string name) + { + + + // TODO: move to string xml file, for translation support + string blankMsg = "❌ Your name appears to be blank. Please set a readable name in your Telegram profile and try again."; + string noEnoughReadableChar = "❌ Your name must contain at least 2 readable letters. Please update your Telegram name."; + + if (string.IsNullOrWhiteSpace(name)) + { + return (false, blankMsg); + } + + var trimmedName = name.Trim(); + var textElements = StringInfo.GetTextElementEnumerator(trimmedName); + + int letterCount = 0; + + while (textElements.MoveNext()) + { + string element = textElements.GetTextElement(); + var category = CharUnicodeInfo.GetUnicodeCategory(element, 0); + + if (category == UnicodeCategory.UppercaseLetter || + category == UnicodeCategory.LowercaseLetter) + { + letterCount++; + } + } + + if (letterCount < 2) + { + return (false,noEnoughReadableChar); + } + + return (true,"Valid."); + } + private static Player GetDBPlayer(long id, WWContext db) { return db.Players.FirstOrDefault(x => x.TelegramId == id); diff --git a/Werewolf for Telegram/Werewolf Control/Program.cs b/Werewolf for Telegram/Werewolf Control/Program.cs index fff203a85..27f86eedf 100644 --- a/Werewolf for Telegram/Werewolf Control/Program.cs +++ b/Werewolf for Telegram/Werewolf Control/Program.cs @@ -43,8 +43,13 @@ class Program internal static readonly HttpClient xsollaClient = new HttpClient(); internal const string MasterLanguage = "English.xml"; internal static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + + static void Main(string[] args) { + // INFO: uncomment below method call for testing. Some names are already there for testing purpose. + //Commands.TestPlayerNameValidation(); + System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12; #if !DEBUG From 1607531ec9a8717ec5d5515e0a28fe60e7a0ab40 Mon Sep 17 00:00:00 2001 From: KMohZaid <68484509+KMohZaid@users.noreply.github.com> Date: Tue, 17 Jun 2025 18:31:46 -0700 Subject: [PATCH 4/5] clean(error spam when node fails to conenct to controller): now it will print readable message without spamming error --- .../Werewolf Node/Program.cs | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/Werewolf for Telegram/Werewolf Node/Program.cs b/Werewolf for Telegram/Werewolf Node/Program.cs index 89d53773c..646ae4c5c 100644 --- a/Werewolf for Telegram/Werewolf Node/Program.cs +++ b/Werewolf for Telegram/Werewolf Node/Program.cs @@ -1,16 +1,18 @@ -using System; +using Database; +using Microsoft.Win32; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Dynamic; using System.IO; using System.Linq; +using System.Net.Sockets; using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Timers; using System.Xml.Linq; -using Microsoft.Win32; -using Newtonsoft.Json; using TcpFramework; using Telegram.Bot; using Telegram.Bot.Types; @@ -441,6 +443,8 @@ internal static void Connect() Client.DataReceived += ClientOnDataReceived; Client.DelimiterDataReceived += ClientOnDelimiterDataReceived; //connection lost, let's try to reconnect + bool hasLoggedRefusedMessage = false; + while (Client.TcpClient == null || !Client.TcpClient.Connected) { try @@ -449,15 +453,34 @@ internal static void Connect() var regInfo = new ClientRegistrationInfo { ClientId = ClientId }; var json = JsonConvert.SerializeObject(regInfo); Client.WriteLine(json); + + Console.WriteLine($"Connected to {Settings.ServerIP}:{Settings.Port}"); } catch (Exception ex) { - while (ex.InnerException != null) - ex = ex.InnerException; - Console.WriteLine($"Error in reconnect: {ex.Message}\n{ex.StackTrace}\n"); + // Dig to root exception + Exception root = ex; + while (root.InnerException != null) + root = root.InnerException; + + if (root is SocketException sockEx && sockEx.SocketErrorCode == SocketError.ConnectionRefused) + { + if (!hasLoggedRefusedMessage) + { + Console.WriteLine($"Waiting for connection at {Settings.ServerIP}:{Settings.Port}... (connection refused — control process might not be started yet)"); + hasLoggedRefusedMessage = true; + } + } + else + { + // Print full error if it's not a connection refused case + Console.WriteLine($"Error in reconnect: {root.Message}\n{root.StackTrace}\n"); + } } + Thread.Sleep(100); } + } public static void KeepAlive() From 4c04706984d345c734da0377c7b92cb73131a77d Mon Sep 17 00:00:00 2001 From: KMohZaid <68484509+KMohZaid@users.noreply.github.com> Date: Tue, 17 Jun 2025 18:34:44 -0700 Subject: [PATCH 5/5] .gitattributes set default line ending to CRLF (as i use linux also, which make it LF) --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 1ff0c4230..beac7a1d1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,7 @@ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### -* text=auto +* text=auto eol=crlf ############################################################################### # Set default behavior for command prompt diff.