Sleazy Fork is available in English.

CamWhores.tv Improved

Infinite scroll (optional). Filter by duration, private/public, include/exclude phrases. Mass friend request button. Download button

  1. // ==UserScript==
  2. // @name CamWhores.tv Improved
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.996
  5. // @license MIT
  6. // @description Infinite scroll (optional). Filter by duration, private/public, include/exclude phrases. Mass friend request button. Download button
  7. // @author smartacephale
  8. // @supportURL https://github.com/smartacephale/sleazy-fork
  9. // @match https://*.camwhores.tv/*
  10. // @exclude *.camwhores.tv/*mode=async*
  11. // @grant GM_addStyle
  12. // @require https://cdn.jsdelivr.net/npm/billy-herrington-utils@1.1.8/dist/billy-herrington-utils.umd.js
  13. // @require https://cdn.jsdelivr.net/npm/jabroni-outfit@1.4.9/dist/jabroni-outfit.umd.js
  14. // @require https://cdn.jsdelivr.net/npm/lskdb@1.0.2/dist/lskdb.umd.js
  15. // @require https://update.greasyfork.org/scripts/494204/data-manager.user.js?version=1458190
  16. // @require https://update.greasyfork.org/scripts/494205/pagination-manager.user.js?version=1459738
  17. // @run-at document-idle
  18. // @icon https://www.google.com/s2/favicons?sz=64&domain=camwhores.tv
  19. // ==/UserScript==
  20. /* globals $ PaginationManager DataManager */
  21.  
  22. const { Tick, parseDom, fetchHtml, AsyncPool, wait, computeAsyncOneAtTime, timeToSeconds,
  23. circularShift, range, watchDomChangesWithThrottle, objectToFormData, parseDataParams, sanitizeStr,
  24. getAllUniqueParents, downloader } = window.bhutils;
  25. Object.assign(unsafeWindow, { bhutils: window.bhutils });
  26. const { JabroniOutfitStore, defaultStateWithDurationAndPrivacy, JabroniOutfitUI, defaultSchemeWithPrivateFilter } = window.jabronioutfit;
  27. const { LSKDB } = window.lskdb;
  28.  
  29. const LOGO = `camwhores admin should burn in hell
  30. ⣿⢏⡩⡙⣭⢫⡍⣉⢉⡉⢍⠩⡭⢭⠭⡭⢩⢟⣿⣿⣻⢿⣿⣿⣿⣿⡿⣏⣉⢉⣿⣿⣻⢿⣿⣿⠛⣍⢯⢋⠹⣛⢯⡅⡎⢱⣠⢈⡿⣽⣻⠽⡇⢘⡿⣯⢻⣝⡣⣍⠸⣏⡿⣭⢋⣽⣻⡏⢬⢹
  31. ⣿⠦⡑⢜⡦⣳⡒⢄⠢⠌⢂⠜⣱⢋⡜⡡⢏⣾⢷⣻⢯⡿⣞⣿⣽⣻⢿⣹⢷⡂⣿⣾⡽⣻⣿⣿⢻⣌⣬⢩⢲⡑⡎⠼⡰⣏⣜⢦⡹⣷⣏⡟⡇⢨⡿⣝⡷⣎⠷⡌⢻⣜⡽⣯⣟⣷⣻⢷⣮⣹
  32. ⣿⢧⢉⢲⢣⢇⡯⢀⠒⢨⠐⢌⠰⣋⠞⡱⢫⡞⢯⡍⠧⡙⠞⣬⠳⣝⠪⡙⡷⣏⣿⢾⡽⣿⣿⣻⡟⡾⣥⢺⢈⣷⣽⣶⣭⣷⣾⣾⢿⣼⣳⡻⡇⠰⣿⣝⢾⡹⠞⡤⣏⣾⣳⣟⣾⣳⣯⡿⣵⢳
  33. ⡿⣇⠎⡜⣧⢺⣜⠠⡉⢄⠊⡄⠛⡌⢩⠳⣝⡎⠡⠐⠠⠉⠳⠄⠃⠄⢂⠱⡌⢃⣾⡿⣝⣿⣽⣿⣼⢳⡍⢞⣼⣿⣿⣿⣿⣿⣿⣿⣿⡞⣷⣛⡇⢘⡷⣯⢾⣹⢫⠔⣿⣞⡷⣿⣾⠿⣿⣿⡧⢿
  34. ⣿⣹⠒⡌⠤⠣⣄⠣⡐⢌⠰⣈⠱⢈⠆⡻⠜⡠⠁⢊⠄⠀⠄⢀⠂⠜⡬⢛⠥⢂⣿⡿⣽⣿⣯⣿⢧⣏⢾⡙⢻⡿⠓⡌⡛⠿⣿⣿⣿⣻⡵⣯⠇⢨⣟⣞⣳⡝⣎⢢⡽⣟⣳⣼⣧⣾⣼⣷⣿⣿
  35. ⣷⣏⠲⡈⢆⢳⣤⠃⡜⣀⠣⡐⢌⠢⢌⠑⡈⠐⡀⠠⠀⠌⠀⢂⠈⢌⡱⢈⠎⢀⣿⣿⣳⣿⡆⢻⡷⣞⡞⡬⢩⡐⣛⠶⣍⡲⣭⣿⣿⣿⡽⣞⡇⢨⣟⣾⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  36. ⣷⣟⠧⡘⢠⠎⠔⠣⡐⢄⠢⡑⠌⠢⠌⡐⠤⢁⠐⠠⢁⠂⡁⠂⢌⡰⢆⡍⢂⠠⣿⡷⢿⠿⣃⢻⡛⡭⣝⢰⠣⡜⣈⠣⣜⣻⣿⣿⣿⣿⣟⣧⡇⠠⡿⣽⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  37. ⣿⣯⣗⢡⠂⠆⠀⠠⠑⡌⢢⠁⠌⡐⢢⢁⠒⡈⢌⠢⢅⢎⡰⢉⢢⡙⢦⡘⢄⠠⢏⡱⣋⢾⣩⠷⣙⠶⡡⢎⢷⡘⢧⣟⡾⣱⠿⣿⣿⣿⢿⣳⡇⢘⡷⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  38. ⡧⠩⠜⣠⢹⠆⠀⠀⠱⠈⠄⡈⠐⡀⠆⡈⠆⡁⢆⡘⠌⢎⡱⢋⡖⣩⠒⠥⢊⠴⣋⢶⡹⣎⡗⣯⠝⣎⡗⢎⠾⣹⢟⣮⢳⣭⢻⡹⣿⣿⡿⣯⡇⠘⣧⣽⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  39. ⣟⣷⣻⢞⣻⡃⠀⠀⢡⠉⡐⠄⣃⠐⡄⠐⠠⢁⠂⠜⡈⠦⣑⢫⠴⡡⢉⠆⣭⢚⡭⢶⡹⢮⡝⣮⣛⢶⡹⣎⡰⣹⢎⡷⣫⢞⢧⡻⣜⢿⣿⣳⡇⢸⡿⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  40. ⣿⣾⣽⣯⡷⣿⣹⠇⡢⠑⡌⠰⣀⠊⠄⠃⡌⠠⢈⠐⡀⢂⠉⢆⡓⠰⡁⢞⡴⣋⡞⣧⣛⢧⣛⢶⡹⣎⠷⣝⢲⣭⣛⢶⡹⣎⢷⡹⣎⠿⣜⢷⡇⢸⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿
  41. ⣿⡿⣿⣷⣿⢷⡛⡜⢠⠓⣌⠱⣀⠣⢌⡐⠠⢁⠂⠂⠄⠡⢈⠂⠬⡑⢬⠳⡜⣥⣛⢶⡹⢮⡝⣮⢳⡭⣟⢮⣳⢮⡝⣮⣗⡻⣎⢷⣭⢻⡜⣯⢇⠼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣏⣿⣷⣿⣽⣻⢿
  42. ⣿⣿⣿⣿⣻⡿⣗⠨⣅⠊⢤⠓⡄⡃⢆⡘⠰⡀⠌⠂⠌⡐⠠⠈⠄⡉⠊⠽⣸⢲⡝⣮⢽⣣⢟⡼⣣⢟⣼⣣⡟⣮⡽⢖⡻⡝⢮⢳⡎⣷⡹⣎⣿⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  43. ⣿⣿⣿⣿⣿⣿⣇⠣⡜⢌⢂⠱⡈⠴⠡⢌⠱⡈⠤⢁⠒⠠⠁⠌⡐⢀⠁⢂⠄⠛⡼⣭⢷⡹⢮⣳⣛⣮⢷⣣⢟⠲⣙⠮⣵⢫⣛⡶⣽⣶⣿⣽⣾⢿⡿⣿⣾⣟⣿⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  44. ⣿⣿⣿⣿⣿⣿⢇⡓⠼⡌⢆⠣⢄⠃⡜⢀⠣⡘⢄⠣⡐⠠⢁⠐⠠⠀⠌⠠⢈⠐⣤⠙⠞⠽⢫⠗⠛⡼⣣⠟⡄⢣⢈⣿⣼⣿⣿⡹⠿⡽⢾⣿⣭⣿⢿⣧⡽⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿⣿⣻⣽
  45. ⣿⣟⣯⣿⢾⣿⡧⢘⠤⣙⠢⡍⢢⠱⡐⠢⠄⡑⠌⢢⢁⠣⠄⠂⠄⢁⠂⡁⠂⡔⠠⢉⠜⡀⠣⢌⡱⠠⢅⠊⡜⢠⠃⠬⠉⢃⢉⡙⡒⠰⡡⠾⣿⣿⣿⣾⣻⣷⣿⣿⣿⣿⣽⣻⣿⢿⣽⣿⣿⢿
  46. ⣿⣾⣿⣾⣿⣿⡻⣌⢆⠡⢣⠙⣆⠡⣉⠳⣌⡔⢨⠀⠎⡰⢉⠜⡐⢢⠐⡠⢁⠐⡌⢂⠐⠠⠑⠢⠱⡉⢎⡱⢈⠣⢞⠤⡈⢄⠢⠖⡙⢢⠑⠢⢍⢿⣿⣿⣿⣷⡿⣷⣿⣻⣿⣿⣿⣮⡿⣿⣿⣿
  47. ⣿⣿⣿⣿⣿⣿⣿⣽⣎⠧⡑⢊⡄⠓⡄⢣⠠⡙⠢⣍⠒⡄⠣⡘⢌⠡⢚⠰⡈⠔⠠⠀⠌⠠⢁⡃⠱⢈⠆⠴⢁⡜⢨⡖⠩⢌⠒⠡⠌⡐⢈⠒⡌⢢⠙⣿⣿⣿⣿⣿⣟⣿⣾⣿⣿⣿⣿⣿⣿⣿
  48. ⣿⣿⣿⣿⣿⣿⣿⡞⢿⣿⣥⡣⠌⡱⢈⠄⢣⠐⡡⢂⠍⡘⠱⢴⡈⢆⡡⢃⠍⡊⡕⡨⢄⢃⠢⡘⢠⢣⡘⢤⠣⡌⢧⡘⢥⣪⣼⣦⣶⣾⣿⣿⣶⠈⠂⠌⠻⣿⣿⣿⣿⣷⣿⣯⢿⣿⣿⣿⣿⣿
  49. ⣿⣿⣿⣿⣿⣿⣿⣿⡳⣎⠿⣿⣧⡄⠣⡘⢄⠣⡐⢡⠊⡔⢡⠎⢉⡑⠲⣧⡚⡴⣠⡑⢎⡴⢣⡝⣣⢖⡹⣎⣳⣹⣶⣭⣾⣿⣿⣿⣿⣿⣿⣿⡿⣧⢄⠀⡀⠉⢿⣿⣿⣿⣿⣿⣿⣽⡿⣿⣿⣿
  50. ⣿⣿⣿⣿⣿⣿⣿⣿⡷⡹⡞⣍⢿⣿⣳⠰⡈⠔⡈⢆⠱⣠⠃⠌⢂⡰⣱⣼⣿⢷⣧⣽⣺⣼⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣻⣽⣿⣿⣿⣿⣿⣿⣷⣾⠤⠀⠁⠂⠹⣿⣿⣿⣿⣿⣿⣿⣟⡿⣿
  51. ⣿⣯⣿⣿⡿⣾⣻⢽⡳⢧⡹⢞⢦⡹⢿⣷⣍⠲⡑⣎⠳⣄⣷⣾⣿⢿⣻⣟⣯⣿⣻⣿⣿⣿⣿⡿⣟⣿⣻⢿⣽⣻⣾⢷⣻⡽⣾⣿⣿⣯⣿⣿⣽⣿⣿⣿⣐⠀⠂⠀⠀⠿⣿⣿⣿⣿⣿⣿⣿⣿
  52. ⣟⢮⡝⡷⣛⠵⣋⢾⣙⢧⡙⣎⢳⡹⡌⢿⡿⣧⣵⢫⣿⠿⣟⣿⣻⢯⣷⡿⣯⣷⣟⣷⡿⣯⣷⢿⣻⣾⣟⣿⣾⣿⣿⣿⢿⣽⣟⣿⣿⣟⣿⣿⣿⣾⣿⣿⣿⣆⠀⠀⠀⠀⠐⣿⣿⣿⣿⣿⣿⣿
  53. ⣟⠮⣼⢱⡹⢎⣝⡲⡝⢮⡕⡎⣥⢓⡹⢆⡹⣿⣯⣟⣮⢷⣻⣞⣿⣻⢯⣿⡽⣷⡿⣯⣿⢿⣽⡿⣟⣯⣿⣿⣿⣿⣿⡿⣟⣿⣞⣯⣿⣿⣻⣿⣿⣷⡿⣿⣿⣿⣬⡀⠀⠀⠀⠀⠋⣻⣿⣿⣿⣿
  54. ⣯⠳⣌⠧⡝⣎⠶⣱⢋⢧⣛⠼⣄⠫⡴⡩⢆⠻⣿⣿⠽⣾⠽⣞⣷⣻⣟⡷⣿⢯⣿⢷⣻⣯⢷⣿⣻⣟⡿⣽⢿⡿⣿⣽⣿⣻⢾⣽⣿⣿⣻⣿⣿⣿⣿⣽⣿⣿⣿⣷⡀⠀⠀⠀⣀⣉⣿⣿⣿⣿
  55. ⣗⡫⣜⢣⡝⣬⠓⣥⢋⠶⣩⢞⡬⡓⡥⢓⠮⣅⠻⣿⣿⣽⣻⣽⣾⣻⢾⣻⣽⣻⣞⣯⢷⡯⣟⣾⢳⡯⢿⡽⣫⣽⡳⣏⡾⣽⣛⢾⡳⣟⢿⣻⢿⣿⣿⣿⡾⣿⣿⣷⣿⡔⣠⣿⣹⣯⣿⣿⣿⣿`;
  56.  
  57. GM_addStyle('.item.private .thumb, .item .thumb.private { opacity: 1 !important; }');
  58.  
  59. class CAMWHORES_RULES {
  60. constructor() {
  61. const { pathname } = window.location;
  62. this.IS_FAVOURITES = /\/my\/\w+\/videos/.test(pathname);
  63. this.IS_MEMBER_PAGE = /\/members\/\d+\/$/.test(pathname);
  64. this.IS_MINE_MEMBER_PAGE = /\/my\/$/.test(pathname);
  65. this.IS_MESSAGES = /^\/my\/messages\//.test(pathname);
  66. this.IS_MEMBER_VIDEOS = /\/members\/\d+\/(favourites\/)?videos/.test(pathname);
  67. this.IS_VIDEO_PAGE = /^\/videos\/\d+\//.test(pathname);
  68. this.IS_LOGGED_IN = document.cookie.includes('kt_member');
  69.  
  70. Object.assign(this, this.CALC_CONTAINER());
  71. this.HAS_VIDEOS = !!this.CONTAINER;
  72.  
  73. if (this.IS_FAVOURITES || this.IS_MEMBER_VIDEOS) {
  74. this.INTERSECTION_OBSERVABLE = document.querySelector('.footer');
  75. watchDomChangesWithThrottle(document.querySelector('.content'), () => {
  76. Object.assign(this, this.CALC_CONTAINER());
  77. }, 10);
  78. }
  79. }
  80.  
  81. CALC_CONTAINER = (document_ = document) => {
  82. const paginationEls = Array.from(document_.querySelectorAll('.pagination'));
  83. const PAGINATION = paginationEls?.[this.IS_MEMBER_PAGE && paginationEls.length > 1 ? 1 : 0];
  84. const PAGINATION_LAST = parseInt(PAGINATION?.querySelector('.last > a')?.getAttribute('data-parameters').match(/from\w*:(\d+)/)?.[1]);
  85. const CONTAINER = (PAGINATION?.parentElement.querySelector('.list-videos>div>form') ||
  86. PAGINATION?.parentElement.querySelector('.list-videos>div') ||
  87. document.querySelector('.list-videos>div'));
  88. return { PAGINATION, PAGINATION_LAST, CONTAINER };
  89. }
  90.  
  91. IS_PRIVATE(thumb) {
  92. return thumb.classList.contains('private');
  93. }
  94.  
  95. GET_THUMBS(html) {
  96. return Array.from(html.querySelectorAll('.list-videos .item') || html.querySelectorAll('.item') || html.children);
  97. }
  98.  
  99. THUMB_IMG_DATA(thumb) {
  100. const img = thumb.querySelector('img.thumb');
  101. const imgSrc = img.getAttribute('data-original');
  102. img.removeAttribute('data-original');
  103. return { img, imgSrc };
  104. }
  105.  
  106. THUMB_URL(thumb) {
  107. return thumb.firstElementChild.href;
  108. }
  109.  
  110. THUMB_DATA(thumb) {
  111. const title = sanitizeStr(thumb.querySelector('.title').innerText);
  112. const duration = timeToSeconds(thumb.querySelector('.duration')?.innerText);
  113. return { title, duration };
  114. }
  115.  
  116. URL_DATA(url_, document_) {
  117. const url = new URL((url_ || window.location).href);
  118. const offset = parseInt((document_ || document).querySelector('.page-current')?.innerText) || 1;
  119.  
  120. const { PAGINATION, PAGINATION_LAST } = this.CALC_CONTAINER(document_ || document);
  121. const el = PAGINATION?.querySelector('a[data-block-id][data-parameters]');
  122. const dataParameters = el?.getAttribute('data-parameters') || "";
  123.  
  124. const attrs = {
  125. mode: 'async',
  126. function: 'get_block',
  127. block_id: el?.getAttribute('data-block-id'),
  128. ...parseDataParams(dataParameters)
  129. };
  130.  
  131. Object.keys(attrs).forEach(k => url.searchParams.set(k, attrs[k]));
  132.  
  133. const iteratable_url = n => {
  134. Object.keys(attrs).forEach(k => k.includes('from') && url.searchParams.set(k, n));
  135. url.searchParams.set('_', Date.now());
  136. return url.href;
  137. }
  138.  
  139. return { offset, iteratable_url, PAGINATION_LAST };
  140. }
  141. }
  142.  
  143. const RULES = new CAMWHORES_RULES();
  144.  
  145. //====================================================================================================
  146.  
  147. function rotateImg(src, count) {
  148. return src.replace(/(\d)(?=\.jpg$)/, (_, n) => `${circularShift(parseInt(n), count)}`);
  149. }
  150.  
  151. function animate() {
  152. const tick = new Tick(ANIMATION_DELAY);
  153. $('img.thumb[data-cnt]').off()
  154. document.body.addEventListener('mouseover', (e) => {
  155. if (!e.target.tagName === 'IMG' || !e.target.classList.contains('thumb') || !e.target.getAttribute('src')) return;
  156. const origin = e.target.src;
  157. if (origin.includes('avatar')) return;
  158. const count = parseInt(e.target.getAttribute('data-cnt')) || 5;
  159. tick.start(
  160. () => { e.target.src = rotateImg(e.target.src, count); },
  161. () => { e.target.src = origin; });
  162. e.target.closest('.item').addEventListener('mouseleave', () => tick.stop(), { once: true });
  163. });
  164. }
  165.  
  166. //====================================================================================================
  167.  
  168. const createDownloadButton = () => downloader({
  169. append: '.tabs-menu > ul',
  170. button: '<li><a href="#tab_comments" class="toggle-button" style="text-decoration: none;">download 📼</a></li>'
  171. })
  172.  
  173. //====================================================================================================
  174.  
  175. // since script cannot be reloaded and scroll params need to be reset according to site options
  176. function shouldReload() {
  177. const sortContainer = document.querySelector('.sort');
  178. if (!sortContainer) return;
  179. watchDomChangesWithThrottle(sortContainer, () => window.location.reload(), 1000);
  180. }
  181.  
  182. //====================================================================================================
  183.  
  184. const DEFAULT_FRIEND_REQUEST_FORMDATA = objectToFormData({
  185. message: "",
  186. action: "add_to_friends_complete",
  187. function: "get_block",
  188. block_id: "member_profile_view_view_profile",
  189. format: "json",
  190. mode: "async"
  191. });
  192.  
  193. const lskdb = new LSKDB();
  194. const spool = new AsyncPool();
  195.  
  196. function friendRequest(id) {
  197. const url = Number.isInteger(id) ? `${window.location.origin}/members/${id}/` : id;
  198. return fetch(url, { body: DEFAULT_FRIEND_REQUEST_FORMDATA, method: "post" });
  199. }
  200.  
  201. function getMemberLinks(document) {
  202. return Array.from(document.querySelectorAll('.item > a')).map(l => l.href).filter(l => /\/members\/\d+\/$/.test(l));
  203. }
  204.  
  205. async function getMemberFriends(id) {
  206. const url = `${window.location.origin}/members/${id}/friends/`;
  207. const document_ = await fetchHtml(url);
  208. const { offset, iteratable_url, PAGINATION_LAST } = RULES.URL_DATA(new URL(url), document_);
  209. const pages = PAGINATION_LAST ? range(PAGINATION_LAST, 1).map(u => iteratable_url(u)) : [url];
  210. const friendlist = (await computeAsyncOneAtTime(pages.map(p => () => fetchHtml(p)))).flatMap(getMemberLinks).map(u => u.match(/\d+/)[0]);
  211. friendlist.forEach(m => lskdb.setKey(m));
  212. await processFriendship();
  213. }
  214.  
  215. async function processFriendship() {
  216. console.log('processFriendship');
  217. if (!lskdb.isLocked()) {
  218. const friendlist = lskdb.getKeys(30);
  219. if (friendlist?.length < 1) return;
  220. lskdb.lock(true);
  221. const urls = friendlist.map(id => `${window.location.origin}/members/${id}/`);
  222. await computeAsyncOneAtTime(urls.map(url => () => friendRequest(url)));
  223. lskdb.lock(false);
  224. await processFriendship();
  225. }
  226. }
  227.  
  228. function createPrivateVideoFriendButton() {
  229. if (!document.querySelector('.no-player')) return;
  230. const member = document.querySelector('.no-player a').href;
  231. const button = parseDom('<button style="background: radial-gradient(#5ccbf4, #e1ccb1)"><span>Friend Request</span></button>');
  232. document.querySelector('.no-player .message').append(button);
  233. button.addEventListener('click', () => friendRequest(member), { once: true });
  234. }
  235.  
  236. function createFriendButton() {
  237. const button = parseDom('<a href="#friend_everyone" style="background: radial-gradient(#5ccbf4, #e1ccb1)" class="button"><span>Friend Everyone</span></a>');
  238. document.querySelector('.main-container-user > .headline').append(button);
  239. const memberid = window.location.pathname.match(/\d+/)[0];
  240. button.addEventListener('click', () => {
  241. button.style.background = 'radial-gradient(#ff6114, #5babc4)';
  242. button.innerText = 'processing requests';
  243. getMemberFriends(memberid).then(() => {
  244. button.style.background = 'radial-gradient(blue, lightgreen)';
  245. button.innerText = 'friend requests sent';
  246. });
  247. }, { once: true });
  248. }
  249.  
  250. //====================================================================================================
  251.  
  252. const greenItem = 'linear-gradient(to bottom, #4e9299 0%, #2c2c2c 100%) green';
  253. const redItem = 'linear-gradient(to bottom, #b50000 0%, #2c2c2c 100%) red';
  254.  
  255. function checkPrivateVidsAccess() {
  256. document.querySelectorAll('.item.private').forEach(async item => {
  257. const videoURL = item.firstElementChild.href;
  258. const doc = await fetchHtml(videoURL);
  259. const haveAccess = !/This video is a private video uploaded by/ig.test(doc?.innerText);
  260. item.style.background = haveAccess ? greenItem : redItem;
  261. });
  262. }
  263.  
  264. //====================================================================================================
  265.  
  266. function getUserInfo(document) {
  267. const uploadedCount = parseInt(document.querySelector('#list_videos_uploaded_videos strong')?.innerText.match(/\d+/)[0]) || 0;
  268. const friendsCount = parseInt(document.querySelector('#list_members_friends .headline')?.innerText.match(/\d+/g).pop()) || 0;
  269. // const isFriend = /is in your friends list/gi.test(document.body.innerText);
  270. return { uploadedCount, friendsCount }
  271. }
  272.  
  273. async function acceptFriendRequest(id) {
  274. const url = `https://www.camwhores.tv/my/messages/${id}/`;
  275. await fetch(url, {
  276. "headers": {
  277. "Accept": "*/*",
  278. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  279. },
  280. "body": `action=confirm_add_to_friends&message_from_user_id=${id}&function=get_block&block_id=list_messages_my_conversation_messages&confirm=Confirm&format=json&mode=async`,
  281. "method": "POST",
  282. });
  283. await fetchHtml(`https://www.camwhores.tv/members/${id}/`).then(doc => console.log('userInfo', getUserInfo(doc), url));
  284. }
  285.  
  286. function clearMessages() {
  287. const messagesURL = id => `https://www.camwhores.tv/my/messages/?mode=async&function=get_block&block_id=list_members_my_conversations&sort_by=added_date&from_my_conversations=${id}&_=${Date.now()}`;
  288. const last = parseInt(document.querySelector('.pagination-holder .last > a').href.match(/\d+/)?.[0]);
  289. if (!last) return;
  290.  
  291. for (let i = 0; i < last; i++) {
  292. spool.push({
  293. v: () =>
  294. fetchHtml(messagesURL(i)).then(html_ => {
  295. const messages = Array.from(html_?.querySelectorAll('#list_members_my_conversations_items .item > a') || []).map(a => a.href);
  296. messages.forEach((m, j) => spool.push({ v: () => checkMessageHistory(m), p: 1 }));
  297. }), p: 2
  298. });
  299. }
  300. spool.run();
  301.  
  302. let c = 0;
  303. function checkMessageHistory(url) {
  304. fetchHtml(url).then(html => {
  305. const hasFriendRequest = html.querySelector('input[value=confirm_add_to_friends]');
  306. const hasOriginalText = html.querySelector('.original-text')?.innerText;
  307. const id = url.match(/\d+/)[0];
  308. if (!(hasOriginalText || hasFriendRequest)) {
  309. const deleteURL = `${url}?mode=async&format=json&function=get_block&block_id=list_messages_my_conversation_messages&action=delete_conversation&conversation_user_id=${id}`;
  310. spool.push({
  311. v: () => fetch(deleteURL).then(r => {
  312. console.log(r.status === 200 ? ++c : '', r.status, 'delete', id);
  313. }), p: 0
  314. });
  315. } else {
  316. console.log(hasOriginalText, url);
  317. if (hasFriendRequest) {
  318. spool.push({ v: () => acceptFriendRequest(id), p: 0 });
  319. }
  320. }
  321. });
  322. }
  323. }
  324.  
  325. //====================================================================================================
  326.  
  327. function route() {
  328. if (RULES.IS_LOGGED_IN) {
  329. setTimeout(processFriendship, 3000);
  330. if (RULES.IS_MEMBER_PAGE) {
  331. createFriendButton();
  332. }
  333. }
  334.  
  335. if (RULES.PAGINATION && !RULES.IS_MEMBER_PAGE && !RULES.IS_MINE_MEMBER_PAGE) {
  336. new PaginationManager(state, stateLocale, RULES, handleLoadedHTML, SCROLL_RESET_DELAY);
  337. shouldReload();
  338. }
  339.  
  340. if (RULES.HAS_VIDEOS) {
  341. const containers = getAllUniqueParents(RULES.GET_THUMBS(document.body));
  342. containers.forEach(c => handleLoadedHTML(c, c));
  343. defaultSchemeWithPrivateFilter.privateFilter.push(
  344. { type: "button", innerText: "check access 🔓", callback: checkPrivateVidsAccess });
  345. new JabroniOutfitUI(store, defaultSchemeWithPrivateFilter);
  346. animate();
  347. }
  348.  
  349. if (RULES.IS_VIDEO_PAGE) {
  350. createDownloadButton();
  351. createPrivateVideoFriendButton();
  352. }
  353.  
  354. if (RULES.IS_MESSAGES) {
  355. const button = parseDom('<button>clear messages</button>');
  356. document.querySelector('.headline').append(button);
  357. button.addEventListener('click', clearMessages);
  358. }
  359. }
  360.  
  361. //====================================================================================================
  362.  
  363. const SCROLL_RESET_DELAY = 500;
  364. const ANIMATION_DELAY = 500;
  365.  
  366. const store = new JabroniOutfitStore(defaultStateWithDurationAndPrivacy);
  367. const { state, stateLocale } = store;
  368. const { applyFilters, handleLoadedHTML } = new DataManager(RULES, state);
  369. store.subscribe(applyFilters);
  370.  
  371. console.log(LOGO);
  372. route();