Giveaway Helper

Enhances Steam key-related giveaways

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name Giveaway Helper
// @namespace https://github.com/Citrinate/giveawayHelper
// @description Enhances Steam key-related giveaways
// @author Citrinate
// @version 2.12.9
// @match *://*.chubbykeys.com/giveaway.php*
// @match *://*.bananagiveaway.com/giveaway/*
// @match *://*.dogebundle.com/index.php?page=redeem&id=*
// @match *://*.dupedornot.com/giveaway*
// @match *://*.embloo.net/task/*
// @match *://*.gamecode.win/giveaway/*
// @match *://*.gamehag.com/giveaway/*
// @match *://*.gleam.io/*
// @match *://*.grabfreegame.com/giveaway/*
// @match *://*.hrkgame.com/en/giveaway/get-free-game/
// @match *://*.keychampions.net/view.php?gid=*
// @match *://*.marvelousga.com/giveaway/*
// @match *://*.prys.ga/giveaway/?id=*
// @match *://*.simplo.gg/index.php?giveaway=*
// @match *://*.steamfriends.info/free-steam-key/
// @match *://*.treasuregiveaways.com/*.php*
// @match *://*.whosgamingnow.net/giveaway/*
// @connect steamcommunity.com
// @connect steampowered.com
// @connect twitter.com
// @connect twitch.tv
// @match https://syndication.twitter.com/
// @match https://player.twitch.tv/
// @grant GM_getValue
// @grant GM.getValue
// @grant GM_setValue
// @grant GM.setValue
// @grant GM_deleteValue
// @grant GM.deleteValue
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/md5.js
// @run-at document-end
// ==/UserScript==

