FetLife ASL Search (Extened Edition)

Allows you to search for FetLife profiles based on age, sex, location, and role.

  1. /**
  2. * This is a Greasemonkey script and must be run using a Greasemonkey-compatible browser.
  3. *
  4. * @author maymay <bitetheappleback@gmail.com>
  5. */
  6. // ==UserScript==
  7. // @name FetLife ASL Search (Extened Edition)
  8. // @version 0.4.6
  9. // @namespace http://maybemaimed.com/playground/fetlife-aslsearch/
  10. // @description Allows you to search for FetLife profiles based on age, sex, location, and role.
  11. // @require https://code.jquery.com/jquery-2.1.4.min.js
  12. // @include https://fetlife.com/*
  13. // @include https://www.creepshield.com/search*
  14. // @exclude https://fetlife.com/adgear/*
  15. // @exclude https://fetlife.com/chat/*
  16. // @exclude https://fetlife.com/im_sessions*
  17. // @exclude https://fetlife.com/polling/*
  18. // @grant GM_log
  19. // @grant GM_xmlhttpRequest
  20. // @grant GM_addStyle
  21. // @grant GM_getValue
  22. // @grant GM_setValue
  23. // @grant GM_deleteValue
  24. // @grant GM_openInTab
  25. // ==/UserScript==
  26.  
  27. FL_UI = {}; // FetLife User Interface module
  28. FL_UI.Text = {
  29. 'donation_appeal': '<p>FetLife ASL Search is provided as free software, but sadly grocery stores do not offer free food. If you like this script, please consider <a href="http://Cyberbusking.org/">making a donation</a> to support its continued development. &hearts; Thank you. :)</p>'
  30. };
  31. FL_UI.Dialog = {};
  32. FL_UI.Dialog.createLink = function (dialog_id, html_content, parent_node) {
  33. var trigger_el = document.createElement('a');
  34. trigger_el.setAttribute('class', 'opens-modal');
  35. trigger_el.setAttribute('data-opens-modal', dialog_id);
  36. trigger_el.innerHTML = html_content;
  37. return parent_node.appendChild(trigger_el);
  38. };
  39. FL_UI.Dialog.inject = function (id, title, html_content) {
  40. // Inject dialog box HTML. FetLife currently uses Rails 3, so mimic that.
  41. // See, for instance, Rails Behaviors: http://josh.github.com/rails-behaviors/
  42. var dialog = document.createElement('div');
  43. dialog.setAttribute('style', 'display: none; position: absolute; overflow: hidden; z-index: 1000; outline: 0px none;');
  44. dialog.setAttribute('class', 'ui-dialog ui-widget ui-widget-content ui-corner-all');
  45. dialog.setAttribute('tabindex', '-1');
  46. dialog.setAttribute('role', 'dialog');
  47. dialog.setAttribute('aria-labelledby', 'ui-dialog-title-' + id);
  48. var html_string = '<div class="ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix" unselectable="on" style="-moz-user-select: none;">';
  49. html_string += '<span class="ui-dialog-title" id="ui-dialog-title-' + id + '" unselectable="on" style="-moz-user-select: none;">' + title + '</span>';
  50. html_string += '<a href="#" class="ui-dialog-titlebar-close ui-corner-all" role="button" unselectable="on" style="-moz-user-select: none;">';
  51. html_string += '<span class="ui-icon ui-icon-closethick" unselectable="on" style="-moz-user-select: none;">close</span>';
  52. html_string += '</a>';
  53. html_string += '</div>';
  54. html_string += '<div data-modal-title="' + title + '" data-modal-height="280" data-modal-auto-open="false" class="modal ui-dialog-content ui-widget-content" id="' + id + '">';
  55. html_string += html_content;
  56. html_string += '</div>';
  57. dialog.innerHTML = html_string;
  58. document.body.appendChild(dialog);
  59. };
  60.  
  61. FL_ASL = {}; // FetLife ASL Search module
  62. FL_ASL.CONFIG = {
  63. 'debug': false, // switch to true to debug.
  64. 'gasapp_url': 'https://script.google.com/macros/s/AKfycbxjpuCSz9uam23hztGYyiE6IbHX22EGzhq7fN4jQGo1jiRp520/exec?embedded=true',
  65. 'gasapp_url_development': 'https://script.google.com/macros/s/AKfycbxl668Zzz6FW9iLMqtyP_vZYkvqOJK3ZKX308fMcCc/dev?embedded=true',
  66. 'progress_id': 'fetlife_asl_search_progress',
  67. 'min_matches': 1, // show at least this many matches before offering to search again
  68. 'search_sleep_interval': 3 // default wait time in seconds between auto-searches
  69. };
  70.  
  71. FL_ASL.total_result_count = 0; // How many matches have we found, across all pages, on this load?
  72.  
  73. // Utility debugging function.
  74. FL_ASL.log = function (msg) {
  75. if (!FL_ASL.CONFIG.debug) { return; }
  76. GM_log('FETLIFE ASL SEARCH: ' + msg);
  77. };
  78.  
  79. // XPath Helper function
  80. // @see http://wiki.greasespot.net/XPath_Helper
  81. function $x() {
  82. var x='';
  83. var node=document;
  84. var type=0;
  85. var fix=true;
  86. var i=0;
  87. var cur;
  88.  
  89. function toArray(xp) {
  90. var final=[], next;
  91. while (next=xp.iterateNext()) {
  92. final.push(next);
  93. }
  94. return final;
  95. }
  96.  
  97. while (cur=arguments[i++]) {
  98. switch (typeof cur) {
  99. case "string": x+=(x=='') ? cur : " | " + cur; continue;
  100. case "number": type=cur; continue;
  101. case "object": node=cur; continue;
  102. case "boolean": fix=cur; continue;
  103. }
  104. }
  105.  
  106. if (fix) {
  107. if (type==6) type=4;
  108. if (type==7) type=5;
  109. }
  110.  
  111. // selection mistake helper
  112. if (!/^\//.test(x)) x="//"+x;
  113.  
  114. // context mistake helper
  115. if (node!=document && !/^\./.test(x)) x="."+x;
  116.  
  117. var result=document.evaluate(x, node, null, type, null);
  118. if (fix) {
  119. // automatically return special type
  120. switch (type) {
  121. case 1: return result.numberValue;
  122. case 2: return result.stringValue;
  123. case 3: return result.booleanValue;
  124. case 8:
  125. case 9: return result.singleNodeValue;
  126. }
  127. }
  128.  
  129. return fix ? toArray(result) : result;
  130. };
  131.  
  132. // Initializations.
  133. var uw = (unsafeWindow) ? unsafeWindow : window ; // Help with Chrome compatibility?
  134. GM_addStyle('\
  135. #fetlife_asl_search_ui_container,\
  136. #fetlife_asl_search_about,\
  137. #fetlife_asl_search_classic\
  138. { display: none; }\
  139. #fetlife_asl_search_ui_container > div {\
  140. clear: both;\
  141. background-color: #111;\
  142. position: relative;\
  143. top: -2px;\
  144. }\
  145. #fetlife_asl_search_ui_container div a, #fetlife_asl_search_results div a {\
  146. text-decoration: underline;\
  147. }\
  148. #fetlife_asl_search_ui_container div a:hover, #fetlife_asl_search_results div a:hover {\
  149. background-color: blue;\
  150. text-decoration: underline;\
  151. }\
  152. #fetlife_asl_search_ui_container a[data-opens-modal] { cursor: help; }\
  153. #fetlife_asl_search_ui_container ul.tabs li {\
  154. display: inline-block;\
  155. margin-right: 10px;\
  156. }\
  157. #fetlife_asl_search_ui_container ul.tabs li a { color: #888; }\
  158. #fetlife_asl_search_ui_container ul.tabs li.in_section a {\
  159. background-color: #1b1b1b;\
  160. color: #fff;\
  161. position: relative;\
  162. top: 2px;\
  163. padding-top: 5px;\
  164. }\
  165. #fetlife_asl_search_classic fieldset { clear: both; margin: 0; padding: 0; }\
  166. #fetlife_asl_search_classic legend { display: none; }\
  167. #fetlife_asl_search_classic label {\
  168. display: inline-block;\
  169. white-space: nowrap;\
  170. }\
  171. #fetlife_asl_search_classic input { width: auto; }\
  172. #fetlife_asl_search_results { clear: both; }\
  173. ');
  174. FL_ASL.init = function () {
  175. FL_ASL.CONFIG.search_form = document.querySelector('form[action="/search"]').parentNode;
  176. if (FL_ASL.getUserProfileHtml()) {
  177. FL_ASL.main();
  178. } else {
  179. FL_ASL.loadUserProfileHtml(FL_ASL.main);
  180. }
  181. };
  182. jQuery(document).ready(function () {
  183. FL_ASL.init();
  184. });
  185.  
  186. FL_ASL.toggleAslSearch = function () {
  187. var el = document.getElementById('fetlife_asl_search_ui_container');
  188. if (el.style.display == 'block') {
  189. el.style.display = 'none';
  190. } else {
  191. el.style.display = 'block';
  192. }
  193. };
  194.  
  195. FL_ASL.toggleLocationFilter = function (e) {
  196. var el = document.getElementById('fl_asl_loc_filter_label');
  197. switch (e.currentTarget.value) {
  198. case 'group':
  199. case 'event':
  200. case 'fetish':
  201. case 'search':
  202. case 'user':
  203. if (el.style.display == 'none') {
  204. el.style.display = 'inline';
  205. }
  206. break;
  207. default:
  208. el.style.display = 'none';
  209. break;
  210. }
  211. };
  212.  
  213. FL_ASL.aslSubmit = function (e) {
  214. var el = document.getElementById('fetlife_asl_search');
  215. if (!el.checked) {
  216. return false;
  217. }
  218.  
  219. // Provide UI feedback.
  220. var prog = document.getElementById(FL_ASL.CONFIG.progress_id);
  221. prog.innerHTML = 'Searching&hellip;<br />';
  222.  
  223. // collect the form parameters
  224. var search_params = FL_ASL.getSearchParams();
  225.  
  226. // search one of the geographic regions "/kinksters" list
  227. FL_ASL.getKinkstersInSet(search_params.loc);
  228.  
  229. return false;
  230. };
  231.  
  232. /**
  233. * Reads and saves the search parameters from the provided form.
  234. */
  235. FL_ASL.getSearchParams = function () {
  236. var r = {
  237. 'age' : {'min': null, 'max': null},
  238. 'sex' : [],
  239. 'role' : [],
  240. 'loc' : {},
  241. 'filter': ''
  242. };
  243.  
  244. // Collect age parameters, setting wide defaults.
  245. r.age.min = (document.getElementById('min_age').value) ? parseInt(document.getElementById('min_age').value) : 1;
  246. r.age.max = (document.getElementById('max_age').value) ? parseInt(document.getElementById('max_age').value) : 99;
  247.  
  248. // Collect gender/sex parameters.
  249. var x = FL_ASL.CONFIG.search_form.querySelectorAll('input[name="user[sex]"]');
  250. for (var i = 0; i < x.length; i++) {
  251. if (x[i].checked) {
  252. r.sex.push(x[i].value);
  253. }
  254. }
  255.  
  256. // Collect role orientation parameters.
  257. var y = FL_ASL.CONFIG.search_form.querySelectorAll('input[name="user[role]"]');
  258. for (var iy = 0; iy < y.length; iy++) {
  259. if (y[iy].checked) {
  260. r.role.push(y[iy].value);
  261. }
  262. }
  263.  
  264. // Collect location parameters.
  265. var search_in = [];
  266. var z = FL_ASL.CONFIG.search_form.querySelectorAll('input[name="fl_asl_loc"]');
  267. for (var iz = 0; iz < z.length; iz++) {
  268. if (z[iz].checked) {
  269. search_in.push(z[iz].value);
  270. }
  271. }
  272. // Match location parameter with known location ID.
  273. switch (search_in[0]) {
  274. // These cases all use numeric object IDs.
  275. case 'group':
  276. case 'event':
  277. case 'user':
  278. case 'fetish':
  279. r.loc[search_in[0]] = parseInt(FL_ASL.CONFIG.search_form.querySelector('input[data-flasl' + search_in[0] + 'id]').getAttribute('data-flasl' + search_in[0] + 'id'));
  280. break;
  281. // This case uses a string, so no need to parseInt() it.
  282. case 'search':
  283. r.loc[search_in[0]] = FL_ASL.CONFIG.search_form.querySelector('input[data-flasl' + search_in[0] + 'id]').getAttribute('data-flasl' + search_in[0] + 'id');
  284. break;
  285. default:
  286. user_loc_ids = FL_ASL.getUserLocationIds();
  287. for (var xk in user_loc_ids) {
  288. if (null !== user_loc_ids[xk] && (-1 !== search_in.indexOf(xk)) ) {
  289. r.loc[xk] = user_loc_ids[xk];
  290. }
  291. }
  292. break;
  293. }
  294.  
  295. // Collect location filter, if one was entered.
  296. if (document.getElementById('fl_asl_loc_filter')) {
  297. r.filter = document.getElementById('fl_asl_loc_filter').value;
  298. }
  299.  
  300. return r;
  301. };
  302.  
  303. FL_ASL.getUserLocationIds = function () {
  304. var r = {
  305. 'city_id': null,
  306. 'area_id': null,
  307. 'country': null
  308. };
  309. var profile_html = FL_ASL.getUserProfileHtml();
  310. var m = profile_html.match(/href="\/countries\/([0-9]+)/);
  311. if (m) {
  312. r.country = m[1];
  313. }
  314. m = profile_html.match(/href="\/administrative_areas\/([0-9]+)/);
  315. if (m) {
  316. r.area_id = m[1];
  317. }
  318. m = profile_html.match(/href="\/cities\/([0-9]+)/);
  319. if (m) {
  320. r.city_id = m[1];
  321. }
  322.  
  323. return r;
  324. };
  325.  
  326. FL_ASL.getUserProfileHtml = function () {
  327. return GM_getValue('currentUser.profile_html', false);
  328. };
  329.  
  330. FL_ASL.loadUserProfileHtml = function (callback, id) {
  331. var id = id || uw.FetLife.currentUser.id;
  332. FL_ASL.log('Fetching profile for user ID ' + id.toString());
  333. GM_xmlhttpRequest({
  334. 'method': 'GET',
  335. 'url': 'https://fetlife.com/users/' + id.toString(),
  336. 'onload': function (response) {
  337. GM_setValue('currentUser.profile_html', response.responseText);
  338. callback();
  339. }
  340. });
  341. };
  342.  
  343. FL_ASL.getKinkstersInSet = function (loc_obj) {
  344. if (loc_obj.group) {
  345. FL_ASL.getKinkstersInGroup(loc_obj.group);
  346. } else if (loc_obj.event) {
  347. FL_ASL.getKinkstersInEvent(loc_obj.event);
  348. } else if (loc_obj.user) {
  349. FL_ASL.getKinkstersInFriend(loc_obj.user);
  350. } else if (loc_obj.fetish) {
  351. FL_ASL.getKinkstersInFetish(loc_obj.fetish);
  352. } else if (loc_obj.search) {
  353. FL_ASL.getKinkstersInSearch(loc_obj.search);
  354. } else if (loc_obj.city_id) {
  355. FL_ASL.getKinkstersInCity(loc_obj.city_id);
  356. } else if (loc_obj.area_id) {
  357. FL_ASL.getKinkstersInArea(loc_obj.area_id);
  358. } else if (loc_obj.country) {
  359. FL_ASL.getKinkstersInCountry(loc_obj.country);
  360. } else {
  361. return false;
  362. }
  363. };
  364.  
  365. FL_ASL.getKinkstersInCity = function (city_id, page) {
  366. var url = 'https://fetlife.com/cities/' + city_id.toString() + '/kinksters';
  367. url = (page) ? url + '?page=' + page.toString() : url ;
  368. FL_ASL.getKinkstersFromURL(url);
  369. };
  370. FL_ASL.getKinkstersInArea = function (area_id, page) {
  371. var url = 'https://fetlife.com/administrative_areas/' + area_id.toString() + '/kinksters';
  372. url = (page) ? url + '?page=' + page.toString() : url ;
  373. FL_ASL.getKinkstersFromURL(url);
  374. };
  375. FL_ASL.getKinkstersInCountry = function (country, page) {
  376. var url = 'https://fetlife.com/countries/' + country.toString() + '/kinksters';
  377. url = (page) ? url + '?page=' + page.toString() : url ;
  378. FL_ASL.getKinkstersFromURL(url);
  379. };
  380. FL_ASL.getKinkstersInGroup = function (group, page) {
  381. var url = 'https://fetlife.com/groups/' + group.toString() + '/group_memberships';
  382. url = (page) ? url + '?page=' + page.toString() : url ;
  383. FL_ASL.getKinkstersFromURL(url);
  384. };
  385. FL_ASL.getKinkstersInEvent = function (event, page) {
  386. var url = 'https://fetlife.com/events/' + event.toString() + '/rsvps';
  387. url = (page) ? url + '?page=' + page.toString() : url ;
  388. FL_ASL.getKinkstersFromURL(url);
  389. };
  390. FL_ASL.getKinkstersInFriend = function (user_id, page) {
  391. var url = 'https://fetlife.com/users/' + user_id.toString() + '/friends';
  392. url = (page) ? url + '?page=' + page.toString() : url ;
  393. FL_ASL.getKinkstersFromURL(url);
  394. };
  395. FL_ASL.getKinkstersInFetish = function (fetish_id, page) {
  396. var url = 'https://fetlife.com/fetishes/' + fetish_id.toString() + '/kinksters';
  397. url = (page) ? url + '?page=' + page.toString() : url ;
  398. FL_ASL.getKinkstersFromURL(url);
  399. };
  400. FL_ASL.getKinkstersInSearch = function (search_string, page) {
  401. var url = 'https://fetlife.com/search/kinksters/?q=' + search_string.toString();
  402. url = (page) ? url + '&page=' + page.toString() : url ;
  403. FL_ASL.getKinkstersFromURL(url);
  404. };
  405. FL_ASL.getKinkstersFromURL = function (url) {
  406. var now = new Date(Date.now());
  407. FL_ASL.log('Current time: ' + now.toUTCString());
  408. FL_ASL.log('Getting Kinksters list from URL: ' + url);
  409. // Set minimum matches, if that's been asked for.
  410. if (document.getElementById('fl_asl_min_matches').value) {
  411. FL_ASL.CONFIG.min_matches = document.getElementById('fl_asl_min_matches').value;
  412. }
  413. if (document.getElementById('fl_asl_search_sleep_interval').value) {
  414. FL_ASL.CONFIG.search_sleep_interval = document.getElementById('fl_asl_search_sleep_interval').value;
  415. }
  416. prog = document.getElementById(FL_ASL.CONFIG.progress_id);
  417. prog.innerHTML = prog.innerHTML + '.';
  418. GM_xmlhttpRequest({
  419. 'method': 'GET',
  420. 'url': url,
  421. 'onload': function (response) {
  422. var parser = new DOMParser();
  423. var doc = parser.parseFromString(response.responseText, 'text/html');
  424. var els = doc.querySelectorAll('.fl-member-card');
  425. var fl_profiles = [];
  426. for (var i = 0; i < els.length; i++) {
  427. fl_profiles.push(FL_ASL.scrapeUserInList(els[i]));
  428. }
  429. FL_ASL.GAS.ajaxPost(fl_profiles);
  430. var end = (!doc.querySelector('.pagination') || doc.querySelector('.pagination .next_page.disabled')) ? true : false;
  431.  
  432. result_count = 0;
  433. for (var i = 0; i < els.length; i++) {
  434. // filter the results based on the form parameters
  435. if (FL_ASL.matchesSearchParams(els[i])) {
  436. // display the results in a "results" section in this portion of the page
  437. FL_ASL.displayResult(els[i]);
  438. result_count++;
  439. // note total results found
  440. FL_ASL.total_result_count += result_count;
  441. }
  442. }
  443.  
  444. if (end) {
  445. jQuery('#fetlife_asl_search_progress').html('Search complete. There are no more matching results. To start a new search, <a href="' + window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search + '">reload this page</a>.');
  446. } else {
  447. // Set up next request.
  448. my_page = (url.match(/\d+$/)) ? parseInt(url.match(/\d+$/)[0]) : 1 ;
  449. next_page = my_page + 1;
  450. if (next_page > 2) {
  451. next_url = url.replace(/\d+$/, next_page.toString());
  452. } else {
  453. // Already have a query string? If so, append (&) rather than create (?).
  454. next_url = (url.match(/\?q=/)) ? url + '&page=' : url + '?page=';
  455. next_url += next_page.toString();
  456. }
  457.  
  458. // Automatically search on next page if no or too few results were found.
  459. if (0 === result_count || FL_ASL.CONFIG.min_matches >= FL_ASL.total_result_count) {
  460. setTimeout(FL_ASL.getKinkstersFromURL, FL_ASL.CONFIG.search_sleep_interval * 1000, next_url);
  461. return false;
  462. } else {
  463. // Reset total_result_count for this load.
  464. FL_ASL.total_result_count = 0;
  465. // Reset UI search feedback.
  466. p = prog.parentNode
  467. p.removeChild(prog);
  468. new_prog = document.createElement('p');
  469. new_prog.setAttribute('id', FL_ASL.CONFIG.progress_id);
  470. p.appendChild(new_prog);
  471. }
  472. var div = document.createElement('div');
  473. div.innerHTML = FL_UI.Text.donation_appeal;
  474. btn = document.createElement('button');
  475. btn.setAttribute('id', 'btn_moar');
  476. btn.setAttribute('onclick', "var xme = document.getElementById('btn_moar'); xme.parentNode.removeChild(xme); return false;");
  477. btn.innerHTML = 'Show me MOAR&hellip;';
  478. btn.addEventListener('click', function(){FL_ASL.getKinkstersFromURL(next_url)});
  479. div.appendChild(btn);
  480. document.getElementById('fetlife_asl_search_results').appendChild(div);
  481. }
  482. }
  483. });
  484. };
  485.  
  486. /**
  487. * Determines whether a "fl-member-card" block matches the searched-for parameters.
  488. *
  489. * @return True if block matches all search parameters, false otherwise.
  490. */
  491. FL_ASL.matchesSearchParams = function (el) {
  492. var search_params = FL_ASL.getSearchParams();
  493.  
  494. // Does block match location string filter?
  495. if (-1 === FL_ASL.getLocationString(el).toLowerCase().search(search_params.filter.toLowerCase())) {
  496. return false;
  497. }
  498.  
  499. // Does block match age range?
  500. var age = FL_ASL.getAge(el);
  501. // Did we supply a minimum age?
  502. if (search_params.age.min && (search_params.age.min > age) ) {
  503. return false;
  504. }
  505. // Did we supply a maximum age?
  506. if (search_params.age.max && (search_params.age.max < age) ) {
  507. return false;
  508. }
  509.  
  510. // Does block match gender/sex selection?
  511. if (-1 === search_params.sex.indexOf(FL_ASL.getGender(el))) {
  512. return false;
  513. }
  514.  
  515. // Does block match role orientation selection?
  516. if (-1 === search_params.role.indexOf(FL_ASL.getRole(el))) {
  517. return false;
  518. }
  519.  
  520. // All conditions match.
  521. return true;
  522. };
  523.  
  524. FL_ASL.getGender = function (el) {
  525. var parsed = FL_ASL.scrapeUserInList(el);
  526. if (parsed.gender) {
  527. return parsed.gender;
  528. } else {
  529. return '';
  530. }
  531. }
  532. FL_ASL.getAge = function (el) {
  533. var parsed = FL_ASL.scrapeUserInList(el);
  534. if (parsed.age) {
  535. return parseInt(parsed.age);
  536. } else {
  537. return '';
  538. }
  539. };
  540. FL_ASL.getRole = function (el) {
  541. var parsed = FL_ASL.scrapeUserInList(el);
  542. if (parsed.role) {
  543. return parsed.role;
  544. } else {
  545. return '';
  546. }
  547. };
  548. FL_ASL.getLocationString = function (el) {
  549. if (el.querySelector('.fl-member-card__location')) {
  550. return el.querySelector('.fl-member-card__location').innerHTML.trim();
  551. } else {
  552. return '';
  553. }
  554. };
  555.  
  556. FL_ASL.displayResult = function (el) {
  557. var id = FL_ASL.scrapeUserInList(el).user_id;
  558. var name = FL_ASL.scrapeUserInList(el).nickname;
  559. var a = document.createElement('a');
  560. a.href = 'https://fetlife.com/conversations/new?with=' + id;
  561. a.innerHTML = '(send ' + name + ' a message)';
  562. a.style.textDecoration = 'underline';
  563. a.setAttribute('target', '_blank');
  564. el.appendChild(a);
  565. document.getElementById('fetlife_asl_search_results').appendChild(el);
  566. };
  567.  
  568. FL_ASL.getActivateSearchButton = function () {
  569. var el = document.getElementById('fetlife_asl_search');
  570. if (!el) {
  571. el = FL_ASL.createActivateSearchButton();
  572. }
  573. return el;
  574. };
  575. FL_ASL.createActivateSearchButton = function () {
  576. var label = document.createElement('label');
  577. label.innerHTML = 'A/S/L?';
  578. var input = document.createElement('input');
  579. input.setAttribute('style', '-webkit-appearance: checkbox');
  580. input.setAttribute('type', 'checkbox');
  581. input.setAttribute('id', 'fetlife_asl_search');
  582. input.setAttribute('name', 'fetlife_asl_search');
  583. input.setAttribute('value', '1');
  584. input.addEventListener('click', FL_ASL.toggleAslSearch);
  585. label.appendChild(input);
  586. return label;
  587. };
  588. FL_ASL.createTabList = function () {
  589. var ul = document.createElement('ul');
  590. ul.setAttribute('class', 'tabs');
  591. html_string = '<li data-fl-asl-section-id="fetlife_asl_search_about"><a href="#">About FetLife ASL Search ' + GM_info.script.version + '</a></li>';
  592. html_string += '<li class="in_section" data-fl-asl-section-id="fetlife_asl_search_extended"><a href="#">Extended A/S/L search</a></li>';
  593. html_string += '<li data-fl-asl-section-id="fetlife_asl_search_classic"><a href="#">Classic (slow) search</a></li>';
  594. ul.innerHTML = html_string;
  595. ul.addEventListener('click', function (e) {
  596. var id_to_show = jQuery(e.target.parentNode).data('fl-asl-section-id');
  597. jQuery('#fetlife_asl_search_ui_container ul.tabs li').each(function (e) {
  598. if (id_to_show === jQuery(this).data('fl-asl-section-id')) {
  599. jQuery(this).addClass('in_section');
  600. jQuery('#' + id_to_show).slideDown();
  601. } else {
  602. jQuery(this).removeClass('in_section');
  603. jQuery('#' + jQuery(this).data('fl-asl-section-id')).slideUp();
  604. }
  605. });
  606. });
  607. return ul;
  608. };
  609.  
  610. FL_ASL.createSearchTab = function (id, html_string) {
  611. var div = document.createElement('div');
  612. div.setAttribute('id', id);
  613. div.innerHTML = html_string + FL_UI.Text.donation_appeal;
  614. return div;
  615. };
  616.  
  617. FL_ASL.importHtmlString = function (html_string, selector) {
  618. var external_dom = new DOMParser().parseFromString(html_string, 'text/html');
  619. var doc_part = external_dom.querySelector(selector);
  620. return document.importNode(doc_part, true);
  621. };
  622.  
  623. FL_ASL.updateUserLocation = function () {
  624. GM_deleteValue('currentUser.profile_html');
  625. FL_ASL.loadUserProfileHtml(FL_ASL.drawUserLocationSearchLabels);
  626. };
  627.  
  628. FL_ASL.drawUserLocationSearchLabels = function () {
  629. var user_loc = FL_ASL.ProfileScraper.getLocation(
  630. FL_ASL.importHtmlString(FL_ASL.getUserProfileHtml(), '#profile')
  631. );
  632. jQuery('#fl_asl_search_loc_fieldset label span').each(function () {
  633. switch (this.previousElementSibling.value) {
  634. case 'country':
  635. this.textContent = user_loc.country;
  636. break;
  637. case 'area_id':
  638. this.textContent = user_loc.region;
  639. break;
  640. case 'city_id':
  641. this.textContent = user_loc.locality;
  642. break;
  643. }
  644. });
  645. };
  646.  
  647. FL_ASL.attachSearchForm = function () {
  648. var html_string;
  649. var user_loc = FL_ASL.ProfileScraper.getLocation(
  650. FL_ASL.importHtmlString(FL_ASL.getUserProfileHtml(), '#profile')
  651. );
  652. var label = FL_ASL.getActivateSearchButton();
  653.  
  654. var container = document.createElement('div');
  655. container.setAttribute('id', 'fetlife_asl_search_ui_container');
  656. container.setAttribute('style', 'display: none;');
  657.  
  658. container.appendChild(FL_ASL.createTabList());
  659.  
  660. // "About FetLife ASL Search" tab
  661. html_string = '<p>The FetLife Age/Sex/Location Search user script allows you to search for profiles on <a href="https://fetlife.com/">FetLife</a> by age, sex, location, or orientation. This user script implements what is, as of this writing, the <a href="https://fetlife.com/improvements/78">most popular suggestion in the FetLife suggestion box</a>:</p>';
  662. html_string += '<blockquote><p>Search for people by Location/Sex/Orientation/Age</p><p>Increase the detail of the kinkster search by allowing us to narrow the definition of the search by the traditional fields.</p></blockquote>';
  663. html_string += '<p>With the FetLife Age/Sex/Location Search user script installed, a few clicks will save hours of time. Now you can find profiles that match your specified criteria in a matter of seconds. The script even lets you send a message to the profiles you found right from the search results list.</p>';
  664. html_string += '<p>Stay up to date with the <a href="https://github.com/fabacab/fetlife-aslsearch/">latest FetLife ASL Search improvements</a>. New versions add new features and improve search performance.</p>';
  665. container.appendChild(FL_ASL.createSearchTab('fetlife_asl_search_about', html_string));
  666.  
  667. // Extended search tab
  668. html_string = '<div id="fetlife_asl_search_extended_wrapper">';
  669. html_string += '<h2><a href="' + FL_ASL.CONFIG.gasapp_url.split('?')[0] + '" target="_blank">Open Extended A/S/L Search</a></h2>';
  670. html_string += '</div><!-- #fetlife_asl_search_extended_wrapper -->';
  671. var newdiv = container.appendChild(FL_ASL.createSearchTab('fetlife_asl_search_extended', html_string));
  672.  
  673. // Main ASL search option interface
  674. html_string = '<fieldset><legend>Search for user profiles of the following gender/sex:</legend><p>';
  675. html_string += 'Show me profiles of people with a gender/sex of&hellip;';
  676. html_string += '<label><input type="checkbox" name="user[sex]" value="M" checked="checked" /> Male</label>';
  677. html_string += '<label><input type="checkbox" name="user[sex]" value="F" /> Female</label>';
  678. html_string += '<label><input type="checkbox" name="user[sex]" value="CD/TV" />Crossdresser/Transvestite</label>';
  679. html_string += '<label><input type="checkbox" name="user[sex]" value="MtF" />Trans - Male to Female</label>';
  680. html_string += '<label><input type="checkbox" name="user[sex]" value="FtM" checked="checked" />Trans - Female to Male</label>';
  681. html_string += '<label><input type="checkbox" name="user[sex]" value="TG" />Transgender</label>';
  682. html_string += '<label><input type="checkbox" name="user[sex]" value="GF" />Gender Fluid</label>';
  683. html_string += '<label><input type="checkbox" name="user[sex]" value="GQ" />Genderqueer</label>';
  684. html_string += '<label><input type="checkbox" name="user[sex]" value="IS" />Intersex</label>';
  685. html_string += '<label><input type="checkbox" name="user[sex]" value="B" />Butch</label>';
  686. html_string += '<label><input type="checkbox" name="user[sex]" value="FEM" />Femme</label>';
  687. html_string += '</p></fieldset>';
  688. html_string += '<fieldset><legend>Search for user profiles between the ages of:</legend><p>';
  689. html_string += '&hellip;who are also <label>at least <input type="text" name="min_age" id="min_age" placeholder="18" size="2" /> years old</label> and <label>at most <input type="text" name="max_age" id="max_age" placeholder="92" size="2" /> years old&hellip;</label>';
  690. html_string += '</p></fieldset>';
  691. html_string += '<fieldset><legend>Search for user profiles whose role is:</legend><p>';
  692. html_string += '&hellip;who identify their role as ';
  693. // Note that these values are what show up, not necessarily what's sent to the FetLife backend.
  694. html_string += '<label><input type="checkbox" value="Dom" name="user[role]" />Dominant</label>';
  695. html_string += '<label><input type="checkbox" value="Domme" name="user[role]" />Domme</label>';
  696. html_string += '<label><input type="checkbox" value="Switch" name="user[role]" />Switch</label>';
  697. html_string += '<label><input type="checkbox" value="sub" name="user[role]" />submissive</label>';
  698. html_string += '<label><input type="checkbox" value="Master" name="user[role]" />Master</label>';
  699. html_string += '<label><input type="checkbox" value="Mistress" name="user[role]" />Mistress</label>';
  700. html_string += '<label><input type="checkbox" value="slave" name="user[role]" />slave</label>';
  701. html_string += '<label><input type="checkbox" value="kajira" name="user[role]" />kajira</label>';
  702. html_string += '<label><input type="checkbox" value="kajirus" name="user[role]" />kajirus</label>';
  703. html_string += '<label><input type="checkbox" value="Top" name="user[role]" />Top</label>';
  704. html_string += '<label><input type="checkbox" value="bottom" name="user[role]" />Bottom</label>';
  705. html_string += '<label><input type="checkbox" value="Sadist" name="user[role]" />Sadist</label>';
  706. html_string += '<label><input type="checkbox" value="Masochist" name="user[role]" />Masochist</label>';
  707. html_string += '<label><input type="checkbox" value="Sadomasochist" name="user[role]" />Sadomasochist</label>';
  708. html_string += '<label><input type="checkbox" value="Kinkster" name="user[role]" />Kinkster</label>';
  709. html_string += '<label><input type="checkbox" value="Fetishist" name="user[role]" />Fetishist</label>';
  710. html_string += '<label><input type="checkbox" value="Swinger" name="user[role]" />Swinger</label>';
  711. html_string += '<label><input type="checkbox" value="Hedonist" name="user[role]" />Hedonist</label>';
  712. html_string += '<label><input type="checkbox" value="Exhibitionist" name="user[role]" />Exhibitionist</label>';
  713. html_string += '<label><input type="checkbox" value="Voyeur" name="user[role]" />Voyeur</label>';
  714. html_string += '<label><input type="checkbox" value="Sensualist" name="user[role]" />Sensualist</label>';
  715. html_string += '<label><input type="checkbox" value="Princess" name="user[role]" />Princess</label>';
  716. html_string += '<label><input type="checkbox" value="Slut" name="user[role]" />Slut</label>';
  717. html_string += '<label><input type="checkbox" value="Doll" name="user[role]" />Doll</label>';
  718. html_string += '<label><input type="checkbox" value="sissy" name="user[role]" />sissy</label>';
  719. html_string += '<label><input type="checkbox" value="Rigger" name="user[role]" />Rigger</label>';
  720. html_string += '<label><input type="checkbox" value="Rope Top" name="user[role]" />Rope Top</label>';
  721. html_string += '<label><input type="checkbox" value="Rope Bottom" name="user[role]" />Rope Bottom</label>';
  722. html_string += '<label><input type="checkbox" value="Rope Bunny" name="user[role]" />Rope Bunny</label>';
  723. html_string += '<label><input type="checkbox" value="Spanko" name="user[role]" />Spanko</label>';
  724. html_string += '<label><input type="checkbox" value="Spanker" name="user[role]" />Spanker</label>';
  725. html_string += '<label><input type="checkbox" value="Spankee" name="user[role]" />Spankee</label>';
  726. html_string += '<label><input type="checkbox" value="Furry" name="user[role]" />Furry</label>';
  727. html_string += '<label><input type="checkbox" value="Leather Man" name="user[role]" />Leather Man</label>';
  728. html_string += '<label><input type="checkbox" value="Leather Woman" name="user[role]" />Leather Woman</label>';
  729. html_string += '<label><input type="checkbox" value="Leather Daddy" name="user[role]" />Leather Daddy</label>';
  730. html_string += '<label><input type="checkbox" value="Leather Top" name="user[role]" />Leather Top</label>';
  731. html_string += '<label><input type="checkbox" value="Leather bottom" name="user[role]" />Leather bottom</label>';
  732. html_string += '<label><input type="checkbox" value="Leather boy" name="user[role]" />Leather boy</label>';
  733. html_string += '<label><input type="checkbox" value="Leather girl" name="user[role]" />Leather girl</label>';
  734. html_string += '<label><input type="checkbox" value="Leather Boi" name="user[role]" />Leather Boi</label>';
  735. html_string += '<label><input type="checkbox" value="Bootblack" name="user[role]" />Bootblack</label>';
  736. html_string += '<label><input type="checkbox" value="Primal" name="user[role]" />Primal</label>';
  737. html_string += '<label><input type="checkbox" value="Primal Predator" name="user[role]" />Primal Predator</label>';
  738. html_string += '<label><input type="checkbox" value="Primal Prey" name="user[role]" />Primal Prey</label>';
  739. html_string += '<label><input type="checkbox" value="Bull" name="user[role]" />Bull</label>';
  740. html_string += '<label><input type="checkbox" value="cuckold" name="user[role]" />cuckold</label>';
  741. html_string += '<label><input type="checkbox" value="cuckquean" name="user[role]" />cuckquean</label>';
  742. html_string += '<label><input type="checkbox" value="Ageplayer" name="user[role]" />Ageplayer</label>';
  743. html_string += '<label><input type="checkbox" value="Daddy" name="user[role]" />Daddy</label>';
  744. html_string += '<label><input type="checkbox" value="Mommy" name="user[role]" />Mommy</label>';
  745. html_string += '<label><input type="checkbox" value="Big" name="user[role]" />Big</label>';
  746. html_string += '<label><input type="checkbox" value="Middle" name="user[role]" />Middle</label>';
  747. html_string += '<label><input type="checkbox" value="little" name="user[role]" />little</label>';
  748. html_string += '<label><input type="checkbox" value="brat" name="user[role]" />brat</label>';
  749. html_string += '<label><input type="checkbox" value="babygirl" name="user[role]" />babygirl</label>';
  750. html_string += '<label><input type="checkbox" value="babyboy" name="user[role]" />babyboy</label>';
  751. html_string += '<label><input type="checkbox" value="pet" name="user[role]" />pet</label>';
  752. html_string += '<label><input type="checkbox" value="kitten" name="user[role]" />kitten</label>';
  753. html_string += '<label><input type="checkbox" value="pup" name="user[role]" />pup</label>';
  754. html_string += '<label><input type="checkbox" value="pony" name="user[role]" />pony</label>';
  755. html_string += '<label><input type="checkbox" value="Evolving" name="user[role]" />Evolving</label>';
  756. html_string += '<label><input type="checkbox" value="Exploring" name="user[role]" />Exploring</label>';
  757. html_string += '<label><input type="checkbox" value="Vanilla" name="user[role]" />Vanilla</label>';
  758. html_string += '<label><input type="checkbox" value="Undecided" name="user[role]" />Undecided</label>';
  759. html_string += '<label><input type="checkbox" value="" name="user[role]" />Not Applicable</label>';
  760. html_string += '</p></fieldset>';
  761. html_string += '<fieldset id="fl_asl_search_loc_fieldset"><legend>Search for user profiles located in:</legend><p>';
  762. html_string += '&hellip;from ';
  763. // If we're on a "groups" or "events" or "user" or "fetish" or "search" page,
  764. var which_thing = window.location.toString().match(/(group|event|user|fetish)e?s\/(\d+)/) || window.location.toString().match(/(search)\/kinksters\/?\?(?:page=\d+&)?q=(\S+)/);
  765. if (null !== which_thing) {
  766. switch (which_thing[1]) {
  767. case 'user':
  768. var label_text = "user's friends";
  769. break;
  770. case 'group': // fall through
  771. case 'event':
  772. case 'fetish':
  773. case 'search':
  774. default:
  775. var label_text = which_thing[1];
  776. break;
  777. }
  778. // offer an additional option to search for users associated with this object rather than geography.
  779. html_string += '<label><input type="radio" name="fl_asl_loc" value="' + which_thing[1] + '" data-flasl' + which_thing[1] + 'id="' + which_thing[2] + '"/>this ' + label_text + '</label>';
  780. html_string += '<label id="fl_asl_loc_filter_label" style="display: none;"> located in <input type="text" id="fl_asl_loc_filter" name="fl_asl_loc_filter" /></label>';
  781. html_string += ', or ';
  782. }
  783. html_string += ' my <label><input type="radio" name="fl_asl_loc" value="city_id" />city (<span>' + user_loc.locality + '</span>)</label>';
  784. html_string += '<label><input type="radio" name="fl_asl_loc" value="area_id" checked="checked" />state/province (<span>' + user_loc.region + '</span>)</label>';
  785. html_string += '<label><input type="radio" name="fl_asl_loc" value="country" />country (<span>' + user_loc.country + '</span>)</label>';
  786. html_string += '. <abbr title="If you changed the location on your profile, click the &ldquo;Update your location&rdquo; button to set FetLife ASL Search to your new location. You can also choose a search set other than your profile location when you load FetLife ASL Search on certain pages that imply their own search set, such as a user profile (for searching a friends list) a group page (for searching group members) an event (for searching RSVPs), or a fetish (for searching kinksters with that fetish). You can then further filter the results from the friend list, event RSVPs, etc. based on the name of a city, state/province, or country."></abbr></p></fieldset>';
  787. html_string += '<fieldset><legend>Result set options:</legend><p>';
  788. html_string += '<label>Return at least <input id="fl_asl_min_matches" name="fl_asl_min_matches" value="" placeholder="1" size="2" /> matches per search.</label> (Set this lower if no results seem to ever appear.)';
  789. html_string += '</p></fieldset>';
  790. html_string += '<fieldset><legend>Search speed options:</legend><p>';
  791. html_string += '<label>Online search speed: Aggressive (faster) <input id="fl_asl_search_sleep_interval" name="fl_asl_search_sleep_interval" type="range" min="0" max="10" step="0.5" value="' + FL_ASL.CONFIG.search_sleep_interval + '" /> Stealthy (slower)</label>';
  792. html_string += '<br />(Wait <output name="fl_asl_search_sleep_interval" for="fl_asl_search_sleep_interval">' + FL_ASL.CONFIG.search_sleep_interval + '</output> seconds between searches.) <abbr title="FetLife has begun banning accounts that search with this script too quickly. An aggressive search is faster, but riskier. A stealthier search is slower, but safer."></span>';
  793. html_string += '</p></fieldset>';
  794. var div = FL_ASL.createSearchTab('fetlife_asl_search_classic', html_string);
  795. div.querySelector('input[name="fl_asl_search_sleep_interval"]').addEventListener('input', function (e) {
  796. div.querySelector('output[name="fl_asl_search_sleep_interval"]').value = this.value;
  797. });
  798. // Help buttons
  799. FL_UI.Dialog.createLink(
  800. 'fl_asl_loc-help',
  801. '(Update location.)',
  802. div.querySelector('#fl_asl_search_loc_fieldset abbr')
  803. );
  804. html_string = '<p><a id="btn_fetlife_asl_update_location" class="btnsqr close" data-closes-modal="fl_asl_loc-help">Update your location</a></p>';
  805. html_string += '<p>' + div.querySelector('#fl_asl_search_loc_fieldset abbr').getAttribute('title') + '</p>';
  806. FL_UI.Dialog.inject(
  807. 'fl_asl_loc-help',
  808. 'Change location',
  809. html_string
  810. );
  811. document.getElementById('btn_fetlife_asl_update_location').addEventListener('click', FL_ASL.updateUserLocation);
  812. FL_UI.Dialog.createLink(
  813. 'fl_asl_search_sleep_interval-help',
  814. '(Help with online search speed.)',
  815. div.querySelector('output[name="fl_asl_search_sleep_interval"] + abbr')
  816. );
  817. FL_UI.Dialog.inject(
  818. 'fl_asl_search_sleep_interval-help',
  819. 'About &ldquo;Online search speed&rdquo;',
  820. div.querySelector('output[name="fl_asl_search_sleep_interval"] + abbr').getAttribute('title')
  821. );
  822. container.appendChild(div);
  823. FL_ASL.CONFIG.search_form.appendChild(label);
  824. FL_ASL.CONFIG.search_form.appendChild(container);
  825. var radio_els = document.querySelectorAll('input[name="fl_asl_loc"]');
  826. for (var i = 0; i < radio_els.length; i++) {
  827. radio_els[i].addEventListener('click', FL_ASL.toggleLocationFilter);
  828. }
  829.  
  830. btn_submit = document.createElement('button');
  831. btn_submit.setAttribute('id', 'btn_fetlife_asl_search_submit');
  832. btn_submit.setAttribute('onclick', "var xme = document.getElementById('btn_fetlife_asl_search_submit'); xme.parentNode.removeChild(xme); return false;");
  833. btn_submit.innerHTML = 'Mine! (I mean, uh, search&hellip;)';
  834. btn_submit.addEventListener('click', FL_ASL.aslSubmit);
  835. div.appendChild(btn_submit);
  836.  
  837. results_container = document.createElement('div');
  838. results_container.setAttribute('id', 'fetlife_asl_search_results');
  839. FL_ASL.CONFIG.search_form.appendChild(results_container);
  840.  
  841. prog = document.createElement('p');
  842. prog.setAttribute('id', FL_ASL.CONFIG.progress_id);
  843. FL_ASL.CONFIG.search_form.appendChild(prog);
  844.  
  845. // Re-attach the search form after page load if for "some reason" it is not here.
  846. // See https://github.com/fabacab/fetlife-aslsearch/issues/27
  847. setInterval(function () {
  848. if (!window.document.querySelector('#fetlife_asl_search_ui_container')) {
  849. FL_ASL.attachSearchForm();
  850. }
  851. }, 2000);
  852. };
  853.  
  854. // ****************************************************
  855. //
  856. // Google Apps Script interface
  857. //
  858. // ****************************************************
  859. FL_ASL.GAS = {};
  860. FL_ASL.GAS.ajaxPost = function (data) {
  861. FL_ASL.log('POSTing profile data for ' + data.length + ' users.');
  862. var url = (FL_ASL.CONFIG.debug)
  863. ? FL_ASL.CONFIG.gasapp_url_development
  864. : FL_ASL.CONFIG.gasapp_url;
  865. GM_xmlhttpRequest({
  866. 'method': 'POST',
  867. 'url': url,
  868. 'data': 'post_data=' + encodeURIComponent(JSON.stringify(data)),
  869. 'headers': {
  870. 'Content-Type': 'application/x-www-form-urlencoded'
  871. },
  872. 'onload': function (response) {
  873. FL_ASL.log('POST response received: ' + response.responseText);
  874. },
  875. 'onerror': function (response) {
  876. FL_ASL.log('Error POSTing to ' + url + ', response received: ' + response.responseText);
  877. }
  878. });
  879. };
  880.  
  881. // ****************************************************
  882. //
  883. // Scrapers
  884. //
  885. // ****************************************************
  886. FL_ASL.ProfileScraper = {};
  887. FL_ASL.ProfileScraper.getNickname = function () {
  888. return jQuery('#main_content h2').first().text().split(' ')[0];
  889. };
  890. FL_ASL.ProfileScraper.getAge = function () {
  891. var x = $x('//h2/*[@class[contains(., "quiet")]]');
  892. var ret;
  893. if (x.length) {
  894. y = x[0].textContent.match(/^\d+/);
  895. if (y) {
  896. ret = y[0];
  897. }
  898. }
  899. return ret;
  900. };
  901. FL_ASL.ProfileScraper.getGender = function () {
  902. var x = $x('//h2/*[@class[contains(., "quiet")]]');
  903. var ret = '';
  904. if (x.length) {
  905. y = x[0].textContent.match(/[^\d ]+/);
  906. if (y) {
  907. ret = y[0];
  908. }
  909. }
  910. return ret;
  911. };
  912. FL_ASL.ProfileScraper.getRole = function (body) {
  913. var x = $x('//h2/*[@class[contains(., "quiet")]]');
  914. var ret = '';
  915. if (x.length) {
  916. y = x[0].textContent.match(/ .+/);
  917. if (y) {
  918. ret = y[0].trim();
  919. }
  920. }
  921. return ret;
  922. };
  923. FL_ASL.ProfileScraper.getFriendCount = function (body) {
  924. var x = $x('//h4[starts-with(., "Friends")]');
  925. var ret = 0;
  926. if (x.length) {
  927. ret = x[0].textContent.match(/\(([\d,]+)\)/)[1].replace(',', '');
  928. }
  929. return ret;
  930. };
  931. FL_ASL.ProfileScraper.isPaidAccount = function () {
  932. return (document.querySelector('.fl-badge')) ? true : false;
  933. };
  934. FL_ASL.ProfileScraper.getLocation = function (dom) {
  935. var dom = dom || document;
  936. var x = $x('//h2[@class="bottom"]/following-sibling::p//a', dom);
  937. var ret = {
  938. 'locality': '',
  939. 'region': '',
  940. 'country': ''
  941. };
  942. if (3 === x.length) {
  943. ret['country'] = x[2].textContent;
  944. ret['region'] = x[1].textContent;
  945. ret['locality'] = x[0].textContent;
  946. } else if (2 === x.length) {
  947. ret['country'] = x[1].textContent;
  948. ret['region'] = x[0].textContent;
  949. } else if (1 === x.length) {
  950. ret['country'] = x[0].textContent;
  951. }
  952. return ret;
  953. };
  954. FL_ASL.ProfileScraper.getAvatar = function () {
  955. var el = document.querySelector('.pan');
  956. var ret;
  957. if (el) {
  958. ret = el.src;
  959. }
  960. return ret;
  961. };
  962. FL_ASL.ProfileScraper.getSexualOrientation = function () {
  963. var x = $x('//table//th[starts-with(., "orientation")]/following-sibling::td');
  964. var ret = '';
  965. if (x.length) {
  966. ret = x[0].textContent.trim();
  967. }
  968. return ret;
  969. };
  970. FL_ASL.ProfileScraper.getInterestLevel = function () {
  971. var x = $x('//table//th[starts-with(., "active")]/following-sibling::td');
  972. var ret = [];
  973. if (x.length) {
  974. ret = x[0].textContent.trim();
  975. }
  976. return ret;
  977. };
  978. FL_ASL.ProfileScraper.getLookingFor = function () {
  979. var x = $x('//table//th[starts-with(., "is looking for")]/following-sibling::td');
  980. var ret = [];
  981. if (x.length) {
  982. ret = x[0].innerHTML.split('<br>');
  983. }
  984. return ret;
  985. };
  986. FL_ASL.ProfileScraper.getRelationships = function () {
  987. var x = $x('//table//th[starts-with(., "relationship status")]/following-sibling::td//a');
  988. var ret = [];
  989. for (var i = 0; i < x.length; i++) {
  990. ret.push(x[i].href.match(/\d+$/)[0]);
  991. }
  992. return ret;
  993. };
  994. FL_ASL.ProfileScraper.getDsRelationships = function () {
  995. var x = $x('//table//th[starts-with(., "D/s relationship status")]/following-sibling::td//a');
  996. var ret = [];
  997. for (var i = 0; i < x.length; i++) {
  998. ret.push(x[i].href.match(/\d+$/)[0]);
  999. }
  1000. return ret;
  1001. };
  1002. FL_ASL.ProfileScraper.getBio = function () {
  1003. var html = '';
  1004. jQuery($x('//h3[@class][starts-with(., "About me")]')).nextUntil('h3.bottom').each(function () {
  1005. html += jQuery(this).html();
  1006. });
  1007. return html;
  1008. };
  1009. FL_ASL.ProfileScraper.getWebsites = function () {
  1010. var x = $x('//h3[@class="bottom"][starts-with(., "Websites")]/following-sibling::ul[1]//a');
  1011. var ret = [];
  1012. for (var i = 0; i < x.length; i++) {
  1013. ret.push(x[i].textContent.trim());
  1014. }
  1015. return ret;
  1016. };
  1017. FL_ASL.ProfileScraper.getLastActivity = function () {
  1018. // TODO: Convert this relative date string to a timestamp
  1019. var x = document.querySelector('#mini_feed .quiet');
  1020. var ret;
  1021. if (x) {
  1022. ret = x.textContent.trim();
  1023. }
  1024. return ret;
  1025. };
  1026. FL_ASL.ProfileScraper.getFetishesInto = function () {
  1027. var x = $x('//h3[@class="bottom"][starts-with(., "Fetishes")]/following-sibling::p[1]//a');
  1028. var ret = [];
  1029. for (var i = 0; i < x.length; i++) {
  1030. ret.push(x[i].textContent.trim());
  1031. }
  1032. return ret;
  1033. };
  1034. FL_ASL.ProfileScraper.getFetishesCuriousAbout = function () {
  1035. var x = $x('//h3[@class="bottom"][starts-with(., "Fetishes")]/following-sibling::p[2]//a');
  1036. var ret = [];
  1037. for (var i = 0; i < x.length; i++) {
  1038. ret.push(x[i].textContent.trim());
  1039. }
  1040. return ret;
  1041. };
  1042. FL_ASL.ProfileScraper.getPicturesCount = function () {
  1043. var el = document.getElementById('user_pictures_link');
  1044. var ret = 0;
  1045. if (el) {
  1046. ret = el.nextSibling.textContent.match(/\d+/)[0];
  1047. }
  1048. return ret;
  1049. };
  1050. FL_ASL.ProfileScraper.getVideosCount = function () {
  1051. var el = document.getElementById('user_videos_link');
  1052. var ret = 0;
  1053. if (el) {
  1054. ret = el.nextSibling.textContent.match(/\d+/)[0];
  1055. }
  1056. return ret;
  1057. };
  1058. FL_ASL.ProfileScraper.getLatestPosts = function () {
  1059. // TODO:
  1060. };
  1061. FL_ASL.ProfileScraper.getGroupsLead = function () {
  1062. // TODO:
  1063. };
  1064. FL_ASL.ProfileScraper.getGroupsMemberOf = function () {
  1065. // TODO:
  1066. };
  1067. FL_ASL.ProfileScraper.getEventsGoingTo = function () {
  1068. // TODO:
  1069. };
  1070. FL_ASL.ProfileScraper.getEventsMaybeGoingTo = function () {
  1071. // TODO:
  1072. };
  1073.  
  1074. FL_ASL.scrapeProfile = function (user_id) {
  1075. if (!window.location.pathname.endsWith(user_id)) {
  1076. FL_ASL.log('Profile page does not match ' + user_id);
  1077. return false;
  1078. }
  1079. var profile_data = {
  1080. 'user_id': user_id,
  1081. 'nickname': FL_ASL.ProfileScraper.getNickname(),
  1082. 'age': FL_ASL.ProfileScraper.getAge(),
  1083. 'gender': FL_ASL.ProfileScraper.getGender(),
  1084. 'role': FL_ASL.ProfileScraper.getRole(),
  1085. 'friend_count': FL_ASL.ProfileScraper.getFriendCount(),
  1086. 'paid_account': FL_ASL.ProfileScraper.isPaidAccount(),
  1087. 'location_locality': FL_ASL.ProfileScraper.getLocation().locality,
  1088. 'location_region': FL_ASL.ProfileScraper.getLocation().region,
  1089. 'location_country': FL_ASL.ProfileScraper.getLocation().country,
  1090. 'avatar_url': FL_ASL.ProfileScraper.getAvatar(),
  1091. 'sexual_orientation': FL_ASL.ProfileScraper.getSexualOrientation(),
  1092. 'interest_level': FL_ASL.ProfileScraper.getInterestLevel(),
  1093. 'looking_for': FL_ASL.ProfileScraper.getLookingFor(),
  1094. 'relationships': FL_ASL.ProfileScraper.getRelationships(),
  1095. 'ds_relationships': FL_ASL.ProfileScraper.getDsRelationships(),
  1096. 'bio': FL_ASL.ProfileScraper.getBio(),
  1097. 'websites': FL_ASL.ProfileScraper.getWebsites(),
  1098. 'last_activity': FL_ASL.ProfileScraper.getLastActivity(),
  1099. 'fetishes_into': FL_ASL.ProfileScraper.getFetishesInto(),
  1100. 'fetishes_curious_about': FL_ASL.ProfileScraper.getFetishesCuriousAbout(),
  1101. 'num_pics': FL_ASL.ProfileScraper.getPicturesCount(),
  1102. 'num_vids': FL_ASL.ProfileScraper.getVideosCount(),
  1103. 'latest_posts': FL_ASL.ProfileScraper.getLatestPosts(),
  1104. 'groups_lead': FL_ASL.ProfileScraper.getGroupsLead(),
  1105. 'groups_member_of': FL_ASL.ProfileScraper.getGroupsMemberOf(),
  1106. 'events_going_to': FL_ASL.ProfileScraper.getEventsGoingTo(),
  1107. 'events_maybe_going_to': FL_ASL.ProfileScraper.getEventsMaybeGoingTo()
  1108. };
  1109. return profile_data;
  1110. }
  1111. FL_ASL.scrapeUserInList = function (node) {
  1112. // Deal with location inconsistencies.
  1113. var loc_parts = jQuery(node).find('.fl-member-card__location').first().text().split(', ');
  1114. var locality = ''; var region = ''; var country = '';
  1115. if (2 === loc_parts.length) {
  1116. locality = loc_parts[0];
  1117. region = loc_parts[1];
  1118. } else if (1 === loc_parts.length) {
  1119. country = loc_parts[0];
  1120. }
  1121. var profile_data = {
  1122. 'user_id': jQuery(node).find('a').first().attr('href').match(/\d+$/)[0],
  1123. 'nickname': jQuery(node).find('img').first().attr('alt'),
  1124. 'location_locality': locality.trim(),
  1125. 'location_region': region.trim(),
  1126. 'location_country': country.trim(),
  1127. 'avatar_url': jQuery(node).find('img').first().attr('src')
  1128. };
  1129. var member_info = jQuery(node).find('.fl-member-card__info').text().trim();
  1130. if (member_info.match(/^\d+/) instanceof Array) {
  1131. profile_data['age'] = member_info.match(/^\d+/)[0].trim();
  1132. }
  1133. if (member_info.match(/[^\d ]+/) instanceof Array) {
  1134. profile_data['gender'] = member_info.match(/[^\d ]+/)[0].trim();
  1135. }
  1136. if (member_info.match(/ (.*)$/) instanceof Array) {
  1137. profile_data['role'] = member_info.match(/ (.*)$/)[1].trim();
  1138. }
  1139. for (var k in profile_data) {
  1140. if ('' === profile_data[k]) {
  1141. delete profile_data[k];
  1142. }
  1143. }
  1144. return profile_data;
  1145. };
  1146. FL_ASL.scrapeAnchoredAvatar = function (node) {
  1147. var profile_data = {
  1148. 'user_id': jQuery(node).attr('href').match(/\d+$/)[0],
  1149. 'nickname': jQuery(node).find('img').first().attr('alt'),
  1150. 'avatar_url': jQuery(node).find('img').first().attr('src')
  1151. };
  1152. return profile_data;
  1153. };
  1154.  
  1155. // This is the main() function, executed on page load.
  1156. FL_ASL.main = function () {
  1157. // Insert ASL search button interface at FetLife "Search" bar.
  1158. FL_ASL.attachSearchForm();
  1159.  
  1160. var fl_profiles = [];
  1161. var m;
  1162. if (m = window.location.pathname.match(/users\/(\d+)/)) {
  1163. FL_ASL.log('Scraping profile ' + m[1]);
  1164. fl_profiles.push(FL_ASL.scrapeProfile(m[1]));
  1165. }
  1166. if (document.querySelectorAll('.fl-member-card').length) {
  1167. jQuery('.fl-member-card').each(function () {
  1168. fl_profiles.push(FL_ASL.scrapeUserInList(this));
  1169. });
  1170. }
  1171. if (document.querySelectorAll('a.avatar').length) {
  1172. jQuery('a.avatar').each(function () {
  1173. fl_profiles.push(FL_ASL.scrapeAnchoredAvatar(this));
  1174. });
  1175. }
  1176. FL_ASL.GAS.ajaxPost(fl_profiles);
  1177. };
  1178.  
  1179. FAADE = {};
  1180. FAADE.CONFIG = {
  1181. 'debug': false, // switch to true to debug.
  1182. 'gdocs_key': '1xJDW-i4oqfCKN02KmOJi8uORiV-xRtw0erXWOw50mOo',
  1183. 'gform_key': '1Zpmq4ZgrcUMAcDHgfT4ne_eAq71IKnONIbrQNfCP8gs',
  1184. 'gdocs_development_key': '1z53rFX1g0E8DzuyXfyDrK9N1E3D-YFGvyFktqnHpLII',
  1185. 'gform_development_key': 'dGxjMUhyR0FzLWJicHNXUFdxckFEQWc6MQ',
  1186. };
  1187.  
  1188. // Utility debugging function.
  1189. FAADE.log = function (msg) {
  1190. if (!FAADE.CONFIG.debug) { return; }
  1191. GM_log('FETLIFE FAADE: ' + msg);
  1192. //console.log('FETLIFE FAADE: ' + msg);
  1193. };
  1194.  
  1195. // Initializations.
  1196. var uw = (unsafeWindow) ? unsafeWindow : window ; // Help with Chrome compatibility?
  1197. GM_addStyle('\
  1198. /* Highlight alleged abusers in bright yellow. */\
  1199. .faade_alleged_abuser {\
  1200. display: inline-block;\
  1201. border: 2px solid yellow;\
  1202. }\
  1203. #faade_abuse_reports caption {\
  1204. background: yellow;\
  1205. color: red;\
  1206. }\
  1207. #faade_abuse_reports tfoot td {\
  1208. padding-top: 1em;\
  1209. text-align: center;\
  1210. }\
  1211. #faade_abuse_reports tr:target > * {\
  1212. border: 1px solid red;\
  1213. }\
  1214. #faade_abuse_reports tr:target th {\
  1215. border-width: 1px 0 1px 1px;\
  1216. }\
  1217. #faade_abuse_reports tr:target td {\
  1218. border-width: 1px 1px 1px 0;\
  1219. }\
  1220. /* FAADE location broadcast dialog styles. */\
  1221. [aria-labelledby="ui-dialog-title-faade"] { border-color: yellow; }\
  1222. #ui-dialog-title-faade { color: red; }\
  1223. /* General prettiness. */\
  1224. #profile #main_content a + a.faade_report_link { padding-left: 5px; }\
  1225. footer .faade_report_link,\
  1226. .blog_entry p.quiet.small .faade_report_link,\
  1227. .byline .faade_report_link {\
  1228. display: block;\
  1229. color: #777;\
  1230. }\
  1231. .mini_feed_title .faade_report_link {\
  1232. float: left;\
  1233. padding-right: 5px;\
  1234. }\
  1235. ul.pictures li a.faade_report_link,\
  1236. #profile ul.friends li { width: auto; }\
  1237. ');
  1238. FAADE.init = function () {
  1239. // Whenever we load CreepShield, just clear the cookies.
  1240. if (window.location.hostname.match(/creepshield.com/)) {
  1241. FAADE.clearCookies();
  1242. return;
  1243. }
  1244. FL_ASL.getUserProfileHtml(uw.FetLife.currentUser.id); // run early
  1245. FAADE.injectDialog();
  1246. FAADE.abuser_database = FAADE.getValue('abuser_database', false);
  1247. if (FAADE.abuserDatabaseExpired()) {
  1248. FAADE.fetchAbuserDatabase();
  1249. }
  1250. FAADE.main();
  1251. };
  1252. jQuery(document).ready(function () {
  1253. FAADE.init();
  1254. });
  1255.  
  1256. // Determines whether the abuser database has expired and needs to be re-fetched.
  1257. FAADE.abuserDatabaseExpired = function () {
  1258. // If we don't have a database, then of course it's "expired."
  1259. if (!FAADE.abuser_database) {
  1260. FAADE.log('Abuser database expired because of false-equivalent value.');
  1261. return true;
  1262. } else if ( (new Date().getTime() > (parseInt(FAADE.getValue('last_fetch_time')) + 86400)) ) {
  1263. // Abuser database was last fetched more than 24 hours (86400 seconds) ago, so refresh.
  1264. FAADE.log('Abuser database expired because of time.');
  1265. return true;
  1266. } else {
  1267. FAADE.log('Abuser database still fresh.');
  1268. return false;
  1269. }
  1270. };
  1271.  
  1272. FAADE.getDatabaseConnectionString = function () {
  1273. return (FAADE.CONFIG.debug) ?
  1274. FAADE.CONFIG.gdocs_development_key :
  1275. FAADE.CONFIG.gdocs_key;
  1276. };
  1277. FAADE.getReportFormKey = function () {
  1278. return (FAADE.CONFIG.debug) ?
  1279. FAADE.CONFIG.gform_development_key :
  1280. FAADE.CONFIG.gform_key;
  1281. };
  1282.  
  1283. FAADE.setValue = function (x, y) {
  1284. return (FAADE.CONFIG.debug) ?
  1285. GM_setValue(x += '_development', y) :
  1286. GM_setValue(x, y);
  1287. };
  1288. FAADE.getValue = function (x, y) {
  1289. if (arguments.length === 1) {
  1290. return (FAADE.CONFIG.debug) ?
  1291. GM_getValue(x += '_development'):
  1292. GM_getValue(x);
  1293. } else {
  1294. return (FAADE.CONFIG.debug) ?
  1295. GM_getValue(x += '_development', y):
  1296. GM_getValue(x, y);
  1297. }
  1298. };
  1299.  
  1300. FAADE.fetchAbuserDatabase = function () {
  1301. var key = FAADE.getDatabaseConnectionString();
  1302. var url = 'https://docs.google.com/spreadsheets/d/' + key + '/pub';
  1303. FAADE.log('fetching abusers database from ' + url);
  1304. GM_xmlhttpRequest({
  1305. 'method': 'GET',
  1306. 'url': url,
  1307. 'onload': function (response) {
  1308. if (!response.finalUrl.match(/^https:\/\/docs.google.com\/spreadsheets\/d/)) {
  1309. FAADE.log('Failed to fetch abuser database from ' + url);
  1310. return false;
  1311. }
  1312. FAADE.setValue('last_fetch_time', new Date().getTime().toString()); // timestamp this fetch
  1313. FAADE.setValue('abuser_database', response.responseText);
  1314. FAADE.abuser_database = FAADE.getValue('abuser_database');
  1315. }
  1316. });
  1317. };
  1318.  
  1319. FAADE.injectDialog = function () {
  1320. FL_UI.Dialog.createLink('faade', '', document.body);
  1321. var html_string = '<p class="mbm">There have been <span id="faade_reports_to_alert">X</span> new consent violations filed to the Predator Alert Tool for FetLife that may have been perpetrated near your location (<span id="faade_user_loc">X, X, X</span>).</p>';
  1322. html_string += '<p>Click "View new nearby PAT-FetLife reports" to view the profiles of the people who have been accused of consent violations near your area in new tabs.</p>';
  1323. html_string += '<p id="faade-actions" class="ac">';
  1324. html_string += '<a rel="nofollow" class="btnsqr close" data-closes-modal="faade" href="#">View new nearby PAT-FetLife reports</a>';
  1325. html_string += '<span class="i s q">&nbsp;-or-&nbsp;</span>';
  1326. html_string += '<a data-closes-modal="faade" class="close tdn q" href="#">Cancel</a>';
  1327. html_string += '</p>';
  1328. html_string += '<p>(Don\'t worry, I\'m not looking for where you actually are. Your location was determined from your FetLife profile.)</p>';
  1329. FL_UI.Dialog.inject(
  1330. 'faade',
  1331. 'Predator Alert Tool for FetLife (PAT-FetLife)',
  1332. html_string
  1333. );
  1334. };
  1335.  
  1336. FAADE.getLocationFromProfileHtml = function (html) {
  1337. var parser = new DOMParser();
  1338. var doc = parser.parseFromString(html, 'text/html');
  1339. return doc.querySelector('h2.bottom + p > em').textContent.split(', '); // split with comma AND space
  1340. };
  1341.  
  1342. FAADE.broadcastNewProximalReports = function (doc) {
  1343. // Recall timestamp of last record checked.
  1344. var last_timestamp_checked = parseInt(FAADE.getValue('last_timestamp_checked', '0')); // default is "never!"
  1345. // Get latest timestamp in stored alleged abuser database.
  1346. var rows = doc.querySelectorAll('table.waffle tr'); // read in every report, in full
  1347. var latest_timestamp_filed = Date.parse(rows[rows.length - 1].childNodes[1].textContent);
  1348.  
  1349. // If never checked, or if there are new records since last timestamp checked
  1350. if (last_timestamp_checked < latest_timestamp_filed) {
  1351. FAADE.log('Last timestamp checked (' + last_timestamp_checked.toString() + ') is older than latest timestamp filed (' + latest_timestamp_filed.toString() + ').');
  1352.  
  1353. // count how many new records there are since last check
  1354. var num_reports = 0;
  1355. for (var i = rows.length - 1; i > 0; i--) {
  1356. if (Date.parse(rows[i].childNodes[1].textContent) > last_timestamp_checked) {
  1357. num_reports++;
  1358. } else {
  1359. break; // we've reached the timestamps we've already checked, we're done
  1360. }
  1361. }
  1362. FAADE.log('Total of ' + num_reports + ' new reports since last check.');
  1363.  
  1364. var user_loc = FAADE.getLocationFromProfileHtml(FL_ASL.getUserProfileHtml());
  1365. FAADE.log('Current user location seems to be ' + user_loc.join(', ') + '.');
  1366.  
  1367. // Loop over all new records one by one
  1368. var reports_to_alert = [];
  1369. for (var i = rows.length - num_reports; i <= rows.length - 1; i++) {
  1370. // extract the location data from the report
  1371. report_loc = rows[i].childNodes[6].textContent;
  1372. // compare current user's FetLife profile location against alleged abuse location
  1373. // and test each substring of the user profile against the reported location
  1374. for (var z = 0; z < user_loc.length; z++) {
  1375. // if a relevant case insensitive substring matches
  1376. if (-1 !== report_loc.toLowerCase().search(user_loc[z].toLowerCase())) {
  1377. FAADE.log('Filed report location ' + report_loc + ' matches user location substring ' + user_loc[z] + '!');
  1378. // store for future pop-up
  1379. reports_to_alert.push(rows[i]);
  1380. break; // we found a match, so stop trying on this row
  1381. }
  1382. }
  1383. }
  1384.  
  1385. // Ask user to view the profiles of the alleged abusers in the user's local vicinity.
  1386. if (reports_to_alert.length) {
  1387. // Fill in the user-facing message with the appropriate info.
  1388. document.getElementById('faade_reports_to_alert').innerHTML = reports_to_alert.length.toString();
  1389. document.getElementById('faade_user_loc').innerHTML = user_loc.join(', ');
  1390. // Create the click event we're going to use.
  1391. var evt = document.createEvent('MouseEvents');
  1392. evt.initEvent('click', true, false); // can bubble, can't be cancelled
  1393. // "Click" event on hidden code.
  1394. document.querySelector('a[data-opens-modal="faade"]').dispatchEvent(evt);
  1395. // Attach event listener to "View" button and pass in appropriate URLs.
  1396. document.querySelector('.btnsqr[data-closes-modal="faade"]').addEventListener('click', function () {
  1397. for (var i = 0; i < reports_to_alert.length; i++) {
  1398. // TODO: Add the permalink to the specific report to this URL, so it's highlighted when opened.
  1399. var url = 'https://fetlife.com/users/';
  1400. GM_openInTab(url + reports_to_alert[i].childNodes[2].textContent.match(/\d+/)[0]);
  1401. }
  1402. });
  1403. }
  1404. }
  1405.  
  1406. // Make a note of the latest timestamp filed, so we start here next time we're loaded.
  1407. FAADE.setValue('last_timestamp_checked', latest_timestamp_filed.toString());
  1408. };
  1409.  
  1410. FAADE.creepShield = {};
  1411. FAADE.creepShield.checkPhotoUrl = function (url) {
  1412. // For Chrome, we need to create the multipart request manually because
  1413. // extensions can't decode FormData objects due to its isolated worlds.
  1414. // See http://code.google.com/p/tampermonkey/issues/detail?id=183
  1415. var multipart_boundary = "---xxx111222333444555666777888999";
  1416. var multipart_data = '--' + multipart_boundary + "\n";
  1417. multipart_data += 'Content-Disposition: form-data; name="linked_image"';
  1418. multipart_data += "\n\n";
  1419. multipart_data += url;
  1420. multipart_data += "\n";
  1421. // Mimic hitting the "Search" button.
  1422. multipart_data += '--' + multipart_boundary + "\n";
  1423. multipart_data += 'Content-Disposition: form-data; name="submit_linked_image"';
  1424. multipart_data += "\n\n";
  1425. multipart_data += 'Search';
  1426. multipart_data += "\n";
  1427. multipart_data += '--' + multipart_boundary + '--'; // end
  1428.  
  1429. GM_xmlhttpRequest({
  1430. 'method': 'POST',
  1431. 'url': 'http://www.creepshield.com/search',
  1432. 'headers': {
  1433. 'Content-Type': 'multipart/form-data; boundary=' + multipart_boundary
  1434. },
  1435. 'data': multipart_data,
  1436. 'onload': function (response) {
  1437. var parser = new DOMParser();
  1438. var doc = parser.parseFromString(response.responseText, 'text/html');
  1439. // If our search was successful,
  1440. if (doc.querySelector('.search-details')) {
  1441. // Parse the CreepShield results and display on FetLife.
  1442. var creep_data = FAADE.creepShield.parseResults(doc);
  1443. FAADE.creepShield.displayOnFetLife(creep_data);
  1444. } else {
  1445. FAADE.log('An error occurred searching CreepShield.com.');
  1446. if (doc.getElementById('messages')) {
  1447. FAADE.creepShield.displayError(doc.getElementById('messages').textContent);
  1448. }
  1449. }
  1450. }
  1451. });
  1452. };
  1453. FAADE.creepShield.parseResults = function (doc) {
  1454. var ret = {
  1455. 'searched_url' : doc.querySelector('.searched-image').getAttribute('src'),
  1456. 'matches_count': doc.querySelectorAll('.person').length,
  1457. 'highest_match': doc.querySelector('.match-percentage p:nth-child(2)').textContent.match(/\d+%/),
  1458. 'highest_photo': doc.querySelector('.person-images-inner img'),
  1459. 'person_detail': doc.querySelector('.person-name').textContent
  1460. };
  1461. return ret;
  1462. };
  1463. FAADE.creepShield.getDisclaimerHtml = function () {
  1464. return '<p>This feature is powered by the facial recognition service at <a href="http://creepshield.com/">CreepShield.com</a>. The registered sex offender database is <em>not</em> always a reliable source of information. <a href="https://www.eff.org/deeplinks/2011/04/sexual-predators-please-check-here-match-com-s">Learn more</a>.</p>';
  1465. };
  1466. FAADE.creepShield.displayOnFetLife = function (creep_data) {
  1467. var base_el = document.querySelector('.pan').parentNode.parentNode;
  1468. var my_el = document.createElement('div');
  1469. my_el.setAttribute('class', 'pat-fetlife-creepshield-results');
  1470. var html = '<h3>Possible Registered Sex Offender matches:</h3>';
  1471. html += '<ul>';
  1472. html += '<li>Highest facial match: ' + creep_data.highest_match + '</li>'
  1473. html += '<li>Most likely offender: <img src="' + creep_data.highest_photo.getAttribute('src') + '" alt="" />' + creep_data.person_detail + '</li>';
  1474. html += '<li>Total possible matches: ' + creep_data.matches_count + '</li>';
  1475. html += '</ul>';
  1476. html += '<form method="POST" action="http://www.creepshield.com/search">';
  1477. html += '<input type="hidden" name="linked_image" value="' + creep_data.searched_url + '" />';
  1478. html += '<p>Search for criminal histories and other possible offenders: ';
  1479. html += '<input type="submit" name="submit_linked_image" value="Search" />';
  1480. html += '</p>';
  1481. html += '</form>';
  1482. html += FAADE.creepShield.getDisclaimerHtml();
  1483. my_el.innerHTML = html;
  1484. base_el.appendChild(my_el);
  1485. };
  1486. FAADE.creepShield.displayError = function (msg) {
  1487. var base_el = document.querySelector('.pan').parentNode.parentNode;
  1488. var my_el = document.createElement('div');
  1489. my_el.setAttribute('class', 'pat-fetlife-creepshield-results error');
  1490. var html = '<p>CreepShield returned an error:</p>';
  1491. html += '<blockquote><p>' + msg + '</p></blockquote>';
  1492. html += '<p>If you are being told you need to login before you can do more searches, simply <a href="javascript:window.location.reload();void(0);">reload this page</a> to try again.</p>';
  1493. html += FAADE.creepShield.getDisclaimerHtml();
  1494. my_el.innerHTML = html;
  1495. base_el.appendChild(my_el);
  1496. // If free search limit was hit, go to CreepShield.com to flush it out.
  1497. if (msg.match(/You cannot perform any more searches/i)) {
  1498. GM_openInTab('http://www.creepshield.com/search');
  1499. }
  1500. };
  1501.  
  1502. FAADE.clearCookies = function () {
  1503. var cookie_list = document.cookie.split(';');
  1504. for (var i = 0; i < cookie_list.length; i++) {
  1505. var cookie_name = cookie_list[i].replace(/\s*(\w+)=.+$/, "$1");
  1506. // To delete a cookie, set its expiration date to a past value.
  1507. document.cookie = cookie_name + '=;expires=Thu, 01-Jan-1970 00:00:01 GMT;';
  1508. }
  1509. };
  1510.  
  1511. // This is the main() function, executed on page load.
  1512. FAADE.main = function () {
  1513. // Make a list of known alleged abuser user IDs.
  1514. var parser = new DOMParser();
  1515. var doc = parser.parseFromString(FAADE.abuser_database, 'text/html');
  1516. var els = doc.querySelectorAll('table.waffle td:nth-child(3)'); // third child is the column of IDs.
  1517. var abuser_ids = [];
  1518. for (var i = 1; i < els.length; i++) { // we never need the first (0'th) cell because Google provides it blank.
  1519. abuser_ids.push(els[i].innerHTML);
  1520. }
  1521. FAADE.log('recalled abuser ids ' + abuser_ids);
  1522.  
  1523. // TODO: Refactor this, it's kludgy.
  1524. setTimeout(function() {
  1525. FAADE.log('Running time-delayed function.');
  1526. if (FL_ASL.getUserProfileHtml()) {
  1527. FAADE.log('We have the current user\'s FetLife profile HTML. Running broadcast checks.');
  1528. FAADE.broadcastNewProximalReports(doc);
  1529. }
  1530. }, 5000); // give us a few seconds to grab the current user's FetLife profile HTML.
  1531.  
  1532. // Are we on a user profile page?
  1533. if (window.location.href.match(/users\/(\d+)\/?$/)) {
  1534.  
  1535. var profile_nick = document.querySelector('h2.bottom').childNodes[0].textContent.match(/\S+/)[0];
  1536. var id_in_url = window.location.href.match(/users\/(\d+)\/?$/)[1];
  1537. var userpic_el = document.querySelector('.pan');
  1538. // If we can grab this person's userpic, send it off to CreepSheild for testing.
  1539. if (userpic_el) {
  1540. var userpic_src = userpic_el.getAttribute('src');
  1541. // This will check and call back to the renderer, so we can move on now.
  1542. FAADE.creepShield.checkPhotoUrl(userpic_src);
  1543. }
  1544.  
  1545. // If we're not viewing our own profile page, insert a report link.
  1546. usr_ops = document.querySelector('#main_content p.quiet');
  1547. if (usr_ops) {
  1548. usr_ops.appendChild(document.createElement('br'));
  1549. usr_ops.appendChild(FAADE.createAbuseReportLink(id_in_url, profile_nick));
  1550. }
  1551.  
  1552. // If this is a profile page of an alleged abuser,
  1553. if (-1 !== abuser_ids.indexOf(id_in_url)) {
  1554.  
  1555. var report_el = document.createElement('table');
  1556. report_el.setAttribute('id', 'faade_abuse_reports');
  1557. report_el.setAttribute('summary', 'Reported consent violations committed by ' + profile_nick + '.');
  1558. var caption = document.createElement('caption');
  1559. caption.innerHTML = 'There are reports ' + profile_nick + ' violated others\' consent in these ways:';
  1560. var tfoot = document.createElement('tfoot');
  1561. tfoot.innerHTML = '<tr><td colspan="2"></td></tr>';
  1562. tfoot.querySelector('td').appendChild(FAADE.createAbuseReportLink(id_in_url, profile_nick));
  1563. report_el.appendChild(caption);
  1564. report_el.appendChild(tfoot);
  1565.  
  1566. // Find all reports that match ID number.
  1567. var abuse_reports = [];
  1568. for (var ix = 0; ix < els.length; ix++) {
  1569. if (id_in_url === els[ix].innerHTML) {
  1570. abuse_reports.push(els[ix].parentNode); // the table row of abuse report
  1571. }
  1572. }
  1573. // Add this information to the top of this user's profile
  1574. for (var iy = 0; iy < abuse_reports.length; iy++) {
  1575. var num = iy + 1;
  1576. var tr = document.createElement('tr');
  1577. tr.setAttribute('id', 'faade_abuse_report-' + num.toString());
  1578. var details_html = '<ul><li class="faade_abuse_report_datetime">' + abuse_reports[iy].childNodes[7].innerHTML + '</li>';
  1579. details_html += '<li class="faade_abuse_report_location">' + abuse_reports[iy].childNodes[6].innerHTML + '</li></ul>';
  1580. var permalink_html = '<a class="faade_abuse_reported_datetime" rel="bookmark" href="'
  1581. + window.location + '#faade_abuse_report-' + num.toString()
  1582. + '" title="Permalink for PAT-FetLife abuse report number ' + num.toString() + ' against '
  1583. + profile_nick + '.">' + abuse_reports[iy].childNodes[1].innerHTML + '</a>';
  1584. tr.innerHTML += '<th>Abuse report ' + num.toString() + ' (' + permalink_html + '):' + details_html + '</th>';
  1585. tr.innerHTML += '<td>' + abuse_reports[iy].childNodes[5].innerHTML + '</td>';
  1586. report_el.appendChild(tr);
  1587. }
  1588.  
  1589. var before = document.querySelector('#main_content table:last-child');
  1590. before.parentNode.insertBefore(report_el, before);
  1591. }
  1592.  
  1593. }
  1594.  
  1595. // Collect all user links on this page.
  1596. var user_links = [];
  1597. for (i = 0; i < document.links.length; i++) {
  1598. var l = document.links[i].href.match(/^(https:\/\/fetlife.com)?\/users\/(\d+)\/?$/);
  1599. if ( l && (l[2] !== uw.FetLife.currentUser.id.toString()) ) {
  1600. user_links.push(document.links[i]);
  1601. }
  1602. }
  1603.  
  1604. // For each user ID found,
  1605. var last_id = null;
  1606. for (i = 0; i < user_links.length; i++) {
  1607. // Collect its user ID number.
  1608. var id = user_links[i].href.match(/\d+\/?$/);
  1609. if (id) { id = id.toString(); } // cast to string for later comparison
  1610.  
  1611. // Get nickname.
  1612. var n;
  1613. if (user_links[i].children.length) {
  1614. // This is an avatar link, not a text link.
  1615. n = user_links[i].childNodes[0].alt;
  1616. } else {
  1617. // This is a text link. Easy.
  1618. n = user_links[i].innerHTML;
  1619. }
  1620.  
  1621. // check the alleged abusers data store for a match.
  1622. if (-1 !== abuser_ids.indexOf(id)) {
  1623. FAADE.log('found match on this page for alleged abuser ID number ' + id);
  1624. // highlight the user's links that matched an alleged abuser using CSS
  1625. user_links[i].setAttribute('class', user_links[i].className + ' faade_alleged_abuser');
  1626.  
  1627. }
  1628.  
  1629. // Don't create another link if we just made one for that user.
  1630. if (id === last_id) { continue; }
  1631.  
  1632. // Offer a link to add another report for this user.
  1633. // See also: https://support.google.com/docs/bin/answer.py?hl=en&answer=160000
  1634. // Add link to report this user for a consent violation.
  1635. var a = FAADE.createAbuseReportLink(id, n);
  1636. user_links[i].parentNode.appendChild(a);
  1637. last_id = id;
  1638. }
  1639. };
  1640.  
  1641. FAADE.createAbuseReportLink = function (id, nick) {
  1642. var a = document.createElement('a');
  1643. a.setAttribute('class', 'faade_report_link');
  1644. a.setAttribute('target', '_blank');
  1645. var href = 'https://docs.google.com/forms/d/' + FAADE.getReportFormKey() + '/viewform';
  1646. href += '?entry_2952262=' + id;
  1647. href += '&entry_1000001=' + nick;
  1648. a.setAttribute('href', href);
  1649. a.innerHTML = '(report a consent violation by ' + nick + ')';
  1650. return a;
  1651. };
  1652.  
  1653. // The following is required for Chrome compatibility, as we need "text/html" parsing.
  1654. /*
  1655. * DOMParser HTML extension
  1656. * 2012-09-04
  1657. *
  1658. * By Eli Grey, http://eligrey.com
  1659. * Public domain.
  1660. * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
  1661. */
  1662.  
  1663. /*! @source https://gist.github.com/1129031 */
  1664. /*global document, DOMParser*/
  1665.  
  1666. (function(DOMParser) {
  1667. "use strict";
  1668.  
  1669. var
  1670. DOMParser_proto = DOMParser.prototype
  1671. , real_parseFromString = DOMParser_proto.parseFromString
  1672. ;
  1673.  
  1674. // Firefox/Opera/IE throw errors on unsupported types
  1675. try {
  1676. // WebKit returns null on unsupported types
  1677. if ((new DOMParser).parseFromString("", "text/html")) {
  1678. // text/html parsing is natively supported
  1679. return;
  1680. }
  1681. } catch (ex) {}
  1682.  
  1683. DOMParser_proto.parseFromString = function(markup, type) {
  1684. if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
  1685. var
  1686. doc = document.implementation.createHTMLDocument("")
  1687. ;
  1688.  
  1689. doc.body.innerHTML = markup;
  1690. return doc;
  1691. } else {
  1692. return real_parseFromString.apply(this, arguments);
  1693. }
  1694. };
  1695. }(DOMParser));