xVKTT

Removing Ads, additional information about the page and many other improvements!

// ==UserScript==
// @name         xVKTT
// @name:en xVKTT
// @license MIT
// @description  Удаление Рекламы, доп. информация о странице и многие другие улучшения!
// @description:en Removing Ads, additional information about the page and many other improvements!
// @author       xZeNice
// @version      29.04.2024
// @match        https://vk.com/*
// @match        https://id.vk.com/*
// @namespace    http://tampermonkey.net/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=vk.com
// @exclude      *://vk.com/notifier.php*
// @exclude      *://vk.com/*widget*.php*
// @run-at       document-start
// @grant        none
// ==/UserScript==

// xVKTT by [ Роман Афанасьев | xZeNice ]
// Скрипт делался по большей части под себя, испытывался в Yandex | Edge | Chrome
//Поддержка автора: https://www.donationalerts.com/r/xzenice

// xVKTT by [ Roman Afanasyev | xZeNice ]
// The script was made mostly for itself, tested in Yandex | Edge | Chrome
//Author's support: https://www.donationalerts.com/r/xzenice

// Создаем объект MutationObserver, который будет следить за изменениями в DOM-дереве
const observer = new MutationObserver(() => {
  // Ищем элемент с классом OwnerPageName, который соответствует имени пользователя на странице профиля
  const profilePage = document.querySelector('.OwnerPageName');

  // Если элемент не найден, прекращаем работу скрипта
  if (!profilePage) return;

  // Если элемент уже имеет класс display_additional_information_in_vk_profile, значит информация уже добавлена, прекращаем работу
  if (profilePage.classList.contains('display_additional_information_in_vk_profile')) return;

  // Создаем объект XMLHttpRequest для отправки запроса на текущую страницу
  const xhr = new XMLHttpRequest();
  // Открываем соединение с сервером, используя метод GET и текущий URL
  xhr.open('GET', window.location.href, false);
  // Отправляем запрос на сервер
  xhr.send();

  // Извлекаем ID профиля из HTML-кода страницы, если статус ответа 200 (успешный)
  const profileId = xhr.status === 200 ? xhr.responseText.match(/"ownerId":(\d+),"/i)?.[1] : null;
  // Если ID профиля не найден, прекращаем работу
  if (!profileId) return;

  // Добавляем класс display_additional_information_in_vk_profile к элементу OwnerPageName, чтобы предотвратить повторное добавление информации
  profilePage.classList.add('display_additional_information_in_vk_profile');

  // Создаем объект XMLHttpRequest для отправки запроса на foaf.php с ID профиля
  const requestVkFoaf = new XMLHttpRequest();
  // Определяем обработчик события загрузки данных
  requestVkFoaf.onload = () => {
    // Если статус ответа 200 (успешный), обрабатываем данные VK Foaf
    if (requestVkFoaf.status === 200) {
      // Парсим полученные данные VK Foaf с помощью функции parseVkFoaf
      const data = parseVkFoaf(requestVkFoaf.responseText);
      // Рендерим дополнительную информацию на странице с помощью функции renderAdditionalInfo
      renderAdditionalInfo(profilePage, data, profileId);
    }
  };

  // Открываем соединение с сервером, используя метод GET и URL foaf.php с ID профиля
  requestVkFoaf.open('GET', `/foaf.php?id=${profileId}`, true);
  // Отправляем запрос на сервер
  requestVkFoaf.send();
});

// Функция для парсинга данных VK Foaf и извлечения даты регистрации, последнего редактирования и последнего посещения
const parseVkFoaf = (foafString) => {
  return {
    vkRegDate: foafString.match(/ya:created dc:date="(.+)"/i)?.[1],
    vkLastProfileEditDate: foafString.match(/ya:modified dc:date="(.+)"/i)?.[1],
    vkLastSeenDate: foafString.match(/ya:lastLoggedIn dc:date="(.+)"/i)?.[1],
  };
};

// Функция для форматирования даты в удобный для пользователя формат
const formatProfileDate = (profileDate) => {
  // Если дата не задана, возвращаем пустую строку
  if (!profileDate) {
    return '';
  }
  // Создаем объект Date из строки даты
  const date = new Date(profileDate);
  // Получаем названия месяцев на английском и русском языках
  const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
  const russianMonthNames = ['Января', 'Февраля', 'Марта', 'Апреля', 'Мая', 'Июня', 'Июля', 'Августа', 'Сентября', 'Октября', 'Ноября', 'Декабря'];

  // Определяем язык страницы и выбираем соответствующее название месяца
  const monthName = language === 'ru' ? russianMonthNames[date.getMonth()] : monthNames[date.getMonth()];

  // Форматируем дату в соответствии с выбранным языком
  return `${date.getDate()} ${monthName} ${date.getFullYear()} ${('0' + date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}:${('0' + date.getSeconds()).slice(-2)}`;
};

// Функция для рендеринга дополнительной информации на странице профиля
const renderAdditionalInfo = (profilePage, data, profileId) => {
  // Создаем контейнер для дополнительной информации
  const container = document.createElement('div');
  // Устанавливаем класс контейнера, чтобы его можно было стилизовать
  container.className = 'display_additional_information_in_vk_profile';

  // Если дата регистрации найдена, добавляем элемент с датой регистрации в контейнер
  if (data.vkRegDate) { container.appendChild(createInfoElement(labels.registration, formatProfileDate(new Date(data.vkRegDate)))); }
  // Если дата последнего редактирования найдена, добавляем элемент с датой последнего редактирования в контейнер
  if (data.vkLastProfileEditDate) { container.appendChild(createInfoElement(labels.lastEdit, formatProfileDate(new Date(data.vkLastProfileEditDate)))); }
  // Если дата последнего посещения найдена, добавляем элемент с датой последнего посещения в контейнер
  if (data.vkLastSeenDate) { container.appendChild(createInfoElement(labels.lastSeen, formatProfileDate(new Date(data.vkLastSeenDate)))); }
  // Добавляем элемент с ID страницы в контейнер
  container.appendChild(createInfoElement(labels.pageId, profileId));
  // Добавляем контейнер с информацией на страницу профиля
  profilePage.appendChild(container);
};

// Функция для создания элемента с информацией о профиле
const createInfoElement = (label, value) => {
  // Создаем новый элемент div
  const element = document.createElement('div');
  // Устанавливаем класс для стилизации
  element.className = 'clear_fix profile_info_row';
  // Устанавливаем размер шрифта
  element.style.fontSize = '0.75rem';
  // Устанавливаем HTML-код элемента
  element.innerHTML = `${label} ${value}`;
  // Возвращаем созданный элемент
  return element;
};

// Создаем объект с подписями для элементов
const labels = {
  registration: 'Registration:',
  lastEdit: 'Last Edit:',
  lastSeen: 'Last Seen:',
  pageId: 'Page ID:'
};

// Получаем язык страницы из атрибута lang элемента html
const language = document.documentElement.lang;

// Если язык страницы русский, меняем подписи на русский язык
if (language === 'ru') {
  labels.registration = 'Регистрация:';
  labels.lastEdit = 'Ред. стр.:';
  labels.lastSeen = 'Посл. вход:';
  labels.pageId = 'ID Страницы:';
}

// Начинаем наблюдение за изменениями в DOM-дереве
observer.observe(document.body, { childList: true, subtree: true }); // Информация о странице под именем
                                                                     // (Регистрация, Последнее редактирование страницы, Последний заход и ID)


// Функция для поддержания постоянного онлайн-статуса в VK
function stayOnline() {
  fetch('https://vk.com/al_im.php', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: 'act=a_enter&al=1',
  });
}

