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
------