using System; using System.Data; using System.IO; using System.Linq; using System.Collections.Generic; 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 IgnoreList(); 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 List(); 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 MailDatabase(); 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 Dictionary>(); int mailCount = 0; foreach (Mail mail in this) { mailCount++; Dictionary iniSection = new Dictionary(); iniSection["sender"] = mail.Sender; iniSection["recipient"] = mail.Recipient; iniSection["content"] = mail.Content; iniSection["timestamp"] = mail.DateSent.ToString(); iniSection["anonymous"] = mail.Anonymous.ToString(); 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 string sender; private string senderLower; private string recipient; private string recipientLower; private string message; private DateTime datesent; private bool delivered; private bool anonymous; public Mail(string sender, string recipient, string message, bool anonymous, DateTime datesent) { this.sender = sender; this.senderLower = sender.ToLower(); this.recipient = recipient; this.recipientLower = recipient.ToLower(); this.message = message; this.datesent = datesent; this.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 { get { return 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 MailDatabase(); private IgnoreList ignoreList = new IgnoreList(); private FileMonitor mailDbFileMonitor; private FileMonitor ignoreListFileMonitor; private object readWriteLock = new object(); /// /// Initialization of the Mailer bot /// public override void Initialize() { LogDebugToConsole("Initializing Mailer with settings:"); LogDebugToConsole(" - Database File: " + Settings.Mailer_DatabaseFile); LogDebugToConsole(" - Ignore List: " + Settings.Mailer_IgnoreListFile); LogDebugToConsole(" - Public Interactions: " + Settings.Mailer_PublicInteractions); LogDebugToConsole(" - Max Mails per Player: " + Settings.Mailer_MaxMailsPerPlayer); LogDebugToConsole(" - Max Database Size: " + Settings.Mailer_MaxDatabaseSize); LogDebugToConsole(" - Mail Retention: " + Settings.Mailer_MailRetentionDays + " days"); if (Settings.Mailer_MaxDatabaseSize <= 0) { LogToConsole("Cannot enable Mailer: Max Database Size must be greater than zero. Please review the settings."); UnloadBot(); return; } if (Settings.Mailer_MaxMailsPerPlayer <= 0) { LogToConsole("Cannot enable Mailer: Max Mails per Player must be greater than zero. Please review the settings."); UnloadBot(); return; } if (Settings.Mailer_MailRetentionDays <= 0) { LogToConsole("Cannot enable Mailer: Mail Retention must be greater than zero. Please review the settings."); UnloadBot(); return; } if (!File.Exists(Settings.Mailer_DatabaseFile)) { LogToConsole("Creating new database file: " + Path.GetFullPath(Settings.Mailer_DatabaseFile)); new MailDatabase().SaveToFile(Settings.Mailer_DatabaseFile); } if (!File.Exists(Settings.Mailer_IgnoreListFile)) { LogToConsole("Creating new ignore list: " + Path.GetFullPath(Settings.Mailer_IgnoreListFile)); new IgnoreList().SaveToFile(Settings.Mailer_IgnoreListFile); } lock (readWriteLock) { LogDebugToConsole("Loading database file: " + Path.GetFullPath(Settings.Mailer_DatabaseFile)); mailDatabase = MailDatabase.FromFile(Settings.Mailer_DatabaseFile); LogDebugToConsole("Loading ignore list: " + 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", "Subcommands: getmails, addignored, getignored, removeignored", 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 Queue(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 Mail(username, recipient, message, anonymous, DateTime.Now); LogToConsole("Saving message: " + 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 LogDebugToConsole(username + " is ignored!"); } } /// /// Called on each MCC tick, around 10 times per second /// public override void Update() { DateTime dateNow = DateTime.Now; if (nextMailSend < dateNow) { LogDebugToConsole("Looking for mails to send @ " + 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 HashSet(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(); LogDebugToConsole("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": return "== Mails in database ==\n" + string.Join("\n", mailDatabase); case "getignored": return "== Ignore list ==\n" + 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 "Added " + args[1] + " to the ignore list!"; } else { lock (readWriteLock) { if (ignoreList.Contains(username)) { ignoreList.Remove(username); ignoreList.SaveToFile(Settings.Mailer_IgnoreListFile); } } return "Removed " + args[1] + " from the ignore list!"; } } else return "Missing or invalid name. Usage: " + commandName + " "; } } return "See usage: /help mailer"; } } }