// Запускаем функцию stayOnline каждые 5 минут (350000 мс)
setInterval(stayOnline, 350000); // Вечный онлайн
                                 // (Работает до закрытия всех вкладок ВКонтакте)

// Объект PROXY для перехвата запросов, связанных с "печатает..."
const PROXY = {
  'vk.com': () => {
    // Переопределяем метод send объекта XMLHttpRequest
    const originalSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = new Proxy(originalSend, {
      // Перехватываем вызов метода send
      apply(target, thisArg, args) {
        const [data] = args;
        // Проверяем, содержит ли данные "typing" или "audiomessage"
        if (/typing|audiomessage/.test(data)) return null;
        // Выполняем оригинальный метод send, если нет "typing" или "audiomessage"
        return Reflect.apply(target, thisArg, args);
      },
    });
  },
  'm.vk.com': () => {
    // Переопределяем функцию fetch
    const originalFetch = window.fetch;
    window.fetch = async (input, init = {}) => {
      // Клонируем объект input
      const clonedInput = input.clone();
      // Получаем данные из формы
      const formData = await clonedInput.formData();
      // Проверяем, есть ли в данных "typing"
      const isTyping = [...formData.values()].includes('typing');
      // Возвращаем пустое Promise, если есть "typing", иначе выполняем оригинальный fetch
      return isTyping ? new Promise(() => null) : originalFetch(input, init);
    };
  },
};

