Fic - Mixify Auto Welcome Script (Tijn Remix)

This script can be used on Mixify.com while streaming your DJ set. The main reason why I created this script is that I couldn't see every single person who enters the stream so I thought it could be nice if a script can announce in chat who entered the stream with a warm welcome message.

  1. /// <reference path="Typings/jquery.d.ts" />
  2. /// <reference path="Typings/mixify.d.ts" />
  3. // ==UserScript==
  4. // @name Fic - Mixify Auto Welcome Script (Tijn Remix)
  5. // @namespace Booth
  6. // @include http://www.mixify.com/*/live/*
  7. // @version 1.7.2
  8. // @grant none
  9. // @description This script can be used on Mixify.com while streaming your DJ set. The main reason why I created this script is that I couldn't see every single person who enters the stream so I thought it could be nice if a script can announce in chat who entered the stream with a warm welcome message.
  10. // ==/UserScript==
  11. // TODO Split settings and implementation scripts
  12. ///////////////////////////////////////////////////////////
  13. ///// SETTINGS /////
  14. ///////////////////////////////////////////////////////////
  15. /** Turn debug mode on/off (true/false) */
  16. var debugMode = true;
  17. /** Here are all the messages configured */
  18. var messageConfiguration = {
  19. /** A collection of joining messages. {0} = placeholder for name of the user */
  20. onJoining: ["Hey {0}", "hey {0}", "sup {0}", "oi {0}", "Hai der, {0}", "hello {0}", "ayy {0}!", "Ermagerd! It's {0}"],
  21. /** A collection of rejoining messages. {0} = placeholder for name of the user */
  22. onRejoining: ["wb {0}"],
  23. /** A collection of messages for special users, based on ID and not on name */
  24. specialUsers: [
  25. { id: "world", onJoining: "Wow hello world", onRejoining: "Woot welcome back world!" }
  26. ]
  27. };
  28. /** Ignore these users by name */
  29. var ignoredUsers = ["Guest"];
  30. /** The minimum amount of time (in milliseconds) before a message gets send */
  31. var messageDelay = 2000;
  32. /** The maximum timespan (in milliseconds) in which the message will be send, after the delay */
  33. var messageMaxTimespan = 18000;
  34. /** Characters that can be removed from a name */
  35. var trimmableCharacters = ["-", "_", "=", ".", ":", "[", "]", "<", ">"];
  36. ///////////////////////////////////////////////////////////
  37. ///// USER STUFF /////
  38. ///////////////////////////////////////////////////////////
  39. /** Collection class for users */
  40. var UserCollection = (function () {
  41. function UserCollection() {
  42. this.users = [];
  43. this.disallowedUsers = [];
  44. }
  45. /**
  46. * Add a new user
  47. * @param user User object
  48. */
  49. UserCollection.prototype.add = function (user) {
  50. logToConsole("Trying to add {0}".format(user.name.fullName));
  51. if (this.userIsAllowed(user)) {
  52. if (!this.userExists(user.id)) {
  53. this.users.push(user);
  54. logToConsole("Succesfully added {0} ({1})".format(user.name.fullName, user.id));
  55. user.message(messageConfiguration.onJoining);
  56. }
  57. else {
  58. }
  59. }
  60. else {
  61. logToConsole("{0} is not allowed to be added".format(user.name.fullName));
  62. }
  63. };
  64. /**
  65. * Check if an user is allowed
  66. * @param name Name of the user
  67. * @returns { User is allowed }
  68. */
  69. UserCollection.prototype.userIsAllowed = function (user) {
  70. var userAllowedByName = $.inArray(user.name.fullName, this.disallowedUsers) === -1;
  71. var userAllowedById = $.inArray(user.id, this.disallowedUsers) === -1;
  72. return userAllowedByName && userAllowedById;
  73. };
  74. /**
  75. * Check if user already exists in the array
  76. * @param id ID of the user
  77. * @returns { User exists }
  78. */
  79. UserCollection.prototype.userExists = function (id) {
  80. for (var _i = 0, _a = this.users; _i < _a.length; _i++) {
  81. var user = _a[_i];
  82. if (user.id === id) {
  83. return true;
  84. }
  85. }
  86. return false;
  87. };
  88. return UserCollection;
  89. })();
  90. /** User class */
  91. var User = (function () {
  92. function User(id, name) {
  93. this.id = id;
  94. this.name = new Name(name);
  95. this.messageName = formatName(this.name);
  96. this.active = true;
  97. this.isDj = $("#djSilhouette").data("querystring").split("=")[1] === this.id;
  98. }
  99. /**
  100. * Greets an user
  101. * @param messages Array of possible greetings
  102. */
  103. User.prototype.message = function (messages) {
  104. var _this = this;
  105. // Determine timeout in ms
  106. var timeout = messageDelay + (Math.random() * messageMaxTimespan);
  107. window.setTimeout(function () {
  108. // First check if user is still in the room, would be silly if not!
  109. if (_this.isStillInRoom()) {
  110. logToConsole("Messaging {0} ({1})".format(_this.name.fullName, _this.id));
  111. // Pick a greeting and send it
  112. var message = messages[Math.floor(Math.random() * messages.length)];
  113. sendChatMessage(message.format(_this.messageName));
  114. }
  115. }, timeout);
  116. };
  117. /**
  118. * Checks if this user is still present in the room
  119. * @returns { user is in the room }
  120. */
  121. User.prototype.isStillInRoom = function () {
  122. if (this.isDj) {
  123. return true;
  124. }
  125. var searchResult = $('#avatar_{0}'.format(this.id));
  126. if (searchResult.length === 0) {
  127. this.active = false;
  128. }
  129. return searchResult.length > 0;
  130. };
  131. return User;
  132. })();
  133. ///////////////////////////////////////////////////////////
  134. ///// NAME STUFF /////
  135. ///////////////////////////////////////////////////////////
  136. var NameImportanceOptions;
  137. (function (NameImportanceOptions) {
  138. NameImportanceOptions[NameImportanceOptions["None"] = 0] = "None";
  139. NameImportanceOptions[NameImportanceOptions["Low"] = 1] = "Low";
  140. NameImportanceOptions[NameImportanceOptions["Moderate"] = 2] = "Moderate";
  141. NameImportanceOptions[NameImportanceOptions["High"] = 3] = "High";
  142. })(NameImportanceOptions || (NameImportanceOptions = {}));
  143. /** Name implementation */
  144. var Name = (function () {
  145. function Name(fullName) {
  146. this.fullName = fullName;
  147. this.capsAreMeaningful = this.setCapsAreMeaningful(fullName);
  148. this.createNameParts();
  149. }
  150. /**
  151. * Determines if uppercase characters mean anything in the grand scheme of things
  152. * @param text The text
  153. */
  154. Name.prototype.setCapsAreMeaningful = function (fullName) {
  155. var amountOfCaps = 0;
  156. var position = 0;
  157. var character;
  158. while (position < fullName.length) {
  159. character = fullName.charAt(position);
  160. if (!isNumeric(character) && isUpperCase(character)) {
  161. amountOfCaps++;
  162. }
  163. position++;
  164. }
  165. return amountOfCaps / fullName.length <= 0.5;
  166. };
  167. /** This extracts name parts out of full name, both parts with and without capital-processing */
  168. Name.prototype.createNameParts = function () {
  169. this.parts = [];
  170. this.fullParts = [];
  171. // First trim the full name, then split it on space-character, then filter out all zero-length entries
  172. var trimmedParts = trimText(this.fullName, trimmableCharacters).split(" ").filter(function (x) { return x.length > 0; });
  173. var parts = [];
  174. // Iterate over all the trimmed parts
  175. var position = 0;
  176. for (var _i = 0; _i < trimmedParts.length; _i++) {
  177. var trimmedPart = trimmedParts[_i];
  178. // Split each part on capitals and push them to the 'parts' collection
  179. this.splitPartsOnCapitals(trimmedPart).filter(function (x) { return x.length > 0; }).map(function (x) { return parts.push(x); });
  180. // Create a full name part and push it to the collection;
  181. var fullPart = new NamePart(trimmedPart, position, this);
  182. this.fullParts.push(fullPart);
  183. position++;
  184. }
  185. // Iterate over all the parts
  186. position = 0;
  187. for (var _a = 0; _a < parts.length; _a++) {
  188. var part = parts[_a];
  189. // Create a namepart and push it to the collection
  190. var namePart = new NamePart(part, position, this);
  191. this.parts.push(namePart);
  192. position++;
  193. }
  194. };
  195. Name.prototype.splitPartsOnCapitals = function (part) {
  196. var results = [];
  197. var remaining = part;
  198. var consecutiveCaps = 0;
  199. var position = 0;
  200. var character;
  201. while (remaining.length > 0) {
  202. if (position === remaining.length) {
  203. results.push(remaining);
  204. remaining = "";
  205. }
  206. character = remaining.charAt(position);
  207. if (!isNumeric(character) && isUpperCase(character)) {
  208. if (position !== 0 && consecutiveCaps === 0) {
  209. // Add new part
  210. results.push(remaining.substring(0, position));
  211. // Reset
  212. remaining = remaining.substring(position, remaining.length);
  213. position = 0;
  214. consecutiveCaps = 0;
  215. }
  216. else {
  217. position++;
  218. consecutiveCaps++;
  219. }
  220. }
  221. else {
  222. if (consecutiveCaps > 1) {
  223. var adjustedPosition = position - 1;
  224. results.push(remaining.substring(0, adjustedPosition));
  225. // Reset
  226. remaining = remaining.substring(adjustedPosition, remaining.length);
  227. position = 0;
  228. }
  229. else {
  230. position++;
  231. }
  232. consecutiveCaps = 0;
  233. }
  234. }
  235. return results;
  236. };
  237. return Name;
  238. })();
  239. /** Name part implementation */
  240. var NamePart = (function () {
  241. function NamePart(value, position, parent) {
  242. this.value = value;
  243. this.position = position;
  244. this.parent = parent;
  245. this.setImportance();
  246. }
  247. NamePart.prototype.setImportance = function () {
  248. var importance;
  249. // Name parts of 1 characters are not important
  250. if (this.value.length <= 1) {
  251. importance = NameImportanceOptions.None;
  252. }
  253. else if (this.value.length <= 3) {
  254. // Name parts of 3 characters or less are either very important, or not at all
  255. if (this.value.toLowerCase() === "dj") {
  256. // 'DJ' is usually an important part
  257. importance = NameImportanceOptions.None;
  258. }
  259. else if (this.parent.capsAreMeaningful && textIsUppercase(this.value)) {
  260. // If caps are used meaninful in the name overall and the part has all caps, then it's probably important
  261. importance = NameImportanceOptions.High;
  262. }
  263. else if (this.position === 0 || (this.position !== 0 && this.parent.parts[this.position - 1].importance === NameImportanceOptions.None)) {
  264. // If the importance isn't determined yet and the word is at the start, high chance it's redundant
  265. importance = NameImportanceOptions.None;
  266. }
  267. else {
  268. // Else just set it on low importance
  269. importance = NameImportanceOptions.Low;
  270. }
  271. }
  272. else {
  273. // Nothing special
  274. importance = NameImportanceOptions.Moderate;
  275. }
  276. this.importance = importance;
  277. };
  278. return NamePart;
  279. })();
  280. /** Add a C#-like format function to string, if not already present */
  281. if (!String.prototype.format) {
  282. String.prototype.format = function () {
  283. var args = arguments;
  284. return this.replace(/{(\d+)}/g, function (match, number) { return (typeof args[number] != 'undefined'
  285. ? args[number]
  286. : match); });
  287. };
  288. }
  289. ///////////////////////////////////////////////////////////
  290. ///// DOCUMENT READY /////
  291. ///////////////////////////////////////////////////////////
  292. // Initialize a new UserCollection
  293. var userList = new UserCollection();
  294. var url;
  295. var dataString;
  296. // Run the script only if you're streaming
  297. if ($('#eventBroadcaster').length > 0) {
  298. // Add ignored users and yourself
  299. userList.disallowedUsers = ignoredUsers;
  300. userList.disallowedUsers.push($('body').attr('data-user-id'));
  301. // Getting url to call AJAX
  302. var queryString = $("#specatorsDockItem").attr("data-querystring");
  303. url = "http://www.mixify.com/room/spectators/cache/1/?" + queryString;
  304. dataString = queryString.split("=")[1];
  305. if (sessionStorage.getItem("active") === null) {
  306. sessionStorage.setItem("active", "true"); /* You entered the stream for the first time */
  307. }
  308. else {
  309. retrieveAttendees();
  310. }
  311. var djQuery = $("#djSilhouette").data("querystring");
  312. var djId = djQuery.split("=")[1];
  313. var djName = getUsernameFromUserData(djQuery);
  314. userList.add(new User(djId, djName));
  315. // Everytime the DOM tree gets modified, fire this event
  316. // TODO Add NodeRemoved to detect a user that leaves
  317. $('#avatarContainer').bind("DOMNodeInserted", function (e) {
  318. var element = $(e.target);
  319. // Only continue if th element that is being added has the 'avatar' class
  320. if (element.attr("class") === "avatar") {
  321. // Get the ID
  322. var id = element.attr("id").split("_")[1];
  323. var querystring = element.attr("data-querystring");
  324. // Get the username
  325. var username = getUsernameFromUserData(querystring);
  326. userList.add(new User(id, username));
  327. }
  328. });
  329. }
  330. ///////////////////////////////////////////////////////////
  331. ///// MIXIFY STUFF /////
  332. ///////////////////////////////////////////////////////////
  333. /**
  334. * Retrieve all attendees
  335. * @todo Make it return a collection of users instead
  336. */
  337. function retrieveAttendees() {
  338. // Wait for fc() to be available
  339. // TODO Figure out if this can be done better
  340. if (fc() != null) {
  341. logToConsole("Retrieving attendance list");
  342. // Get data from the spectator cache
  343. jQuery.ajaxSetup({ async: false });
  344. var data = jQuery.get(url);
  345. var responseHtml = $(data.responseText);
  346. // Search for module-info elements in the response and iterate through it
  347. // TODO Turn into a more useful method
  348. $(responseHtml).find('.module-info').each(function (index, element) {
  349. // Find the username and id and initialize a new user based on it
  350. var jqueryElement = $(element);
  351. var username = jqueryElement.find('.username')[0].innerHTML;
  352. var id = jqueryElement.find('.bt-wrapper').attr('data-uuid');
  353. userList.add(new User(id, username));
  354. });
  355. }
  356. }
  357. /**
  358. * Sends message to chat
  359. * @param message Message
  360. */
  361. function sendChatMessage(message) {
  362. fc().fc_sendChat(message, dataString);
  363. }
  364. /**
  365. * Logs to console, if debug mode is true
  366. * @param message Message to log
  367. * @param optionalParams Optional parameters
  368. */
  369. function logToConsole(message) {
  370. var optionalParams = [];
  371. for (var _i = 1; _i < arguments.length; _i++) {
  372. optionalParams[_i - 1] = arguments[_i];
  373. }
  374. if (debugMode) {
  375. console.log(message, optionalParams);
  376. }
  377. }
  378. /**
  379. * Get the username from querying the user data
  380. * @param query query string
  381. * @returns { Username }
  382. */
  383. function getUsernameFromUserData(query) {
  384. // Get the user data
  385. var data = getUserData(query);
  386. // Parse the response for the username
  387. var responseText = $(data.responseText);
  388. var usernameElement = $(responseText).find(".username");
  389. if (usernameElement.length === 0) {
  390. logToConsole("No username found for user using query {0}".format(query));
  391. }
  392. return usernameElement[0].innerHTML;
  393. }
  394. /**
  395. * Gets the user data with a query string
  396. * @param query query string
  397. * @returns { GET response }
  398. */
  399. function getUserData(query) {
  400. // Get data from the user api
  401. jQuery.ajaxSetup({ async: false });
  402. return jQuery.get("http://www.mixify.com/user/info-basic/?{0}".format(query));
  403. }
  404. ///////////////////////////////////////////////////////////
  405. ///// NAME FORMATTING /////
  406. ///////////////////////////////////////////////////////////
  407. function formatName(name) {
  408. // Get all the parts that we'll work with
  409. var nameParts = getSignificantNameParts(name.parts);
  410. // If all parts have 'none' importance, we'll try to get significant name parts using the full parts
  411. // These don't have the capital processed parts
  412. if (nameParts.every(function (x) { return x.importance === NameImportanceOptions.None; })) {
  413. nameParts = getSignificantNameParts(name.fullParts);
  414. }
  415. if (nameParts.length === 2) {
  416. return nameParts[0].value;
  417. }
  418. if (nameParts.length > 2) {
  419. if (nameParts.join("").length / nameParts.length > 5) {
  420. return nameParts.map(function (x) { return x.value[0].toUpperCase(); }).join("");
  421. }
  422. var firstLargestPart = nameParts[0];
  423. for (var _i = 0; _i < nameParts.length; _i++) {
  424. var namePart = nameParts[_i];
  425. if (namePart.value.length > firstLargestPart.value.length) {
  426. firstLargestPart = namePart;
  427. }
  428. }
  429. return firstLargestPart.value;
  430. }
  431. // Join all parts
  432. return nameParts.map(function (x) { return x.value; }).join(" ");
  433. }
  434. function getSignificantNameParts(nameParts) {
  435. // If there are any high important parts, return the first one
  436. // TODO: Determine if there are better alternatives to returning the first item
  437. var highImportance = nameParts.filter(function (value) { return value.importance === NameImportanceOptions.High; });
  438. if (highImportance.length > 0) {
  439. return [highImportance[0]];
  440. }
  441. // If there's only 1 moderate important part, return that one
  442. var moderateImportance = nameParts.filter(function (value) { return value.importance === NameImportanceOptions.Moderate; });
  443. if (moderateImportance.length === 1) {
  444. return moderateImportance;
  445. }
  446. // If the amount of low importance parts is higher than 0 and lower than the amount of moderate parts, then return low + moderate parts
  447. var lowImportance = nameParts.filter(function (value) { return value.importance === NameImportanceOptions.Low; });
  448. if (lowImportance.length > 0 && lowImportance.length < moderateImportance.length) {
  449. return nameParts.filter(function (value) { return value.importance === NameImportanceOptions.Moderate || value.importance === NameImportanceOptions.Low; });
  450. }
  451. // If at this point there are moderate parts, return those
  452. if (moderateImportance.length !== 0) {
  453. return moderateImportance;
  454. }
  455. if (lowImportance.length !== 0) {
  456. return lowImportance;
  457. }
  458. // Return whatever is left
  459. return nameParts;
  460. }
  461. ///////////////////////////////////////////////////////////
  462. ///// GENERAL STUFF /////
  463. ///////////////////////////////////////////////////////////
  464. /**
  465. * Checks if a text is all uppercase
  466. * @param text Text
  467. */
  468. function textIsUppercase(text) {
  469. var position = 0;
  470. var character;
  471. while (position < text.length) {
  472. character = text.charAt(position);
  473. // If any character is a numeric, or matches a lowercase, then the text isn't fully uppercase
  474. if (isNumeric(character) || isLowerCase(character)) {
  475. return false;
  476. }
  477. position++;
  478. }
  479. return true;
  480. }
  481. function isLetter(character) {
  482. // Cheeky way to determine if it's a letter
  483. return character.toLowerCase() !== character.toUpperCase();
  484. }
  485. function isNumeric(character) {
  486. return (character >= "0" && character <= "9");
  487. }
  488. function isUpperCase(character) {
  489. return character === character.toUpperCase();
  490. }
  491. function isLowerCase(character) {
  492. return character === character.toLowerCase();
  493. }
  494. /**
  495. * Trims a string
  496. * @param text
  497. * @param trimCharacters
  498. */
  499. function trimText(text, trimCharacters) {
  500. var processedName = text;
  501. for (var _i = 0; _i < trimCharacters.length; _i++) {
  502. var trimCharacter = trimCharacters[_i];
  503. processedName = processedName.split(trimCharacter).join(" ");
  504. }
  505. // Replace trailing numerics (regex) and trim
  506. return processedName.replace(/\d+$/, "").trim();
  507. }