(function() {

	/**
	 *
	 */
	var setup = (function() {
		return {
			/**
			 * Determine what to do for this page based on what's defined in the "config" variable
			 *
			 * 		hostname: A string
			 *			The hostname of the site we're setting the config for. Must be the same as what's defined
			 *			as @match in the metadata block above.
			 *
			 *		helper: An object
			 * 			The class which will determine how the do/undo buttons are added to the page. Usually this will
			 *			be set to basicHelper, which simply searches for links to Steam Groups and adds buttons for
			 *			them at the top of the page.
			 *
			 *		domMatch: An array of strings
			 *			In some cases, we don't know what page a giveaway will be on.  For example, Indiegala embeds
			 *			giveaways on various parts of their site which they want to attract attention to.  Instead we
			 *			need to search the page for a DOM element that only appears when there is a giveaway on that
			 *			page. If any of the elements in this array match, then the script will be run on this page.
			 *
			 *		urlMatch: An array of regular expressions
			 *			Used in conjunction with domMatch.  Used for pages on the domain that we do know are relevant
			 *			to giveaways, and we always want to run the script on.  For example, the giveaway confirmation
			 *			page on Indiegala.  The regular expressions will be tested against the url of the pages, and if
			 *			any of them match, the script will be run on this page.
			 *
			 *		cache: Boolean
			 *			For use with basicHelper.  Some sites will remove links to Steam groups after the entry has
			 *			been completed.  Set this to true so that any groups we find will be saved and presented later.
			 *
			 *		offset: Array of integers
			 *			For use with basicHelper.  Used to correct instances where the script's UI blocks parts of a
			 *			site.  Offsets the UI by X number of pixels in the order of [top, left, right].
			 *			Directions that shouldn't be offset should be set to 0.
			 *
			 *      zIndex: Integer
			 *          For use with basicHelper.  Used to correct instances where the site's UI might overlay the
			 *          the script's UI and will be blocked by it.
			 *
			 *		requires: An object: {twitch: Boolean}
			 *			For use with basicHelper.  Some sites may have links asking you to follow a twitch channel, but
			 *			don't verify that you've done so.  In these cases there's no need to display a "follow/unfollow"
			 *			button.  For sites that do verify, set the value to true.
			 *
			 *		redirect_urls: A function which returns a jQuery object
			 *			For use with basicHelper.  Used on sites which may hide URLs behind a redirection link.
			 *			The jQuery object should contain the anchors that contain these links, and should be specific
			 *			enough so that it only contains links we know must be resolved.
			 *
			 *		redirect_url_extract: A function which returns a string
			 *			For use with basicHelper and redirect_urls.  Used in instances where redirections are used, but
			 *			the links can't be found within anchors.  This function is used to extract the url from whatever
			 *			elements the redirect_urls function returns.
			 *
			 *		onLoad: A function
			 *			For use with basicHelper.  A function that executes after the page loads.
			 *
			 */
			run: function() {
				var found = false,
					config = [
						{
							hostname: "chubbykeys.com",
							helper: basicHelper,
							cache: false
						},
						{
							hostname: "bananagiveaway.com",
							helper: basicHelper,
							cache: true,
							redirect_urls: function() {
								return $("li:contains('Join')")
									.find("button:nth-child(1)");
							},
							redirect_url_extract: function(element) {
								return element.attr("onclick").replace("window.open('", "").replace("')", "");
							}
						},
						{
							hostname: "dogebundle.com",
							helper: basicHelper,
							cache: true,
							offset: [50, 0, 0]
						},
						{
							hostname: "dupedornot.com",
							helper: basicHelper,
							cache: false,
							requires: {twitch: true}
						},
						{
							hostname: "embloo.net",
							helper: basicHelper,
							cache: true
						},
						{
							hostname: "gamecode.win",
							helper: basicHelper,
							cache: true,
							requires: {twitch: true}
						},
						{
							hostname: "gamehag.com",
							helper: basicHelper,
							cache: true,
							offset: [80, 0, 300],
							zIndex: 80,
							redirect_urls: function() {
								return $(".element-list .task-content:contains('Steam Community group')")
									.find("a[href*='/giveaway/click/']");
							}
						},
						{
							hostname: "gleam.io",
							helper: gleamHelper,
							cache: false
						},
						{
							hostname: "grabfreegame.com",
							helper: basicHelper,
							cache: true,
							offset: [56, 0, 0],
							redirect_urls: function() {
								return $("li p:contains('Steam Group')").parent()
									.find("button:contains('To do')");
							},
							redirect_url_extract: function(element) {
								return element.attr("onclick").replace("window.open('", "").replace("')", "");
							}
						},
						{
							hostname: "hrkgame.com",
							helper: basicHelper,
							cache: false
						},
						{
							hostname: "keychampions.net",
							helper: basicHelper,
							cache: true,
							offset: [0, 120, 0]
						},
						{
							hostname: "marvelousga.com",
							helper: basicHelper,
							cache: false,
							zIndex: 1,
							requires: {twitch: true}
						},
						{
							hostname: "prys.ga",
							helper: basicHelper,
							cache: false,
							offset: [50, 0, 0],
							zIndex: 1029
						},
						{
							hostname: "simplo.gg",
							helper: basicHelper,
							cache: true
						},
						{
							hostname: "steamfriends.info",
							helper: basicHelper,
							cache: false
						},
						{
							hostname: "treasuregiveaways.com",
							helper: basicHelper,
							cache: true,
							offset: [50, 0, 0],
							zIndex: 1029
						},
						{
							hostname: "whosgamingnow.net",
							helper: basicHelper,
							cache: true
						}
					];

				for(var i = 0; i < config.length; i++) {
					var site = config[i];

					if(document.location.hostname.split(".").splice(-2).join(".") == site.hostname) {
						found = true;

						// determine whether to run the script based on the content of the page
						if(typeof site.domMatch !== "undefined" ||
							typeof site.urlMatch !== "undefined"
						) {
							var match_found = false;

							// check the DOM for matches as defined by domMatch
							if(typeof site.domMatch !== "undefined") {
								for(var k = 0; k < site.domMatch.length; k++) {
									if($(site.domMatch[k]).length !== 0) {
										match_found = true;
										break;
									}
								}
							}

							// check the URL for matches as defined by urlMatch
							if(typeof site.urlMatch !== "undefined") {
								for(var l = 0; l < site.urlMatch.length; l++) {
									var reg = new RegExp(site.urlMatch[l]);

									if(reg.test(location.href)) {
										match_found = true;
										break;
									}
								}
							}

							if(!match_found) break;
						}

						giveawayHelperUI.loadUI(site.zIndex, site.onLoad);
						site.helper.init(site.cache, site.cache_id, site.offset, site.requires, site.redirect_urls,
							site.redirect_url_extract);
					}
				}

				if(!found) {
					commandHub.init();
				}
			}
		};
	})();

	/**
	 *
	 */
	var gleamHelper = (function() {
		var gleam = null,
			authentications = { steam: false, twitter: false, twitch: false };

		/**
		 * Check to see what accounts the user has linked to gleam
		 */
		function checkAuthentications() {
			if(gleam.contestantState.contestant.authentications) {
				var authentication_data = gleam.contestantState.contestant.authentications;

				for(var i = 0; i < authentication_data.length; i++) {
					var current_authentication = authentication_data[i];
					authentications[current_authentication.provider == "twitchtv" ? "twitch" : current_authentication.provider] = current_authentication;
				}
			}
		}

		/**
		 * Decide what to do for each of the entries
		 */
		function handleEntries() {
			var entries = $(".entry-method");

			for(var i = 0; i < entries.length; i++) {
				var entry_element = entries[i],
					entry = unsafeWindow.angular.element(entry_element).scope();

				switch(entry.entry_method.entry_type) {
					case "steam_join_group":
						createSteamButton(entry, entry_element);
						break;

					case "twitter_follow":
					case "twitter_retweet":
					case "twitter_tweet":
					case "twitter_hashtags":
						//createTwitterButton(entry, entry_element);
						break;

					case "twitchtv_follow":
						createTwitchButton(entry, entry_element);
						break;

					default:
						break;
				}
			}
		}

		/**
		 *
		 */
		function handleReward() {
			var temp_interval = setInterval(function() {
				if(gleam.bestCouponCode() !== null) {
					clearInterval(temp_interval);
					SteamHandler.getInstance().findKeys(addRedeemButton, gleam.bestCouponCode(), false);
				}
			}, 100);
		}

		/**
		 * Places the button onto the page
		 */
		function addButton(entry_element) {
			return function(new_button) {
				new_button.addClass("btn btn-embossed btn-info");
				$(entry_element).find(">a").first().append(new_button);
			};
		}

		/**
		 *
		 */
		function addRedeemButton(new_button) {
			new_button.find("button").first().addClass("btn btn-embossed btn-success");
			$(".redeem-container").first().after(new_button);
		}

		/**
		 * Returns true when an entry has been completed
		 */
		function isCompleted(entry) {
			return function() {
				return gleam.isEntered(entry.entry_method) && !gleam.canEnter(entry.entry_method);
			};
		}

		/**
		 *
		 */
		function createSteamButton(entry, entry_element) {
			SteamHandler.getInstance().handleEntry({
					group_name: entry.entry_method.config3.toLowerCase(),
					group_id: entry.entry_method.config4
				},
				addButton(entry_element),
				false,
				authentications.steam === false ? false : {
					user_id: authentications.steam.uid
				}
			);
		}

		/**
		 *
		 */
		function createTwitterButton(entry, entry_element) {
			// Don't do anything for a tweet entry that's already been completed
			if(isCompleted(entry)() &&
				(entry.entry_method.entry_type == "twitter_tweet" ||
					entry.entry_method.entry_type == "twitter_hashtags")) {

				return;
			}

			TwitterHandler.getInstance().handleEntry({
					action: entry.entry_method.entry_type,
					id: entry.entry_method.config1
				},
				addButton(entry_element),
				isCompleted(entry),
				false,
				authentications.twitter === false ? false : {
					user_id: authentications.twitter.uid,
					user_handle: authentications.twitter.reference
				}
			);
		}

		/**
		 *
		 */
		function createTwitchButton(entry, entry_element) {
			TwitchHandler.getInstance().handleEntry(
				entry.entry_method.config1,
				addButton(entry_element),
				isCompleted(entry),
				false,
				authentications.twitch === false ? false : {
					user_handle: authentications.twitch.reference
				}
			);
		}

		return {
			/**
			 *
			 */
			init: function() {
				MKY.addStyle(`
					.${giveawayHelperUI.gh_button} {
						bottom: 0px;
						height: 32px;
						margin: auto;
						padding: 6px;
						position: absolute;
						right: 64px;
						top: 0px;
						z-index: 9999999999;
					}

					.${giveawayHelperUI.gh_redeem_button} {
						margin-bottom: 32px;
						position: static;
					}
				`);

				// Show exact end date when hovering over any times
				$("[data-ends]").each(function() {
					$(this).attr("title", new Date(parseInt($(this).attr("data-ends")) * 1000));
				});

				// wait for gleam to finish loading
				var temp_interval = setInterval(function() {
					if($(".popup-blocks-container") !== null) {
						clearInterval(temp_interval);
						gleam = unsafeWindow.angular.element($(".popup-blocks-container").get(0)).scope();

						// wait for gleam to fully finish loading
						var another_temp_interval = setInterval(function() {
							if(typeof gleam.campaign.entry_count !== "undefined") {
								clearInterval(another_temp_interval);
								checkAuthentications();
								handleReward();

								if(!gleam.showPromotionEnded()) {
									handleEntries();
								}
							}
						}, 100);
					}
				}, 100);
			}
		};
	})();

	/**
	 *
	 */
	var basicHelper = (function() {
		return {
			/**
			 *
			 */
			init: function(do_cache, cache_id, offset, requires, redirect_urls, redirect_url_extract) {
				if(typeof do_cache !== "undefined" && do_cache) {
					if(typeof cache_id === "undefined") {
						cache_id = document.location.hostname + document.location.pathname + document.location.search;
					}

					cache_id = `cache_${CryptoJS.MD5(cache_id)}`;
				} else {
					do_cache = false;
				}

				giveawayHelperUI.defaultButtonSetup(offset);

				// Some sites load the giveaway data dynamically.  Check every second for changes
				setInterval(function() {
					// Add Steam buttons
					SteamHandler.getInstance().findGroups(
						giveawayHelperUI.addButton,
						$("body").html(),
						true,
						do_cache,
						cache_id
					);

					// Add Steam Key redeem buttons
					SteamHandler.getInstance().findKeys(giveawayHelperUI.addButton, $("body").html(), true);

					if(typeof requires !== "undefined") {
						if(typeof requires.twitch !== "undefined" && requires.twitch === true) {
							// Add Twitch buttons
							TwitchHandler.getInstance().findChannels(
								giveawayHelperUI.addButton,
								$("body").html(),
								true,
								do_cache,
								`twitch_${cache_id}`
							);
						}

						if(typeof requires.steam_curators !== "undefined" && requires.steam_curators === true) {
							// Add Steam Curator buttons
							SteamCuratorHandler.getInstance().findCurators(
								giveawayHelperUI.addButton,
								$("body").html(),
								true,
								do_cache,
								`steam_curators_${cache_id}`
							);
						}
					}

					// Check for redirects
					if(typeof redirect_urls !== "undefined") {
						redirect_urls().each(function() {
							var redirect_url;

							if(typeof redirect_url_extract !== "undefined") {
								redirect_url = redirect_url_extract($(this));
							} else {
								redirect_url = $(this).attr("href");
							}

							giveawayHelperUI.resolveUrl(redirect_url, function(url) {
								// Add Steam button
								SteamHandler.getInstance().findGroups(
									giveawayHelperUI.addButton,
									url,
									true,
									do_cache,
									cache_id
								);

								if(typeof requires !== "undefined") {
									if(typeof requires.twitch !== "undefined" && requires.twitch === true) {
										// Add Twitch button
										TwitchHandler.getInstance().findChannels(
											giveawayHelperUI.addButton,
											url,
											true,
											do_cache,
											`twitch_${cache_id}`
										);
									}

									if(typeof requires.steam_curators !== "undefined" && requires.steam_curators === true) {
										// Steam Curator buttons
										SteamCuratorHandler.getInstance().findCurators(
											giveawayHelperUI.addButton,
											url,
											true,
											do_cache,
											`steam_curators_${cache_id}`
										);
									}
								}
							});
						});
					}
				}, 1000);
			},
		};
	})();

	/**
	 * Handles Steam group buttons
	 */
	var SteamHandler = (function() {
		function init() {
			var re_group_name = /steamcommunity\.com\/groups\/([a-zA-Z0-9\-\_]{2,32})/g,
				re_group_id = /steamcommunity.com\/gid\/(([0-9]+)|\[g:[0-9]:([0-9]+)\])/g,
				re_steam_key = /([A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}|[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5})/g,
				redeem_key_url = "https://store.steampowered.com/account/registerkey?key=",
				user_id = null,
				session_id = null,
				process_url = null,
				active_groups = [],
				button_count = 1,
				handled_group_names = [],
				handled_group_ids = [],
				handled_keys = [],
				ready = false;

			// Get all the user data we'll need to make join/leave group requests
			MKY.xmlHttpRequest({
				url: "https://steamcommunity.com/my/groups",
				method: "GET",
				onload: function(response) {
					user_id = response.responseText.match(/g_steamID = \"(.+?)\";/);
					session_id = response.responseText.match(/g_sessionID = \"(.+?)\";/);
					process_url = response.responseText.match(/steamcommunity.com\/(id\/.+?|profiles\/[0-9]+)\/friends\//);
					user_id = user_id === null ? null : user_id[1];
					session_id = session_id === null ? null : session_id[1];
					process_url = process_url === null ? null : "https://steamcommunity.com/" + process_url[1] + "/home_process";

					$(response.responseText).find("a[href^='https://steamcommunity.com/groups/']").each(function() {
						var group_name = $(this).attr("href").replace("https://steamcommunity.com/groups/", "");

						if(group_name.indexOf("/") == -1) {
							active_groups.push(group_name.toLowerCase());
						}
					});

					active_groups = giveawayHelperUI.removeDuplicates(active_groups);
					ready = true;
				}
			});

			function verifyLogin(expected_user) {
				if(typeof expected_user !== "undefined" && !expected_user) {
					// The user doesn't have a Steam account linked, do nothing
				} else if(user_id === null || session_id === null || process_url === null) {
					// We're not logged in
					giveawayHelperUI.showError(`You must be logged into
						<a href="https://steamcommunity.com/login" target="_blank">steamcommunity.com</a>`);
				} else if(typeof expected_user !== "undefined" && expected_user.user_id != user_id) {
					// We're logged in as the wrong user
					giveawayHelperUI.showError(`You must be logged into the linked Steam account:
						<a href="https://steamcommunity.com/profiles/${expected_user.user_id}" target="_blank">
						https://steamcommunity.com/profiles/${expected_user.user_id}</a>`);
				} else if(active_groups === null) {
					// Couldn't get user's group data
					giveawayHelperUI.showError("Unable to determine what Steam groups you're a member of");
				} else {
					return true;
				}

				return false;
			}

			/**
			 *
			 */
			function prepCreateButton(group_data, button_callback, show_name, expected_user) {
				if(typeof group_data.group_id == "undefined") {
					// Group ID is missing
					getGroupID(group_data.group_name, function(group_id) {
						group_data.group_id = group_id;
						createButton(group_data, button_callback, show_name, expected_user);
					});
				} else if(typeof group_data.group_name == "undefined") {
					// Group name is missing
					getGroupName(group_data.group_id, function(group_name) {
						group_data.group_name = group_name;

						// Fetch a separate numeric group id that we'll need
						getGroupID(group_data.group_name, function(group_id) {
							group_data.group_id = group_id;
							createButton(group_data, button_callback, show_name, expected_user);
						});
					});
				} else {
					createButton(group_data, button_callback, show_name, expected_user);
				}
			}

			/**
			 * Create a join/leave toggle button
			 */
			function createButton(group_data, button_callback, show_name, expected_user) {
				if(verifyLogin(expected_user)) {
					// Create the button
					var group_name = group_data.group_name,
						group_id = group_data.group_id,
						in_group = active_groups.indexOf(group_name) != -1,
						button_id = "steam_button_" + button_count++,
						label = in_group ?
							`Leave ${show_name ? group_name : "Group"}`
							: `Join ${show_name ? group_name : "Group"}`;

					button_callback(
						giveawayHelperUI.buildButton(button_id, label, in_group, function() {
							toggleGroupStatus(button_id, group_name, group_id, show_name);
							giveawayHelperUI.showButtonLoading(button_id);
						})
					);
				}
			}


			/**
			 * Toggle group status between "joined" and "left"
			 */
			function toggleGroupStatus(button_id, group_name, group_id, show_name) {
				var steam_community_down_error = `
					The Steam Community is experiencing issues.  Please handle any remaining Steam entries manually, or reload the page and try again.
				`;

				if(active_groups.indexOf(group_name) == -1) {
					joinSteamGroup(group_name, group_id, function(success) {
						if(success) {
							active_groups.push(group_name);
							giveawayHelperUI.toggleButtonClass(button_id);
							giveawayHelperUI.setButtonLabel(button_id, `Leave ${show_name ? group_name : "Group"}`);
						} else {
							giveawayHelperUI.showError(steam_community_down_error);
						}

						giveawayHelperUI.hideButtonLoading(button_id);
					});
				} else {
					leaveSteamGroup(group_name, group_id, function(success) {
						if(success) {
							active_groups.splice(active_groups.indexOf(group_name), 1);
							giveawayHelperUI.toggleButtonClass(button_id);
							giveawayHelperUI.setButtonLabel(button_id, `Join ${show_name ? group_name : "Group"}`);
						} else {
							giveawayHelperUI.showError(steam_community_down_error);
						}

						giveawayHelperUI.hideButtonLoading(button_id);
					});
				}
			}

			/**
			 * Join a steam group
			 */
			function joinSteamGroup(group_name, group_id, callback) {
				MKY.xmlHttpRequest({
					url: "https://steamcommunity.com/groups/" + group_name,
					method: "POST",
					headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
					data: $.param({ action: "join", sessionID: session_id }),
					onload: function(response) {
						MKY.xmlHttpRequest({
							url: "https://steamcommunity.com/my/groups",
							method: "GET",
							onload: function(response) {
								if(typeof callback == "function") {
									if($(response.responseText.toLowerCase()).find(
										`a[href='https://steamcommunity.com/groups/${group_name}']`).length === 0) {

										// Failed to join the group, Steam Community is probably down
										callback(false);
									} else {
										callback(true);
									}
								}
							}
						});
					}
				});
			}

			/**
			 * Leave a steam group
			 */
			function leaveSteamGroup(group_name, group_id, callback) {
				MKY.xmlHttpRequest({
					url: process_url,
					method: "POST",
					headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
					data: $.param({ sessionID: session_id, action: "leaveGroup", groupId: group_id }),
					onload: function(response) {
						if(typeof callback == "function") {
							if($(response.responseText.toLowerCase()).find(
								`a[href='https://steamcommunity.com/groups/${group_name}']`).length !== 0) {

								// Failed to leave the group, Steam Community is probably down
								callback(false);
							} else {
								callback(true);
							}
						}
					}
				});
			}

			/**
			 * Get the numeric ID for a Steam group
			 */
			function getGroupID(group_name, callback) {
				MKY.xmlHttpRequest({
					url: "https://steamcommunity.com/groups/" + group_name,
					method: "GET",
					headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
					onload: function(response) {
						var group_id = response.responseText.match(/OpenGroupChat\( \'([0-9]+)\'/);
						group_id = group_id === null ? null : group_id[1];

						callback(group_id);
					}
				});
			}

			/**
			 * Get the name for a Steam group given the numeric ID
			 */
			function getGroupName(group_id, callback) {
				MKY.xmlHttpRequest({
					url: "https://steamcommunity.com/gid/" + group_id,
					method: "GET",
					headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
					onload: function(response) {
						var group_name = response.finalUrl.match(/steamcommunity\.com\/groups\/([a-zA-Z0-9\-\_]{2,32})/);
						group_name = group_name === null ? null : group_name[1];

						callback(group_name.toLowerCase());
					}
				});
			}

			return {
				/**
				 *
				 */
				handleEntry: function(group_data, button_callback, show_name, expected_user) {
					if(ready) {
						prepCreateButton(group_data, button_callback, show_name, expected_user);
					} else {
						// Wait for the command hub to load
						var temp_interval = setInterval(function() {
							if(ready) {
								clearInterval(temp_interval);
								prepCreateButton(group_data, button_callback, show_name, expected_user);
							}
						}, 100);
					}
				},

				/**
				 *
				 */
				findGroups: function(button_callback, target, show_name, do_cache, cache_id) {
					var self = this;

					giveawayHelperUI.restoreCachedLinks(cache_id).then(function(group_names) {
						giveawayHelperUI.restoreCachedLinks(cache_id + "_ids").then(function(group_ids) {
							var match;

							if(!do_cache) {
								group_names = [];
								group_ids = [];
							}

							// Look for any links containing steam group names
							while((match = re_group_name.exec(target)) !== null) {
								group_names.push(match[1].toLowerCase());
							}

							// Look for any links containing steam group ids
							while((match = re_group_id.exec(target)) !== null) {
								if(typeof match[2] !== "undefined") {
									group_ids.push(match[2].toLowerCase());
								} else {
									group_ids.push(match[3].toLowerCase());
								}
							}

							group_names = giveawayHelperUI.removeDuplicates(group_names);
							group_ids = giveawayHelperUI.removeDuplicates(group_ids);

							// Cache the results
							if(do_cache) {
								giveawayHelperUI.cacheLinks(group_names, cache_id);
								giveawayHelperUI.cacheLinks(group_ids, cache_id + "_ids");
							}

							// Create the buttons
							for(var i = 0; i < group_names.length; i++) {
								if($.inArray(group_names[i], handled_group_names) == -1) {
									handled_group_names.push(group_names[i]);
									self.handleEntry({ group_name: group_names[i] }, button_callback, show_name);
								}
							}

							for(var j = 0; j < group_ids.length; j++) {
								if($.inArray(group_ids[i], handled_group_ids) == -1) {
									handled_group_ids.push(group_ids[i]);
									self.handleEntry({ group_id: group_ids[j] }, button_callback, show_name);
								}
							}
						});
					});
				},

				/**
				 *
				 */
				findKeys: function(button_callback, target, show_key) {
					var keys = [],
						match;

					while((match = re_steam_key.exec(target)) !== null) {
						keys.push(match[1]);
					}

					for(var i = 0; i < keys.length; i++) {
						if($.inArray(keys[i], handled_keys) == -1) {
							var steam_key = keys[i],
								button_id = 'redeem_' + handled_keys.length,
								label = show_key ? `Redeem ${steam_key}` : "Redeem Key",
								redeem_url = `${redeem_key_url}${steam_key}`;

							handled_keys.push(steam_key);
							button_callback(
								giveawayHelperUI.buildRedeemButton(button_id, label, redeem_url)
							);
						}
					}
				}
			};
		}

		var instance;
		return {
			getInstance: function() {
				if(!instance) instance = init();
				return instance;
			}
		};
	})();



	/**
	 * Handles Steam curator buttons
	 */
	var SteamCuratorHandler = (function() {
		function init() {
			var re_curator_id = /steampowered.com\/curator\/([0-9]+)/g,
				session_id = null,
				active_curators = [],
				button_count = 1,
				handled_curator_ids = [],
				ready = false;
				curator_ready_status = 0;

			// Get all the user data we'll need to make follow/unfollow curator requests
			MKY.xmlHttpRequest({
				url: "https://store.steampowered.com",
				method: "GET",
				onload: function(response) {
					session_id = response.responseText.match(/g_sessionID = \"(.+?)\";/);
					session_id = session_id === null ? null : session_id[1];

					MKY.xmlHttpRequest({
						url: "https://store.steampowered.com/curators/ajaxgetcurators//?query=&start=0&count=1000&filter=mycurators",
						method: "GET",
						headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
						onload: function(response) {
							var curator_urls_completed = 1;

							try {
								var data = JSON.parse(response.responseText);

								if(typeof data.success != "undefined" && typeof data.pagesize != "undefined" && typeof data.total_count != "undefined" && data.success == true) {
									parseActiveCurators(data);

									for(var i = 1; i < Math.ceil(data.total_count/data.pagesize); i++) {
										setTimeout(function(page_num) {
											if(ready) return;

											MKY.xmlHttpRequest({
												url: "https://store.steampowered.com/curators/ajaxgetcurators//?query=&start=" + (page_num * data.pagesize) + "&count=1000&filter=mycurators",
												method: "GET",
												headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
												onload: function(response) {
													try {
														var data = JSON.parse(response.responseText);

														if(typeof data.success != "undefined" && data.success == true) {
															parseActiveCurators(data);
														} else {
															ready = true;
															active_curators = null;
														}

														curator_urls_completed++;
													} catch(e) {
														ready = true;
														active_curators = null;
													}
												}
											});
										}, i * 500, i);
									}

									var temp_interval = setInterval(function() {
										if(curator_urls_completed >= Math.ceil(data.total_count/data.pagesize)) {
											clearInterval(temp_interval);
											ready = true;
										}
									}, 100)
								}
							} catch(e) {
								ready = true;
								active_curators = null;
							}
						}
					});
				}
			});

			function verifyLogin(expected_user) {
				if(typeof expected_user !== "undefined" && !expected_user) {
					// The user doesn't have a Steam account linked, do nothing
				} else if(session_id === null) {
					// We're not logged in
					giveawayHelperUI.showError(`You must be logged into
						<a href="https://steamcommunity.com/login" target="_blank">steamcommunity.com</a>`);
				}  else if(active_curators === null) {
					// Couldn't get user's group data
					giveawayHelperUI.showError("Unable to determine what Steam curators you're following");
				} else {
					return true;
				}

				return false;
			}

			/**
			 *
			 */
			function parseActiveCurators(data) {
				if(typeof data.results_html == "undefined") {
					curator_ready_status = 2;
					active_curators = null;
					return;
				}

				var re_curator_results_id = /\"clanID\":\"([0-9]+)\"/g;

				while((match = re_curator_results_id.exec(data.results_html)) !== null) {
					active_curators.push(match[1]);
				}

				return;
			}

			/**
			 * Create a join/leave Curator toggle button
			 */
			function createButton(curator_id, button_callback, show_name, expected_user) {
				if(verifyLogin(expected_user)) {
					// Create the button
					var is_following = active_curators.indexOf(curator_id) != -1,
						button_id = "steam_curator_button_" + button_count++,
						label = is_following ?
							`Unfollow ${show_name ? curator_id : "Curator"}`
							: `Follow ${show_name ? curator_id : "Curator"}`;

					button_callback(
						giveawayHelperUI.buildButton(button_id, label, is_following, function() {
							toggleCuratorStatus(button_id, curator_id, show_name);
							giveawayHelperUI.showButtonLoading(button_id);
						})
					);
				}
			}


			/**
			 * Toggle steam curator status between "following" and "not following"
			 */
			function toggleCuratorStatus(button_id, curator_id, show_name) {
				var steam_community_down_error = `
					The Steam Community is experiencing issues.  Please handle any remaining Steam entries manually, or reload the page and try again.
				`;

				if(active_curators.indexOf(curator_id) == -1) {
					followCurator(curator_id, function(success) {
						if(success) {
							active_curators.push(curator_id);
							giveawayHelperUI.toggleButtonClass(button_id);
							giveawayHelperUI.setButtonLabel(button_id, `Unfollow ${show_name ? curator_id : "Curator"}`);
						} else {
							giveawayHelperUI.showError(steam_community_down_error);
						}

						giveawayHelperUI.hideButtonLoading(button_id);
					});
				} else {
					unfollowCurator(curator_id, function(success) {
						if(success) {
							active_curators.splice(active_curators.indexOf(curator_id), 1);
							giveawayHelperUI.toggleButtonClass(button_id);
							giveawayHelperUI.setButtonLabel(button_id, `Follow ${show_name ? curator_id : "Curator"}`);
						} else {
							giveawayHelperUI.showError(steam_community_down_error);
						}

						giveawayHelperUI.hideButtonLoading(button_id);
					});
				}
			}

			/**
			 * Follow a steam curator
			 */
			function followCurator(curator_id, callback) {
				MKY.xmlHttpRequest({
					url: "https://store.steampowered.com/curators/ajaxfollow",
					method: "POST",
					data: $.param({ clanid: curator_id, sessionid: session_id, follow: "1" }),
					headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
					onload: function(response) {
						try {
							var data = JSON.parse(response.responseText);

							if(typeof data.success.success != "undefined" && data.success.success == 1) {
								callback(true);
							} else {
								callback(false);
							}
						} catch(e) {
							callback(false)
						}
					}
				});
			}

			/**
			 * Unfollow a steam curator
			 */
			function unfollowCurator(curator_id, callback) {
				MKY.xmlHttpRequest({
					url: "https://store.steampowered.com/curators/ajaxfollow",
					method: "POST",
					data: $.param({ clanid: curator_id, sessionid: session_id, follow: "0" }),
					headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
					onload: function(response) {
						try {
							var data = JSON.parse(response.responseText);

							if(typeof data.success.success != "undefined" && data.success.success == 1) {
								callback(true);
							} else {
								callback(false);
							}
						} catch(e) {
							callback(false)
						}
					}
				});
			}

			return {
				/**
				 *
				 */
				handleEntry: function(curator_id, button_callback, show_name, expected_user) {
					if(ready) {
						createButton(curator_id, button_callback, show_name, expected_user);
					} else {
						// Wait for the command hub to load
						var temp_interval = setInterval(function() {
							if(ready) {
								clearInterval(temp_interval);
								createButton(curator_id, button_callback, show_name, expected_user);
							}
						}, 100);
					}
				},

				/**
				 *
				 */
				findCurators: function(button_callback, target, show_name, do_cache, cache_id) {
					var self = this;

					giveawayHelperUI.restoreCachedLinks(cache_id).then(function(curator_ids) {
						var match;

						if(!do_cache) {
							curator_ids = [];
						}

						// Look for any links containing steam curator ids
						while((match = re_curator_id.exec(target)) !== null) {
							curator_ids.push(match[1].toLowerCase());
						}

						curator_ids = giveawayHelperUI.removeDuplicates(curator_ids);

						// Cache the results
						if(do_cache) {
							giveawayHelperUI.cacheLinks(curator_ids, cache_id);
						}

						// Create the buttons
						for(var i = 0; i < curator_ids.length; i++) {
							if($.inArray(curator_ids[i], handled_curator_ids) == -1) {
								handled_curator_ids.push(curator_ids[i]);
								self.handleEntry(curator_ids[i], button_callback, show_name);
							}
						}
					});
				}
			};
		}

		var instance;
		return {
			getInstance: function() {
				if(!instance) instance = init();
				return instance;
			}
		};
	})();

	/**
	 * Handles Twitter undo buttons
	 */
	var TwitterHandler = (function() {
		function init() {
			var command_hub_url = "https://syndication.twitter.com/",
				command_hub_host = "syndication.twitter.com",
				auth_token = null,
				csrf_token = null,
				user_handle = null,
				user_id = null,
				start_time = +new Date(),
				deleted_tweets = [], // used to make sure we dont try to delete the same (re)tweet more than once
				button_count = 1,
				ready_a = false;
				ready_b = false;

			// Get all the user data we'll need to undo twitter entries
			commandHub.load(
				command_hub_url,
				command_hub_host,
				function() {
					return {
						csrf_token: getCookie("ct0")
					};
				},
				function(data) {
					csrf_token = data.csrf_token;
					ready_a = true;
				}
			);

			MKY.xmlHttpRequest({
				url: "https://twitter.com",
				method: "GET",
				onload: function(response) {
					auth_token = $($(response.responseText)
						.find("input[id='authenticity_token']").get(0))
						.attr("value");
					user_handle = $(response.responseText)
						.find(".current-user a")
						.attr("href");
					user_id = $(response.responseText)
						.find("#current-user-id")
						.attr("value");

					auth_token = typeof auth_token == "undefined" ? null : auth_token;
					user_handle = typeof user_handle == "undefined" ? null : user_handle.replace("/", "");
					user_id = typeof user_id == "undefined" ? null : user_id;

					ready_b = true;
				}
			});

			/**
			 * Get ready to create an item
			 */
			function prepCreateButton(action_data, button_callback, ready_check, show_name, expected_user) {
				// Wait until the entry is completed before showing the button
				var temp_interval = setInterval(function() {
					if(ready_check()) {
						clearInterval(temp_interval);
						createButton(action_data, button_callback, show_name, expected_user, +new Date());
					}
				}, 100);
			}

			/**
			 * Create the button
			 */
			function createButton(action_data, button_callback, show_name, expected_user, end_time) {
				if(!expected_user) {
					// The user doesn't have a Twitter account linked, do nothing
				} else if(auth_token === null || user_handle === null || csrf_token === null) {
					// We're not logged in
					giveawayHelperUI.showError(`You must be logged into
						<a href="https://twitter.com/login" target="_blank">twitter.com</a>`);
				} else if(expected_user.user_id != user_id) {
					// We're logged in as the wrong user
					giveawayHelperUI.showError(`You must be logged into the Twitter account linked to Gleam.io:
						<a href="https://twitter.com/${expected_user.user_handle}" target="_blank">
						https://twitter.com/${expected_user.user_handle}</a>`);
				} else {
					// Create the button
					var button_id = "twitter_button_" + button_count++;

					if(action_data.action == "twitter_follow") {
						// Unfollow button
						var twitter_handle = action_data.id;

						button_callback(
							giveawayHelperUI.buildButton(button_id, `Unfollow${show_name ? ` ${twitter_handle}` : ""}`,
								false,
								function() {
									giveawayHelperUI.removeButton(button_id);

									// Get user's Twitter ID
									getTwitterUserData(twitter_handle, function(twitter_id, is_following) {
										deleteTwitterFollow(twitter_handle, twitter_id);
									});
							})
						);
					} else if(action_data.action == "twitter_retweet") {
						// Delete Retweet button
						button_callback(
							giveawayHelperUI.buildButton(button_id, "Delete Retweet", false, function() {
								giveawayHelperUI.removeButton(button_id);
								deleteTwitterRetweet(action_data.id.match(/\/([0-9]+)/)[1]);
							})
						);
					} else if(action_data.action == "twitter_tweet" || action_data.action == "twitter_hashtags") {
						// Delete Tweet button
						button_callback(
							giveawayHelperUI.buildButton(button_id, "Delete Tweet", false, function() {
								giveawayHelperUI.removeButton(button_id);

								/* We don't have an id for the tweet, so instead delete the first tweet we can find
								that was posted after we handled the entry, but before it was marked completed. */
								getTwitterTweet(end_time, function(tweet_id) {
									if(tweet_id === false) {
										giveawayHelperUI.showError(`Failed to find
											<a href="https://twitter.com/${user_handle}" target="_blank">Tweet</a>`);
									} else {
										deleteTwitterTweet(tweet_id);
									}
								});
							})
						);
					}
				}
			}

			/**
			 * @return {String} twitter_id - Twitter id for this handle
			 * @return {Boolean} is_following - True for "following", false for "not following"
			 */
			function getTwitterUserData(twitter_handle, callback) {
				MKY.xmlHttpRequest({
					url: "https://twitter.com/" + twitter_handle,
					method: "GET",
					onload: function(response) {
						var twitter_id = $($(response.responseText.toLowerCase()).find(
								`[data-screen-name='${twitter_handle.toLowerCase()}'][data-user-id]`).get(0)).attr(
								"data-user-id"),
							is_following = $($(response.responseText.toLowerCase()).find(
								`[data-screen-name='${twitter_handle.toLowerCase()}'][data-you-follow]`).get(0)).attr(
								"data-you-follow");

						if(typeof twitter_id !== "undefined" && typeof is_following !== "undefined") {
							callback(twitter_id, is_following !== "false");
						} else {
							callback(null, null);
						}
					}
				});
			}

			/**
			 * We don't have an id for the tweet, so instead delete the first tweet we can find
			 * that was posted after we handled the entry, but before it was marked completed.
			 *
			 * @param {Number} end_time - Unix timestamp in ms
			 * @return {Array|Boolean} tweet_id - The oldest (re)tweet id between start and end time, false if not found
			 */
			function getTwitterTweet(end_time, callback) {
				/* Tweets are instantly posted to our profile, but there's a delay before they're made
				public (a few seconds).  Increase the range by a few seconds to compensate. */
				end_time += (60 * 1000);

				MKY.xmlHttpRequest({
					url: "https://twitter.com/" + user_handle,
					method: "GET",
					onload: function(response) {
						var found_tweet = false,
							now = +new Date();

						// reverse the order so that we're looking at oldest to newest
						$($(response.responseText.toLowerCase()).find(
							`a[href*='${user_handle.toLowerCase()}/status/']`).get().reverse()).each(function() {

							var tweet_time = $(this).find("span").attr("data-time-ms"),
								tweet_id = $(this).attr("href").match(/\/([0-9]+)/);

							if(typeof tweet_time != "undefined" && tweet_id !== null) {
								if(deleted_tweets.indexOf(tweet_id[1]) == -1 &&
									tweet_time > start_time &&
									(tweet_time < end_time || tweet_time > now)) {

									// return the first match
									found_tweet = true;
									deleted_tweets.push(tweet_id[1]);
									callback(tweet_id[1]);
									return false;
								}
							}
						});

						// couldn't find any tweets between the two times
						if(!found_tweet) {
							callback(false);
						}
					}
				});
			}

			/**
			 * Unfollow a twitter user
			 */
			function deleteTwitterFollow(twitter_handle, twitter_id) {
				if(twitter_id === null) {
					giveawayHelperUI.showError(`Failed to unfollow Twitter user:
						<a href="https://twitter.com/${twitter_handle}" target="_blank">${twitter_handle}</a>`);
				} else {
					MKY.xmlHttpRequest({
						url: "https://api.twitter.com/1.1/friendships/destroy.json",
						method: "POST",
						headers: {
							"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
							"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw",
							"x-csrf-token": csrf_token,
						},
						data: $.param({ user_id: twitter_id }),
						onload: function(response) {
							if(response.status != 200) {
								giveawayHelperUI.showError(`Failed to unfollow Twitter user:
									<a href="https://twitter.com/${twitter_handle}" target="_blank">
										${twitter_handle}
									</a>`);
							}
						}
					});
				}
			}

			/**
			 * Delete a tweet
			 * @param {Array} tweet_id - A single tweet ID
			 */
			function deleteTwitterTweet(tweet_id) {
				MKY.xmlHttpRequest({
					url: "https://twitter.com/i/tweet/destroy",
					method: "POST",
					headers: {
						"Origin": "https://twitter.com",
						"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
					},
					data: $.param({ _method: "DELETE", authenticity_token: auth_token, id: tweet_id }),
					onload: function(response) {
						if(response.status != 200) {
							giveawayHelperUI.showError(`Failed to delete
								<a href="https://twitter.com/${user_handle}" target="_blank">Tweet}</a>`);
						}
					}
				});
			}

			/**
			 * Delete a retweet
			 * @param {Array} tweet_id - A single retweet ID
			 */
			function deleteTwitterRetweet(tweet_id) {
				MKY.xmlHttpRequest({
					url: "https://api.twitter.com/1.1/statuses/unretweet.json",
					method: "POST",
					headers: {
						"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
						"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw",
						"x-csrf-token": csrf_token,
					},
					data: $.param({ _method: "DELETE", id: tweet_id }),
					onload: function(response) {
						if(response.status != 200) {
							giveawayHelperUI.showError(`Failed to delete
								<a href="https://twitter.com/${user_handle}" target="_blank">Retweet</a>`);
						}
					}
				});
			}

			return {
				/**
				 *
				 */
				handleEntry: function(action_data, button_callback, ready_check, show_name, expected_user) {
					if(ready_a && ready_b) {
						prepCreateButton(action_data, button_callback, ready_check, show_name, expected_user);
					} else {
						// Wait for the command hub to load
						var temp_interval = setInterval(function() {
							if(ready_a && ready_b) {
								clearInterval(temp_interval);
								prepCreateButton(action_data, button_callback, ready_check, show_name, expected_user);
							}
						}, 100);
					}
				}
			};
		}

		var instance;
		return {
			getInstance: function() {
				if(!instance) instance = init();
				return instance;
			}
		};
	})();

	/**
	 * Handles all Twitch entries that may need to interact with Twitch
	 */
	var TwitchHandler = (function() {
		function init() {
			var command_hub_url = "https://player.twitch.tv/",
				command_hub_host = "player.twitch.tv",
				user_handle = null,
				api_token = null,
				button_count = 1,
				following_status = {},
				handled_channels = [],
				ready = false;

			// Get all the user data we'll need to undo twitch entries
			commandHub.load(
				command_hub_url,
				command_hub_host,
				function() {
					return {
						user_handle: getCookie("login"),
						api_token: getCookie("auth-token")
					};
				},
				function(data) {
					user_handle = data.user_handle;
					api_token = data.api_token;
					ready = true;
				}
			);

			/**
			 * Get ready to create an item
			 */
			function prepCreateButton(twitch_handle, button_callback, ready_check, show_name, expected_user) {
				// Wait until the entry is completed before showing the button
				var temp_interval = setInterval(function() {
					if(ready_check === null || ready_check()) {
						clearInterval(temp_interval);
						createButton(twitch_handle, button_callback, show_name, expected_user, ready_check === null);
					}
				}, 100);
			}

			/**
			 * Create the button
			 */
			function createButton(twitch_handle, button_callback, show_name, expected_user, toggle_button) {
				if(typeof expected_user !== "undefined" && !expected_user) {
					// The user doesn't have a Twitter account linked, do nothing
				} else if(user_handle === null || api_token === null) {
					// We're not logged in
					giveawayHelperUI.showError(`You must be logged into
						<a href="https://www.twitch.tv/login" target="_blank">twitch.tv</a>`);
				} else if(typeof expected_user !== "undefined" && expected_user.user_handle != user_handle) {
					// We're logged in as the wrong user
					giveawayHelperUI.showError(`You must be logged into the Twitch account linked to Gleam.io:
						<a href="https://twitch.tv/${expected_user.user_handle}" target="_blank">
						https://twitch.tv/${expected_user.user_handle}</a>`);
				} else {
					// Create the button
					var button_id = "twitch_button_" + button_count++;

					if(toggle_button) {
						getTwitchUserData(twitch_handle, function(is_following) {
							var label = is_following ? `Unfollow ${twitch_handle}` : `Follow ${twitch_handle}`;

							following_status[twitch_handle] = is_following;

							button_callback(
								giveawayHelperUI.buildButton(button_id, label, is_following, function() {
									toggleFollowStatus(button_id, twitch_handle);
									giveawayHelperUI.showButtonLoading(button_id);
								})
							);
						});
					} else {
						var label = `Unfollow${(show_name ? ` ${twitch_handle}` : "")}`;

						button_callback(
							giveawayHelperUI.buildButton(button_id, label, false, function() {
								giveawayHelperUI.removeButton(button_id);
								deleteTwitchFollow(twitch_handle);
							})
						);
					}
				}
			}

			/**
			 *
			 */
			function deleteTwitchFollow(twitch_handle, callback) {
				MKY.xmlHttpRequest({
					url: "https://api.twitch.tv/kraken/users/" + user_handle + "/follows/channels/" + twitch_handle,
					method: "DELETE",
					headers: { "Authorization": "OAuth " + api_token },
					onload: function(response) {
						if(response.status != 204 && response.status != 200) {
							giveawayHelperUI.showError(`Failed to unfollow Twitch user:
								<a href="https://twitch.tv/${twitch_handle}" target="_blank">${twitch_handle}</a>`);

							if(typeof callback == "function") callback(false);
						} else {
							if(typeof callback == "function") callback(true);
						}
					}
				});
			}

			/**
			 *
			 */
			function twitchFollow(twitch_handle, callback) {
				MKY.xmlHttpRequest({
					url: "https://api.twitch.tv/kraken/users/" + user_handle + "/follows/channels/" + twitch_handle,
					method: "PUT",
					headers: { "Authorization": "OAuth " + api_token },
					onload: function(response) {
						if(response.status != 204 && response.status != 200) {
							giveawayHelperUI.showError(`Failed to follow Twitch user:
								<a href="https://twitch.tv/${twitch_handle}" target="_blank">${twitch_handle}</a>`);

							callback(false);
						} else {
							callback(true);
						}
					}
				});
			}

			/**
			 * @return {Boolean} is_follow - True for "following", false for "not following"
			 */
			function getTwitchUserData(twitch_handle, callback) {
				MKY.xmlHttpRequest({
					url: "https://api.twitch.tv/kraken/users/" + user_handle + "/follows/channels/" + twitch_handle,
					method: "GET",
					headers: { "Authorization": "OAuth " + api_token },
					onload: function(response) {
						if(response.status === 404) {
							callback(false);
						} else if(response.status != 204 && response.status != 200) {
							giveawayHelperUI.showError(`Failed to determine follow status of Twtich user`);
						} else {
							callback(true);
						}
					}
				});
			}

			/**
			 *
			 */
			function toggleFollowStatus(button_id, twitch_handle) {
				if(following_status[twitch_handle]) {
					deleteTwitchFollow(twitch_handle, function(success) {
						if(success) {
							following_status[twitch_handle] = false;
							giveawayHelperUI.toggleButtonClass(button_id);
							giveawayHelperUI.setButtonLabel(button_id, `Follow ${twitch_handle}`);
						}

						giveawayHelperUI.hideButtonLoading(button_id);
					});
				} else {
					twitchFollow(twitch_handle, function(success) {
						if(success) {
							following_status[twitch_handle] = true;
							giveawayHelperUI.toggleButtonClass(button_id);
							giveawayHelperUI.setButtonLabel(button_id, `Unfollow ${twitch_handle}`);
						}

						giveawayHelperUI.hideButtonLoading(button_id);
					});
				}
			}

			return {
				/**
				 *
				 */
				handleEntry: function(twitch_handle, button_callback, ready_check, show_name, expected_user) {
					if(ready) {
						prepCreateButton(twitch_handle, button_callback, ready_check, show_name, expected_user);
					} else {
						// Wait for the command hub to load
						var temp_interval = setInterval(function() {
							if(ready) {
								clearInterval(temp_interval);
								prepCreateButton(twitch_handle, button_callback, ready_check, show_name, expected_user);
							}
						}, 100);
					}
				},

				/**
				 *
				 */
				findChannels: function(button_callback, target, show_name, do_cache, cache_id) {
					var self = this;

					giveawayHelperUI.restoreCachedLinks(cache_id).then(function(channels) {
						var re = /twitch\.tv\/([a-zA-Z0-9_]{2,25})/g,
							match;

						if(!do_cache) {
							channels = [];
						}

						while((match = re.exec(target)) !== null) {
							channels.push(match[1].toLowerCase());
						}

						channels = giveawayHelperUI.removeDuplicates(channels);
						if(do_cache) giveawayHelperUI.cacheLinks(channels, cache_id);

						for(var i = 0; i < channels.length; i++) {
							if(channels[i] == "login") continue;

							if($.inArray(channels[i], handled_channels) == -1) {
								handled_channels.push(channels[i]);
								self.handleEntry(channels[i], button_callback, null, show_name);
							}
						}
					});
				}
			};
		}

		var instance;
		return {
			getInstance: function() {
				if(!instance) instance = init();
				return instance;
			}
		};
	})();

    /**
     *
     */
	var giveawayHelperUI = (function() {
		var active_errors = [],
			active_buttons = {},
			gh_main_container = randomString(10),
			gh_button_container = randomString(10),
			gh_button_title = randomString(10),
			gh_button_loading = randomString(10),
			gh_spin = randomString(10),
			gh_notification_container = randomString(10),
			gh_notification = randomString(10),
			gh_error = randomString(10),
			gh_close = randomString(10),
			main_container = $("<div>", { class: gh_main_container }),
			button_container = $("<span>"),
			resolved_urls = [],
			offset_top = 0;

		/**
		 * Generate a random alphanumeric string
		 * http://stackoverflow.com/questions/10726909/random-alpha-numeric-string-in-javascript
		 */
		function randomString(length) {
			var result = '';
			var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

			for(var i = length; i > 0; --i) {
				result += chars[Math.floor(Math.random() * chars.length)];
			}

			return result;
		}

		/**
		 * Push the page down to make room for notifications
		 */
		function updateTopMargin() {
			var new_margin_top = main_container.outerHeight() + main_container.position().top - offset_top;

			$("html").css("margin-top", main_container.is(":visible") ? new_margin_top : 0);
		}

		return {
			gh_button: randomString(10),
			gh_button_on: randomString(10),
			gh_button_off: randomString(10),
			gh_redeem_button: randomString(10),

			/**
			 * Print the UI
			 */
			loadUI: function(zIndex, onLoad) {
				zIndex = typeof zIndex == "undefined" ? 9999999999 : zIndex;

				if(typeof onLoad == "function") onLoad();

				MKY.addStyle(`
					html {
						overflow-y: scroll !important;
					}

					.${gh_main_container} {
						font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
						font-size: 16.5px;
						left: 0px;
						line-height: 21px;
						position: fixed;
						text-align: left;
						top: 0px;
						right: 0px;
						z-index: ${zIndex};
					}

					.${gh_button_container} {
						background-color: #000;
						border-top: 1px solid rgba(52, 152, 219, .5);
						box-shadow: 0px 2px 10px rgba(0, 0, 0, .5);
						box-sizing: border-box;
						color: #3498db;
						padding: 8px;
					}

					.${gh_button_title} {
						font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
						padding: 10px 15px;
						margin-right:8px;
					}

					.${gh_button_loading} {
						-webkit-animation: ${gh_spin} 2s infinite linear;
						animation: ${gh_spin} 2s infinite linear;
						display: inline-block;
						font: normal normal normal 14px/1;
						transform-origin: 45% 55%;
					}

					.${gh_button_loading}:before {
						content: "\\21B7";
					}

					@-webkit-keyframes ${gh_spin} {
						0% {
							-webkit-transform:rotate(0deg);
							transform:rotate(0deg);
						}
						100%{
							-webkit-transform:rotate(359deg);
							transform:rotate(359deg);
						}
					}

					@keyframes ${gh_spin} {
						0% {
							-webkit-transform:rotate(0deg);
							transform:rotate(0deg);
						}
						100% {
							-webkit-transform:rotate(359deg);
							transform:rotate(359deg);
						}
					}

					.${gh_notification} {
						box-sizing: border-box;
						padding: 8px;
					}

					.${gh_error} {
						background: #f2dede;
						box-shadow: 0px 2px 10px rgba(231, 76, 60, .5);
						color: #a94442;
					}

					.${gh_error} a {
						color: #a94442;
						font-weight: 700;
					}

					.${gh_close} {
						color: #000;
						background: 0 0;
						border: 0;
						cursor: pointer;
						display: block;
						float: right;
						font-size: 21px;
						font-weight: 700;
						height: auto;
						line-height: 1;
						margin: 0px;
						opacity: .2;
						padding: 0px;
						text-shadow: 0 1px 0 #fff;
						width: auto;
					}

					.${gh_close}:hover {
						opacity: .5;
					}
				`);

				$("body").append(main_container);
			},

			/**
			 *
			 */
			defaultButtonSetup: function(offset) {
				MKY.addStyle(`
					.${this.gh_button} {
						background-image:none;
						border: 1px solid transparent;
						border-radius: 3px !important;
						cursor: pointer;
						display: inline-block;
						font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
						font-size: 12px;
						font-weight: 400;
						height: 30px;
						line-height: 1.5 !important;
						margin: 4px 8px 4px 0px;
						padding: 5px 10px;
						text-align: center;
						vertical-align: middle;
						white-space: nowrap;
					}

					.${this.gh_button}:active {
						box-shadow: inset 0 3px 5px rgba(0,0,0,.125);
						outline: 0;
					}

					.${this.gh_button_on} {
						background-color: #337ab7;
						border-color: #2e6da4;
						color: #fff;
					}

					.${this.gh_button_on}:hover {
						background-color: #286090;
						border-color: #204d74;
						color: #fff;
					}

					.${this.gh_button_off} {
						background-color: #d9534f;
						border-color: #d43f3a;
						color: #fff;
					}

					.${this.gh_button_off}:hover {
						background-color: #c9302c;
						border-color: #ac2925;
						color: #fff;
					}

					.${this.gh_redeem_button} {
						background-color: #5cb85c;
						border-color: #4cae4c;
						color: #fff;
					}
				`);

				if(typeof offset !== "undefined") {
					main_container.css({top: offset[0], left: offset[1], right: offset[2]});
					offset_top = offset[0];
				}

				main_container.append(
					$("<div>", { class: gh_button_container }).append(
						$("<span>", { class: gh_button_title }).html("Giveaway Helper v" + MKY.info.script.version)
					).append(button_container)
				);

				updateTopMargin();
			},

			/**
			 *
			 */
			addButton: function(new_button) {
				button_container.append(new_button);
				new_button.width(new_button.outerWidth());
				updateTopMargin();
			},

			/**
			 *
			 */
			buildButton: function(button_id, label, button_on, click_function) {
				var new_button =
						$("<button>", { type: "button",
							class: `${this.gh_button} ${button_on ? this.gh_button_on : this.gh_button_off}`
						}).append(
							$("<span>").append(label)).append(
							$("<span>", { class: gh_button_loading, css: { display: "none" }})
						).click(function(e) {
							e.stopPropagation();
							if(!active_buttons[button_id].find(`.${gh_button_loading}`).is(":visible")) {
								click_function();
							}
						});

				active_buttons[button_id] = new_button;
				return new_button;
			},

			/**
			 *
			 */
			buildRedeemButton: function(button_id, label, redeem_url) {
				var new_button =
						$("<a>", { href: redeem_url, target: "_blank" }).append(
							$("<button>", { type: "button",
								class: `${this.gh_button} ${this.gh_redeem_button}`
							}).append(
								$("<span>").append(label)
							)
						);

				active_buttons[button_id] = new_button;
				return new_button;
			},



			/**
			 *
			 */
			removeButton: function(button_id) {
				active_buttons[button_id].remove();
				delete active_buttons[button_id];
			},

			/**
			 *
			 */
			setButtonLabel: function(button_id, label, color) {
				active_buttons[button_id].find("span").first().text(label);

				if(color !== undefined) {
					active_buttons[button_id].css("background-color", color);
					active_buttons[button_id].css("border-color", color);
				}
			},

			/**
			 *
			 */
			toggleButtonClass: function(button_id) {
				active_buttons[button_id].toggleClass(this.gh_button_on);
				active_buttons[button_id].toggleClass(this.gh_button_off);
			},

			/**
			 *
			 */
			showButtonLoading: function(button_id) {
				active_buttons[button_id].find("span").first().hide();
				active_buttons[button_id].find(`.${gh_button_loading}`).show();
			},

			/**
			 *
			 */
			hideButtonLoading: function(button_id) {
				active_buttons[button_id].find("span").first().show();
				active_buttons[button_id].find(`.${gh_button_loading}`).hide();
			},

			/**
			 * Print an error
			 */
			showError: function(msg) {
				// Don't print the same error multiple times
				if(active_errors.indexOf(msg) != -1) return;

				var self = this;

				active_errors.push(msg);
				main_container.append(
					$("<div>", { class: `${gh_notification} ${gh_error}` }).append(
						$("<button>", { class: gh_close}).append(
							$("<span>").html("&times;")
						).click(function() {
							$(this).unbind("click");
							$(this).parent().slideUp(400, function() {
								active_errors.splice(active_errors.indexOf(msg), 1);
								$(this).remove();
								updateTopMargin();
							});
						})
					).append(
						$("<strong>").html("Giveaway Helper Error: ")
					).append(msg)
				);

				updateTopMargin();
			},

			/**
			 * Remove duplicate items from an array
			 */
			removeDuplicates: function(arr) {
				var out = [];

				for(var i = 0; i < arr.length; i++) {
					if (out.indexOf(arr[i]) == -1) {
						out.push(arr[i]);
					}
				}

				return out;
			},

			/**
			 * Some sites remove links to a group after you get your reward, remember which links we've seen where
			 */
			cacheLinks: function(data, id) {
				MKY.setValue(id, JSON.stringify(data));
			},

			/**
			 *
			 */
			restoreCachedLinks: function(id) {
				return MKY.getValue(id, JSON.stringify([])).then(function(value) {
					return JSON.parse(value);
				});
			},

			/**
			 *
			 */
			resolveUrl: function(url, callback) {
				var self = this,
					cached_url_id = `cache_${MKY.info.script.version.replace(/\./g,"_")}_${CryptoJS.MD5(url)}`;

				self.restoreCachedLinks(cached_url_id).then(function(value){
					if(value.length !== 0) {
						callback(value[0]);
					} else {
						self.cacheLinks([false], cached_url_id);

						MKY.xmlHttpRequest({
							url: url,
							method: "GET",
							onload: function(response) {
								if(response.status == 200 && response.finalUrl !== null) {
									self.cacheLinks([response.finalUrl], cached_url_id);
								}

								self.restoreCachedLinks(cached_url_id).then(function(final_url){
									callback(final_url);
								});
							}
						});
					}
				});
			}
		};
	})();

    /**
     * Used to communicate with and run code on a different domain
     * Usualy with the intent to grab necessary cookies
     */
	var commandHub = (function() {
		/**
		 * http://stackoverflow.com/a/15724300
		 */
		function getCookie(name) {
			var value = "; " + document.cookie,
				parts = value.split("; " + name + "=");

			if(parts.length == 2) {
				return parts.pop().split(";").shift();
			} else {
				return null;
			}
		}

		return {
			/**
			 * Load an iframe so that we can run code on a different domain
			 * @param {String} url - The url to be loaded into the iframe
			 * @param {Function} data_func - The code that we're going to run inside the iframe
			 * @param {Function} callback - Runs after data_func returns
			 */
			load: function(url, hostname, data_func, callback) {
				var command_hub = document.createElement('iframe');

				command_hub.style.display = "none";
				command_hub.src = url;
				document.body.appendChild(command_hub);

				hostname = hostname.replace(/\./g, "_");

				var funcvar = `command_hub_func_${hostname}`,
					retvar = `command_hub_return_${hostname}`;

				window.addEventListener("message", function(event) {
					if(event.source == command_hub.contentWindow) {
						if(event.data.status == "ready") {
							// the iframe has finished loading, tell it what to do
							MKY.setValue(funcvar, encodeURI(data_func.toString()));
							command_hub.contentWindow.postMessage({ status: "run" }, "*");
						} else if(event.data.status == "finished") {
							// wait until the values have been set
							var temp_interval = setInterval(function() {
								MKY.getValue(retvar).then(function(value) {
									if(typeof value !== "undefined") {
										clearInterval(temp_interval);

										// the iframe has finished, send the data to the callback and close the frame
										document.body.removeChild(command_hub);
										callback(value);
										MKY.deleteValue(retvar);
									}
								});
							}, 100);
						}
					}
				});
			},

			/**
			 *
			 */
			init: function() {
				var hostname = document.location.hostname.replace(/\./g, "_"),
					funcvar = `command_hub_func_${hostname}`,
					retvar = `command_hub_return_${hostname}`;

				// wait for our parent to tell us what to do
				window.addEventListener("message", function(event) {
					if(event.source == parent) {
						if(event.data.status == "run") {
							// wait until the values have been set
							var temp_interval = setInterval(function() {
								MKY.getValue(funcvar).then(function(value) {
									if(typeof value !== "undefined") {
										clearInterval(temp_interval);
										MKY.setValue(retvar, eval(`(${decodeURI(value)})`)());
										MKY.deleteValue(funcvar);
										parent.postMessage({ status: "finished" }, "*");
									}
								});
							}, 100);
						}
					}
				});

				// let the parent know the iframe is ready
				parent.postMessage({status: "ready"}, "*");
			}
		};
	})();

	// Greasemonkey 4 polyfill
	// https://arantius.com/misc/greasemonkey/imports/greasemonkey4-polyfill.js

	var MKY = typeof GM !== "undefined" ? GM : {
			'info': GM_info,
			'addStyle': GM_addStyle,
			'xmlHttpRequest': GM_xmlhttpRequest,
			'deleteValue': GM_deleteValue,
			'setValue': GM_setValue,
			'getValue': function () {
				return new Promise((resolve, reject) => {
						try {
							resolve(GM_getValue.apply(this, arguments));
						} catch (e) {
							reject(e);
						}
					});
				}
	};

	if(typeof GM_addStyle == 'undefined' || typeof MKY.addStyle == 'undefined') {
		MKY.addStyle = function(aCss) {
			'use strict';
			let head = document.getElementsByTagName('head')[0];
			if(head) {
				let style = document.createElement('style');
				style.setAttribute('type', 'text/css');
				style.textContent = aCss;
				head.appendChild(style);
				return style;
			}
			return null;
		};
	}

	setup.run();
})();