diff --git a/MinecraftClient/config/ChatBots/DiscordWebhook.cs b/MinecraftClient/config/ChatBots/DiscordWebhook.cs
new file mode 100644
index 00000000..42a14c66
--- /dev/null
+++ b/MinecraftClient/config/ChatBots/DiscordWebhook.cs
@@ -0,0 +1,732 @@
+//MCCScript 1.0
+//using System.Collections.Specialized;
+
+MCC.LoadBot(new DiscordWebhook());
+
+//MCCScript Extensions
+///
+/// Stores all settings for the script.
+///
+class WebhoookSettings
+{
+ #region All variables for the main class
+ public string WebhookURL { get; set; }
+ public int SecondsToSaveInCache { get; set; }
+ public bool SendPrivateMsg { get; set; }
+ public bool CustomChatDetection { get; set; }
+ public bool SendServerMsg { get; set; }
+ public bool GetUUIDDirectlyFromMojang { get; set; }
+ public bool Togglesending { get; set; }
+ public bool AllowMentions { get; set; }
+ public bool NormalChatDetection { get; set; }
+ private Dictionary> messageCache = new Dictionary>();
+ private Dictionary messageContains = new Dictionary();
+ private Dictionary messageFrom = new Dictionary();
+ private List ignoredPlayers = new List();
+ #endregion
+
+ #region All variables for the API class
+ private string currentSkinMode;
+ private int size;
+ private int scale;
+ private bool overlay;
+ private bool checkUUID;
+ private string fallbackSkin;
+
+ private Dictionary skinModes = new Dictionary();
+ public string CurrentSkinMode
+ {
+ set { if (skinModes.ContainsKey(value)) { currentSkinMode = value; } }
+ get { return currentSkinMode; }
+ }
+ public int Size
+ {
+ set { if (value <= 512) { size = value; } }
+ get { return size; }
+ }
+ public int Scale
+ {
+ set { if (value <= 10) { this.scale = value; } }
+ get { return scale; }
+ }
+ public bool Overlay
+ {
+ set { overlay = value; }
+ get { return overlay; }
+ }
+ public bool CheckUUID
+ {
+ set { checkUUID = value; }
+ get { return checkUUID; }
+ }
+ public string FallbackSkin
+ {
+ set { if (value == "MHF_Steve" || value == "MHF_Alex") { fallbackSkin = value; } }
+ get { return fallbackSkin; }
+ }
+ #endregion
+
+ ///
+ /// Setup standard settings.
+ ///
+ public WebhoookSettings()
+ {
+ // Set preconfigured skinModes
+ skinModes.Add("flatFace", "https://crafatar.com/avatars/{0}");
+ skinModes.Add("cubeHead", "https://crafatar.com/renders/head/{0}");
+ skinModes.Add("fullSkin", "https://crafatar.com/renders/body/{0}");
+
+ // Define standard values for main class
+ SendPrivateMsg = true;
+ CustomChatDetection = true;
+ SendServerMsg = true;
+ GetUUIDDirectlyFromMojang = false;
+ NormalChatDetection = true;
+ Togglesending = true;
+ checkUUID = true;
+ AllowMentions = false;
+ currentSkinMode = "flatFace";
+ SecondsToSaveInCache = 10;
+
+ // Define standard values for API class
+ size = 100;
+ scale = 4;
+ overlay = true;
+ fallbackSkin = "MHF_Steve";
+ }
+
+ public Dictionary GetMessageContains() { return this.messageContains; }
+ public void SetMessageContains(Dictionary value) { this.messageContains = value; }
+
+ public Dictionary GetMessageFrom() { return this.messageFrom; }
+ public void SetMessageFrom(Dictionary value) { this.messageFrom = value; }
+
+ public Dictionary> GetCachedMessages() { return this.messageCache; }
+ public void SetCachedMessages(Dictionary> value) { this.messageCache = value; }
+
+ public Dictionary GetSkinModes() { return this.skinModes; }
+
+ public List GetIgnoredPlayers() { return ignoredPlayers; }
+}
+
+class SkinAPI
+{
+ WebhoookSettings settings = new WebhoookSettings();
+
+ public SkinAPI(WebhoookSettings s)
+ {
+ settings = s;
+ }
+
+ ///
+ /// Sends a request with the minecraft name to the mojang servers to optain its UUID.
+ /// Fails if there are invalid names, due to titles getting in it.
+ ///
+ /// Minecraft IGN
+ ///
+ public string GetUUIDFromMojang(string name)
+ {
+ WebClient wc = new WebClient();
+ try
+ {
+ return Json.ParseJson(wc.DownloadString("https://api.mojang.com/users/profiles/minecraft/" + name)).Properties["id"].StringValue;
+ }
+ catch (Exception) { return "00000000000000000000000000000000"; }
+ }
+
+ ///
+ /// Gets the UUID from the internal bot command, which is faster and should be safer on servers with titles.
+ /// Fails on cracked servers.
+ ///
+ /// Minecraft IGN
+ /// Dictionary of UUID's matched with the playername.
+ ///
+ public string GetUUIDFromPlayerList(string name, Dictionary playerList)
+ {
+ foreach (KeyValuePair player in playerList)
+ {
+ if (name.ToLower().Contains(player.Value.ToLower()))
+ {
+ return player.Key.Replace("-", "");
+ }
+ }
+
+ // Falback if player leaves the server.
+ return GetUUIDFromMojang(name);
+ }
+
+ ///
+ /// Creates the url which is forewarded to the webhook.
+ ///
+ /// Player UUID
+ ///
+ public string GetSkinURLCrafatar(string UUID)
+ {
+ string parameters = string.Join("&", "size=" + settings.Size, "scale=" + settings.Scale, "default=" + settings.FallbackSkin, (settings.Overlay ? "overlay" : ""));
+ return string.Format(settings.GetSkinModes()[settings.CurrentSkinMode], UUID + "?" + parameters);
+ }
+}
+
+///
+/// Stores messages in a uniform format.
+///
+class Message
+{
+ private string senderName;
+ private string senderUUID;
+ private string content;
+ private DateTime time;
+
+ ///
+ /// Initialize a Message.
+ ///
+ /// Player IGN
+ /// Player UUID
+ /// Message content
+ ///
+ public Message(string sN, string sU, string c, DateTime t)
+ {
+ senderName = sN;
+ senderUUID = sU;
+ content = c;
+ time = t;
+ }
+
+ public string SenderName
+ {
+ get { return senderName; }
+ }
+ public string SenderUUID
+ {
+ get { return senderUUID; }
+ }
+ public DateTime Time
+ {
+ get { return time; }
+ }
+ public string Content
+ {
+ get { return content; }
+ set { content = value; }
+ }
+}
+
+///
+/// Caches messages, until it is emptied by update(), or
+/// a message from another user is entered.
+///
+class MessageCache
+{
+ private WebhoookSettings settings;
+ private Message msg;
+ public Message Msg { get { return msg; } }
+
+ public MessageCache(WebhoookSettings s)
+ {
+ msg = null;
+ settings = s;
+ }
+
+ ///
+ /// Add a Message to cache.
+ /// Current message gets appended, if the new message is from the same sender.
+ /// Otherwise the current message is given back and the new message is saved.
+ ///
+ /// New message to add.
+ /// Current saved message, if the new message is from another player.
+ public Message Add(Message newMsg)
+ {
+ if (msg == null)
+ {
+ msg = newMsg;
+ return null;
+ }
+ else
+ {
+ if (((msg.SenderUUID == newMsg.SenderUUID && settings.CheckUUID)
+ || (msg.SenderName == newMsg.SenderName && !settings.CheckUUID))
+ && msg.Content.Length + newMsg.Content.Length <= 2000)
+ {
+ msg.Content += "\n" + newMsg.Content;
+ return null;
+ }
+ else
+ {
+ Message temp = msg;
+ msg = newMsg;
+ return temp;
+ }
+ }
+ }
+
+ ///
+ /// Clears the cache.
+ ///
+ /// Current message
+ public Message Clear()
+ {
+ Message temp = msg;
+ msg = null;
+ return temp;
+ }
+}
+
+class HTTP
+{
+ public static byte[] Post(string url, NameValueCollection pairs)
+ {
+ using (WebClient webClient = new WebClient())
+ {
+ return webClient.UploadValues(url, pairs);
+ }
+ }
+}
+
+class DiscordWebhook : ChatBot
+{
+ private WebhoookSettings settings = new WebhoookSettings();
+ private SkinAPI sAPI;
+ private MessageCache cache;
+
+ public DiscordWebhook()
+ {
+ sAPI = new SkinAPI(settings);
+ cache = new MessageCache(settings);
+ }
+
+ public override void Initialize()
+ {
+ LogToConsole("Made by Daenges.\nSpecial thanks to Crafatar for providing the beautiful avatars!");
+ LogToConsole("Please set a Webhook with '/dw changeurl [URL]'. For further information type '/discordwebhook help'.");
+ RegisterChatBotCommand("discordWebhook", "/DiscordWebhook 'size', 'scale', 'fallbackSkin', 'overlay', 'skintype'", GetHelp(), CommandHandler);
+ RegisterChatBotCommand("dw", "/DiscordWebhook 'size', 'scale', 'fallbackSkin', 'overlay', 'skintype'", GetHelp(), CommandHandler);
+ }
+
+ public override void Update()
+ {
+ if (cache.Msg != null && (DateTime.Now - cache.Msg.Time).TotalSeconds >= settings.SecondsToSaveInCache)
+ {
+ SendWebhook(cache.Clear());
+ LogDebugToConsole("Cleared cache!");
+ }
+ }
+
+ public override void GetText(string text)
+ {
+ if (settings.Togglesending)
+ {
+ string message = "";
+ string username = "";
+ text = settings.AllowMentions ? GetVerbatim(text) : GetVerbatim(text).Replace("@", "[at]");
+ Message msg = null;
+
+ if (IsChatMessage(text, ref message, ref username) && settings.NormalChatDetection)
+ {
+ if (!settings.GetIgnoredPlayers().Contains(username))
+ {
+ msg = cache.Add(new Message(username,
+ settings.CheckUUID ? settings.GetUUIDDirectlyFromMojang ? sAPI.GetUUIDFromMojang(username) : sAPI.GetUUIDFromPlayerList(username, GetOnlinePlayersWithUUID()) : "00000000000000000000000000000000",
+ message,
+ DateTime.Now));
+ }
+
+ }
+ else if (IsPrivateMessage(text, ref message, ref username) && settings.SendPrivateMsg)
+ {
+ if (!settings.GetIgnoredPlayers().Contains(username))
+ {
+ msg = cache.Add(new Message(username,
+ settings.CheckUUID ? settings.GetUUIDDirectlyFromMojang ? sAPI.GetUUIDFromMojang(username) : sAPI.GetUUIDFromPlayerList(username, GetOnlinePlayersWithUUID()) : "00000000000000000000000000000000",
+ message,
+ DateTime.Now));
+ }
+ }
+ else if (text.Contains(":") && settings.CustomChatDetection) // Some servers have strange chat formats.
+ {
+ var messageArray = text.Split(new[] { ':' }, 2);
+ if (!settings.GetIgnoredPlayers().Contains(messageArray[0]))
+ {
+ msg = cache.Add(new Message(messageArray[0],
+ settings.CheckUUID ? settings.GetUUIDDirectlyFromMojang ? sAPI.GetUUIDFromMojang(messageArray[0]) : sAPI.GetUUIDFromPlayerList(messageArray[0], GetOnlinePlayersWithUUID()) : "00000000000000000000000000000000",
+ messageArray[1],
+ DateTime.Now));
+ }
+ }
+ else if (settings.SendServerMsg)
+ {
+ msg = cache.Add(new Message("[Server]",
+ "",
+ text,
+ DateTime.Now));
+
+ }
+ if (msg == null)
+ {
+ LogDebugToConsole("Saved message to cache.");
+ }
+ else { SendWebhook(msg); }
+ }
+ }
+
+ ///
+ /// Appends pings to the discord message content, if
+ /// given keywords are included.
+ ///
+ /// Minecraft IGN
+ /// Message content
+ /// Message plus appended pings
+ public string AddPingsToMessage(string username, string msg)
+ {
+ string pings = "";
+ if (settings.GetMessageContains().Count > 0)
+ {
+ msg = new string(msg.Where(c => !char.IsPunctuation(c)).ToArray());
+
+ foreach (string word in msg.Split(' '))
+ {
+ if (settings.GetMessageContains().ContainsKey(word.ToLower())) { pings += string.Join(" ", settings.GetMessageContains()[word.ToLower()]); }
+ }
+ }
+ if (settings.GetMessageFrom().ContainsKey(username.ToLower()))
+ {
+ pings += settings.GetMessageFrom()[username.ToLower()];
+ }
+ return pings;
+ }
+
+ ///
+ /// Sends the message to the discord webhook.
+ ///
+ /// Message that will be sent.
+ public void SendWebhook(Message msg)
+ {
+ msg.Content += " " + AddPingsToMessage(msg.SenderName, msg.Content);
+
+ if (settings.WebhookURL != "" && settings.WebhookURL != null)
+ {
+ LogDebugToConsole("Send webhook request to Discord.");
+ try
+ {
+ HTTP.Post(settings.WebhookURL, new NameValueCollection()
+ {
+ {
+ "username",
+ msg.SenderName
+ },
+ {
+ "content",
+ msg.Content
+ },
+ {
+ "avatar_url",
+ msg.SenderName == "[Server]" ? sAPI.GetSkinURLCrafatar("f78a4d8dd51b4b3998a3230f2de0c670") : sAPI.GetSkinURLCrafatar(msg.SenderUUID)
+ }
+ }
+ );
+ }
+ catch (Exception e)
+ {
+ LogToConsole("An error occured while posting messages to Discord! (Enable Debug to view it.)");
+ LogDebugToConsole(string.Format("Requested Link {0}; Username {1}; message: {2}; error: {3}",
+ msg.SenderName == "[Server]" ? sAPI.GetSkinURLCrafatar("f78a4d8dd51b4b3998a3230f2de0c670") : sAPI.GetSkinURLCrafatar(msg.SenderUUID), msg.SenderName, msg.Content, e.ToString()));
+ }
+ }
+ else
+ {
+ LogToConsole("No webhook link provided. Please enter one with '/discordwebhook changeurl [link]'");
+ }
+ }
+
+ ///
+ /// Help Page
+ ///
+ /// Help Page
+ public string GetHelp()
+ {
+ return "/discordWebhook or /dw 'avatar', 'secondstosaveincache', " +
+ "'ping', 'uuidfrommojang'," +
+ " 'normalchatdetection', 'customchatdetection'," +
+ " 'toggleignored', 'checkuuid'," +
+ " 'sendprivate', 'changeurl'," +
+ " 'togglesending', 'allowmentions'," +
+ " 'onlyprivate', 'help'," +
+ " 'getsettings', 'quit'";
+ }
+
+ ///
+ /// Returns an array of strings, which are in quotes.
+ /// "\"Hello\" \"There\"" => ["Hello", "There"]
+ ///
+ /// Raw string
+ /// Array with words in quotes.
+ public List GetStringsInQuotes(string rawData)
+ {
+ List result = new List { "" };
+ int currentResultPos = 0;
+ bool startCopy = false;
+
+ foreach (char c in rawData)
+ {
+ if (c == '\"')
+ {
+ if (startCopy) { result[currentResultPos] = result[currentResultPos].Replace("\"", ""); currentResultPos++; result.Add(""); }
+ startCopy = !startCopy;
+ }
+ if (startCopy)
+ {
+ result[currentResultPos] += c.ToString();
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Handles all commands.
+ ///
+ /// Whole command
+ /// Only arguments
+ ///
+ public string CommandHandler(string cmd, string[] args)
+ {
+ if (args.Length > 0)
+ {
+ switch (args[0])
+ {
+ case "avatar":
+ if (args.Length > 1)
+ {
+ if (args[1] == "size")
+ {
+
+ try
+ {
+ settings.Size = int.Parse(args[2]);
+ return "Changed avatarsize to " + args[2] + " pixel.";
+ }
+ catch (Exception)
+ {
+ return "That was not a number.";
+ }
+ }
+
+ if (args[1] == "scale")
+ {
+ try
+ {
+ settings.Scale = int.Parse(args[2]);
+ return "Changed scale to " + args[2] + ".";
+ }
+ catch (Exception)
+ {
+ return "That was not a number.";
+ }
+ }
+
+ if (args[1] == "fallbackskin")
+ {
+ settings.FallbackSkin = settings.FallbackSkin == "MHF_Steve" ? "MHF_Alex" : "MHF_Steve";
+ return "Changed fallback skin to: " + settings.FallbackSkin;
+ }
+
+ if (args[1] == "overlay")
+ {
+ settings.Overlay = !settings.Overlay;
+ return "Changed the overlay to: " + settings.Overlay;
+ }
+
+ if (args[1] == "skintype")
+ {
+ if (args.Length > 2)
+ {
+ if (settings.GetSkinModes().ContainsKey(args[2]))
+ {
+ settings.CurrentSkinMode = args[2];
+ return "Changed skin mode to " + args[2];
+ }
+ else
+ {
+ return "This mode does not exsist. ('flatFace', 'cubeHead', 'fullSkin')";
+ }
+ }
+ else
+ {
+ return "Enter a value! ('flatFace', 'cubeHead', 'fullSkin')";
+ }
+ }
+ }
+ return "This was not a valid option! Try '/dw avatar size/scale/skintype [value]' or '/dw avatar overlay/fallbackskin'.";
+
+ case "secondstosaveincache":
+ try
+ {
+ settings.SecondsToSaveInCache = int.Parse(args[1]);
+ return "Changed the maximum time for a message in cache to " + args[1] + " seconds.";
+ }
+ catch (Exception)
+ {
+ return "That was not a number.";
+ }
+
+ case "ping":
+ if (args.Length > 1)
+ {
+ if (args[1] == "message")
+ {
+ if (args[2] == "add")
+ {
+ List tempList = GetStringsInQuotes(string.Join(" ", args));
+ if (tempList.Count >= 2)
+ {
+ settings.GetMessageContains().Add(tempList[0].ToLower(), string.Join(" ", tempList[1]));
+ return "Added " + tempList[0].ToLower() + " " + string.Join(" ", tempList[1]);
+ }
+ else
+ {
+ return "Too many arguments";
+ }
+
+ }
+ else
+ {
+ List tempList = GetStringsInQuotes(string.Join(" ", args));
+ if (settings.GetMessageContains().ContainsKey(tempList[0].ToLower()))
+ {
+ settings.GetMessageContains().Remove(tempList[0].ToLower());
+ return "Removed " + tempList[0].ToLower();
+ }
+ else
+ {
+ return "This key does not exsist.";
+ }
+ }
+ }
+ if (args[1] == "sender")
+ {
+ if (args[2] == "add")
+ {
+ List tempList = GetStringsInQuotes(string.Join(" ", args));
+ if (tempList.Count >= 2)
+ {
+ settings.GetMessageFrom().Add(tempList[0].ToLower(), string.Join(" ", tempList[1]));
+ return "Added " + tempList[0].ToLower() + " " + string.Join(" ", tempList[1]);
+ }
+ else
+ {
+ return "Too many arguments";
+ }
+
+ }
+ else
+ {
+ List tempList = GetStringsInQuotes(string.Join(" ", args));
+ if (settings.GetMessageFrom().ContainsKey(tempList[0].ToLower()))
+ {
+ settings.GetMessageFrom().Remove(tempList[0].ToLower());
+ return "Removed " + tempList[0].ToLower();
+ }
+ else
+ {
+ return "This key does not exsist.";
+ }
+ }
+ }
+ else
+ {
+ return "This is not a valid option. /discordwebhook ping message/sender add/remove \"Keywords in message\" \"@here <@DiscordID> <@&RoleID>\"";
+ }
+ }
+ else { return "This is not a valid option. /discordwebhook ping message/sender add/remove \"Keywords in message\" \"@here <@DiscordID> <@&RoleID>\""; }
+
+ case "uuidfrommojang":
+ settings.GetUUIDDirectlyFromMojang = !settings.GetUUIDDirectlyFromMojang;
+ return "Getting UUID's from Mojang: " + settings.GetUUIDDirectlyFromMojang.ToString();
+
+ case "checkuuid":
+ settings.CheckUUID = !settings.CheckUUID;
+ return "Getting UUID's: " + settings.CheckUUID.ToString();
+
+ case "sendprivate":
+ settings.SendPrivateMsg = !settings.SendPrivateMsg;
+ return "Send private messages: " + settings.SendPrivateMsg.ToString();
+
+ case "allowmentions":
+ settings.AllowMentions = !settings.AllowMentions;
+ return "People can @Members: " + settings.AllowMentions.ToString();
+
+ case "normalchatdetection":
+ settings.NormalChatDetection = !settings.NormalChatDetection;
+ return "Detect messages with the regular chat detection: " + settings.NormalChatDetection.ToString();
+
+ case "customchatdetection":
+ settings.CustomChatDetection = !settings.CustomChatDetection;
+ return "Detect messages with the custom chat detection: " + settings.CustomChatDetection.ToString();
+
+ case "sendservermsg":
+ settings.SendServerMsg = !settings.SendServerMsg;
+ return "Server messages get forewarded: " + settings.SendServerMsg.ToString();
+
+ case "togglesending":
+ settings.Togglesending = !settings.Togglesending;
+ return "Forewarding messages to Discord: " + settings.Togglesending.ToString();
+
+ case "toggleignored":
+ if (args.Length >= 2)
+ {
+ if (settings.GetIgnoredPlayers().Contains(args[1]))
+ {
+ settings.GetIgnoredPlayers().Remove(args[1]);
+ return "Unignored: " + args[1];
+ }
+ else
+ {
+ settings.GetIgnoredPlayers().Add(args[1]);
+ return "Ignored: " + args[1];
+ }
+ }
+ else { return "Enter a playername."; }
+
+ case "changeurl":
+ if (args.Length > 1)
+ {
+ settings.WebhookURL = args[1];
+ return "Changed webhook URL to: " + args[1];
+ }
+ else
+ {
+ return "Enter a valid Discord Webhook link.";
+ }
+
+ case "getsettings":
+ return "WebhookURL: " + settings.WebhookURL + "\r\n" +
+ "SecondsToSaveInCache: " + settings.SecondsToSaveInCache.ToString() + "\r\n" +
+ "SendPrivateMsg: " + settings.SendPrivateMsg.ToString() + "\r\n" +
+ "SendPublicMsg: " + settings.CustomChatDetection.ToString() + "\r\n" +
+ "SendServerMsg: " + settings.SendServerMsg.ToString() + "\r\n" +
+ "GetUUIDDirectlyFromMojang: " + settings.GetUUIDDirectlyFromMojang.ToString() + "\r\n" +
+ "ToggleSending: " + settings.Togglesending.ToString() + "\r\n" +
+ "AllowMentions: " + settings.AllowMentions.ToString() + "\r\n" +
+ "NormalChatDetection: " + settings.NormalChatDetection.ToString() + "\r\n" +
+ "CustomChatDetection: " + settings.CustomChatDetection.ToString() + "\r\n" +
+ "CurrentSkinMode: " + settings.CurrentSkinMode + "\r\n" +
+ "Size: " + settings.Size.ToString() + "\r\n" +
+ "Scale: " + settings.Scale.ToString() + "\r\n" +
+ "Overlay: " + settings.Overlay.ToString() + "\r\n" +
+ "CheckUUID: " + settings.CheckUUID.ToString() + "\r\n" +
+ "IgnoredPlayers: " + string.Join(" ;", settings.GetIgnoredPlayers()) + "\r\n" +
+ "FallbackSkin: " + settings.FallbackSkin;
+
+ case "help":
+ return GetHelp();
+
+ case "quit":
+ UnloadBot();
+ return "Turned discordwebhook off!";
+
+ default:
+ return GetHelp();
+ }
+ }
+ else { return GetHelp(); }
+ }
+}