- // ==UserScript==
- // @name Drive2 Old Auto UnSubscriber
- // @namespace drive2.com
- // @version 0.87
- // @description работает на стренице подписок на сашины. проврка на бывшие авто и взаимные подписки, автоматически отписывается от бывших авто и от машин чьи владельцы не во взаимной подписке.
- // @author drive2 lover
- // @match https://www.drive2.com/*
- // @match https://www.drive2.ru/*
- // @license MIT
- // @grant none
-
- // ==/UserScript==
- let tail = '';
- let fctx = '';
-
- const MY_CARS_KEY = 'my_cars__';
- let myCars = localStorage.getItem(MY_CARS_KEY) ? JSON.parse(localStorage.getItem(MY_CARS_KEY)) : false;
- const localStorageKey = 'saveLastPage';
-
- let stats = {
- pages: 0,
- totalBlocks: 0,
- processedBlocks: 0,
- removedBlocks: 0,
- unsubscribedBlocks: 0,
- oldSubscribedBlocks: 0
- };
-
- // Создаем блок статистики
- const statsDiv = document.createElement('div');
- statsDiv.style.position = 'fixed';
- statsDiv.style.top = '0';
- statsDiv.style.left = '0';
- statsDiv.style.backgroundColor = '#333';
- statsDiv.style.color = '#fff';
- statsDiv.style.padding = '15px';
- statsDiv.style.margin = '10px';
- statsDiv.style.zIndex = '1000';
-
- // Блок "Отписываться от авто"
- const unsubscribeCheckboxDiv = document.createElement('div');
- const unsubscribeCheckbox = document.createElement('input');
- unsubscribeCheckbox.type = 'checkbox';
- unsubscribeCheckbox.checked = true;
- unsubscribeCheckbox.id = 'unsubscribe-checkbox';
- const unsubscribeLabel = document.createElement('label');
- unsubscribeLabel.textContent = ' Отписываться от авто';
- unsubscribeLabel.style.marginRight = '10px';
- unsubscribeLabel.setAttribute('for', 'unsubscribe-checkbox');
- unsubscribeCheckboxDiv.appendChild(unsubscribeCheckbox);
- unsubscribeCheckboxDiv.appendChild(unsubscribeLabel);
- // statsDiv.appendChild(unsubscribeCheckboxDiv);
-
- // Блок "Скрывать старые авто"
- const hideOldCarsCheckboxDiv = document.createElement('div');
- const hideOldCarsCheckbox = document.createElement('input');
- hideOldCarsCheckbox.type = 'checkbox';
- hideOldCarsCheckbox.checked = true;
- hideOldCarsCheckbox.id = 'hide-old-cars-checkbox';
- const hideOldCarsLabel = document.createElement('label');
- hideOldCarsLabel.textContent = ' Скрывать старые авто';
- hideOldCarsLabel.style.marginRight = '10px';
- hideOldCarsLabel.setAttribute('for', 'hide-old-cars-checkbox');
- hideOldCarsCheckboxDiv.appendChild(hideOldCarsCheckbox);
- hideOldCarsCheckboxDiv.appendChild(hideOldCarsLabel);
- // statsDiv.appendChild(hideOldCarsCheckboxDiv);
-
- // Кнопка "Проверить"
- const checkButton = document.createElement('button');
- checkButton.innerText = 'Проверить';
- checkButton.style.marginTop = '10px';
- checkButton.style.padding = '5px 10px';
- checkButton.style.backgroundColor = '#007bff';
- checkButton.style.color = '#fff';
- checkButton.style.border = 'none';
- checkButton.style.borderRadius = '5px';
- checkButton.style.cursor = 'pointer';
- // statsDiv.appendChild(checkButton);
-
- // Блок для ввода страниц и кнопки "Пропустить страниц"
- const skipPagesDiv = document.createElement('div');
- skipPagesDiv.style.marginTop = '10px';
-
- // Поле ввода количества страниц
- const pagesInput = document.createElement('input');
- pagesInput.type = 'number';
- pagesInput.min = '1';
- pagesInput.value = localStorage.getItem(localStorageKey) ?? 1;
- pagesInput.style.width = '50px';
- pagesInput.style.marginRight = '10px';
-
- // Кнопка "Пропустить страниц"
- const skipButton = document.createElement('button');
- skipButton.innerText = 'Пропустить страниц';
- skipButton.style.padding = '5px 10px';
- skipButton.style.backgroundColor = '#dc3545';
- skipButton.style.color = '#fff';
- skipButton.style.border = 'none';
- skipButton.style.borderRadius = '5px';
- skipButton.style.cursor = 'pointer';
-
- // Добавляем инпут и кнопку в блок
- skipPagesDiv.appendChild(pagesInput);
- skipPagesDiv.appendChild(skipButton);
- //statsDiv.appendChild(skipPagesDiv);
-
- const carBlock = document.querySelector('.l-container .u-link-area');
- const carBlockClass = carBlock?.parentElement?.classList?.[0] ?? null;
- const carsBlockClass = carBlock?.parentElement?.parentElement?.className ?? null;
- const carsBlockClassFormatted = carsBlockClass?.split(' ').map(className => '.' + className).join('') ?? '';
-
- console.log('carBlockClass ' + carBlockClass);
- console.log('carsBlockClassFormatted ' + carsBlockClassFormatted);
-
- async function init_me() {
- if (window.d2Env) {
- tail = window.d2Env.userId;
- fctx = window.d2Env.formContext['.FCTX'];
- } else {
- alert('обновите версию скрипта!');
- return;
- }
- }
-
- // Функция для нажатия кнопки загрузки страниц и очистки элементов
- async function skipPages() {
- let pagesToSkip = parseInt(pagesInput.value, 10);
- if (isNaN(pagesToSkip) || pagesToSkip <= 0) {
- alert('Введите корректное число страниц');
- return;
- }
-
- for (let i = 0; i < pagesToSkip; i++) {
- const loadMoreButton = document.querySelector('button.x-box-more');
- if (loadMoreButton) {
- await clearGrid();
- loadMoreButton.click();
- console.log(`Нажатие ${i + 1} на кнопку "Показать ещё"`);
- await new Promise(resolve => setTimeout(resolve, 3000)); // Ждём 3 секунды
- stats.pages++;
- updateStats();
- await clearEmptyBlocks();
- } else {
- alert('Кнопка "Показать ещё" не найдена, остановка.');
- break;
- }
- }
- }
-
- // Функция очистки блока .o-grid.o-grid--2.o-grid--equal
- async function clearGrid() {
- const grids = document.querySelectorAll(carsBlockClassFormatted);
- if (grids.length > 0) {
- let blocks;
- for (const grid of grids) {
- blocks = grid.querySelectorAll('.' + carBlockClass);
- if (blocks.length) {
- stats.processedBlocks += blocks.length;
- blocks.forEach(car => car.remove());
- }
- }
- console.log('Удалены авто из пропущенных страниц.');
- } else {
- console.log('Не найдено блоков для очистки.');
- }
- }
-
- skipButton.addEventListener('click', skipPages);
-
- const updateStats = () => {
- statsDiv.innerHTML = `<div>
- Страниц пройдено: ${stats.pages}<br>
- Иконок авто в очереди: ${stats.totalBlocks}<br>
- Обработано: ${stats.processedBlocks}<br>
- Отписались автоматом: ${stats.unsubscribedBlocks}<br>
- Подписан на старые авто: ${stats.oldSubscribedBlocks}
- </div>`;
- statsDiv.appendChild(unsubscribeCheckboxDiv);
- statsDiv.appendChild(hideOldCarsCheckboxDiv);
- statsDiv.appendChild(checkButton);
- statsDiv.appendChild(skipPagesDiv);
- localStorage.setItem(localStorageKey, stats.pages);
- };
-
- // Функция для поиска и клика по кнопке "Загрузить еще"
- const clickMoreButton = async () => {
- const button = document.querySelector('button.x-box-more');
-
- if (button) {
- console.error('Нашли кнопку дальнейшей загрузки');
-
- stats.pages++;
- console.log('Загружаем страницу ' + stats.pages);
- button.click();
-
- updateStats();
- await clearEmptyBlocks();
- await new Promise(resolve => setTimeout(resolve, 3000));
- console.log('Загрузили блоки с авто, приступаем к их обработке');
- processBlocks();
- } else {
- alert('Кнопка не найдена, остановка процесса');
- }
- };
-
- async function clearEmptyBlocks()
- {
- document.querySelectorAll(carsBlockClassFormatted).forEach(grid => {if (!grid.querySelector('div')) { grid.remove(); }});
- }
-
- async function loadMyCars() {
- if (!localStorage.getItem(MY_CARS_KEY)) {
- const response = await fetch('/my/r/');
- const html = await response.text();
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
-
- const cars = [];
- doc.querySelectorAll('.c-car-draglist__item .c-car-card__caption a').forEach(car => {
- if (!car.classList.contains('x-secondary-color')) {
- const id = car.href.match(/\/(\d+)\//)[1];
- cars.push({
- id,
- name: car.textContent.trim()
- });
- }
- });
-
- localStorage.setItem(MY_CARS_KEY, JSON.stringify(cars));
-
- console.log('обновил кеш моих авто', cars);
- } else {
- console.log('кеш моих авто уже загружен', myCars);
- }
- }
-
- function addCloseButton(element) {
- if (!element) return;
- const closeButton = document.createElement('button');
- closeButton.innerHTML = '×'; // Символ "×" (крестик)
- closeButton.style.position = 'absolute';
- closeButton.style.top = '5px';
- closeButton.style.right = '5px';
- closeButton.style.background = 'red';
- closeButton.style.color = 'white';
- closeButton.style.border = 'none';
- closeButton.style.padding = '5px 10px';
- closeButton.style.cursor = 'pointer';
- closeButton.style.fontSize = '16px';
- closeButton.style.borderRadius = '50%';
- closeButton.addEventListener('click', function () {
- this.parentNode.remove(); // Удаляет родительский элемент кнопки
- });
- if (window.getComputedStyle(element).position === 'static') {
- element.style.position = 'relative';
- }
- element.appendChild(closeButton);
-
- element.classList.remove(carBlockClass);
- element.querySelector('a.u-link-area').remove();
- element.style.position = 'sticky';
- }
-
- const unsubscribeCar = async (id) => {
- const url = '/ajax/subscription';
- const data = {
- _: 'unsubscribe',
- type: 'car',
- id: id,
- '.FCTX': fctx
- };
-
- try {
- const response = await fetch(url, {
- method: 'POST',
- headers: {'Content-Type': 'application/x-www-form-urlencoded'},
- body: new URLSearchParams(data).toString()
- });
-
- if (response.ok) {
- const result = await response.json();
- console.log('Ответ unsubscribeCar:', result);
- return result;
- } else {
- console.error('Ошибка запроса:', response.status);
- }
- } catch (error) {
- console.error('Ошибка выполнения POST-запроса:', error);
- }
- };
-
- const followUser = async (id) => {
- const url = '/ajax/subscription';
- const data = {
- _: 'subscribe',
- type: 'user',
- id: id,
- '.FCTX': fctx
- };
-
- try {
- const response = await fetch(url, {
- method: 'POST',
- headers: {'Content-Type': 'application/x-www-form-urlencoded'},
- body: new URLSearchParams(data).toString()
- });
-
- if (response.ok) {
- const result = await response.json();
- console.log('Ответ subscribeCar:', result);
- return result;
- } else {
- console.error('Ошибка запроса:', response.status);
- }
- } catch (error) {
- console.error('Ошибка выполнения POST-запроса:', error);
- }
- };
-
- const unfollowUser = async (id) => {
- const url = '/ajax/subscription';
- const data = {
- _: 'unsubscribe',
- type: 'user',
- id: id,
- '.FCTX': fctx
- };
-
- try {
- const response = await fetch(url, {
- method: 'POST',
- headers: {'Content-Type': 'application/x-www-form-urlencoded'},
- body: new URLSearchParams(data).toString()
- });
-
- if (response.ok) {
- const result = await response.json();
- return result?.success;
- } else {
- console.error('Ошибка запроса:', response.status);
- }
- } catch (error) {
- console.error('Ошибка выполнения POST-запроса:', error);
- }
- };
-
-
- const shareCar = async (token, comment) => {
- const url = '/_api/share';
- const data = {
- token: token,
- comment: comment,
- '.FCTX': fctx
- };
-
- try {
- const response = await fetch(url, {
- method: 'POST',
- headers: {'Content-Type': 'application/x-www-form-urlencoded'},
- body: new URLSearchParams(data).toString()
- });
-
- if (response.ok) {
- const result = await response.json();
- return result?.success;
- } else {
- console.error('Ошибка запроса:', response.status);
- }
- } catch (error) {
- console.error('Ошибка выполнения POST-запроса:', error);
- }
- };
-
- const likesCar = async (token) => {
- const url = '/_api/likes';
- const data = {
- token: token,
- '.FCTX': fctx
- };
-
- try {
- const response = await fetch(url, {
- method: 'POST',
- headers: {'Content-Type': 'application/x-www-form-urlencoded'},
- body: new URLSearchParams(data).toString()
- });
-
- if (response.ok) {
- const result = await response.json();
- console.log('Ответ like:', result);
- return result;
- } else {
- console.error('Ошибка запроса:', response.status);
- }
- } catch (error) {
- console.error('Ошибка выполнения POST-запроса:', error);
- }
- };
-
- const subscribeCar = async (id) => {
- const url = '/ajax/subscription';
- const data = {
- _: 'subscribe',
- type: 'car',
- id: id,
- '.FCTX': fctx
- };
-
- try {
- const response = await fetch(url, {
- method: 'POST',
- headers: {'Content-Type': 'application/x-www-form-urlencoded'},
- body: new URLSearchParams(data).toString()
- });
-
- if (response.ok) {
- const result = await response.json();
- console.log('Ответ subscribeCar:', result);
- return result;
- } else {
- console.error('Ошибка запроса:', response.status);
- }
- } catch (error) {
- console.error('Ошибка выполнения POST-запроса:', error);
- }
- };
-
- async function loadUser(userId) {
- const response = await fetch(`/_api/hovercards/${userId}?tail=${tail}`);
- return await response.json();
- }
-
- function isUserSubscribedOnMyCars(data) {
- const followedCarIds = data?.subscriptions?.followedCars?.map(car => {
- const match = car.url.match(/\/(\d+)\//);
- return match ? match[1] : null;
- }).filter(Boolean);
- const myCarIds = myCars ? myCars.map(car => car.id) : [];
- if (myCarIds.length == 0) {
- console.error('У текущего юзера не найдены авто');
- return false;
- }
- if (followedCarIds ? followedCarIds.some(carId => myCarIds.includes(carId)) : false) {
- return myCars
- .filter(car => followedCarIds.includes(car.id))
- .map(car => car.name)
- .join(', ');
- }
- return false;
- }
-
- const processBlocks = async () => {
- const blocks = document.querySelectorAll('.' + carBlockClass);
- console.error('На странице найдено ' + blocks.length + ' авто');
- updateStats();
- let myCarNames = '';
-
- for (const block of blocks) {
- const titleElement = block.querySelector('.c-car-title');
- stats.totalBlocks = document.querySelectorAll('.' + carBlockClass).length;
-
- // Если чекбокс скрытия старых авто включен и блок старый – пропускаем или удаляем
- if (titleElement.classList.contains('x-secondary-color')) {
- stats.oldSubscribedBlocks++;
- stats.processedBlocks++;
- if (hideOldCarsCheckbox.checked) {
- block.remove();
- stats.removedBlocks++;
- console.error('Старое авто, удаляем');
- } else {
- console.error('Старое авто, оставляем на странице');
- addCloseButton(block);
- }
- updateStats();
- continue;
- }
-
- const subscribeButton = block.querySelector('subscribe-button');
- const userId = block.querySelector('a.c-username')?.getAttribute('data-ihc-token');
-
- if (!userId) {
- console.error('Не найден userId, пропускаем');
- continue
- };
-
- const data = await loadUser(userId);
- const myCarNames = isUserSubscribedOnMyCars(data);
- if (myCarNames) {
- console.log(`Юзер номер ${userId} подписан на (${myCarNames}), пропускаем.`);
- } else {
- let uid = subscribeButton.getAttribute('uid');
- console.log('Юзер номер ' + userId + ' не подписан на мои авто.');
- if (unsubscribeCheckbox.checked) {
- unsubscribeCar(uid);
- stats.unsubscribedBlocks++;
- console.error('Отписываемся от авто с номером ' + uid);
- }
- }
-
- // Удаляем блок, если он не содержит мою машину
- block.remove();
- stats.removedBlocks++;
- stats.processedBlocks++;
- updateStats();
-
- // Ждём 1 секунду перед обработкой следующего блока
- await new Promise(resolve => setTimeout(resolve, 2000));
- }
- console.error('Обработали все авто');
-
- clickMoreButton();
- };
-
- function isCarsFollowingPage()
- {
- return (/^\/users\/(.*)\/carsfollowing/).test(window.location.pathname);
- }
-
- function isSomeCarFollowingPage()
- {
- return (/^\/r\/(.*)\/followers/).test(window.location.pathname);
- }
-
- function addSubscribeButton() {
- const counterElement = document.querySelector('.x-title-header .x-title .c-counter');
- if (counterElement) {
- const button = document.createElement('button');
- button.textContent = 'Подписаться на эти авто';
- button.classList.add('c-button');
- button.classList.add('c-button--primary');
- button.style.marginLeft = '10px';
- button.addEventListener('click', function () {
- scrapeUsers();
- });
- counterElement.after(button);
- }
- }
-
- async function scrapeUsers() {
- let users = new Set();
- let usersDone = 0;
- let pagesProcessed = 0;
- let subscribeToCar = 0;
- let lostUsers = 0;
- let lostSubscribe = 0;
- let repostCar = 0;
-
- let infoBox = document.createElement('div');
- infoBox.style.position = 'fixed';
- infoBox.style.top = '10px';
- infoBox.style.right = '10px';
- infoBox.style.background = 'rgb(51, 51, 51)';
- infoBox.style.color = 'white';
- infoBox.style.padding = '10px';
- infoBox.style.borderRadius = '5px';
- infoBox.style.zIndex = '1000';
- document.body.appendChild(infoBox);
-
- function updateInfoBox() {
- let progress = users.size > 0 ? Math.round((usersDone / users.size) * 100) : 0;
- infoBox.innerHTML = `Страниц обработано: ${pagesProcessed}
- <br> Пользователей собрано: ${users.size}
- <br> Пользователей обработано: ${usersDone}
- <br> Пользователей пропущено: ${lostUsers}<br> (посещали сайт > чем месяц назад или нет актуальных авто)
- <br> Подписано на авто: ${subscribeToCar}<br> (даже если уже был на авто подписан)
- <br> Не удалось подписаться: ${lostSubscribe}
- <br> Репост: ${repostCar}
- <br>
- <div style='width: 100%; background: #555; height: 10px; border-radius: 5px; margin-top: 5px;'>
- <div style='width: ${progress}%; background: #4caf50; height: 10px; border-radius: 5px;'></div>
- </div>`;
- }
-
- async function likeCar(carUrl, fullCaption, myCarNames) {
- try {
- await new Promise(resolve => setTimeout(resolve, 1000));
- let response = await fetch(carUrl);
- let html = await response.text();
- let parser = new DOMParser();
- let doc = parser.parseFromString(html, 'text/html');
- let likeButton = doc.querySelector('like-button');
- if (likeButton) {
- let active = likeButton.hasAttribute('active');
- let disabled = likeButton.hasAttribute('disabled');
- let key = likeButton.getAttribute('key');
- if (key && !active && !disabled) {
- await new Promise(resolve => setTimeout(resolve, 1000));
- likesCar(key);
- }
- }
- let subscribeButton = doc.querySelector('subscribe-button');
- if (subscribeButton && !subscribeButton.hasAttribute('subscribed') && subscribeButton.hasAttribute('uid')) {
- await new Promise(resolve => setTimeout(resolve, 1000));
- let result = await subscribeCar(subscribeButton.getAttribute('uid'));
- if (result?.types.length) {
- subscribeToCar++;
- } else {
- lostSubscribe++;
- }
- }
- let repostButton = doc.querySelector('repost-button');
- if (myCarNames && repostButton && !repostButton.hasAttribute('disabled') && repostButton.hasAttribute('token')) {
- await new Promise(resolve => setTimeout(resolve, 1000));
- let symbolsToReplace = ['🇷🇺', '☭'];
- let result = await shareCar(repostButton.getAttribute('token'), 'Подписывайтесь на ' + fixString(fullCaption));
- if (result) {
- repostCar++;
- }
- }
-
- } catch (error) {
- console.error(`Ошибка загрузки страницы авто ${carUrl}:`, error);
- }
- return null;
- }
-
- function fixString(str) {
- let symbolsToReplace = ['🇷🇺', '☭'];
- return symbolsToReplace.reduce((acc, symbol) => acc.replaceAll(symbol, '🇺🇦'), str);
- }
-
- async function processUsers(userList) {
- for (let userId of userList) {
- try {
- usersDone++;
- await new Promise(resolve => setTimeout(resolve, 1000));
- const data = await loadUser(userId);
- if (data?.lastVisit && shouldSkipUser(data.lastVisit)) {
- console.log(`Пропускаем пользователя ${data.nickname} (${userId}) из-за даты последнего визита: ${data.lastVisit}`);
- lostUsers++;
- continue;
- }
- const myCarNames = isUserSubscribedOnMyCars(data);
- if (data?.cars) {
- for (let car of data.cars) {
- if (car.belongState === "My") {
- let carId = parseCarId(car.url);
- if (carId) {
- console.log(`Проверяем авто: ${car.fullCaption}`);
- await likeCar(car.url, car.fullCaption, myCarNames);
- updateInfoBox();
- }
- }
- }
- } else {
- lostUsers++;
- console.log('Не найдено авто у ' + userId);
- }
- if (data?.isFollowable) {
- if (data?.isFollowed === false && data?.subscriptions?.followsMe === true) {
- await new Promise(resolve => setTimeout(resolve, 1000));
- followUser(userId);
- }
- if (data?.isFollowed === true && data?.subscriptions?.followsMe === false) {
- await new Promise(resolve => setTimeout(resolve, 1000));
- unfollowUser(userId);
- }
- }
- } catch (error) {
- console.error(`Ошибка загрузки данных пользователя ${userId}:`, error);
- }
- }
- console.log('Обработка всех пользователей завершена.');
- alert('Обработка всех пользователей завершена.');
- }
-
- function shouldSkipUser(lastVisit) {
- if (!lastVisit) return true;
- if (lastVisit.includes("Сейчас онлайн")) return false;
- if (lastVisit.includes("Был больше года назад")) return true;
- let monthMatch = lastVisit.match(/Был (\d+) (month|месяц)/);
- if (monthMatch) {
- let months = parseInt(monthMatch[1], 10);
- return months > 1;
- }
- return false;
- }
-
- function parseCarId(carUrl) {
- let match = carUrl.match(/\/(\d+)\/?$/);
- return match ? match[1] : null;
- }
-
- async function processPage() {
- let mainContainer = document.querySelector('.l-page-main.g-column-mid');
- if (!mainContainer) {
- console.log('Основной контейнер не найден');
- return;
- }
-
- let boxes = mainContainer.querySelectorAll('.x-box.o-f');
- boxes.forEach(box => {
- let userDivs = box.querySelectorAll('div > div');
- userDivs.forEach(div => {
- let userLink = div.querySelector('a.c-username');
- if (userLink) {
- let token = userLink.getAttribute('data-ihc-token');
- if (token) {
- users.add(token);
- }
- }
- div.remove();
- });
-
- if (box.children.length === 0) {
- console.log('Удаляем пустой контейнер');
- box.remove();
- }
- });
-
- updateInfoBox();
-
- let loadMoreButton = document.querySelector('.x-box-more');
- if (loadMoreButton) {
- loadMoreButton.click();
- pagesProcessed++;
- updateInfoBox();
- await new Promise(resolve => setTimeout(resolve, 2000));
- processPage();
- } else {
- console.log(`Сбор завершен. Всего пользователей собрано: ${users.size}`);
- processUsers(Array.from(users));
- }
- }
-
- processPage();
- }
-
- checkButton.addEventListener('click', processBlocks);
-
- loadMyCars();
- init_me();
-
- if (isCarsFollowingPage()) {
- document.body.appendChild(statsDiv);
- updateStats();
- }
-
- if (isSomeCarFollowingPage()) {
- addSubscribeButton();
- }
-
- document.querySelector('.l-dv__i')?.remove();
- document.querySelector('.c-dv-side.o-row.o-sticky.o-f')?.remove();