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(); } + } +}