[skip ci] Normalize all line endings

This commit is contained in:
ReinforceZwei 2023-09-15 14:44:11 +08:00
parent bdad4f302d
commit 37bcad37e0
11 changed files with 10827 additions and 10827 deletions

View file

@ -1,470 +1,470 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using Brigadier.NET;
using Brigadier.NET.Builder;
using MinecraftClient.CommandHandler;
using MinecraftClient.CommandHandler.Patch;
using MinecraftClient.Scripting;
using Tomlet.Attributes;
namespace MinecraftClient.ChatBots
{
/// <summary>
/// ChatBot for storing and delivering Mails
/// </summary>
public class Mailer : ChatBot
{
public const string CommandName = "mailer";
public static Configs Config = new();
[TomlDoNotInlineObject]
public class Configs
{
[NonSerialized]
private const string BotName = "Mailer";
public bool Enabled = false;
public string DatabaseFile = "MailerDatabase.ini";
public string IgnoreListFile = "MailerIgnoreList.ini";
public bool PublicInteractions = false;
public int MaxMailsPerPlayer = 10;
public int MaxDatabaseSize = 10000;
public int MailRetentionDays = 30;
public void OnSettingUpdate()
{
DatabaseFile ??= string.Empty;
IgnoreListFile ??= string.Empty;
if (!Enabled) return;
bool checkSuccessed = true;
if (Config.MaxDatabaseSize <= 0)
{
LogToConsole(BotName, Translations.bot_mailer_init_fail_db_size);
checkSuccessed = false;
}
if (Config.MaxMailsPerPlayer <= 0)
{
LogToConsole(BotName, Translations.bot_mailer_init_fail_max_mails);
checkSuccessed = false;
}
if (Config.MailRetentionDays <= 0)
{
LogToConsole(BotName, Translations.bot_mailer_init_fail_mail_retention);
checkSuccessed = false;
}
if (!checkSuccessed)
{
LogToConsole(BotName, Translations.general_bot_unload);
Enabled = false;
}
}
}
/// <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();
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();
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();
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();
int mailCount = 0;
foreach (Mail mail in this)
{
mailCount++;
Dictionary<string, string> 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"));
}
}
/// <summary>
/// Represents a Mail sent from a player to another player
/// </summary>
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();
/// <summary>
/// Initialization of the Mailer bot
/// </summary>
public override void Initialize()
{
LogDebugToConsole(Translations.bot_mailer_init);
LogDebugToConsole(Translations.bot_mailer_init_db + Config.DatabaseFile);
LogDebugToConsole(Translations.bot_mailer_init_ignore + Config.IgnoreListFile);
LogDebugToConsole(Translations.bot_mailer_init_public + Config.PublicInteractions);
LogDebugToConsole(Translations.bot_mailer_init_max_mails + Config.MaxMailsPerPlayer);
LogDebugToConsole(Translations.bot_mailer_init_db_size + Config.MaxDatabaseSize);
LogDebugToConsole(Translations.bot_mailer_init_mail_retention + Config.MailRetentionDays + " days");
if (!File.Exists(Config.DatabaseFile))
{
LogToConsole(string.Format(Translations.bot_mailer_create_db, Path.GetFullPath(Config.DatabaseFile)));
new MailDatabase().SaveToFile(Config.DatabaseFile);
}
if (!File.Exists(Config.IgnoreListFile))
{
LogToConsole(string.Format(Translations.bot_mailer_create_ignore, Path.GetFullPath(Config.IgnoreListFile)));
new IgnoreList().SaveToFile(Config.IgnoreListFile);
}
lock (readWriteLock)
{
LogDebugToConsole(string.Format(Translations.bot_mailer_load_db, Path.GetFullPath(Config.DatabaseFile)));
mailDatabase = MailDatabase.FromFile(Config.DatabaseFile);
LogDebugToConsole(string.Format(Translations.bot_mailer_load_ignore, Path.GetFullPath(Config.IgnoreListFile)));
ignoreList = IgnoreList.FromFile(Config.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(Config.DatabaseFile)!, Path.GetFileName(Config.DatabaseFile), FileMonitorCallback);
ignoreListFileMonitor = new FileMonitor(Path.GetDirectoryName(Config.IgnoreListFile)!, Path.GetFileName(Config.IgnoreListFile), FileMonitorCallback);
McClient.dispatcher.Register(l => l.Literal("help")
.Then(l => l.Literal(CommandName)
.Executes(r => OnCommandHelp(r.Source, string.Empty)))
);
McClient.dispatcher.Register(l => l.Literal(CommandName)
.Then(l => l.Literal("getmails")
.Executes(r => OnCommandGetMails()))
.Then(l => l.Literal("getignored")
.Executes(r => OnCommandGetIgnored()))
.Then(l => l.Literal("addignored")
.Then(l => l.Argument("username", Arguments.String())
.Executes(r => OnCommandAddIgnored(Arguments.GetString(r, "username")))))
.Then(l => l.Literal("removeignored")
.Then(l => l.Argument("username", Arguments.String())
.Executes(r => OnCommandRemoveIgnored(Arguments.GetString(r, "username")))))
.Then(l => l.Literal("_help")
.Executes(r => OnCommandHelp(r.Source, string.Empty))
.Redirect(McClient.dispatcher.GetRoot().GetChild("help").GetChild(CommandName)))
);
}
public override void OnUnload()
{
McClient.dispatcher.Unregister(CommandName);
McClient.dispatcher.GetRoot().GetChild("help").RemoveChild(CommandName);
}
private int OnCommandHelp(CmdResult r, string? cmd)
{
return r.SetAndReturn(cmd switch
{
#pragma warning disable format // @formatter:off
_ => Translations.bot_mailer_cmd_help + ": /mailer <getmails|addignored|getignored|removeignored>"
+ '\n' + McClient.dispatcher.GetAllUsageString(CommandName, false),
#pragma warning restore format // @formatter:on
});
}
private int OnCommandGetMails()
{
LogToConsole(string.Format(Translations.bot_mailer_cmd_getmails, string.Join("\n", mailDatabase)));
return 1;
}
private int OnCommandGetIgnored()
{
LogToConsole(string.Format(Translations.bot_mailer_cmd_getignored, string.Join("\n", ignoreList)));
return 1;
}
private int OnCommandAddIgnored(string username)
{
if (IsValidName(username))
{
username = username.ToLower();
lock (readWriteLock)
{
if (!ignoreList.Contains(username))
{
ignoreList.Add(username);
ignoreList.SaveToFile(Config.IgnoreListFile);
}
}
LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_added, username));
return 1;
}
else
{
LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_invalid, "addignored"));
return 0;
}
}
private int OnCommandRemoveIgnored(string username)
{
if (IsValidName(username))
{
username = username.ToLower();
lock (readWriteLock)
{
if (ignoreList.Contains(username))
{
ignoreList.Remove(username);
ignoreList.SaveToFile(Config.IgnoreListFile);
}
}
LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_removed, username));
return 1;
}
else
{
LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_invalid, "removeignored"));
return 0;
}
}
/// <summary>
/// Standard settings for the bot.
/// </summary>
public override void AfterGameJoined()
{
maxMessageLength = GetMaxChatMessageLength()
- 44 // Deduct length of "/ 16CharPlayerName 16CharPlayerName mailed: "
- Settings.Config.Main.Advanced.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) || (Config.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 < Config.MaxDatabaseSize
&& mailDatabase.Where(mail => mail.SenderLowercase == usernameLower).Count() < Config.MaxMailsPerPlayer)
{
Queue<string> 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);
LogToConsole(string.Format(Translations.bot_mailer_saving, mail.ToString()));
lock (readWriteLock)
{
mailDatabase.Add(mail);
mailDatabase.SaveToFile(Config.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(string.Format(Translations.bot_mailer_user_ignored, username));
}
}
/// <summary>
/// Called on each MCC tick, around 10 times per second
/// </summary>
public override void Update()
{
DateTime dateNow = DateTime.Now;
if (nextMailSend < dateNow)
{
LogDebugToConsole(string.Format(Translations.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<string> 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();
LogDebugToConsole(string.Format(Translations.bot_mailer_delivered, mail.ToString()));
}
lock (readWriteLock)
{
mailDatabase.RemoveAll(mail => mail.Delivered);
mailDatabase.RemoveAll(mail => mail.DateSent.AddDays(Config.MailRetentionDays) < DateTime.Now);
mailDatabase.SaveToFile(Config.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(Config.DatabaseFile);
ignoreList = IgnoreList.FromFile(Config.IgnoreListFile);
}
}
}
}
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using Brigadier.NET;
using Brigadier.NET.Builder;
using MinecraftClient.CommandHandler;
using MinecraftClient.CommandHandler.Patch;
using MinecraftClient.Scripting;
using Tomlet.Attributes;
namespace MinecraftClient.ChatBots
{
/// <summary>
/// ChatBot for storing and delivering Mails
/// </summary>
public class Mailer : ChatBot
{
public const string CommandName = "mailer";
public static Configs Config = new();
[TomlDoNotInlineObject]
public class Configs
{
[NonSerialized]
private const string BotName = "Mailer";
public bool Enabled = false;
public string DatabaseFile = "MailerDatabase.ini";
public string IgnoreListFile = "MailerIgnoreList.ini";
public bool PublicInteractions = false;
public int MaxMailsPerPlayer = 10;
public int MaxDatabaseSize = 10000;
public int MailRetentionDays = 30;
public void OnSettingUpdate()
{
DatabaseFile ??= string.Empty;
IgnoreListFile ??= string.Empty;
if (!Enabled) return;
bool checkSuccessed = true;
if (Config.MaxDatabaseSize <= 0)
{
LogToConsole(BotName, Translations.bot_mailer_init_fail_db_size);
checkSuccessed = false;
}
if (Config.MaxMailsPerPlayer <= 0)
{
LogToConsole(BotName, Translations.bot_mailer_init_fail_max_mails);
checkSuccessed = false;
}
if (Config.MailRetentionDays <= 0)
{
LogToConsole(BotName, Translations.bot_mailer_init_fail_mail_retention);
checkSuccessed = false;
}
if (!checkSuccessed)
{
LogToConsole(BotName, Translations.general_bot_unload);
Enabled = false;
}
}
}
/// <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();
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();
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();
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();
int mailCount = 0;
foreach (Mail mail in this)
{
mailCount++;
Dictionary<string, string> 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"));
}
}
/// <summary>
/// Represents a Mail sent from a player to another player
/// </summary>
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();
/// <summary>
/// Initialization of the Mailer bot
/// </summary>
public override void Initialize()
{
LogDebugToConsole(Translations.bot_mailer_init);
LogDebugToConsole(Translations.bot_mailer_init_db + Config.DatabaseFile);
LogDebugToConsole(Translations.bot_mailer_init_ignore + Config.IgnoreListFile);
LogDebugToConsole(Translations.bot_mailer_init_public + Config.PublicInteractions);
LogDebugToConsole(Translations.bot_mailer_init_max_mails + Config.MaxMailsPerPlayer);
LogDebugToConsole(Translations.bot_mailer_init_db_size + Config.MaxDatabaseSize);
LogDebugToConsole(Translations.bot_mailer_init_mail_retention + Config.MailRetentionDays + " days");
if (!File.Exists(Config.DatabaseFile))
{
LogToConsole(string.Format(Translations.bot_mailer_create_db, Path.GetFullPath(Config.DatabaseFile)));
new MailDatabase().SaveToFile(Config.DatabaseFile);
}
if (!File.Exists(Config.IgnoreListFile))
{
LogToConsole(string.Format(Translations.bot_mailer_create_ignore, Path.GetFullPath(Config.IgnoreListFile)));
new IgnoreList().SaveToFile(Config.IgnoreListFile);
}
lock (readWriteLock)
{
LogDebugToConsole(string.Format(Translations.bot_mailer_load_db, Path.GetFullPath(Config.DatabaseFile)));
mailDatabase = MailDatabase.FromFile(Config.DatabaseFile);
LogDebugToConsole(string.Format(Translations.bot_mailer_load_ignore, Path.GetFullPath(Config.IgnoreListFile)));
ignoreList = IgnoreList.FromFile(Config.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(Config.DatabaseFile)!, Path.GetFileName(Config.DatabaseFile), FileMonitorCallback);
ignoreListFileMonitor = new FileMonitor(Path.GetDirectoryName(Config.IgnoreListFile)!, Path.GetFileName(Config.IgnoreListFile), FileMonitorCallback);
McClient.dispatcher.Register(l => l.Literal("help")
.Then(l => l.Literal(CommandName)
.Executes(r => OnCommandHelp(r.Source, string.Empty)))
);
McClient.dispatcher.Register(l => l.Literal(CommandName)
.Then(l => l.Literal("getmails")
.Executes(r => OnCommandGetMails()))
.Then(l => l.Literal("getignored")
.Executes(r => OnCommandGetIgnored()))
.Then(l => l.Literal("addignored")
.Then(l => l.Argument("username", Arguments.String())
.Executes(r => OnCommandAddIgnored(Arguments.GetString(r, "username")))))
.Then(l => l.Literal("removeignored")
.Then(l => l.Argument("username", Arguments.String())
.Executes(r => OnCommandRemoveIgnored(Arguments.GetString(r, "username")))))
.Then(l => l.Literal("_help")
.Executes(r => OnCommandHelp(r.Source, string.Empty))
.Redirect(McClient.dispatcher.GetRoot().GetChild("help").GetChild(CommandName)))
);
}
public override void OnUnload()
{
McClient.dispatcher.Unregister(CommandName);
McClient.dispatcher.GetRoot().GetChild("help").RemoveChild(CommandName);
}
private int OnCommandHelp(CmdResult r, string? cmd)
{
return r.SetAndReturn(cmd switch
{
#pragma warning disable format // @formatter:off
_ => Translations.bot_mailer_cmd_help + ": /mailer <getmails|addignored|getignored|removeignored>"
+ '\n' + McClient.dispatcher.GetAllUsageString(CommandName, false),
#pragma warning restore format // @formatter:on
});
}
private int OnCommandGetMails()
{
LogToConsole(string.Format(Translations.bot_mailer_cmd_getmails, string.Join("\n", mailDatabase)));
return 1;
}
private int OnCommandGetIgnored()
{
LogToConsole(string.Format(Translations.bot_mailer_cmd_getignored, string.Join("\n", ignoreList)));
return 1;
}
private int OnCommandAddIgnored(string username)
{
if (IsValidName(username))
{
username = username.ToLower();
lock (readWriteLock)
{
if (!ignoreList.Contains(username))
{
ignoreList.Add(username);
ignoreList.SaveToFile(Config.IgnoreListFile);
}
}
LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_added, username));
return 1;
}
else
{
LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_invalid, "addignored"));
return 0;
}
}
private int OnCommandRemoveIgnored(string username)
{
if (IsValidName(username))
{
username = username.ToLower();
lock (readWriteLock)
{
if (ignoreList.Contains(username))
{
ignoreList.Remove(username);
ignoreList.SaveToFile(Config.IgnoreListFile);
}
}
LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_removed, username));
return 1;
}
else
{
LogToConsole(string.Format(Translations.bot_mailer_cmd_ignore_invalid, "removeignored"));
return 0;
}
}
/// <summary>
/// Standard settings for the bot.
/// </summary>
public override void AfterGameJoined()
{
maxMessageLength = GetMaxChatMessageLength()
- 44 // Deduct length of "/ 16CharPlayerName 16CharPlayerName mailed: "
- Settings.Config.Main.Advanced.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) || (Config.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 < Config.MaxDatabaseSize
&& mailDatabase.Where(mail => mail.SenderLowercase == usernameLower).Count() < Config.MaxMailsPerPlayer)
{
Queue<string> 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);
LogToConsole(string.Format(Translations.bot_mailer_saving, mail.ToString()));
lock (readWriteLock)
{
mailDatabase.Add(mail);
mailDatabase.SaveToFile(Config.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(string.Format(Translations.bot_mailer_user_ignored, username));
}
}
/// <summary>
/// Called on each MCC tick, around 10 times per second
/// </summary>
public override void Update()
{
DateTime dateNow = DateTime.Now;
if (nextMailSend < dateNow)
{
LogDebugToConsole(string.Format(Translations.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<string> 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();
LogDebugToConsole(string.Format(Translations.bot_mailer_delivered, mail.ToString()));
}
lock (readWriteLock)
{
mailDatabase.RemoveAll(mail => mail.Delivered);
mailDatabase.RemoveAll(mail => mail.DateSent.AddDays(Config.MailRetentionDays) < DateTime.Now);
mailDatabase.SaveToFile(Config.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(Config.DatabaseFile);
ignoreList = IgnoreList.FromFile(Config.IgnoreListFile);
}
}
}
}

View file

@ -1,55 +1,55 @@
using Brigadier.NET;
using Brigadier.NET.Builder;
using MinecraftClient.CommandHandler;
namespace MinecraftClient.Commands
{
public class Animation : Command
{
public override string CmdName { get { return "animation"; } }
public override string CmdUsage { get { return "animation <mainhand|offhand>"; } }
public override string CmdDesc { get { return Translations.cmd_animation_desc; } }
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
{
dispatcher.Register(l => l.Literal("help")
.Then(l => l.Literal(CmdName)
.Executes(r => GetUsage(r.Source, string.Empty))
.Then(l => l.Literal("mainhand")
.Executes(r => GetUsage(r.Source, "mainhand")))
.Then(l => l.Literal("offhand")
.Executes(r => GetUsage(r.Source, "offhand")))
)
);
dispatcher.Register(l => l.Literal(CmdName)
.Executes(r => DoAnimation(r.Source, mainhand: true))
.Then(l => l.Literal("mainhand")
.Executes(r => DoAnimation(r.Source, mainhand: true)))
.Then(l => l.Literal("offhand")
.Executes(r => DoAnimation(r.Source, mainhand: false)))
.Then(l => l.Literal("_help")
.Executes(r => GetUsage(r.Source, string.Empty))
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
);
}
private int GetUsage(CmdResult r, string? cmd)
{
return r.SetAndReturn(cmd switch
{
#pragma warning disable format // @formatter:off
"mainhand" => GetCmdDescTranslated(),
"offhand" => GetCmdDescTranslated(),
_ => GetCmdDescTranslated(),
#pragma warning restore format // @formatter:on
});
}
private static int DoAnimation(CmdResult r, bool mainhand)
{
McClient handler = CmdResult.currentHandler!;
return r.SetAndReturn(handler.DoAnimation(mainhand ? 1 : 0));
}
}
}
using Brigadier.NET;
using Brigadier.NET.Builder;
using MinecraftClient.CommandHandler;
namespace MinecraftClient.Commands
{
public class Animation : Command
{
public override string CmdName { get { return "animation"; } }
public override string CmdUsage { get { return "animation <mainhand|offhand>"; } }
public override string CmdDesc { get { return Translations.cmd_animation_desc; } }
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
{
dispatcher.Register(l => l.Literal("help")
.Then(l => l.Literal(CmdName)
.Executes(r => GetUsage(r.Source, string.Empty))
.Then(l => l.Literal("mainhand")
.Executes(r => GetUsage(r.Source, "mainhand")))
.Then(l => l.Literal("offhand")
.Executes(r => GetUsage(r.Source, "offhand")))
)
);
dispatcher.Register(l => l.Literal(CmdName)
.Executes(r => DoAnimation(r.Source, mainhand: true))
.Then(l => l.Literal("mainhand")
.Executes(r => DoAnimation(r.Source, mainhand: true)))
.Then(l => l.Literal("offhand")
.Executes(r => DoAnimation(r.Source, mainhand: false)))
.Then(l => l.Literal("_help")
.Executes(r => GetUsage(r.Source, string.Empty))
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
);
}
private int GetUsage(CmdResult r, string? cmd)
{
return r.SetAndReturn(cmd switch
{
#pragma warning disable format // @formatter:off
"mainhand" => GetCmdDescTranslated(),
"offhand" => GetCmdDescTranslated(),
_ => GetCmdDescTranslated(),
#pragma warning restore format // @formatter:on
});
}
private static int DoAnimation(CmdResult r, bool mainhand)
{
McClient handler = CmdResult.currentHandler!;
return r.SetAndReturn(handler.DoAnimation(mainhand ? 1 : 0));
}
}
}

View file

@ -1,63 +1,63 @@
using System.Linq;
using Brigadier.NET;
using Brigadier.NET.Builder;
using MinecraftClient.CommandHandler;
using MinecraftClient.Inventory;
namespace MinecraftClient.Commands
{
public class NameItem : Command
{
public override string CmdName => "nameitem";
public override string CmdUsage => "nameitem <item name>";
public override string CmdDesc => Translations.cmd_nameitem_desc;
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
{
dispatcher.Register(l => l.Literal("help")
.Then(l => l.Literal(CmdName)
.Executes(r => GetUsage(r.Source, string.Empty))
)
);
dispatcher.Register(l => l.Literal(CmdName)
.Then(l => l.Argument("any", Arguments.GreedyString())
.Executes(r => DoSetItemName(r.Source, Arguments.GetString(r, "any"))))
);
}
private int GetUsage(CmdResult r, string? cmd)
{
return r.SetAndReturn(cmd switch
{
#pragma warning disable format // @formatter:off
_ => GetCmdDescTranslated(),
#pragma warning restore format // @formatter:on
});
}
private int DoSetItemName(CmdResult r, string itemName)
{
var handler = CmdResult.currentHandler!;
if (itemName.Trim().Length == 0)
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_item_name_empty);
var currentInventory = handler.GetInventories().Count == 0
? null
: handler.GetInventories().Values.ToList().Last();
if (currentInventory is not { Type: ContainerType.Anvil })
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_no_anvil_inventory_open);
if (currentInventory.Items[0].IsEmpty)
return r.SetAndReturn(CmdResult.Status.Fail,
Translations.cmd_nameitem_first_slot_empty);
return handler.SendRenameItem(itemName)
? r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_nameitem_successful)
: r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_failed);
}
}
using System.Linq;
using Brigadier.NET;
using Brigadier.NET.Builder;
using MinecraftClient.CommandHandler;
using MinecraftClient.Inventory;
namespace MinecraftClient.Commands
{
public class NameItem : Command
{
public override string CmdName => "nameitem";
public override string CmdUsage => "nameitem <item name>";
public override string CmdDesc => Translations.cmd_nameitem_desc;
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
{
dispatcher.Register(l => l.Literal("help")
.Then(l => l.Literal(CmdName)
.Executes(r => GetUsage(r.Source, string.Empty))
)
);
dispatcher.Register(l => l.Literal(CmdName)
.Then(l => l.Argument("any", Arguments.GreedyString())
.Executes(r => DoSetItemName(r.Source, Arguments.GetString(r, "any"))))
);
}
private int GetUsage(CmdResult r, string? cmd)
{
return r.SetAndReturn(cmd switch
{
#pragma warning disable format // @formatter:off
_ => GetCmdDescTranslated(),
#pragma warning restore format // @formatter:on
});
}
private int DoSetItemName(CmdResult r, string itemName)
{
var handler = CmdResult.currentHandler!;
if (itemName.Trim().Length == 0)
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_item_name_empty);
var currentInventory = handler.GetInventories().Count == 0
? null
: handler.GetInventories().Values.ToList().Last();
if (currentInventory is not { Type: ContainerType.Anvil })
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_no_anvil_inventory_open);
if (currentInventory.Items[0].IsEmpty)
return r.SetAndReturn(CmdResult.Status.Fail,
Translations.cmd_nameitem_first_slot_empty);
return handler.SendRenameItem(itemName)
? r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_nameitem_successful)
: r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_nameitem_failed);
}
}
}

