using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; namespace MinecraftClient.ChatBots { /// /// ChatBot for storing and delivering Mails /// public class Mailer : ChatBot { /// /// Holds the list of ignored players /// private class IgnoreList : HashSet { /// /// Read ignore list from file /// /// Path to the ignore list /// Ignore list public static IgnoreList FromFile(string filePath) { IgnoreList ignoreList = new(); foreach (string line in FileMonitor.ReadAllLinesWithRetries(filePath)) { if (!line.StartsWith("#")) { string entry = line.ToLower(); if (!ignoreList.Contains(entry)) ignoreList.Add(entry); } } return ignoreList; } /// /// Save ignore list to file /// /// Path to destination file public void SaveToFile(string filePath) { List lines = new(); lines.Add("#Ignored Players"); foreach (string player in this) lines.Add(player); FileMonitor.WriteAllLinesWithRetries(filePath, lines); } } /// /// Holds the Mail database: a collection of Mails sent from a player to another player /// private class MailDatabase : List { /// /// Read mail database from file /// /// Path to the database /// Mail database public static MailDatabase FromFile(string filePath) { MailDatabase database = new(); Dictionary> iniFileDict = INIFile.ParseFile(FileMonitor.ReadAllLinesWithRetries(filePath)); foreach (KeyValuePair> iniSection in iniFileDict) { //iniSection.Key is "mailXX" but we don't need it here string sender = iniSection.Value["sender"]; string recipient = iniSection.Value["recipient"]; string content = iniSection.Value["content"]; DateTime timestamp = DateTime.Parse(iniSection.Value["timestamp"]); bool anonymous = INIFile.Str2Bool(iniSection.Value["anonymous"]); database.Add(new Mail(sender, recipient, content, anonymous, timestamp)); } return database; } /// /// Save mail database to file /// /// Path to destination file public void SaveToFile(string filePath) { Dictionary> iniFileDict = new(); int mailCount = 0; foreach (Mail mail in this) { mailCount++; Dictionary iniSection = new() { #pragma warning disable format // @formatter:off ["sender"] = mail.Sender, ["recipient"] = mail.Recipient, ["content"] = mail.Content, ["timestamp"] = mail.DateSent.ToString(), ["anonymous"] = mail.Anonymous.ToString() #pragma warning restore format // @formatter:on }; iniFileDict["mail" + mailCount] = iniSection; } FileMonitor.WriteAllLinesWithRetries(filePath, INIFile.Generate(iniFileDict, "Mail Database")); } } /// /// Represents a Mail sent from a player to another player /// private class Mail { private readonly string sender; private readonly string senderLower; private readonly string recipient; private readonly string recipientLower; private readonly string message; private readonly DateTime datesent; private bool delivered; private readonly bool anonymous; public Mail(string sender, string recipient, string message, bool anonymous, DateTime datesent) { this.sender = sender; senderLower = sender.ToLower(); this.recipient = recipient; recipientLower = recipient.ToLower(); this.message = message; this.datesent = datesent; delivered = false; this.anonymous = anonymous; } public string Sender { get { return sender; } } public string SenderLowercase { get { return senderLower; } } public string Recipient { get { return recipient; } } public string RecipientLowercase { get { return recipientLower; } } public string Content { get { return message; } } public DateTime DateSent { get { return datesent; } } public bool Delivered => delivered; public bool Anonymous { get { return anonymous; } } public void SetDelivered() { delivered = true; } public override string ToString() { return String.Format("{0} {1} {2} {3}", Sender, Recipient, Content, DateSent); } } // Internal variables private int maxMessageLength = 0; private DateTime nextMailSend = DateTime.Now; private MailDatabase mailDatabase = new(); private IgnoreList ignoreList = new(); private FileMonitor? mailDbFileMonitor; private FileMonitor? ignoreListFileMonitor; private readonly object readWriteLock = new(); /// /// Initialization of the Mailer bot /// public override void Initialize() { LogDebugToConsoleTranslated("bot.mailer.init"); LogDebugToConsoleTranslated("bot.mailer.init.db" + Settings.Mailer_DatabaseFile); LogDebugToConsoleTranslated("bot.mailer.init.ignore" + Settings.Mailer_IgnoreListFile); LogDebugToConsoleTranslated("bot.mailer.init.public" + Settings.Mailer_PublicInteractions); LogDebugToConsoleTranslated("bot.mailer.init.max_mails" + Settings.Mailer_MaxMailsPerPlayer); LogDebugToConsoleTranslated("bot.mailer.init.db_size" + Settings.Mailer_MaxDatabaseSize); LogDebugToConsoleTranslated("bot.mailer.init.mail_retention" + Settings.Mailer_MailRetentionDays + " days"); if (Settings.Mailer_MaxDatabaseSize <= 0) { LogToConsoleTranslated("bot.mailer.init_fail.db_size"); UnloadBot(); return; } if (Settings.Mailer_MaxMailsPerPlayer <= 0) { LogToConsoleTranslated("bot.mailer.init_fail.max_mails"); UnloadBot(); return; } if (Settings.Mailer_MailRetentionDays <= 0) { LogToConsoleTranslated("bot.mailer.init_fail.mail_retention"); UnloadBot(); return; } if (!File.Exists(Settings.Mailer_DatabaseFile)) { LogToConsoleTranslated("bot.mailer.create.db", Path.GetFullPath(Settings.Mailer_DatabaseFile)); new MailDatabase().SaveToFile(Settings.Mailer_DatabaseFile); } if (!File.Exists(Settings.Mailer_IgnoreListFile)) { LogToConsoleTranslated("bot.mailer.create.ignore", Path.GetFullPath(Settings.Mailer_IgnoreListFile)); new IgnoreList().SaveToFile(Settings.Mailer_IgnoreListFile); } lock (readWriteLock) { LogDebugToConsoleTranslated("bot.mailer.load.db", Path.GetFullPath(Settings.Mailer_DatabaseFile)); mailDatabase = MailDatabase.FromFile(Settings.Mailer_DatabaseFile); LogDebugToConsoleTranslated("bot.mailer.load.ignore", Path.GetFullPath(Settings.Mailer_IgnoreListFile)); ignoreList = IgnoreList.FromFile(Settings.Mailer_IgnoreListFile); } //Initialize file monitors. In case the bot needs to unload for some reason in the future, do not forget to .Dispose() them mailDbFileMonitor = new FileMonitor(Path.GetDirectoryName(Settings.Mailer_DatabaseFile)!, Path.GetFileName(Settings.Mailer_DatabaseFile), FileMonitorCallback); ignoreListFileMonitor = new FileMonitor(Path.GetDirectoryName(Settings.Mailer_IgnoreListFile)!, Path.GetFileName(Settings.Mailer_IgnoreListFile), FileMonitorCallback); RegisterChatBotCommand("mailer", Translations.Get("bot.mailer.cmd"), "mailer ", ProcessInternalCommand); } /// /// Standard settings for the bot. /// public override void AfterGameJoined() { maxMessageLength = GetMaxChatMessageLength() - 44 // Deduct length of "/ 16CharPlayerName 16CharPlayerName mailed: " - Settings.PrivateMsgsCmdName.Length; // Deduct length of "tell" command } /// /// Process chat messages from the server /// public override void GetText(string text) { string message = ""; string username = ""; text = GetVerbatim(text); if (IsPrivateMessage(text, ref message, ref username) || (Settings.Mailer_PublicInteractions && IsChatMessage(text, ref message, ref username))) { string usernameLower = username.ToLower(); if (!ignoreList.Contains(usernameLower)) { string command = message.Split(' ')[0].ToLower(); switch (command) { case "mail": case "tellonym": if (usernameLower != GetUsername().ToLower() && mailDatabase.Count < Settings.Mailer_MaxDatabaseSize && mailDatabase.Where(mail => mail.SenderLowercase == usernameLower).Count() < Settings.Mailer_MaxMailsPerPlayer) { Queue args = new(Command.GetArgs(message)); if (args.Count >= 2) { bool anonymous = (command == "tellonym"); string recipient = args.Dequeue(); message = string.Join(" ", args); if (IsValidName(recipient)) { if (message.Length <= maxMessageLength) { Mail mail = new(username, recipient, message, anonymous, DateTime.Now); LogToConsoleTranslated("bot.mailer.saving", mail.ToString()); lock (readWriteLock) { mailDatabase.Add(mail); mailDatabase.SaveToFile(Settings.Mailer_DatabaseFile); } SendPrivateMessage(username, "Message saved!"); } else SendPrivateMessage(username, "Your message cannot be longer than " + maxMessageLength + " characters."); } else SendPrivateMessage(username, "Recipient '" + recipient + "' is not a valid player name."); } else SendPrivateMessage(username, "Usage: " + command + " "); } else SendPrivateMessage(username, "Couldn't save Message. Limit reached!"); break; } } else LogDebugToConsoleTranslated("bot.mailer.user_ignored", username); } } /// /// Called on each MCC tick, around 10 times per second /// public override void Update() { DateTime dateNow = DateTime.Now; if (nextMailSend < dateNow) { LogDebugToConsoleTranslated("bot.mailer.process_mails", DateTime.Now); // Process at most 3 mails at a time to avoid spamming. Other mails will be processed on next mail send HashSet onlinePlayersLowercase = new(GetOnlinePlayers().Select(name => name.ToLower())); foreach (Mail mail in mailDatabase.Where(mail => !mail.Delivered && onlinePlayersLowercase.Contains(mail.RecipientLowercase)).Take(3)) { string sender = mail.Anonymous ? "Anonymous" : mail.Sender; SendPrivateMessage(mail.Recipient, sender + " mailed: " + mail.Content); mail.SetDelivered(); LogDebugToConsoleTranslated("bot.mailer.delivered", mail.ToString()); } lock (readWriteLock) { mailDatabase.RemoveAll(mail => mail.Delivered); mailDatabase.RemoveAll(mail => mail.DateSent.AddDays(Settings.Mailer_MailRetentionDays) < DateTime.Now); mailDatabase.SaveToFile(Settings.Mailer_DatabaseFile); } nextMailSend = dateNow.AddSeconds(10); } } /// /// Called when the Mail Database or Ignore list has changed on disk /// /// /// private void FileMonitorCallback(object sender, FileSystemEventArgs e) { lock (readWriteLock) { mailDatabase = MailDatabase.FromFile(Settings.Mailer_DatabaseFile); ignoreList = IgnoreList.FromFile(Settings.Mailer_IgnoreListFile); } } /// /// Interprets local commands. /// private string ProcessInternalCommand(string cmd, string[] args) { if (args.Length > 0) { string commandName = args[0].ToLower(); switch (commandName) { case "getmails": // Sorry, I (ReinforceZwei) replaced "=" to "-" because it would affect the parsing of translation file (key=value) return Translations.Get("bot.mailer.cmd.getmails", string.Join("\n", mailDatabase)); case "getignored": return Translations.Get("bot.mailer.cmd.getignored", string.Join("\n", ignoreList)); case "addignored": case "removeignored": if (args.Length > 1 && IsValidName(args[1])) { string username = args[1].ToLower(); if (commandName == "addignored") { lock (readWriteLock) { if (!ignoreList.Contains(username)) { ignoreList.Add(username); ignoreList.SaveToFile(Settings.Mailer_IgnoreListFile); } } return Translations.Get("bot.mailer.cmd.ignore.added", args[1]); } else { lock (readWriteLock) { if (ignoreList.Contains(username)) { ignoreList.Remove(username); ignoreList.SaveToFile(Settings.Mailer_IgnoreListFile); } } return Translations.Get("bot.mailer.cmd.ignore.removed", args[1]); } } else return Translations.Get("bot.mailer.cmd.ignore.invalid", commandName); } } return Translations.Get("bot.mailer.cmd.help") + ": /help mailer"; } } }