diff --git a/.gitattributes b/.gitattributes index 1ff0c423..beac7a1d 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. diff --git a/Werewolf for Telegram/Database/Group.cs b/Werewolf for Telegram/Database/Group.cs index deaf32e8..0ad60f32 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 31386a04..6b6a0f4b 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 74ef243a..d360ef31 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/Attributes/Command.cs b/Werewolf for Telegram/Werewolf Control/Attributes/Command.cs index 9c897e3e..dfb91e11 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 2674466a..0358843f 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; @@ -23,6 +23,114 @@ namespace Werewolf_Control { public static partial class Commands { + [Attributes.Command(Trigger = "setgrouptopic", GroupAdminOnly = true, InGroupOnly = true)] + public static void SetGroupTopic(Update update, string[] args) + { + long chatId = update.Message.Chat.Id; + int? topicId = update.Message.MessageThreadId; + + int? currentTopicId; + + using (var db = new WWContext()) + { + 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, query.Message.Chat.Title, "setgrouptopic_callback"); + db.Groups.Add(group); + } + + 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.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 6a2bea18..3d72e7e1 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/Commands/GeneralCommands.cs b/Werewolf for Telegram/Werewolf Control/Commands/GeneralCommands.cs index c2f97589..89a89a8a 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")] @@ -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) @@ -581,7 +625,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 0493f4fd..a4a7ed40 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 2d199537..d8dd03cf 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); @@ -170,9 +264,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 82d4ed35..8ca805da 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") { @@ -824,6 +855,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 055ab05c..264feeab 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,10 +510,24 @@ 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 && forceTopic) + { + 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 b1a3f0d4..ddb0f07a 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 Control/Models/Command.cs b/Werewolf for Telegram/Werewolf Control/Models/Command.cs index effbf14e..6b78de58 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; } } } diff --git a/Werewolf for Telegram/Werewolf Control/Program.cs b/Werewolf for Telegram/Werewolf Control/Program.cs index fff203a8..27f86eed 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 diff --git a/Werewolf for Telegram/Werewolf Node/Program.cs b/Werewolf for Telegram/Werewolf Node/Program.cs index 9aec7ff3..646ae4c5 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; @@ -380,23 +382,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); } } @@ -427,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 @@ -435,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() diff --git a/Werewolf for Telegram/Werewolf Node/Werewolf.cs b/Werewolf for Telegram/Werewolf Node/Werewolf.cs index bb9c57fc..78333033 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 c5a35041..d468682b 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,