View file

@ -1,134 +1,134 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace MinecraftClient
{
/// <summary>
/// INI File tools for parsing and generating user-friendly INI files
/// By ORelio (c) 2014-2020 - CDDL 1.0
/// </summary>
static class INIFile
{
/// <summary>
/// Parse a INI file into a dictionary.
/// Values can be accessed like this: dict["section"]["setting"]
/// </summary>
/// <param name="iniFile">INI file to parse</param>
/// <param name="lowerCase">INI sections and keys will be converted to lowercase unless this parameter is set to false</param>
/// <exception cref="IOException">If failed to read the file</exception>
/// <returns>Parsed data from INI file</returns>
public static Dictionary<string, Dictionary<string, string>> ParseFile(string iniFile, bool lowerCase = true)
{
return ParseFile(File.ReadAllLines(iniFile, Encoding.UTF8), lowerCase);
}
/// <summary>
/// Parse a INI file into a dictionary.
/// Values can be accessed like this: dict["section"]["setting"]
/// </summary>
/// <param name="lines">INI file content to parse</param>
/// <param name="lowerCase">INI sections and keys will be converted to lowercase unless this parameter is set to false</param>
/// <exception cref="IOException">If failed to read the file</exception>
/// <returns>Parsed data from INI file</returns>
public static Dictionary<string, Dictionary<string, string>> ParseFile(IEnumerable<string> lines, bool lowerCase = true)
{
var iniContents = new Dictionary<string, Dictionary<string, string>>();
string iniSection = "default";
foreach (string lineRaw in lines)
{
string line = lineRaw.Split('#')[0].Trim();
if (line.Length > 0 && line[0] != ';')
{
if (line[0] == '[' && line[^1] == ']')
{
iniSection = line[1..^1];
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[(argName.Length + 1)..];
if (!iniContents.ContainsKey(iniSection))
iniContents[iniSection] = new Dictionary<string, string>();
iniContents[iniSection][argName] = argValue;
}
}
}
}
return iniContents;
}
/// <summary>
/// Write given data into an INI file
/// </summary>
/// <param name="iniFile">File to write into</param>
/// <param name="contents">Data to put into the file</param>
/// <param name="description">INI file description, inserted as a comment on first line of the INI file</param>
/// <param name="autoCase">Automatically change first char of section and keys to uppercase</param>
public static void WriteFile(string iniFile, Dictionary<string, Dictionary<string, string>> contents, string? description = null, bool autoCase = true)
{
File.WriteAllLines(iniFile, Generate(contents, description, autoCase), Encoding.UTF8);
}
/// <summary>
/// Generate given data into the INI format
/// </summary>
/// <param name="contents">Data to put into the INI format</param>
/// <param name="description">INI file description, inserted as a comment on first line of the INI file</param>
/// <param name="autoCase">Automatically change first char of section and keys to uppercase</param>
/// <returns>Lines of the INI file</returns>
public static string[] Generate(Dictionary<string, Dictionary<string, string>> contents, string? description = null, bool autoCase = true)
{
List<string> lines = new();
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[1..] : section.Key) + ']');
foreach (var item in section.Value)
if (!String.IsNullOrEmpty(item.Key))
lines.Add((autoCase ? char.ToUpper(item.Key[0]) + item.Key[1..] : item.Key) + '=' + item.Value);
}
}
return lines.ToArray();
}
/// <summary>
/// Convert an integer from a string or return 0 if failed to parse
/// </summary>
/// <param name="str">String to parse</param>
/// <returns>Int value</returns>
public static int Str2Int(string str)
{
try
{
return Convert.ToInt32(str);
}
catch
{
return 0;
}
}
/// <summary>
/// Convert a 0/1 or True/False value to boolean
/// </summary>
/// <param name="str">String to parse</param>
/// <returns>Boolean value</returns>
public static bool Str2Bool(string str)
{
return str.ToLower() == "true" || str == "1";
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace MinecraftClient
{
/// <summary>
/// INI File tools for parsing and generating user-friendly INI files
/// By ORelio (c) 2014-2020 - CDDL 1.0
/// </summary>
static class INIFile
{
/// <summary>
/// Parse a INI file into a dictionary.
/// Values can be accessed like this: dict["section"]["setting"]
/// </summary>
/// <param name="iniFile">INI file to parse</param>
/// <param name="lowerCase">INI sections and keys will be converted to lowercase unless this parameter is set to false</param>
/// <exception cref="IOException">If failed to read the file</exception>
/// <returns>Parsed data from INI file</returns>
public static Dictionary<string, Dictionary<string, string>> ParseFile(string iniFile, bool lowerCase = true)
{
return ParseFile(File.ReadAllLines(iniFile, Encoding.UTF8), lowerCase);
}
/// <summary>
/// Parse a INI file into a dictionary.
/// Values can be accessed like this: dict["section"]["setting"]
/// </summary>
/// <param name="lines">INI file content to parse</param>
/// <param name="lowerCase">INI sections and keys will be converted to lowercase unless this parameter is set to false</param>
/// <exception cref="IOException">If failed to read the file</exception>
/// <returns>Parsed data from INI file</returns>
public static Dictionary<string, Dictionary<string, string>> ParseFile(IEnumerable<string> lines, bool lowerCase = true)
{
var iniContents = new Dictionary<string, Dictionary<string, string>>();
string iniSection = "default";
foreach (string lineRaw in lines)
{
string line = lineRaw.Split('#')[0].Trim();
if (line.Length > 0 && line[0] != ';')
{
if (line[0] == '[' && line[^1] == ']')
{
iniSection = line[1..^1];
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[(argName.Length + 1)..];
if (!iniContents.ContainsKey(iniSection))
iniContents[iniSection] = new Dictionary<string, string>();
iniContents[iniSection][argName] = argValue;
}
}
}
}
return iniContents;
}
/// <summary>
/// Write given data into an INI file
/// </summary>
/// <param name="iniFile">File to write into</param>
/// <param name="contents">Data to put into the file</param>
/// <param name="description">INI file description, inserted as a comment on first line of the INI file</param>
/// <param name="autoCase">Automatically change first char of section and keys to uppercase</param>
public static void WriteFile(string iniFile, Dictionary<string, Dictionary<string, string>> contents, string? description = null, bool autoCase = true)
{
File.WriteAllLines(iniFile, Generate(contents, description, autoCase), Encoding.UTF8);
}
/// <summary>
/// Generate given data into the INI format
/// </summary>
/// <param name="contents">Data to put into the INI format</param>
/// <param name="description">INI file description, inserted as a comment on first line of the INI file</param>
/// <param name="autoCase">Automatically change first char of section and keys to uppercase</param>
/// <returns>Lines of the INI file</returns>
public static string[] Generate(Dictionary<string, Dictionary<string, string>> contents, string? description = null, bool autoCase = true)
{
List<string> lines = new();
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[1..] : section.Key) + ']');
foreach (var item in section.Value)
if (!String.IsNullOrEmpty(item.Key))
lines.Add((autoCase ? char.ToUpper(item.Key[0]) + item.Key[1..] : item.Key) + '=' + item.Value);
}
}
return lines.ToArray();
}
/// <summary>
/// Convert an integer from a string or return 0 if failed to parse
/// </summary>
/// <param name="str">String to parse</param>
/// <returns>Int value</returns>
public static int Str2Int(string str)
{
try
{
return Convert.ToInt32(str);
}
catch
{
return 0;
}
}
/// <summary>
/// Convert a 0/1 or True/False value to boolean
/// </summary>
/// <param name="str">String to parse</param>
/// <returns>Boolean value</returns>
public static bool Str2Bool(string str)
{
return str.ToLower() == "true" || str == "1";
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,272 +1,272 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Timers;
using static MinecraftClient.Settings;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig;
namespace MinecraftClient.Protocol.Session
{
/// <summary>
/// Handle sessions caching and storage.
/// </summary>
public static class SessionCache
{
private const string SessionCacheFilePlaintext = "SessionCache.ini";
private const string SessionCacheFileSerialized = "SessionCache.db";
private static readonly string SessionCacheFileMinecraft = String.Concat(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
Path.DirectorySeparatorChar,
".minecraft",
Path.DirectorySeparatorChar,
"launcher_profiles.json"
);
private static FileMonitor? cachemonitor;
private static readonly Dictionary<string, SessionToken> sessions = new();
private static readonly Timer updatetimer = new(100);
private static readonly List<KeyValuePair<string, SessionToken>> pendingadds = new();
private static readonly BinaryFormatter formatter = new();
/// <summary>
/// Retrieve whether SessionCache contains a session for the given login.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>TRUE if session is available</returns>
public static bool Contains(string login)
{
return sessions.ContainsKey(login);
}
/// <summary>
/// Store a session and save it to disk if required.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <param name="session">User session token used with Minecraft.net</param>
public static void Store(string login, SessionToken session)
{
if (Contains(login))
{
sessions[login] = session;
}
else
{
sessions.Add(login, session);
}
if (Config.Main.Advanced.SessionCache == CacheType.disk && updatetimer.Enabled == true)
{
pendingadds.Add(new KeyValuePair<string, SessionToken>(login, session));
}
else if (Config.Main.Advanced.SessionCache == CacheType.disk)
{
SaveToDisk();
}
}
/// <summary>
/// Retrieve a session token for the given login.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>SessionToken for given login</returns>
public static SessionToken Get(string login)
{
return sessions[login];
}
/// <summary>
/// Initialize cache monitoring to keep cache updated with external changes.
/// </summary>
/// <returns>TRUE if session tokens are seeded from file</returns>
public static bool InitializeDiskCache()
{
cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, SessionCacheFilePlaintext, new FileSystemEventHandler(OnChanged));
updatetimer.Elapsed += HandlePending;
return LoadFromDisk();
}
/// <summary>
/// Reloads cache on external cache file change.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void OnChanged(object sender, FileSystemEventArgs e)
{
updatetimer.Stop();
updatetimer.Start();
}
/// <summary>
/// Called after timer elapsed. Reads disk cache and adds new/modified sessions back.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void HandlePending(object? sender, ElapsedEventArgs e)
{
updatetimer.Stop();
LoadFromDisk();
foreach (KeyValuePair<string, SessionToken> pending in pendingadds.ToArray())
{
Store(pending.Key, pending.Value);
pendingadds.Remove(pending);
}
}
/// <summary>
/// Reads cache file and loads SessionTokens into SessionCache.
/// </summary>
/// <returns>True if data is successfully loaded</returns>
private static bool LoadFromDisk()
{
//Grab sessions in the Minecraft directory
if (File.Exists(SessionCacheFileMinecraft))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading, Path.GetFileName(SessionCacheFileMinecraft)));
Json.JSONData mcSession = new(Json.JSONData.DataType.String);
try
{
mcSession = Json.ParseJson(File.ReadAllText(SessionCacheFileMinecraft));
}
catch (IOException) { /* Failed to read file from disk -- ignoring */ }
if (mcSession.Type == Json.JSONData.DataType.Object
&& mcSession.Properties.ContainsKey("clientToken")
&& mcSession.Properties.ContainsKey("authenticationDatabase"))
{
string clientID = mcSession.Properties["clientToken"].StringValue.Replace("-", "");
Dictionary<string, Json.JSONData> sessionItems = mcSession.Properties["authenticationDatabase"].Properties;
foreach (string key in sessionItems.Keys)
{
if (Guid.TryParseExact(key, "N", out Guid temp))
{
Dictionary<string, Json.JSONData> sessionItem = sessionItems[key].Properties;
if (sessionItem.ContainsKey("displayName")
&& sessionItem.ContainsKey("accessToken")
&& sessionItem.ContainsKey("username")
&& sessionItem.ContainsKey("uuid"))
{
string login = Settings.ToLowerIfNeed(sessionItem["username"].StringValue);
try
{
SessionToken session = SessionToken.FromString(String.Join(",",
sessionItem["accessToken"].StringValue,
sessionItem["displayName"].StringValue,
sessionItem["uuid"].StringValue.Replace("-", ""),
clientID
));
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID));
sessions[login] = session;
}
catch (InvalidDataException) { /* Not a valid session */ }
}
}
}
}
}
//Serialized session cache file in binary format
if (File.Exists(SessionCacheFileSerialized))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_converting, SessionCacheFileSerialized));
try
{
using FileStream fs = new(SessionCacheFileSerialized, FileMode.Open, FileAccess.Read, FileShare.Read);
#pragma warning disable SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete
// Possible risk of information disclosure or remote code execution. The impact of this vulnerability is limited to the user side only.
Dictionary<string, SessionToken> sessionsTemp = (Dictionary<string, SessionToken>)formatter.Deserialize(fs);
#pragma warning restore SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete
foreach (KeyValuePair<string, SessionToken> item in sessionsTemp)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, item.Key, item.Value.ID));
sessions[item.Key] = item.Value;
}
}
catch (IOException ex)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail, ex.Message));
}
catch (SerializationException ex2)
{
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_malformed, ex2.Message));
}
}
//User-editable session cache file in text format
if (File.Exists(SessionCacheFilePlaintext))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFilePlaintext));
try
{
foreach (string line in FileMonitor.ReadAllLinesWithRetries(SessionCacheFilePlaintext))
{
if (!line.Trim().StartsWith("#"))
{
string[] keyValue = line.Split('=');
if (keyValue.Length == 2)
{
try
{
string login = Settings.ToLowerIfNeed(keyValue[0]);
SessionToken session = SessionToken.FromString(keyValue[1]);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID));
sessions[login] = session;
}
catch (InvalidDataException e)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string, keyValue[1], e.Message));
}
}
else if (Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line, line));
}
}
}
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
}
}
return sessions.Count > 0;
}
/// <summary>
/// Saves SessionToken's from SessionCache into cache file.
/// </summary>
private static void SaveToDisk()
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving, acceptnewlines: true);
List<string> sessionCacheLines = new()
{
"# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!",
"# Login=SessionID,PlayerName,UUID,ClientID,RefreshToken,ServerIDhash,ServerPublicKey"
};
foreach (KeyValuePair<string, SessionToken> entry in sessions)
sessionCacheLines.Add(entry.Key + '=' + entry.Value.ToString());
try
{
FileMonitor.WriteAllLinesWithRetries(SessionCacheFilePlaintext, sessionCacheLines);
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message));
}
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Timers;
using static MinecraftClient.Settings;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig;
namespace MinecraftClient.Protocol.Session
{
/// <summary>
/// Handle sessions caching and storage.
/// </summary>
public static class SessionCache
{
private const string SessionCacheFilePlaintext = "SessionCache.ini";
private const string SessionCacheFileSerialized = "SessionCache.db";
private static readonly string SessionCacheFileMinecraft = String.Concat(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
Path.DirectorySeparatorChar,
".minecraft",
Path.DirectorySeparatorChar,
"launcher_profiles.json"
);
private static FileMonitor? cachemonitor;
private static readonly Dictionary<string, SessionToken> sessions = new();
private static readonly Timer updatetimer = new(100);
private static readonly List<KeyValuePair<string, SessionToken>> pendingadds = new();
private static readonly BinaryFormatter formatter = new();
/// <summary>
/// Retrieve whether SessionCache contains a session for the given login.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>TRUE if session is available</returns>
public static bool Contains(string login)
{
return sessions.ContainsKey(login);
}
/// <summary>
/// Store a session and save it to disk if required.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <param name="session">User session token used with Minecraft.net</param>
public static void Store(string login, SessionToken session)
{
if (Contains(login))
{
sessions[login] = session;
}
else
{
sessions.Add(login, session);
}
if (Config.Main.Advanced.SessionCache == CacheType.disk && updatetimer.Enabled == true)
{
pendingadds.Add(new KeyValuePair<string, SessionToken>(login, session));
}
else if (Config.Main.Advanced.SessionCache == CacheType.disk)
{
SaveToDisk();
}
}
/// <summary>
/// Retrieve a session token for the given login.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>SessionToken for given login</returns>
public static SessionToken Get(string login)
{
return sessions[login];
}
/// <summary>
/// Initialize cache monitoring to keep cache updated with external changes.
/// </summary>
/// <returns>TRUE if session tokens are seeded from file</returns>
public static bool InitializeDiskCache()
{
cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, SessionCacheFilePlaintext, new FileSystemEventHandler(OnChanged));
updatetimer.Elapsed += HandlePending;
return LoadFromDisk();
}
/// <summary>
/// Reloads cache on external cache file change.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void OnChanged(object sender, FileSystemEventArgs e)
{
updatetimer.Stop();
updatetimer.Start();
}
/// <summary>
/// Called after timer elapsed. Reads disk cache and adds new/modified sessions back.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void HandlePending(object? sender, ElapsedEventArgs e)
{
updatetimer.Stop();
LoadFromDisk();
foreach (KeyValuePair<string, SessionToken> pending in pendingadds.ToArray())
{
Store(pending.Key, pending.Value);
pendingadds.Remove(pending);
}
}
/// <summary>
/// Reads cache file and loads SessionTokens into SessionCache.
/// </summary>
/// <returns>True if data is successfully loaded</returns>
private static bool LoadFromDisk()
{
//Grab sessions in the Minecraft directory
if (File.Exists(SessionCacheFileMinecraft))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading, Path.GetFileName(SessionCacheFileMinecraft)));
Json.JSONData mcSession = new(Json.JSONData.DataType.String);
try
{
mcSession = Json.ParseJson(File.ReadAllText(SessionCacheFileMinecraft));
}
catch (IOException) { /* Failed to read file from disk -- ignoring */ }
if (mcSession.Type == Json.JSONData.DataType.Object
&& mcSession.Properties.ContainsKey("clientToken")
&& mcSession.Properties.ContainsKey("authenticationDatabase"))
{
string clientID = mcSession.Properties["clientToken"].StringValue.Replace("-", "");
Dictionary<string, Json.JSONData> sessionItems = mcSession.Properties["authenticationDatabase"].Properties;
foreach (string key in sessionItems.Keys)
{
if (Guid.TryParseExact(key, "N", out Guid temp))
{
Dictionary<string, Json.JSONData> sessionItem = sessionItems[key].Properties;
if (sessionItem.ContainsKey("displayName")
&& sessionItem.ContainsKey("accessToken")
&& sessionItem.ContainsKey("username")
&& sessionItem.ContainsKey("uuid"))
{
string login = Settings.ToLowerIfNeed(sessionItem["username"].StringValue);
try
{
SessionToken session = SessionToken.FromString(String.Join(",",
sessionItem["accessToken"].StringValue,
sessionItem["displayName"].StringValue,
sessionItem["uuid"].StringValue.Replace("-", ""),
clientID
));
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID));
sessions[login] = session;
}
catch (InvalidDataException) { /* Not a valid session */ }
}
}
}
}
}
//Serialized session cache file in binary format
if (File.Exists(SessionCacheFileSerialized))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_converting, SessionCacheFileSerialized));
try
{
using FileStream fs = new(SessionCacheFileSerialized, FileMode.Open, FileAccess.Read, FileShare.Read);
#pragma warning disable SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete
// Possible risk of information disclosure or remote code execution. The impact of this vulnerability is limited to the user side only.
Dictionary<string, SessionToken> sessionsTemp = (Dictionary<string, SessionToken>)formatter.Deserialize(fs);
#pragma warning restore SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete
foreach (KeyValuePair<string, SessionToken> item in sessionsTemp)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, item.Key, item.Value.ID));
sessions[item.Key] = item.Value;
}
}
catch (IOException ex)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail, ex.Message));
}
catch (SerializationException ex2)
{
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_malformed, ex2.Message));
}
}
//User-editable session cache file in text format
if (File.Exists(SessionCacheFilePlaintext))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFilePlaintext));
try
{
foreach (string line in FileMonitor.ReadAllLinesWithRetries(SessionCacheFilePlaintext))
{
if (!line.Trim().StartsWith("#"))
{
string[] keyValue = line.Split('=');
if (keyValue.Length == 2)
{
try
{
string login = Settings.ToLowerIfNeed(keyValue[0]);
SessionToken session = SessionToken.FromString(keyValue[1]);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID));
sessions[login] = session;
}
catch (InvalidDataException e)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string, keyValue[1], e.Message));
}
}
else if (Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line, line));
}
}
}
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
}
}
return sessions.Count > 0;
}
/// <summary>
/// Saves SessionToken's from SessionCache into cache file.
/// </summary>
private static void SaveToDisk()
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving, acceptnewlines: true);
List<string> sessionCacheLines = new()
{
"# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!",
"# Login=SessionID,PlayerName,UUID,ClientID,RefreshToken,ServerIDhash,ServerPublicKey"
};
foreach (KeyValuePair<string, SessionToken> entry in sessions)
sessionCacheLines.Add(entry.Key + '=' + entry.Value.ToString());
try
{
FileMonitor.WriteAllLinesWithRetries(SessionCacheFilePlaintext, sessionCacheLines);
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message));
}
}
}
}