// Получаем хост текущей страницы
const { host } = location;
// Создаем новый скрипт
const script = document.createElement('script');
// Устанавливаем текст скрипта, который выполняет функцию из PROXY для текущего хоста
script.textContent = `(${PROXY[host].toString()})()`;
// Добавляем скрипт в заголовок страницы
document.head.appendChild(script);

// Скрытие "***** Печатает..." у пользователей
// (Работает если перейти в мессенджер, через список чатов который справа снизу — не работает)

// Регулярные выражения для исключения некоторых ссылок
const r0 = /(\/(share|intent\/tweet|submitlink|submit)([^?]*)\?|api\.addthis\.com\/oexchange|cms\/\?url=|downloads\.sourceforge\.net|translate\.google\.)/i; // exclude
const r1 = /[?&](url|r|p|z|to|u|go|goto|q|st\.link|link|href|redirect_url)=([^&]+)(&|$)/i;
const r2 = /(\/leech_out\.php\?.:|\/phpBB2\/goto\/|\/go\/\?)(.+)/i; // Dude Smart Leech (DLE), phpBB2
const r3 = /outgoing\.prod\.mozaws\.net\/v1\/([^/]+)\/(.+)/i; // addons.mozilla.org
const r4 = /([^:]+):([^:]+)(|$)/; // Disqus FIX

// Коды для декодирования
const impCodes = '%3B%2C%2F%3F%3A%40%26%3D%2B%24%23';
// Регулярное выражение для поиска кодов
const impRegex = new RegExp((impCodes.replace(/%/g,'|%').replace('|','')), 'gi');
// Декодированные коды
const impDecoded = decodeURIComponent(impCodes);
// Функция для замены кодов на декодированные значения
const impReplacer = (ch) => impDecoded[impCodes.indexOf(ch.toUpperCase())/3];
// Функция для декодирования важных символов
const decodeImportant = (text) => text.replace(impRegex, impReplacer);

