Drive2 Auto Helper

работает на стренице подписок на сашины. проврка на бывшие авто и взаимные подписки, автоматически отписывается от бывших авто и от машин чьи владельцы не во взаимной подписке.

  1. // ==UserScript==
  2. // @name Drive2 Auto Helper
  3. // @namespace drive2.com
  4. // @version 0.91
  5. // @description работает на стренице подписок на сашины. проврка на бывшие авто и взаимные подписки, автоматически отписывается от бывших авто и от машин чьи владельцы не во взаимной подписке.
  6. // @author drive2 lover
  7. // @match https://www.drive2.com/*
  8. // @match https://www.drive2.ru/*
  9. // @license MIT
  10. // @grant none
  11.  
  12. // ==/UserScript==
  13. let tail = '';
  14. let fctx = '';
  15.  
  16. const MY_CARS_KEY = '__my_cached_cars__';
  17. let myCars = localStorage.getItem(MY_CARS_KEY) ? JSON.parse(localStorage.getItem(MY_CARS_KEY)) : [];
  18.  
  19. const localStorageKey = 'saveLastPage';
  20.  
  21. let stats = {
  22. pages: 0,
  23. totalBlocks: 0,
  24. processedBlocks: 0,
  25. removedBlocks: 0,
  26. unsubscribedBlocks: 0,
  27. oldSubscribedBlocks: 0
  28. };
  29.  
  30. // Создаем блок статистики
  31. const statsDiv = document.createElement('div');
  32. statsDiv.style.position = 'fixed';
  33. statsDiv.style.top = '0';
  34. statsDiv.style.left = '0';
  35. statsDiv.style.backgroundColor = '#333';
  36. statsDiv.style.color = '#fff';
  37. statsDiv.style.padding = '15px';
  38. statsDiv.style.margin = '10px';
  39. statsDiv.style.zIndex = '1000';
  40.  
  41. // Блок "Отписываться от авто"
  42. const unsubscribeCheckboxDiv = document.createElement('div');
  43. const unsubscribeCheckbox = document.createElement('input');
  44. unsubscribeCheckbox.type = 'checkbox';
  45. unsubscribeCheckbox.checked = true;
  46. unsubscribeCheckbox.id = 'unsubscribe-checkbox';
  47. const unsubscribeLabel = document.createElement('label');
  48. unsubscribeLabel.textContent = ' Отписываться от авто';
  49. unsubscribeLabel.style.marginRight = '10px';
  50. unsubscribeLabel.setAttribute('for', 'unsubscribe-checkbox');
  51. unsubscribeCheckboxDiv.appendChild(unsubscribeCheckbox);
  52. unsubscribeCheckboxDiv.appendChild(unsubscribeLabel);
  53. // statsDiv.appendChild(unsubscribeCheckboxDiv);
  54.  
  55. // Блок "Скрывать старые авто"
  56. const hideOldCarsCheckboxDiv = document.createElement('div');
  57. const hideOldCarsCheckbox = document.createElement('input');
  58. hideOldCarsCheckbox.type = 'checkbox';
  59. hideOldCarsCheckbox.checked = true;
  60. hideOldCarsCheckbox.id = 'hide-old-cars-checkbox';
  61. const hideOldCarsLabel = document.createElement('label');
  62. hideOldCarsLabel.textContent = ' Скрывать старые авто';
  63. hideOldCarsLabel.style.marginRight = '10px';
  64. hideOldCarsLabel.setAttribute('for', 'hide-old-cars-checkbox');
  65. hideOldCarsCheckboxDiv.appendChild(hideOldCarsCheckbox);
  66. hideOldCarsCheckboxDiv.appendChild(hideOldCarsLabel);
  67. // statsDiv.appendChild(hideOldCarsCheckboxDiv);
  68.  
  69. // Кнопка "Проверить"
  70. const checkButton = document.createElement('button');
  71. checkButton.innerText = 'Проверить';
  72. checkButton.style.marginTop = '10px';
  73. checkButton.style.padding = '5px 10px';
  74. checkButton.style.backgroundColor = '#007bff';
  75. checkButton.style.color = '#fff';
  76. checkButton.style.border = 'none';
  77. checkButton.style.borderRadius = '5px';
  78. checkButton.style.cursor = 'pointer';
  79. // statsDiv.appendChild(checkButton);
  80.  
  81. // Блок для ввода страниц и кнопки "Пропустить страниц"
  82. const skipPagesDiv = document.createElement('div');
  83. skipPagesDiv.style.marginTop = '10px';
  84.  
  85. // Поле ввода количества страниц
  86. const pagesInput = document.createElement('input');
  87. pagesInput.type = 'number';
  88. pagesInput.min = '1';
  89. pagesInput.value = localStorage.getItem(localStorageKey) ?? 1;
  90. pagesInput.style.width = '50px';
  91. pagesInput.style.marginRight = '10px';
  92.  
  93. // Кнопка "Пропустить страниц"
  94. const skipButton = document.createElement('button');
  95. skipButton.innerText = 'Пропустить страниц';
  96. skipButton.style.padding = '5px 10px';
  97. skipButton.style.backgroundColor = '#dc3545';
  98. skipButton.style.color = '#fff';
  99. skipButton.style.border = 'none';
  100. skipButton.style.borderRadius = '5px';
  101. skipButton.style.cursor = 'pointer';
  102.  
  103. // Добавляем инпут и кнопку в блок
  104. skipPagesDiv.appendChild(pagesInput);
  105. skipPagesDiv.appendChild(skipButton);
  106. //statsDiv.appendChild(skipPagesDiv);
  107.  
  108. const carBlock = document.querySelector('.l-container .u-link-area');
  109. const carBlockClass = carBlock?.parentElement?.classList?.[0] ?? null;
  110. const carsBlockClass = carBlock?.parentElement?.parentElement?.className ?? null;
  111. const carsBlockClassFormatted = carsBlockClass?.split(' ').map(className => '.' + className).join('') ?? '';
  112.  
  113. console.log('carBlockClass ' + carBlockClass);
  114. console.log('carsBlockClassFormatted ' + carsBlockClassFormatted);
  115.  
  116. async function init_me() {
  117. if (window.d2Env) {
  118. tail = window.d2Env.userId;
  119. fctx = window.d2Env.formContext['.FCTX'];
  120. } else {
  121. alert('обновите версию скрипта!');
  122. return;
  123. }
  124. }
  125.  
  126. // Функция для нажатия кнопки загрузки страниц и очистки элементов
  127. async function skipPages() {
  128. let pagesToSkip = parseInt(pagesInput.value, 10);
  129. if (isNaN(pagesToSkip) || pagesToSkip <= 0) {
  130. alert('Введите корректное число страниц');
  131. return;
  132. }
  133.  
  134. for (let i = 0; i < pagesToSkip; i++) {
  135. const loadMoreButton = document.querySelector('button.x-box-more');
  136. if (loadMoreButton) {
  137. await clearGrid();
  138. loadMoreButton.click();
  139. console.log(`Нажатие ${i + 1} на кнопку "Показать ещё"`);
  140. await new Promise(resolve => setTimeout(resolve, 3000)); // Ждём 3 секунды
  141. stats.pages++;
  142. updateStats();
  143. await clearEmptyBlocks();
  144. } else {
  145. alert('Кнопка "Показать ещё" не найдена, остановка.');
  146. break;
  147. }
  148. }
  149. }
  150.  
  151. // Функция очистки блока .o-grid.o-grid--2.o-grid--equal
  152. async function clearGrid() {
  153. const grids = document.querySelectorAll(carsBlockClassFormatted);
  154. if (grids.length > 0) {
  155. let blocks;
  156. for (const grid of grids) {
  157. blocks = grid.querySelectorAll('.' + carBlockClass);
  158. if (blocks.length) {
  159. stats.processedBlocks += blocks.length;
  160. blocks.forEach(car => car.remove());
  161. }
  162. }
  163. console.log('Удалены авто из пропущенных страниц.');
  164. } else {
  165. console.log('Не найдено блоков для очистки.');
  166. }
  167. }
  168.  
  169. skipButton.addEventListener('click', skipPages);
  170.  
  171. const updateStats = () => {
  172. statsDiv.innerHTML = `<div>
  173. Страниц пройдено: ${stats.pages}<br>
  174. Иконок авто в очереди: ${stats.totalBlocks}<br>
  175. Обработано: ${stats.processedBlocks}<br>
  176. Отписались автоматом: ${stats.unsubscribedBlocks}<br>
  177. Подписан на старые авто: ${stats.oldSubscribedBlocks}
  178. </div>`;
  179. statsDiv.appendChild(unsubscribeCheckboxDiv);
  180. statsDiv.appendChild(hideOldCarsCheckboxDiv);
  181. statsDiv.appendChild(checkButton);
  182. statsDiv.appendChild(skipPagesDiv);
  183. localStorage.setItem(localStorageKey, stats.pages);
  184. };
  185.  
  186. // Функция для поиска и клика по кнопке "Загрузить еще"
  187. const clickMoreButton = async () => {
  188. const button = document.querySelector('button.x-box-more');
  189.  
  190. if (button) {
  191. console.error('Нашли кнопку дальнейшей загрузки');
  192.  
  193. stats.pages++;
  194. console.log('Загружаем страницу ' + stats.pages);
  195. button.click();
  196.  
  197. updateStats();
  198. await clearEmptyBlocks();
  199. await new Promise(resolve => setTimeout(resolve, 3000));
  200. console.log('Загрузили блоки с авто, приступаем к их обработке');
  201. processBlocks();
  202. } else {
  203. alert('Кнопка не найдена, остановка процесса');
  204. }
  205. };
  206.  
  207. async function clearEmptyBlocks()
  208. {
  209. document.querySelectorAll(carsBlockClassFormatted).forEach(grid => {if (!grid.querySelector('div')) { grid.remove(); }});
  210. }
  211.  
  212. async function loadMyCars() {
  213. if (!myCars || myCars.length === 0) {
  214. const response = await fetch('/my/r/');
  215. const html = await response.text();
  216. const parser = new DOMParser();
  217. const doc = parser.parseFromString(html, 'text/html');
  218.  
  219. const cars = [];
  220. doc.querySelectorAll('.c-car-draglist__item .c-car-card__caption a').forEach(car => {
  221. if (!car.classList.contains('x-secondary-color')) {
  222. const id = car.href.match(/\/(\d+)\//)[1];
  223. cars.push({
  224. id,
  225. name: car.textContent.trim()
  226. });
  227. }
  228. });
  229.  
  230. localStorage.setItem(MY_CARS_KEY, JSON.stringify(cars));
  231. myCars = cars;
  232.  
  233. console.log('обновил кеш моих авто');
  234. }
  235. console.log('кеш моих авто', myCars?.map(car => car.name)?.join(', ') ?? '');
  236. }
  237.  
  238. function addCloseButton(element) {
  239. if (!element) return;
  240. const closeButton = document.createElement('button');
  241. closeButton.innerHTML = '&times;'; // Символ "×" (крестик)
  242. closeButton.style.position = 'absolute';
  243. closeButton.style.top = '5px';
  244. closeButton.style.right = '5px';
  245. closeButton.style.background = 'red';
  246. closeButton.style.color = 'white';
  247. closeButton.style.border = 'none';
  248. closeButton.style.padding = '5px 10px';
  249. closeButton.style.cursor = 'pointer';
  250. closeButton.style.fontSize = '16px';
  251. closeButton.style.borderRadius = '50%';
  252. closeButton.addEventListener('click', function () {
  253. this.parentNode.remove(); // Удаляет родительский элемент кнопки
  254. });
  255. if (window.getComputedStyle(element).position === 'static') {
  256. element.style.position = 'relative';
  257. }
  258. element.appendChild(closeButton);
  259.  
  260. element.classList.remove(carBlockClass);
  261. element.querySelector('a.u-link-area').remove();
  262. element.style.position = 'sticky';
  263. }
  264.  
  265. const unsubscribeCar = async (id) => {
  266. const url = '/ajax/subscription';
  267. const data = {
  268. _: 'unsubscribe',
  269. type: 'car',
  270. id: id,
  271. '.FCTX': fctx
  272. };
  273.  
  274. try {
  275. const response = await fetch(url, {
  276. method: 'POST',
  277. headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  278. body: new URLSearchParams(data).toString()
  279. });
  280.  
  281. if (response.ok) {
  282. const result = await response.json();
  283. console.log('Ответ unsubscribeCar:', result);
  284. return result;
  285. } else {
  286. console.error('Ошибка запроса:', response.status);
  287. }
  288. } catch (error) {
  289. console.error('Ошибка выполнения POST-запроса:', error);
  290. }
  291. };
  292.  
  293. const followUser = async (id) => {
  294. const url = '/ajax/subscription';
  295. const data = {
  296. _: 'subscribe',
  297. type: 'user',
  298. id: id.replace('p/',''),
  299. '.FCTX': fctx
  300. };
  301.  
  302. try {
  303. const response = await fetch(url, {
  304. method: 'POST',
  305. headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  306. body: new URLSearchParams(data).toString()
  307. });
  308.  
  309. if (response.ok) {
  310. const result = await response.json();
  311. console.log('Ответ subscribeCar:', result);
  312. return result;
  313. } else {
  314. console.error('Ошибка запроса:', response.status);
  315. }
  316. } catch (error) {
  317. console.error('Ошибка выполнения POST-запроса:', error);
  318. }
  319. };
  320.  
  321. const unfollowUser = async (id) => {
  322. const url = '/ajax/subscription';
  323. const data = {
  324. _: 'unsubscribe',
  325. type: 'user',
  326. id: id,
  327. '.FCTX': fctx
  328. };
  329.  
  330. try {
  331. const response = await fetch(url, {
  332. method: 'POST',
  333. headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  334. body: new URLSearchParams(data).toString()
  335. });
  336.  
  337. if (response.ok) {
  338. const result = await response.json();
  339. return result?.success;
  340. } else {
  341. console.error('Ошибка запроса:', response.status);
  342. }
  343. } catch (error) {
  344. console.error('Ошибка выполнения POST-запроса:', error);
  345. }
  346. };
  347.  
  348.  
  349. const shareCar = async (token, comment) => {
  350. const url = '/_api/share';
  351. const data = {
  352. token: token,
  353. comment: comment,
  354. '.FCTX': fctx
  355. };
  356.  
  357. try {
  358. const response = await fetch(url, {
  359. method: 'POST',
  360. headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  361. body: new URLSearchParams(data).toString()
  362. });
  363.  
  364. if (response.ok) {
  365. const result = await response.json();
  366. return result?.success;
  367. } else {
  368. console.error('Ошибка запроса:', response.status);
  369. }
  370. } catch (error) {
  371. console.error('Ошибка выполнения POST-запроса:', error);
  372. }
  373. };
  374.  
  375. const likesSend = async (token) => {
  376. const url = '/_api/likes';
  377. const data = {
  378. token: token,
  379. '.FCTX': fctx
  380. };
  381.  
  382. try {
  383. const response = await fetch(url, {
  384. method: 'POST',
  385. headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  386. body: new URLSearchParams(data).toString()
  387. });
  388.  
  389. if (response.ok) {
  390. const result = await response.json();
  391. console.log('like');
  392. return result;
  393. } else {
  394. console.error('Ошибка запроса:', response.status);
  395. }
  396. } catch (error) {
  397. console.error('Ошибка выполнения POST-запроса:', error);
  398. }
  399. };
  400.  
  401. const subscribeCar = async (id) => {
  402. const url = '/ajax/subscription';
  403. const data = {
  404. _: 'subscribe',
  405. type: 'car',
  406. id: id,
  407. '.FCTX': fctx
  408. };
  409.  
  410. try {
  411. const response = await fetch(url, {
  412. method: 'POST',
  413. headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  414. body: new URLSearchParams(data).toString()
  415. });
  416.  
  417. if (response.ok) {
  418. const result = await response.json();
  419. console.log('subscribeCar');
  420. return result;
  421. } else {
  422. console.error('Ошибка запроса:', response.status);
  423. }
  424. } catch (error) {
  425. console.error('Ошибка выполнения POST-запроса:', error);
  426. }
  427. };
  428.  
  429. async function loadUser(userId) {
  430. const response = await fetch(`/_api/hovercards/${userId}?tail=${tail}`);
  431. return await response.json();
  432. }
  433.  
  434. function isUserSubscribedOnMyCars(data) {
  435. const followedCarIds = data?.subscriptions?.followedCars?.map(car => {
  436. const match = car.url.match(/\/(\d+)\//);
  437. return match ? match[1] : null;
  438. }).filter(Boolean);
  439. const myCarIds = myCars ? myCars.map(car => car.id) : [];
  440. if (myCarIds.length == 0) {
  441. console.error('У текущего юзера не найдены авто');
  442. return false;
  443. }
  444. if (followedCarIds ? followedCarIds.some(carId => myCarIds.includes(carId)) : false) {
  445. return myCars
  446. .filter(car => followedCarIds.includes(car.id))
  447. .map(car => car.name)
  448. .join(', ');
  449. }
  450. return false;
  451. }
  452.  
  453. const processBlocks = async () => {
  454. const blocks = document.querySelectorAll('.' + carBlockClass);
  455. console.error('На странице найдено ' + blocks.length + ' авто');
  456. updateStats();
  457. let myCarNames = '';
  458.  
  459. for (const block of blocks) {
  460. const titleElement = block.querySelector('.c-car-title');
  461. stats.totalBlocks = document.querySelectorAll('.' + carBlockClass).length;
  462.  
  463. // Если чекбокс скрытия старых авто включен и блок старый – пропускаем или удаляем
  464. if (titleElement.classList.contains('x-secondary-color')) {
  465. stats.oldSubscribedBlocks++;
  466. stats.processedBlocks++;
  467. if (hideOldCarsCheckbox.checked) {
  468. block.remove();
  469. stats.removedBlocks++;
  470. console.error('Старое авто, удаляем');
  471. } else {
  472. console.error('Старое авто, оставляем на странице');
  473. addCloseButton(block);
  474. }
  475. updateStats();
  476. continue;
  477. }
  478.  
  479. const subscribeButton = block.querySelector('subscribe-button');
  480. const userId = block.querySelector('a.c-username')?.getAttribute('data-ihc-token');
  481.  
  482. if (!userId) {
  483. console.error('Не найден userId, пропускаем');
  484. continue
  485. };
  486.  
  487. const data = await loadUser(userId);
  488. const myCarNames = isUserSubscribedOnMyCars(data);
  489. if (myCarNames) {
  490. console.log(`Юзер номер ${userId} подписан на (${myCarNames}), пропускаем.`);
  491. } else {
  492. let uid = subscribeButton.getAttribute('uid');
  493. console.log('Юзер номер ' + userId + ' не подписан на мои авто.');
  494. if (unsubscribeCheckbox.checked) {
  495. unsubscribeCar(uid);
  496. stats.unsubscribedBlocks++;
  497. console.error('Отписываемся от авто с номером ' + uid);
  498. }
  499. }
  500.  
  501. // Удаляем блок, если он не содержит мою машину
  502. block.remove();
  503. stats.removedBlocks++;
  504. stats.processedBlocks++;
  505. updateStats();
  506.  
  507. // Ждём 1 секунду перед обработкой следующего блока
  508. await new Promise(resolve => setTimeout(resolve, 2000));
  509. }
  510. console.error('Обработали все авто');
  511.  
  512. clickMoreButton();
  513. };
  514.  
  515. function isCarsFollowingPage()
  516. {
  517. return (/^\/users\/(.*)\/carsfollowing/).test(window.location.pathname);
  518. }
  519.  
  520. function isSomeCarFollowingPage()
  521. {
  522. return (/^\/r\/(.*)\/followers/).test(window.location.pathname);
  523. }
  524.  
  525. function addSubscribeButton() {
  526. const counterElement = document.querySelector('.x-title-header .x-title .c-counter');
  527. if (counterElement) {
  528. const button = document.createElement('button');
  529. button.textContent = 'Подписаться на эти авто';
  530. button.classList.add('c-button');
  531. button.classList.add('c-button--primary');
  532. button.style.marginLeft = '10px';
  533. button.addEventListener('click', function () {
  534. scrapeUsers();
  535. });
  536. counterElement.after(button);
  537. }
  538. }
  539.  
  540. async function scrapeUsers() {
  541. let users = new Set();
  542. let usersDone = 0;
  543. let pagesProcessed = 0;
  544. let subscribeToCar = 0;
  545. let lostUsers = 0;
  546. let lostSubscribe = 0;
  547. let repostCar = 0;
  548.  
  549. let infoBox = document.createElement('div');
  550. infoBox.style.position = 'fixed';
  551. infoBox.style.top = '10px';
  552. infoBox.style.right = '10px';
  553. infoBox.style.background = 'rgb(51, 51, 51)';
  554. infoBox.style.color = 'white';
  555. infoBox.style.padding = '10px';
  556. infoBox.style.borderRadius = '5px';
  557. infoBox.style.zIndex = '1000';
  558. document.body.appendChild(infoBox);
  559.  
  560. function updateInfoBox() {
  561. let progress = users.size > 0 ? Math.round((usersDone / users.size) * 100) : 0;
  562. infoBox.innerHTML = `Страниц обработано: ${pagesProcessed}
  563. <br> Пользователей собрано: ${users.size}
  564. <br> Пользователей обработано: ${usersDone}
  565. <br> Пользователей пропущено: ${lostUsers}<br> (посещали сайт > чем месяц назад или нет актуальных авто)
  566. <br> Подписано на авто: ${subscribeToCar}<br> (даже если уже был на авто подписан)
  567. <br> Не удалось подписаться: ${lostSubscribe}
  568. <br> Репост: ${repostCar}
  569. <br>
  570. <div style='width: 100%; background: #555; height: 10px; border-radius: 5px; margin-top: 5px;'>
  571. <div style='width: ${progress}%; background: #4caf50; height: 10px; border-radius: 5px;'></div>
  572. </div>`;
  573. }
  574.  
  575. async function likeCar(carUrl, fullCaption, myCarNames) {
  576. try {
  577. await new Promise(resolve => setTimeout(resolve, 1000));
  578. let response = await fetch(carUrl);
  579. let html = await response.text();
  580. let parser = new DOMParser();
  581. let doc = parser.parseFromString(html, 'text/html');
  582. let likeButton = doc.querySelector('like-button');
  583. let likeButtonActive = likeButton?.hasAttribute('active') ?? false;
  584. let likeButtonDisabled = likeButton?.hasAttribute('disabled') ?? false;
  585. if (likeButton) {
  586. let key = likeButton.getAttribute('key');
  587. if (key && !likeButtonActive && !likeButtonDisabled) {
  588. await new Promise(resolve => setTimeout(resolve, 1000));
  589. likesSend(key);
  590. }
  591. }
  592.  
  593. let repostButton = doc.querySelector('repost-button');
  594. let repostButtonDisabled = repostButton?.hasAttribute('disabled');
  595. if (myCarNames && repostButton && !repostButtonDisabled && repostButton.hasAttribute('token')) {
  596. await new Promise(resolve => setTimeout(resolve, 1000));
  597. let result = await shareCar(repostButton.getAttribute('token'), 'Подписывайтесь на ' + fixString(fullCaption));
  598. if (result) {
  599. repostCar++;
  600. }
  601. }
  602.  
  603. let subscribeButton = doc.querySelector('subscribe-button');
  604. let subscribeButtonSubscribed = subscribeButton?.hasAttribute('subscribed') ?? false;
  605.  
  606. // если авто уже было когда-то подписано
  607. // был сделан лайк и репост
  608. // но по какой-то причине отписались
  609. // то подписываться повторно наверное и не нужно
  610. if (subscribeButton && !subscribeButtonSubscribed && repostButtonDisabled && likeButtonActive) {
  611. console.error(`Авто уже имеет лайк и репост но мы не подписаны, видимо есть причина, пропускам его`);
  612. lostSubscribe++;
  613. return;
  614. }
  615.  
  616. if (subscribeButton && !subscribeButtonSubscribed && subscribeButton.hasAttribute('uid')) {
  617. await new Promise(resolve => setTimeout(resolve, 1000));
  618. let result = await subscribeCar(subscribeButton.getAttribute('uid'));
  619. if (result?.types.length) {
  620. subscribeToCar++;
  621. } else {
  622. lostSubscribe++;
  623. }
  624. }
  625.  
  626.  
  627. } catch (error) {
  628. console.error(`Ошибка загрузки страницы авто ${carUrl}:`, error);
  629. }
  630. return null;
  631. }
  632.  
  633. function fixString(str) {
  634. let symbolsToReplace = ['🇷🇺', '☭'];
  635. return symbolsToReplace.reduce((acc, symbol) => acc.replaceAll(symbol, '🇺🇦'), str);
  636. }
  637.  
  638. async function processUsers(userList) {
  639. for (let userId of userList) {
  640. try {
  641. usersDone++;
  642. await new Promise(resolve => setTimeout(resolve, 1000));
  643. const data = await loadUser(userId);
  644. if (data?.lastVisit && shouldSkipUser(data.lastVisit)) {
  645. console.log(`Пропускаем пользователя ${data.nickname} (${userId}) из-за даты последнего визита: ${data.lastVisit}`);
  646. lostUsers++;
  647. continue;
  648. }
  649. const myCarNames = isUserSubscribedOnMyCars(data);
  650. if (data?.cars) {
  651. let i = 0;
  652. for (let car of data.cars) {
  653. i++;
  654. if (car.belongState === "My" && i <= myCars.length) {
  655. let carId = parseCarId(car.url);
  656. if (carId) {
  657. console.log(`Проверяем авто: ${car.fullCaption} ${car.url}`);
  658. updateInfoBox();
  659. }
  660. await likeCar(car.url, car.fullCaption, myCarNames);
  661. }
  662. }
  663. } else {
  664. lostUsers++;
  665. console.log('Не найдено авто у ' + userId);
  666. }
  667. if (data?.isFollowable) {
  668. if (data?.isFollowed === false && data?.subscriptions?.followsMe === true) {
  669. await new Promise(resolve => setTimeout(resolve, 1000));
  670. followUser(userId);
  671. }
  672. if (data?.isFollowed === true && data?.subscriptions?.followsMe === false) {
  673. await new Promise(resolve => setTimeout(resolve, 1000));
  674. unfollowUser(userId);
  675. }
  676. }
  677. } catch (error) {
  678. console.error(`Ошибка загрузки данных пользователя ${userId}:`, error);
  679. }
  680. }
  681. console.log('Обработка всех пользователей завершена.');
  682. alert('Обработка всех пользователей завершена.');
  683. }
  684.  
  685. function shouldSkipUser(lastVisit) {
  686. if (!lastVisit) return true;
  687. if (lastVisit.includes("Сейчас онлайн")) return false;
  688. if (lastVisit.includes("Был больше года назад")) return true;
  689. let monthMatch = lastVisit.match(/Был (\d+) (month|месяц)/);
  690. if (monthMatch) {
  691. let months = parseInt(monthMatch[1], 10);
  692. return months > 1;
  693. }
  694. return false;
  695. }
  696.  
  697. function parseCarId(carUrl) {
  698. let match = carUrl.match(/\/(\d+)\/?$/);
  699. return match ? match[1] : null;
  700. }
  701.  
  702. async function processPage() {
  703. let mainContainer = document.querySelector('.l-container div.g-column-mid');
  704. if (!mainContainer) {
  705. console.log('Основной контейнер не найден');
  706. return;
  707. }
  708.  
  709. let boxes = mainContainer.querySelectorAll('.x-box.o-f');
  710. boxes.forEach(box => {
  711. let userDivs = box.querySelectorAll('div > div');
  712. userDivs.forEach(div => {
  713. let userLink = div.querySelector('a.c-username');
  714. if (userLink) {
  715. let token = userLink.getAttribute('data-ihc-token');
  716. if (token) {
  717. users.add(token);
  718. }
  719. }
  720. div.remove();
  721. });
  722.  
  723. if (box.children.length === 0) {
  724. console.log('Удаляем пустой контейнер');
  725. box.remove();
  726. }
  727. });
  728.  
  729. updateInfoBox();
  730.  
  731. let loadMoreButton = document.querySelector('.x-box-more');
  732. if (loadMoreButton) {
  733. loadMoreButton.click();
  734. pagesProcessed++;
  735. updateInfoBox();
  736. await new Promise(resolve => setTimeout(resolve, 2000));
  737. processPage();
  738. } else {
  739. console.log(`Сбор завершен. Всего пользователей собрано: ${users.size}`);
  740. processUsers(Array.from(users));
  741. }
  742. }
  743.  
  744. processPage();
  745. }
  746.  
  747. checkButton.addEventListener('click', processBlocks);
  748.  
  749. loadMyCars();
  750. init_me();
  751.  
  752. if (isCarsFollowingPage()) {
  753. document.body.appendChild(statsDiv);
  754. updateStats();
  755. }
  756.  
  757. if (isSomeCarFollowingPage()) {
  758. addSubscribeButton();
  759. }
  760.  
  761. document.querySelector('.l-dv__i')?.remove();
  762. document.querySelector('.c-dv-side.o-row.o-sticky.o-f')?.remove();
  763.  
  764. async function autoLike() {
  765. const buttons = document.querySelectorAll('like-button'); // или '.like-button', если это класс
  766. for (const button of buttons) {
  767. let kind = button?.getAttribute('kind') ?? '';
  768. if (!button.hasAttribute('active') && button.hasAttribute('kind') && (kind === 'c' || kind === 'ubr' || kind === 'cjr')) {
  769. button.setAttribute('active', '');
  770. await new Promise(resolve => setTimeout(resolve, 500));
  771. likesSend(button.getAttribute('key'));
  772. }
  773. }
  774. }
  775. setInterval(autoLike, 3000);