diff --git a/MinecraftClient/ChatBot.cs b/MinecraftClient/ChatBot.cs index 039be7eb..d362f4a0 100644 --- a/MinecraftClient/ChatBot.cs +++ b/MinecraftClient/ChatBot.cs @@ -1193,6 +1193,15 @@ namespace MinecraftClient return Handler.CloseInventory(inventoryID); } + /// + /// Get max length for chat messages + /// + /// Max length, in characters + protected int GetMaxChatMessageLength() + { + return Handler.GetMaxChatMessageLength(); + } + /// /// Command runner definition. /// Returned string will be the output of the command diff --git a/MinecraftClient/ChatBots/Mailer.cs b/MinecraftClient/ChatBots/Mailer.cs new file mode 100644 index 00000000..8fd752b1 --- /dev/null +++ b/MinecraftClient/ChatBots/Mailer.cs @@ -0,0 +1,345 @@ +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 File.ReadAllLines(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); + File.WriteAllLines(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(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; + } + INIFile.WriteFile(filePath, 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 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.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 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(); + + /// + /// 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); + } + + 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); + + 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()); + 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); + + // Reload mail and ignore list database in case several instances are sharing the same database + mailDatabase = MailDatabase.FromFile(Settings.Mailer_DatabaseFile); + ignoreList = IgnoreList.FromFile(Settings.Mailer_IgnoreListFile); + + // Process at most 3 mails at a time to avoid spamming. Other mails will be processed on next mail send + HashSet onlinePlayer = new HashSet(GetOnlinePlayers()); + foreach (Mail mail in mailDatabase.Where(mail => !mail.Delivered && onlinePlayer.Contains(mail.Recipient)).Take(3)) + { + string sender = mail.Anonymous ? "Anonymous" : mail.Sender; + SendPrivateMessage(mail.Recipient, sender + " mailed: " + mail.Content); + mail.setDelivered(); + LogDebugToConsole("Delivered: " + mail.ToString()); + } + + 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); + } + } + + /// + /// 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") + { + if (!ignoreList.Contains(username)) + { + ignoreList.Add(username); + ignoreList.SaveToFile(Settings.Mailer_IgnoreListFile); + } + return "Added " + args[1] + " to the ignore list!"; + } + else + { + 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"; + } + } +} diff --git a/MinecraftClient/INIFile.cs b/MinecraftClient/INIFile.cs new file mode 100644 index 00000000..a6bcfb75 --- /dev/null +++ b/MinecraftClient/INIFile.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace MinecraftClient +{ + /// + /// INI File tools for parsing and generating user-friendly INI files + /// By ORelio (c) 2014 - CDDL 1.0 + /// + static class INIFile + { + /// + /// Parse a INI file into a dictionary. + /// Values can be accessed like this: dict["section"]["setting"] + /// + /// INI file to parse + /// INI sections and keys will be converted to lowercase unless this parameter is set to false + /// If failed to read the file + /// Parsed data from INI file + public static Dictionary> ParseFile(string iniFile, bool lowerCase = true) + { + var iniContents = new Dictionary>(); + string[] lines = File.ReadAllLines(iniFile, Encoding.UTF8); + string iniSection = "default"; + foreach (string lineRaw in lines) + { + string line = lineRaw.Split('#')[0].Trim(); + if (line.Length > 0 && line[0] != ';') + { + if (line[0] == '[' && line[line.Length - 1] == ']') + { + iniSection = line.Substring(1, line.Length - 2); + if (lowerCase) + iniSection = iniSection.ToLower(); + } + else + { + string argName = line.Split('=')[0]; + if (lowerCase) + argName = argName.ToLower(); + if (line.Length > (argName.Length + 1)) + { + string argValue = line.Substring(argName.Length + 1); + if (!iniContents.ContainsKey(iniSection)) + iniContents[iniSection] = new Dictionary(); + iniContents[iniSection][argName] = argValue; + } + } + } + } + return iniContents; + } + + /// + /// Write given data into an INI file + /// + /// File to write into + /// Data to put into the file + /// INI file description, inserted as a comment on first line of the INI file + /// Automatically change first char of section and keys to uppercase + public static void WriteFile(string iniFile, Dictionary> contents, string description = null, bool autoCase = true) + { + List lines = new List(); + if (!String.IsNullOrWhiteSpace(description)) + lines.Add('#' + description); + foreach (var section in contents) + { + if (lines.Count > 0) + lines.Add(""); + if (!String.IsNullOrEmpty(section.Key)) + { + lines.Add("[" + (autoCase ? char.ToUpper(section.Key[0]) + section.Key.Substring(1) : section.Key) + ']'); + foreach (var item in section.Value) + if (!String.IsNullOrEmpty(item.Key)) + lines.Add((autoCase ? char.ToUpper(item.Key[0]) + item.Key.Substring(1) : item.Key) + '=' + item.Value); + } + } + File.WriteAllLines(iniFile, lines, Encoding.UTF8); + } + + /// + /// Convert an integer from a string or return 0 if failed to parse + /// + /// String to parse + /// Int value + public static int Str2Int(string str) + { + try + { + return Convert.ToInt32(str); + } + catch + { + return 0; + } + } + + /// + /// Convert a 0/1 or True/False value to boolean + /// + /// String to parse + /// Boolean value + public static bool Str2Bool(string str) + { + return str.ToLower() == "true" || str == "1"; + } + } +} diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 48ba9e32..e32c22be 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -175,6 +175,7 @@ namespace MinecraftClient if (Settings.AutoAttack_Enabled) { BotLoad(new ChatBots.AutoAttack()); } if (Settings.AutoFishing_Enabled) { BotLoad(new ChatBots.AutoFishing()); } if (Settings.AutoEat_Enabled) { BotLoad(new ChatBots.AutoEat(Settings.AutoEat_hungerThreshold)); } + if (Settings.Mailer_Enabled) { BotLoad(new ChatBots.Mailer()); } if (Settings.AutoCraft_Enabled) { BotLoad(new AutoCraft(Settings.AutoCraft_configFile)); } if (Settings.AutoDrop_Enabled) { BotLoad(new AutoDrop(Settings.AutoDrop_Mode, Settings.AutoDrop_items)); } @@ -751,6 +752,15 @@ namespace MinecraftClient #region Getters: Retrieve data for use in other methods or ChatBots + /// + /// Get max length for chat messages + /// + /// Max length, in characters + public int GetMaxChatMessageLength() + { + return handler.GetMaxChatMessageLength(); + } + /// /// Get all inventories. ID 0 is the player inventory. /// diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index ff1858b0..cfac0d44 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -74,6 +74,7 @@ + @@ -111,6 +112,7 @@ + diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index a96b9532..cd291792 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -167,18 +167,30 @@ namespace MinecraftClient //AutoCraft public static bool AutoCraft_Enabled = false; public static string AutoCraft_configFile = @"autocraft\config.ini"; + + //Mailer + public static bool Mailer_Enabled = false; + public static string Mailer_DatabaseFile = "MailerDatabase.ini"; + public static string Mailer_IgnoreListFile = "MailerIgnoreList.ini"; + public static bool Mailer_PublicInteractions = false; + public static int Mailer_MaxMailsPerPlayer = 10; + public static int Mailer_MaxDatabaseSize = 10000; + public static int Mailer_MailRetentionDays = 30; //AutoDrop public static bool AutoDrop_Enabled = false; public static string AutoDrop_Mode = "include"; public static string AutoDrop_items = ""; + //Custom app variables and Minecraft accounts private static readonly Dictionary AppVars = new Dictionary(); private static readonly Dictionary> Accounts = new Dictionary>(); private static readonly Dictionary> Servers = new Dictionary>(); - private enum ParseMode { Default, Main, AppVars, Proxy, MCSettings, AntiAFK, Hangman, Alerts, ChatLog, AutoRelog, ScriptScheduler, RemoteControl, ChatFormat, AutoRespond, AutoAttack, AutoFishing, AutoEat, AutoCraft, AutoDrop }; + + private enum ParseMode { Default, Main, AppVars, Proxy, MCSettings, AntiAFK, Hangman, Alerts, ChatLog, AutoRelog, ScriptScheduler, RemoteControl, ChatFormat, AutoRespond, AutoAttack, AutoFishing, AutoEat, AutoCraft, AutoDrop, Mailer }; + /// /// Load settings from the give INI file @@ -223,7 +235,9 @@ namespace MinecraftClient case "autofishing": pMode = ParseMode.AutoFishing; break; case "autoeat": pMode = ParseMode.AutoEat; break; case "autocraft": pMode = ParseMode.AutoCraft; break; + case "mailer": pMode = ParseMode.Mailer; break; case "autodrop": pMode = ParseMode.AutoDrop; break; + default: pMode = ParseMode.Default; break; } } @@ -576,6 +590,19 @@ namespace MinecraftClient break; } break; + + case ParseMode.Mailer: + switch (argName.ToLower()) + { + case "enabled": Mailer_Enabled = str2bool(argValue); break; + case "database": Mailer_DatabaseFile = argValue; break; + case "ignorelist": Mailer_IgnoreListFile = argValue; break; + case "publicinteractions": Mailer_PublicInteractions = str2bool(argValue); break; + case "maxmailsperplayer": Mailer_MaxMailsPerPlayer = str2int(argValue); break; + case "maxdatabasesize": Mailer_MaxDatabaseSize = str2int(argValue); break; + case "retentiondays": Mailer_MailRetentionDays = str2int(argValue); break; + } + break; } } } @@ -735,6 +762,16 @@ namespace MinecraftClient + "enabled=false\r\n" + "configfile=autocraft\\config.ini\r\n" + "\r\n" + + "[Mailer]\r\n" + + "# Let the bot act like a mail plugin\r\n" + + "enabled=false\r\n" + + "database=MailerDatabase.ini\r\n" + + "ignorelist=MailerIgnoreList.ini\r\n" + + "publicinteractions=false\r\n" + + "maxmailsperplayer=10\r\n" + + "maxdatabasesize=10000\r\n" + + "retentiondays=30\r\n" + + "\r\n" + "[AutoDrop]\r\n" + "# Inventory Handling NEED to be enabled first\r\n" + "enabled=false\r\n" diff --git a/MinecraftClient/config/README.md b/MinecraftClient/config/README.md index 2d3b4622..eef76b79 100644 --- a/MinecraftClient/config/README.md +++ b/MinecraftClient/config/README.md @@ -253,6 +253,30 @@ Steps for using this bot: 4. Do `/useitem` and you should see "threw a fishing rod" 5. To stop fishing, do `/useitem` again +Using the Mailer +------ + +The Mailer bot can store and relay mails much like Essential's /mail command. + +* /tell mail [RECIPIENT] [MESSAGE]: Save your message for future delivery +* /tell tellonym [RECIPIENT] [MESSAGE]: Same, but the recipient will receive an anonymous mail + +The bot will automatically deliver the mail when the recipient is online. +The bot also offers a /mailer command from the MCC command prompt: + +* /mailer getmails: Show all mails in the console +* /mailer addignored [NAME]: Prevent a specific player from sending mails +* /mailer removeignored [NAME]: Lift the mailer restriction for this player +* /mailer getignored: Show all ignored players + +**CAUTION:** The bot identifies players by their name (Not by UUID!). +A nickname plugin or a minecraft rename may cause mails going to the wrong player! +Never write something to the bot you wouldn't say in the normal chat (You have been warned!) + +**Mailer Network:** The Mailer bot can relay messages between servers. +To set up a network of two or more bots, launch several instances with the bot activated and the same database. +If you launch two instances from one .exe they should syncronize automatically to the same file. + Disclaimer ------