FetLife Demographics

Displays the demographics of FetLife events and user friend lists by age, sex, and role. May help you quickly determine whether an event is worth participating in or not, or whether a user is an objectifying troll.

  1. /**
  2. *
  3. * This is a Greasemonkey script and must be run using a Greasemonkey-compatible browser.
  4. *
  5. * @author maymay <bitetheappleback@gmail.com>
  6. */
  7. // ==UserScript==
  8. // @name FetLife Demographics
  9. // @version 0.2.1
  10. // @namespace com.maybemaimed.fetlife.demographics
  11. // @description Displays the demographics of FetLife events and user friend lists by age, sex, and role. May help you quickly determine whether an event is worth participating in or not, or whether a user is an objectifying troll.
  12. // @include https://fetlife.com/events/*
  13. // @exclude https://fetlife.com/events/*/*
  14. // @include https://fetlife.com/users/*
  15. // @exclude https://fetlife.com/users/*/*
  16. // @grant GM_xmlhttpRequest
  17. // @grant GM_addStyle
  18. // @grant GM_log
  19. // ==/UserScript==
  20.  
  21. FL_ASL = {}; // We'll need some stock code from FetLife ASL Search.
  22. FL_DEMOGRAPHICS = {};
  23. FL_DEMOGRAPHICS.CONFIG = {
  24. 'debug': false, // switch to true to debug.
  25. };
  26.  
  27. FL_DEMOGRAPHICS.users = {}; // stores our collected totals
  28. FL_DEMOGRAPHICS.users.ages = {}; // stores our collected totals by age
  29. FL_DEMOGRAPHICS.users.sexes = {}; // stores our collected totals by sexes
  30. FL_DEMOGRAPHICS.users.roles = {}; // stores our collected totals by roles
  31.  
  32. // Utility debugging function.
  33. FL_DEMOGRAPHICS.log = function (msg) {
  34. if (!FL_DEMOGRAPHICS.CONFIG.debug) { return; }
  35. GM_log('FETLIFE DEMOGRAPHICS: ' + msg);
  36. };
  37.  
  38. // Initializations.
  39. var uw = (unsafeWindow) ? unsafeWindow : window ; // Help with Chrome compatibility?
  40. GM_addStyle('\
  41. /* Hide ages for now. */\
  42. #fl-demographics-ages { display: none; }\
  43. .fl-demographics-list { text-transform: capitalize; }\
  44. .fl-demographics-list * { text-transform: none; }\
  45. #fl-demographics-container ul ul ul {\
  46. display: none;\
  47. list-style: none;\
  48. }\
  49. ');
  50. FL_DEMOGRAPHICS.init = function () {
  51. FL_DEMOGRAPHICS.main();
  52. };
  53. window.addEventListener('DOMContentLoaded', FL_DEMOGRAPHICS.init);
  54.  
  55. // @see "FetLife Age/Sex/Location Search" #getKinkstersFromURL
  56. FL_DEMOGRAPHICS.getKinkstersFromURL = function (url) {
  57. FL_DEMOGRAPHICS.log('Getting Kinksters list from URL: ' + url);
  58. GM_xmlhttpRequest({
  59. 'method': 'GET',
  60. 'url': url,
  61. 'onload': function (response) {
  62. var parser = new DOMParser();
  63. var doc = parser.parseFromString(response.responseText, 'text/html');
  64. var els = doc.querySelectorAll('.user_in_list');
  65.  
  66. result_count = 0;
  67. for (var i = 0; i < els.length; i++) {
  68. // Parse results for this page and make note of each demographic.
  69. // TODO: Tag source ("yes" or "maybe" RSVP) to sort later.
  70. // FIXME: This should actually be filtered elsewhere.
  71. var rsvp_type = (response.finalUrl.match(/maybe$/)) ? 'maybe' : 'yes';
  72. FL_DEMOGRAPHICS.parseUserInList(els[i]);
  73. result_count++;
  74. }
  75.  
  76. // Set up next request.
  77. my_page = (url.match(/\d+$/)) ? parseInt(url.match(/\d+$/)[0]) : 1 ;
  78. next_page = my_page + 1;
  79. if (next_page > 2) {
  80. next_url = url.replace(/\d+$/, next_page.toString());
  81. } else {
  82. next_url = url + '?page=' + next_page.toString();
  83. }
  84.  
  85. // No pagination? This is the end.
  86. if (!doc.querySelector('.previous_page')) {
  87. // We're done paginating, so this was the last page.
  88. FL_DEMOGRAPHICS.log('Done after searching ' + response.finalUrl)
  89. FL_DEMOGRAPHICS.displayTotals();
  90. } else if (!doc.querySelector('.next_page.disabled')) {
  91. // Automatically search on next page if not end of pagination.
  92. FL_DEMOGRAPHICS.getKinkstersFromURL(next_url);
  93. return false;
  94. } else {
  95. // We're done paginating, so this was the last page.
  96. FL_DEMOGRAPHICS.log('Done after searching ' + response.finalUrl)
  97. FL_DEMOGRAPHICS.displayTotals();
  98. }
  99. }
  100. });
  101. };
  102.  
  103. FL_DEMOGRAPHICS.parseUserInList = function (el, rsvp_type) {
  104. var sex = FL_ASL.getSex(el);
  105. var age = FL_ASL.getAge(el);
  106. var role = FL_ASL.getRole(el);
  107.  
  108. // Record this user under demographic of their sex.
  109. if (FL_DEMOGRAPHICS.users.sexes[sex]) {
  110. FL_DEMOGRAPHICS.users.sexes[sex].push({
  111. 'html' : el,
  112. 'rsvp' : rsvp_type
  113. });
  114. } else {
  115. FL_DEMOGRAPHICS.users.sexes[sex] = [{
  116. 'html' : el,
  117. 'rsvp' : rsvp_type
  118. }];
  119. }
  120.  
  121. // Record this user under demographic of their age.
  122. if (FL_DEMOGRAPHICS.users.ages[age]) {
  123. FL_DEMOGRAPHICS.users.ages[age].push({
  124. 'html' : el,
  125. 'rsvp' : rsvp_type
  126. });
  127. } else {
  128. FL_DEMOGRAPHICS.users.ages[age] = [{
  129. 'html' : el,
  130. 'rsvp' : rsvp_type
  131. }];
  132. }
  133.  
  134. // Record this user under demographic of their role.
  135. if (FL_DEMOGRAPHICS.users.roles[role]) {
  136. FL_DEMOGRAPHICS.users.roles[role].push({
  137. 'html' : el,
  138. 'rsvp' : rsvp_type
  139. });
  140. } else {
  141. FL_DEMOGRAPHICS.users.roles[role] = [{
  142. 'html' : el,
  143. 'rsvp' : rsvp_type
  144. }];
  145. }
  146. };
  147.  
  148. FL_DEMOGRAPHICS.displayTotals = function () {
  149. var x = document.getElementById('fl-demographics-loading');
  150. x.parentNode.removeChild(x);
  151. var div = document.getElementById('fl-demographics-container');
  152. var ul = document.createElement('ul');
  153. var html_string = '';
  154. for (var key in FL_DEMOGRAPHICS.users) {
  155. html_string += '<li id="fl-demographics-' + key + '" class="fl-demographics-list">' + key + '<ul>';
  156. for (var v in FL_DEMOGRAPHICS.users[key]) {
  157. html_string += '<li>' + FL_DEMOGRAPHICS.users[key][v].length + ' ' + v + ' (<a href="#" class="fl-demographics-show-list">show</a>)<ul>';
  158. for (var x in FL_DEMOGRAPHICS.users[key][v]) {
  159. html_string += '<li>' + FL_DEMOGRAPHICS.users[key][v][x].html.outerHTML + '</li>';
  160. }
  161. html_string += '</ul></li>';
  162. }
  163. html_string += '</ul></li>';
  164. }
  165. ul.innerHTML = html_string;
  166.  
  167. div.appendChild(ul);
  168.  
  169. // Attach event handlers.
  170. var els = document.querySelectorAll('.fl-demographics-show-list');
  171. for (var i = 0; i < els.length; i++) {
  172. els[i].addEventListener('click', FL_DEMOGRAPHICS.toggleShowHideList);
  173. }
  174. };
  175.  
  176. FL_DEMOGRAPHICS.toggleShowHideList = function (e) {
  177. e.preventDefault();
  178. var ul = e.target.nextElementSibling;
  179. var me = e.target.childNodes[0];
  180. if (ul.style.display === 'block') {
  181. ul.style.display = 'none';
  182. } else {
  183. ul.style.display = 'block';
  184. }
  185. if (me.nodeValue === 'show') {
  186. me.nodeValue = 'hide';
  187. } else {
  188. me.nodeValue = 'show';
  189. }
  190. return false;
  191. };
  192.  
  193. // @see FetLife Age/Sex/Location
  194. FL_ASL.getSex = function (el) {
  195. var x = el.querySelector('.quiet').innerHTML;
  196. var sex = x.match(/^\d\d(\S*)/);
  197. return sex[1];
  198. };
  199.  
  200. FL_ASL.getAge = function (el) {
  201. var x = el.querySelector('.quiet').innerHTML;
  202. var age = x.match(/^\d\d/);
  203. return parseInt(age);
  204. };
  205.  
  206. FL_ASL.getRole = function (el) {
  207. var x = el.querySelector('.quiet').innerHTML;
  208. var role = x.match(/ ?(\S+)?$/);
  209. return role[1];
  210. };
  211.  
  212. FL_DEMOGRAPHICS.getKinkstersGoing = function (event, page) {
  213. var url = 'https://fetlife.com/events/' + event.toString() + '/rsvps';
  214. url = (page) ? url + '?page=' + page.toString() : url ;
  215. FL_DEMOGRAPHICS.getKinkstersFromURL(url);
  216. };
  217. FL_DEMOGRAPHICS.getKinkstersMaybeGoing = function (event, page) {
  218. var url = 'https://fetlife.com/events/' + event.toString() + '/rsvps/maybe';
  219. url = (page) ? url + '?page=' + page.toString() : url ;
  220. FL_DEMOGRAPHICS.getKinkstersFromURL(url);
  221. };
  222. FL_DEMOGRAPHICS.getKinkstersInFriend = function (user_id, page) {
  223. var url = 'https://fetlife.com/users/' + user_id.toString() + '/friends';
  224. url = (page) ? url + '?page=' + page.toString() : url ;
  225. FL_DEMOGRAPHICS.getKinkstersFromURL(url);
  226. };
  227.  
  228. // This is the main() function, executed on page load.
  229. FL_DEMOGRAPHICS.main = function () {
  230. // Find page anchor.
  231. html_el = document.querySelector('table.mbxxl td') || document.querySelector('.friends');
  232. if (!html_el) {
  233. FL_DEMOGRAPHICS.log('No relevant HTML found, page ' + window.location.href + ' likely not user profile or event.');
  234. return;
  235. }
  236.  
  237. // Get object ID.
  238. var m = window.location.href.match(/^https:\/\/fetlife.com\/(event|user)s\/(\d+)/);
  239. if (!m) {
  240. FL_DEMOGRAPHICS.log('No user or event ID found in URL: ' + window.location.href);
  241. return;
  242. }
  243.  
  244. var div = document.createElement('div');
  245. div.setAttribute('id', 'fl-demographics-container');
  246. div.innerHTML = 'Demographics:<div id="fl-demographics-loading">Loading&hellip;</div>';
  247.  
  248. switch (m[1]) {
  249. case 'user':
  250. html_el.parentNode.insertBefore(div, html_el);
  251. var friends = FL_DEMOGRAPHICS.getKinkstersInFriend(m[2]);
  252. break;
  253. case 'event':
  254. default:
  255. html_el.appendChild(div);
  256. // Get the list of "yes" and "maybe" RSVPs
  257. var rsvp_yes = FL_DEMOGRAPHICS.getKinkstersGoing(m[2]);
  258. //var rsvp_maybe = FL_DEMOGRAPHICS.getKinkstersMaybeGoing(m[1]);
  259.  
  260. break;
  261. }
  262.  
  263. };
  264.  
  265. // The following is required for Chrome compatibility, as we need "text/html" parsing.
  266. /*
  267. * DOMParser HTML extension
  268. * 2012-09-04
  269. *
  270. * By Eli Grey, http://eligrey.com
  271. * Public domain.
  272. * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
  273. */
  274.  
  275. /*! @source https://gist.github.com/1129031 */
  276. /*global document, DOMParser*/
  277.  
  278. (function(DOMParser) {
  279. "use strict";
  280.  
  281. var
  282. DOMParser_proto = DOMParser.prototype
  283. , real_parseFromString = DOMParser_proto.parseFromString
  284. ;
  285.  
  286. // Firefox/Opera/IE throw errors on unsupported types
  287. try {
  288. // WebKit returns null on unsupported types
  289. if ((new DOMParser).parseFromString("", "text/html")) {
  290. // text/html parsing is natively supported
  291. return;
  292. }
  293. } catch (ex) {}
  294.  
  295. DOMParser_proto.parseFromString = function(markup, type) {
  296. if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
  297. var
  298. doc = document.implementation.createHTMLDocument("")
  299. ;
  300.  
  301. doc.body.innerHTML = markup;
  302. return doc;
  303. } else {
  304. return real_parseFromString.apply(this, arguments);
  305. }
  306. };
  307. }(DOMParser));