// Обработчик события mouseenter для ссылок
const Handler = (e) => {
  // Получаем ссылку
  const link = e.target;
  // Получаем URL ссылки
  let url = link.href;
  // Ищем URL, если его нет в текущем элементе
  while (!url && link !== this) {
    link = link.parentNode;
    url = link.href;
  }
  // Удаляем обработчик события
  link.removeEventListener('mouseenter', Handler);
  // Проверяем URL
  if (!url || url.length < 5 || r0.test(url)) {
    return;
  }
  // Извлекаем целевой URL из ссылки
  let tourl = ((url.match(r1) || url.match(r2) || url.match(r3) || [])[2]);
  // Проверяем целевой URL
  if (!tourl) {
    return;
  }
  try {
    // Декодируем целевой URL
    tourl = decodeURIComponent(tourl);
    tourl = window.atob(tourl);
    tourl = decodeURIComponent(tourl);
    tourl = escape(tourl);
  } catch (err) {}
  // Декодируем важные символы
  tourl = decodeImportant(tourl);
  // Извлекаем протокол и хост из целевого URL
  tourl = ((tourl.match(r4)||[])[0]);
  // Проверяем, является ли целевой URL ссылкой HTTP или HTTPS
  if (tourl && tourl.match(/^http(|s):\/\//i)) {
    // Удаляем обработчик onclick ссылки
    link.removeAttribute('onclick');
    // Устанавливаем атрибут rel ссылки на noreferrer
    link.rel = 'noreferrer';
    // Устанавливаем новый URL ссылки
    link.href = tourl;
  }
};

// Функция для добавления обработчиков событий к ссылкам
const attachEvent = (e) => {
  // Ищем все ссылки, содержащие "/ " или "?" в href
  for (const link of e.querySelectorAll('a[href*="/"], a[href*="?"]')) {
    // Добавляем обработчик события mouseenter
    link.addEventListener('mouseenter', Handler);
  }
};

// Добавляем обработчики событий к ссылкам в текущем документе
attachEvent(document.body);

// Создаем наблюдателя за изменениями DOM
new MutationObserver((ms) => {
  // Перебираем изменения DOM
  for (const m of ms) {
    // Перебираем добавленные узлы
    for (const n of m.addedNodes) {
      // Проверяем, является ли узел элементом
      if (n.nodeType === Node.ELEMENT_NODE) {
        // Если узел - ссылка, добавляем обработчик события mouseenter
        if (n.href) {
          n.addEventListener('mouseenter', Handler);
        } else {
          // Иначе добавляем обработчики событий к ссылкам в узле
          attachEvent(n);
        }
      }
    }
  }
}).observe(document, {childList: true, subtree: true});

// Прямые ссылки, отключение "Подозрительная ссылка и т.п"

// Запускаем функцию каждые 5 мс для удаления ненужных элементов
setInterval(function () {
  // Селекторы для удаления элементов
  const selectorToRemove = [
    '#profile_redesigned > div > div > div > div.vkuiPopoutRoot.vkuiSplitLayout > div > div.ScrollStickyWrapper > div > section:nth-child(2)',
    '.page_block.apps_feedRightAppsBlock.apps_feedRightAppsBlock_single_app.apps_feedRightAppsBlock_single_app--',
    '#react_rootEcosystemAccountMenuEntry > div > div > div.EcosystemAccountButton_row__iVqJo',
    '#index_rcolumn > div:nth-child(2) > div > div > div.JoinForm__connectAgreement',
    '#top_profile_menu > a.top_profile_mrow.TopProfileItem--appearance',
    '#side_bar_inner > div.LegalRecommendationsLinkLeftMenuAuthorized',
    '#index_rcolumn > div:nth-child(2) > div > div > div > div',
    '#index_rcolumn > div.LegalRecommendationsLink',
    '#side_bar_inner > div.left_menu_nav_wrap',
    '.page_block.feed_friends_recomm',
    '.feed_groups_recomm_friends',
    '#friends_right_blocks_root',
    '#groups_invites_wrap',
    '._ads_block_data_w',
    '#feed_row-23_0_0',
    '#ads_left',
  ];
  // Находим элементы, соответствующие селекторам
  const elementsToRemove = document.querySelectorAll(selectorToRemove);
  // Удаляем найденные элементы
  elementsToRemove.forEach(element => element.remove());
}, 5);

// Удаление Рекламы / Всяких кнопок и т.п

// Селектор для элемента, который нужно размыть
const blurSelector = '#top_audio_player';
// Селектор для элемента, при наведении на который нужно снять размытие
const hoverSelector = blurSelector;

// Функция для включения/выключения размытия
function toggleBlur(add) {
  document.querySelector(blurSelector).style.filter = add ? 'blur(500px)' : 'none';
}

// Обработчик события mouseover
document.addEventListener('mouseover', (e) => {
  // Получаем целевой элемент
  const target = e.target;
  // Проверяем, является ли целевой элемент или его родительский элемент элементом, на который нужно наводить курсор
  const isHovered = target.matches(hoverSelector) || target.closest(hoverSelector);

  // Включаем/выключаем размытие в зависимости от того, наведен ли курсор на целевой элемент
  toggleBlur(!isHovered);
}); // Блюр музыки

// Находим элемент, который нужно переместить
const elementToMove = document.querySelector('#index_rcolumn');
// Если элемент найден, перемещаем его
if (elementToMove) {
  // Координаты и размеры элемента
  const [pixelsToMoveX, pixelsToMoveY, elementSizeX] = [0, 33, 350];
  // Устанавливаем позиционирование элемента
  elementToMove.style.position = 'relative';
  // Устанавливаем левое смещение элемента
  elementToMove.style.left = `${pixelsToMoveX}px`;
  // Устанавливаем верхнее смещение элемента
  elementToMove.style.top = `${pixelsToMoveY}px`;
  // Устанавливаем ширину элемента
  elementToMove.style.width = `${elementSizeX}px`;
} // Изменение местоположения окна Авторизации

// Находим элемент "Сохранить вход"
const element = document.querySelector('#index_login > div > form > div.VkIdCheckbox.VkIdCheckbox--save label .VkIdCheckbox__name');
// Если элемент найден, нажимаем на него
element?.click();
// Удаляем элемент "Сохранить вход" через 25 мс
setTimeout(() => {
  const elementToRemove = document.querySelector('#index_login > div > form > div.VkIdCheckbox.VkIdCheckbox--save');
  elementToRemove?.remove();
}, 25); // Нажатие на "Сохранить вход" и удаление этого элемента
        // (Чтобы не предлагало сохранить вход при Авторизации)

// Функция для получения объекта VK
function getVK(){
  // Проверяем, доступен ли объект VK
  if (!!window.vk){
    // Устанавливаем свойство audioAdsConfig объекта VK в null, чтобы отключить аудиорекламу
    window.vk.audioAdsConfig = null;

  } else {
    // Если объект VK недоступен, запускаем функцию getVK через 100 мс
    window.setTimeout(getVK, 100);
  }
}

// Запускаем функцию getVK после загрузки страницы
(function() {
  window.addEventListener('load', function() {
    window.setTimeout(getVK, 100);
  });
})();

// Отключение аудиорекламы