Minecraft-Console-Client/MinecraftClient/ChatBots/Mailer.cs
ORelio ce83cc0a33 Use FileMonitor to synchronize Mailer bot (#1108) (v2)
Move SessionFileMonitor into a generic FileMonitor class
Use FileMonintor for both SessionCache and the Mailer bot
Allows multiple MCC instances to share the same database files

(Add files missing in the previous commit)
2020-08-07 11:58:44 +02:00

380 lines
17 KiB
C#

using System;
using System.Data;
using System.IO;
using System.Linq;
using System.Collections.Generic;
namespace MinecraftClient.ChatBots
{
/// <summary>
/// ChatBot for storing and delivering Mails
/// </summary>
public class Mailer : ChatBot
{
/// <summary>
/// Holds the list of ignored players
/// </summary>
private class IgnoreList : HashSet<string>
{
/// <summary>
/// Read ignore list from file
/// </summary>
/// <param name="filePath">Path to the ignore list</param>
/// <returns>Ignore list</returns>
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;
}
/// <summary>
/// Save ignore list to file
/// </summary>
/// <param name="filePath">Path to destination file</param>
public void SaveToFile(string filePath)
{
List<string> lines = new List<string>();
lines.Add("#Ignored Players");
foreach (string player in this)
lines.Add(player);
FileMonitor.WriteAllLinesWithRetries(filePath, lines);
}
}
/// <summary>
/// Holds the Mail database: a collection of Mails sent from a player to another player
/// </summary>
private class MailDatabase : List<Mail>
{
/// <summary>
/// Read mail database from file
/// </summary>
/// <param name="filePath">Path to the database</param>
/// <returns>Mail database</returns>
public static MailDatabase FromFile(string filePath)
{
MailDatabase database = new MailDatabase();
Dictionary<string, Dictionary<string, string>> iniFileDict = INIFile.ParseFile(FileMonitor.ReadAllLinesWithRetries(filePath));
foreach (KeyValuePair<string, Dictionary<string, string>> 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;
}
/// <summary>
/// Save mail database to file
/// </summary>
/// <param name="filePath">Path to destination file</param>
public void SaveToFile(string filePath)
{
Dictionary<string, Dictionary<string, string>> iniFileDict = new Dictionary<string, Dictionary<string, string>>();
int mailCount = 0;
foreach (Mail mail in this)
{
mailCount++;
Dictionary<string, string> iniSection = new Dictionary<string, string>();
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"));
}
}
/// <summary>
/// Represents a Mail sent from a player to another player
/// </summary>
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();
/// <summary>
/// Initialization of the Mailer bot
/// </summary>
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);
}
/// <summary>
/// Standard settings for the bot.
/// </summary>
public override void AfterGameJoined()
{
maxMessageLength = GetMaxChatMessageLength()
- 44 // Deduct length of "/ 16CharPlayerName 16CharPlayerName mailed: "
- Settings.PrivateMsgsCmdName.Length; // Deduct length of "tell" command
}
/// <summary>
/// Process chat messages from the server
/// </summary>
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<string> args = new Queue<string>(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 + " <recipient> <message>");
}
else SendPrivateMessage(username, "Couldn't save Message. Limit reached!");
break;
}
}
else LogDebugToConsole(username + " is ignored!");
}
}
/// <summary>
/// Called on each MCC tick, around 10 times per second
/// </summary>
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<string> onlinePlayersLowercase = new HashSet<string>(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);
}
}
/// <summary>
/// Called when the Mail Database or Ignore list has changed on disk
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FileMonitorCallback(object sender, FileSystemEventArgs e)
{
lock (readWriteLock)
{
mailDatabase = MailDatabase.FromFile(Settings.Mailer_DatabaseFile);
ignoreList = IgnoreList.FromFile(Settings.Mailer_IgnoreListFile);
}
}
/// <summary>
/// Interprets local commands.
/// </summary>
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 + " <username>";
}
}
return "See usage: /help mailer";
}
}
}