您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
/** * * This is a Greasemonkey script and must be run using a Greasemonkey-compatible browser. * * @author maymay <[email protected]> */ // ==UserScript== // @name FetLife Demographics // @version 0.2.1 // @namespace com.maybemaimed.fetlife.demographics // @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. // @include https://fetlife.com/events/* // @exclude https://fetlife.com/events/*/* // @include https://fetlife.com/users/* // @exclude https://fetlife.com/users/*/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_log // ==/UserScript== FL_ASL = {}; // We'll need some stock code from FetLife ASL Search. FL_DEMOGRAPHICS = {}; FL_DEMOGRAPHICS.CONFIG = { 'debug': false, // switch to true to debug. }; FL_DEMOGRAPHICS.users = {}; // stores our collected totals FL_DEMOGRAPHICS.users.ages = {}; // stores our collected totals by age FL_DEMOGRAPHICS.users.sexes = {}; // stores our collected totals by sexes FL_DEMOGRAPHICS.users.roles = {}; // stores our collected totals by roles // Utility debugging function. FL_DEMOGRAPHICS.log = function (msg) { if (!FL_DEMOGRAPHICS.CONFIG.debug) { return; } GM_log('FETLIFE DEMOGRAPHICS: ' + msg); }; // Initializations. var uw = (unsafeWindow) ? unsafeWindow : window ; // Help with Chrome compatibility? GM_addStyle('\ /* Hide ages for now. */\ #fl-demographics-ages { display: none; }\ .fl-demographics-list { text-transform: capitalize; }\ .fl-demographics-list * { text-transform: none; }\ #fl-demographics-container ul ul ul {\ display: none;\ list-style: none;\ }\ '); FL_DEMOGRAPHICS.init = function () { FL_DEMOGRAPHICS.main(); }; window.addEventListener('DOMContentLoaded', FL_DEMOGRAPHICS.init); // @see "FetLife Age/Sex/Location Search" #getKinkstersFromURL FL_DEMOGRAPHICS.getKinkstersFromURL = function (url) { FL_DEMOGRAPHICS.log('Getting Kinksters list from URL: ' + url); GM_xmlhttpRequest({ 'method': 'GET', 'url': url, 'onload': function (response) { var parser = new DOMParser(); var doc = parser.parseFromString(response.responseText, 'text/html'); var els = doc.querySelectorAll('.user_in_list'); result_count = 0; for (var i = 0; i < els.length; i++) { // Parse results for this page and make note of each demographic. // TODO: Tag source ("yes" or "maybe" RSVP) to sort later. // FIXME: This should actually be filtered elsewhere. var rsvp_type = (response.finalUrl.match(/maybe$/)) ? 'maybe' : 'yes'; FL_DEMOGRAPHICS.parseUserInList(els[i]); result_count++; } // Set up next request. my_page = (url.match(/\d+$/)) ? parseInt(url.match(/\d+$/)[0]) : 1 ; next_page = my_page + 1; if (next_page > 2) { next_url = url.replace(/\d+$/, next_page.toString()); } else { next_url = url + '?page=' + next_page.toString(); } // No pagination? This is the end. if (!doc.querySelector('.previous_page')) { // We're done paginating, so this was the last page. FL_DEMOGRAPHICS.log('Done after searching ' + response.finalUrl) FL_DEMOGRAPHICS.displayTotals(); } else if (!doc.querySelector('.next_page.disabled')) { // Automatically search on next page if not end of pagination. FL_DEMOGRAPHICS.getKinkstersFromURL(next_url); return false; } else { // We're done paginating, so this was the last page. FL_DEMOGRAPHICS.log('Done after searching ' + response.finalUrl) FL_DEMOGRAPHICS.displayTotals(); } } }); }; FL_DEMOGRAPHICS.parseUserInList = function (el, rsvp_type) { var sex = FL_ASL.getSex(el); var age = FL_ASL.getAge(el); var role = FL_ASL.getRole(el); // Record this user under demographic of their sex. if (FL_DEMOGRAPHICS.users.sexes[sex]) { FL_DEMOGRAPHICS.users.sexes[sex].push({ 'html' : el, 'rsvp' : rsvp_type }); } else { FL_DEMOGRAPHICS.users.sexes[sex] = [{ 'html' : el, 'rsvp' : rsvp_type }]; } // Record this user under demographic of their age. if (FL_DEMOGRAPHICS.users.ages[age]) { FL_DEMOGRAPHICS.users.ages[age].push({ 'html' : el, 'rsvp' : rsvp_type }); } else { FL_DEMOGRAPHICS.users.ages[age] = [{ 'html' : el, 'rsvp' : rsvp_type }]; } // Record this user under demographic of their role. if (FL_DEMOGRAPHICS.users.roles[role]) { FL_DEMOGRAPHICS.users.roles[role].push({ 'html' : el, 'rsvp' : rsvp_type }); } else { FL_DEMOGRAPHICS.users.roles[role] = [{ 'html' : el, 'rsvp' : rsvp_type }]; } }; FL_DEMOGRAPHICS.displayTotals = function () { var x = document.getElementById('fl-demographics-loading'); x.parentNode.removeChild(x); var div = document.getElementById('fl-demographics-container'); var ul = document.createElement('ul'); var html_string = ''; for (var key in FL_DEMOGRAPHICS.users) { html_string += '<li id="fl-demographics-' + key + '" class="fl-demographics-list">' + key + '<ul>'; for (var v in FL_DEMOGRAPHICS.users[key]) { html_string += '<li>' + FL_DEMOGRAPHICS.users[key][v].length + ' ' + v + ' (<a href="#" class="fl-demographics-show-list">show</a>)<ul>'; for (var x in FL_DEMOGRAPHICS.users[key][v]) { html_string += '<li>' + FL_DEMOGRAPHICS.users[key][v][x].html.outerHTML + '</li>'; } html_string += '</ul></li>'; } html_string += '</ul></li>'; } ul.innerHTML = html_string; div.appendChild(ul); // Attach event handlers. var els = document.querySelectorAll('.fl-demographics-show-list'); for (var i = 0; i < els.length; i++) { els[i].addEventListener('click', FL_DEMOGRAPHICS.toggleShowHideList); } }; FL_DEMOGRAPHICS.toggleShowHideList = function (e) { e.preventDefault(); var ul = e.target.nextElementSibling; var me = e.target.childNodes[0]; if (ul.style.display === 'block') { ul.style.display = 'none'; } else { ul.style.display = 'block'; } if (me.nodeValue === 'show') { me.nodeValue = 'hide'; } else { me.nodeValue = 'show'; } return false; }; // @see FetLife Age/Sex/Location FL_ASL.getSex = function (el) { var x = el.querySelector('.quiet').innerHTML; var sex = x.match(/^\d\d(\S*)/); return sex[1]; }; FL_ASL.getAge = function (el) { var x = el.querySelector('.quiet').innerHTML; var age = x.match(/^\d\d/); return parseInt(age); }; FL_ASL.getRole = function (el) { var x = el.querySelector('.quiet').innerHTML; var role = x.match(/ ?(\S+)?$/); return role[1]; }; FL_DEMOGRAPHICS.getKinkstersGoing = function (event, page) { var url = 'https://fetlife.com/events/' + event.toString() + '/rsvps'; url = (page) ? url + '?page=' + page.toString() : url ; FL_DEMOGRAPHICS.getKinkstersFromURL(url); }; FL_DEMOGRAPHICS.getKinkstersMaybeGoing = function (event, page) { var url = 'https://fetlife.com/events/' + event.toString() + '/rsvps/maybe'; url = (page) ? url + '?page=' + page.toString() : url ; FL_DEMOGRAPHICS.getKinkstersFromURL(url); }; FL_DEMOGRAPHICS.getKinkstersInFriend = function (user_id, page) { var url = 'https://fetlife.com/users/' + user_id.toString() + '/friends'; url = (page) ? url + '?page=' + page.toString() : url ; FL_DEMOGRAPHICS.getKinkstersFromURL(url); }; // This is the main() function, executed on page load. FL_DEMOGRAPHICS.main = function () { // Find page anchor. html_el = document.querySelector('table.mbxxl td') || document.querySelector('.friends'); if (!html_el) { FL_DEMOGRAPHICS.log('No relevant HTML found, page ' + window.location.href + ' likely not user profile or event.'); return; } // Get object ID. var m = window.location.href.match(/^https:\/\/fetlife.com\/(event|user)s\/(\d+)/); if (!m) { FL_DEMOGRAPHICS.log('No user or event ID found in URL: ' + window.location.href); return; } var div = document.createElement('div'); div.setAttribute('id', 'fl-demographics-container'); div.innerHTML = 'Demographics:<div id="fl-demographics-loading">Loading…</div>'; switch (m[1]) { case 'user': html_el.parentNode.insertBefore(div, html_el); var friends = FL_DEMOGRAPHICS.getKinkstersInFriend(m[2]); break; case 'event': default: html_el.appendChild(div); // Get the list of "yes" and "maybe" RSVPs var rsvp_yes = FL_DEMOGRAPHICS.getKinkstersGoing(m[2]); //var rsvp_maybe = FL_DEMOGRAPHICS.getKinkstersMaybeGoing(m[1]); break; } }; // The following is required for Chrome compatibility, as we need "text/html" parsing. /* * DOMParser HTML extension * 2012-09-04 * * By Eli Grey, http://eligrey.com * Public domain. * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. */ /*! @source https://gist.github.com/1129031 */ /*global document, DOMParser*/ (function(DOMParser) { "use strict"; var DOMParser_proto = DOMParser.prototype , real_parseFromString = DOMParser_proto.parseFromString ; // Firefox/Opera/IE throw errors on unsupported types try { // WebKit returns null on unsupported types if ((new DOMParser).parseFromString("", "text/html")) { // text/html parsing is natively supported return; } } catch (ex) {} DOMParser_proto.parseFromString = function(markup, type) { if (/^\s*text\/html\s*(?:;|$)/i.test(type)) { var doc = document.implementation.createHTMLDocument("") ; doc.body.innerHTML = markup; return doc; } else { return real_parseFromString.apply(this, arguments); } }; }(DOMParser));