4PDA Link Checker

Проверка ссылок + нарушений 3.9 (с обработкой скрытых/удалённых постов, с переходом к ним)

当前为 2025-05-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name 4PDA Link Checker
  3. // @author brant34
  4. // @namespace http://tampermonkey.net/
  5. // @version 2.6
  6. // @description Проверка ссылок + нарушений 3.9 (с обработкой скрытых/удалённых постов, с переходом к ним)
  7. // @match https://4pda.to/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @connect *
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. const style = document.createElement('style');
  18. style.textContent = `
  19. #link-checker-panel {
  20. position: fixed;
  21. top: 0;
  22. left: 0;
  23. right: 0;
  24. z-index: 9999;
  25. background: #0055A4;
  26. color: #fff;
  27. padding: 10px;
  28. font-size: 14px;
  29. border-bottom: 1px solid #004080;
  30. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  31. border-radius: 0 0 6px 6px;
  32. max-height: 200px;
  33. overflow-y: auto;
  34. transition: max-height 0.3s ease;
  35. }
  36.  
  37. #link-checker-panel.collapsed {
  38. max-height: 28px !important;
  39. overflow: hidden !important;
  40. }
  41. #link-checker-panel.collapsed #link-checker-list,
  42. #link-checker-panel.collapsed #link-checker-menu {
  43. display: none !important;
  44. }
  45.  
  46. #link-checker-menu {
  47. margin-top: 8px;
  48. display: none;
  49. }
  50. #link-checker-menu button {
  51. margin-right: 8px;
  52. margin-bottom: 4px;
  53. background: #ffffff22;
  54. color: #fff;
  55. border: 1px solid #fff;
  56. border-radius: 4px;
  57. padding: 3px 6px;
  58. cursor: pointer;
  59. }
  60. #link-checker-header {
  61. display: flex;
  62. align-items: center;
  63. gap: 10px;
  64. }
  65.  
  66. .link-checker-entry[data-keyword] {
  67. background-color: #fff3e0;
  68. border-left: 3px solid orange;
  69. padding: 2px;
  70. margin-bottom: 2px;
  71. }
  72.  
  73. .link-checker-entry a {
  74. color: #ffe;
  75. }
  76.  
  77. #ignore-domains-modal {
  78. display: none;
  79. position: fixed;
  80. top: 50%;
  81. left: 50%;
  82. transform: translate(-50%, -50%);
  83. background: #fff;
  84. padding: 20px;
  85. border-radius: 5px;
  86. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  87. z-index: 10000;
  88. color: #000;
  89. }
  90.  
  91. #ignore-domains-modal.show {
  92. display: block;
  93. }
  94.  
  95. #modal-overlay {
  96. display: none;
  97. position: fixed;
  98. top: 0;
  99. left: 0;
  100. width: 100%;
  101. height: 100%;
  102. background: rgba(0,0,0,0.5);
  103. z-index: 9999;
  104. }
  105.  
  106. #modal-overlay.show {
  107. display: block;
  108. }
  109.  
  110. #domain-list li {
  111. cursor: pointer;
  112. padding: 5px;
  113. margin-bottom: 5px;
  114. border-radius: 3px;
  115. }
  116.  
  117. #domain-list li:hover {
  118. background-color: #f0f0f0;
  119. }
  120. `;
  121. document.head.appendChild(style);
  122.  
  123. const links = document.querySelectorAll('a[href*="4pda.to/stat/go?u="]');
  124. let pendingRequests = links.length;
  125. let brokenLinksCount = 0;
  126.  
  127. const rule39Exceptions = [
  128. 'https://vk.com/4pdaru',
  129. 'http://vk.com/4pdaru',
  130. 'https://t.me/real4pda'
  131. ];
  132.  
  133. const rule39Hosts = [
  134. 'boosty.to',
  135. 'gofile.io',
  136. 'hyp.sh',
  137. 'halabtech.com',
  138. 'needrom.com',
  139. 't.me',
  140. 'tx.me',
  141. 'telegram.org',
  142. 'terabox.com',
  143. 'vk.com'
  144. ];
  145.  
  146. const skipHosts = [
  147. 'https://invisioncommunity.com',
  148. 'https://twitter.com/4pdaru',
  149. 'http://www.invisionboard.com',
  150. 'http://www.invisionpower.com'
  151. ];
  152.  
  153. const skipUrls = [
  154. 'http://twitter.com/4pdaru',
  155. 'https://twitter.com/4pdaru'
  156. ];
  157.  
  158. // Базовый список проблемных хостов
  159. const defaultKnownFalse403Hosts = [
  160. 't.me',
  161. 'tx.me',
  162. 'telegram.org',
  163. 'vk.com',
  164. 'samsung.com',
  165. 'kfhost.net',
  166. 'samsung-up.com'
  167. ];
  168.  
  169. // Получение или инициализация списка игнорируемых доменов
  170. let ignoredDomains = GM_getValue('ignoredDomains', [...defaultKnownFalse403Hosts]);
  171.  
  172. const panel = document.createElement('div');
  173. panel.id = 'link-checker-panel';
  174.  
  175. const header = document.createElement('div');
  176. header.id = 'link-checker-header';
  177.  
  178. const gear = document.createElement('span');
  179. gear.innerHTML = '⚙️';
  180. gear.style.cursor = 'pointer';
  181. gear.title = 'Меню';
  182. header.appendChild(gear);
  183.  
  184. const title = document.createElement('span');
  185. title.innerHTML = `<b>Проверка ссылок... (${pendingRequests} осталось)</b>`;
  186. title.id = 'link-checker-title';
  187. header.appendChild(title);
  188.  
  189. panel.appendChild(header);
  190.  
  191. const menu = document.createElement('div');
  192. menu.id = 'link-checker-menu';
  193. menu.innerHTML = `
  194. <button id="manual-check">🔄 Ручной поиск</button>
  195. <button id="remove-broken">🗑 Скрыть битые ссылки</button>
  196. <button id="ignore-domains">⚠️ Настроить игнорируемые домены</button>
  197. `;
  198. panel.appendChild(menu);
  199.  
  200. const container = document.createElement('div');
  201. container.id = 'link-checker-list';
  202. panel.appendChild(container);
  203.  
  204. document.body.appendChild(panel);
  205.  
  206. const panelEntries = new Map();
  207.  
  208. // Рекурсивная проверка текста
  209. function getAllTextContent(element) {
  210. let text = '';
  211. const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
  212. let node;
  213. while (node = walker.nextNode()) {
  214. text += node.textContent.trim();
  215. }
  216. return text;
  217. }
  218.  
  219. // Проверка видимости поста
  220. function isPostVisible(post) {
  221. if (!post) return false;
  222. const table = post.closest('table[data-post]');
  223. const isInDOM = document.contains(post);
  224. const hasOffsetParent = post.offsetParent !== null;
  225. const style = table ? window.getComputedStyle(table) : window.getComputedStyle(post);
  226. const isHiddenByClass = table && (table.classList.contains('hidepin') || table.classList.contains('deletedpost'));
  227. const isHiddenByStyle = style.display === 'none' || style.visibility === 'hidden';
  228. const content = getAllTextContent(post);
  229. const isHiddenByText = content.includes('[HIDE]');
  230. const isDeletedByText = content.includes('[DELETE]');
  231. console.log(`Проверка видимости: ${post.id}, inDOM: ${isInDOM}, offsetParent: ${hasOffsetParent}, hiddenByClass: ${isHiddenByClass}, hiddenByStyle: ${isHiddenByStyle}, hiddenByText: ${isHiddenByText}, deletedByText: ${isDeletedByText}, content: "${content}"`);
  232. return isInDOM && hasOffsetParent && !isHiddenByClass && !isHiddenByStyle && !isHiddenByText && !isDeletedByText;
  233. }
  234.  
  235. // Поиск слов VPN, ВПН, КВН
  236. const keywordRegex = /(^|[^а-яa-z0-9])(VPN|ВПН|КВН)(?![а-яa-z0-9])/gi;
  237. const posts = document.querySelectorAll('.post_body');
  238.  
  239. for (const post of posts) {
  240. const postContainer = post.closest('div.postcolor[id^="post-"]');
  241. if (!postContainer) continue;
  242. if (keywordRegex.test(post.textContent)) {
  243. const matched = post.textContent.match(keywordRegex).join(', ');
  244. const postId = postContainer.id;
  245. const isVisible = isPostVisible(postContainer);
  246. const entry = document.createElement('div');
  247. entry.className = 'link-checker-entry';
  248. entry.setAttribute('data-keyword', 'true');
  249. entry.setAttribute('data-post-id', postId);
  250. const entryHTML = `🟠 Найдено слово: <b>${matched}</b> (пост: ${postId})`;
  251. entry.setAttribute('data-original-html', entryHTML);
  252. entry.innerHTML = isVisible ? entryHTML : `${entryHTML} <i>(скрыт/удалён)</i>`;
  253. entry.style.cursor = 'pointer';
  254. entry.onclick = () => handleEntryClick(postContainer, postId);
  255. document.getElementById('link-checker-list').appendChild(entry);
  256. panelEntries.set(postId + '-keyword', { entry, postId });
  257. if (isVisible) {
  258. post.style.backgroundColor = '#fff3e0';
  259. post.style.border = '2px solid orange';
  260. }
  261. }
  262. }
  263.  
  264. // Проверка всех ссылок на нарушение правила 3.9
  265. const allLinks = document.querySelectorAll('a[href]');
  266. for (const a of allLinks) {
  267. const href = a.href;
  268. try {
  269. const url = new URL(href);
  270. if (rule39Hosts.some(host => url.hostname.includes(host))) {
  271. const linkText = a.innerText || 'Без текста';
  272. addRule39Link(href, linkText, a);
  273. }
  274. } catch (e) {
  275. // ignore malformed URLs
  276. }
  277. }
  278.  
  279. gear.addEventListener('click', () => {
  280. menu.style.display = (menu.style.display === 'block') ? 'none' : 'block';
  281. });
  282.  
  283. document.getElementById('manual-check')?.addEventListener('click', () => location.reload());
  284.  
  285. document.getElementById('remove-broken')?.addEventListener('click', () => {
  286. document.querySelectorAll('[data-broken-link="true"]').forEach(e => e.remove());
  287. container.innerHTML = '';
  288. title.innerHTML = `<b>🛠 Битые ссылки скрыты.</b>`;
  289. menu.style.display = 'none';
  290. });
  291.  
  292. // Модальное окно для настройки доменов
  293. const modalOverlay = document.createElement('div');
  294. modalOverlay.id = 'modal-overlay';
  295. document.body.appendChild(modalOverlay);
  296.  
  297. const modal = document.createElement('div');
  298. modal.id = 'ignore-domains-modal';
  299. modal.innerHTML = `
  300. <h3>Игнорируемые домены</h3>
  301. <p>Введите домен (например, example.com) и нажмите "Добавить". Кликните на домен, чтобы удалить его.</p>
  302. <input type="text" id="domain-input" placeholder="Домен" style="width: 200px; padding: 5px; margin-right: 10px;">
  303. <button id="add-domain">Добавить</button>
  304. <ul id="domain-list" style="list-style-type: none; padding: 0; margin-top: 10px;"></ul>
  305. <button id="close-modal" style="margin-top: 10px;">Закрыть</button>
  306. `;
  307. document.body.appendChild(modal);
  308.  
  309. function updateDomainList() {
  310. const list = document.getElementById('domain-list');
  311. list.innerHTML = '';
  312. ignoredDomains.forEach(domain => {
  313. const li = document.createElement('li');
  314. li.textContent = domain;
  315. li.addEventListener('click', () => {
  316. const index = ignoredDomains.indexOf(domain);
  317. if (index !== -1) {
  318. ignoredDomains.splice(index, 1);
  319. updateDomainList();
  320. showNotification(`Домен ${domain} удалён из списка игнорируемых.`);
  321. }
  322. });
  323. list.appendChild(li);
  324. });
  325. GM_setValue('ignoredDomains', ignoredDomains);
  326. }
  327.  
  328. document.getElementById('ignore-domains')?.addEventListener('click', () => {
  329. modal.classList.add('show');
  330. modalOverlay.classList.add('show');
  331. updateDomainList();
  332. });
  333.  
  334. document.getElementById('close-modal')?.addEventListener('click', () => {
  335. modal.classList.remove('show');
  336. modalOverlay.classList.remove('show');
  337. });
  338.  
  339. document.getElementById('add-domain')?.addEventListener('click', () => {
  340. const input = document.getElementById('domain-input');
  341. const domain = input.value.trim();
  342. if (domain && !ignoredDomains.includes(domain)) {
  343. ignoredDomains.push(domain);
  344. updateDomainList();
  345. showNotification(`Домен ${domain} добавлен в список игнорируемых.`);
  346. }
  347. input.value = '';
  348. });
  349.  
  350. modalOverlay.addEventListener('click', () => {
  351. modal.classList.remove('show');
  352. modalOverlay.classList.remove('show');
  353. });
  354.  
  355. function showNotification(message) {
  356. const notification = document.createElement('div');
  357. notification.style.cssText = 'position: fixed; top: 10px; right: 10px; background: #ff4444; color: white; padding: 10px; border-radius: 4px; z-index: 10000;';
  358. notification.textContent = message;
  359. document.body.appendChild(notification);
  360. setTimeout(() => notification.remove(), 3000);
  361. }
  362.  
  363. function addRule39Link(href, linkText, originalLink) {
  364. if (rule39Exceptions.includes(href)) return;
  365.  
  366. const post = originalLink.closest('div.postcolor[id^="post-"]');
  367. const postId = post ? post.id : 'unknown';
  368. const isVisible = post ? isPostVisible(post) : false;
  369.  
  370. const entry = document.createElement('div');
  371. entry.className = 'link-checker-entry';
  372. entry.setAttribute('data-post-id', postId);
  373. const entryHTML = `🚫 Нарушение п. 3.9: <a href="${href}" target="_blank">${href}</a> (текст: ${linkText}, пост: ${postId})`;
  374. entry.setAttribute('data-original-html', entryHTML);
  375. entry.innerHTML = isVisible ? entryHTML : `${entryHTML} <i>(скрыт/удалён)</i>`;
  376. entry.style.cursor = 'pointer';
  377. entry.onclick = () => handleEntryClick(post, postId);
  378. document.getElementById('link-checker-list').appendChild(entry);
  379. panelEntries.set(postId + '-rule39-' + href, { entry, postId });
  380.  
  381. if (isVisible) {
  382. originalLink.style.border = '2px solid green';
  383. originalLink.style.backgroundColor = '#e8f5e9';
  384. originalLink.style.padding = '2px';
  385. originalLink.title = 'Нарушение правила 3.9 (запрещённый ресурс)';
  386. originalLink.setAttribute('data-rule-39', 'true');
  387. }
  388. }
  389.  
  390. function addBrokenLink(realUrl, status, linkText, originalLink) {
  391. brokenLinksCount++;
  392. const post = originalLink.closest('div.postcolor[id^="post-"]');
  393. const postId = post ? post.id : 'unknown';
  394. const isVisible = post ? isPostVisible(post) : false;
  395.  
  396. const entry = document.createElement('div');
  397. entry.className = 'link-checker-entry';
  398. entry.setAttribute('data-post-id', postId);
  399. const entryHTML = `❌ <a href="${realUrl}" target="_blank">${realUrl}</a> (статус: ${status}, текст: ${linkText}, пост: ${postId})`;
  400. entry.setAttribute('data-original-html', entryHTML);
  401. entry.innerHTML = isVisible ? entryHTML : `${entryHTML} <i>(скрыт/удалён)</i>`;
  402. entry.style.cursor = 'pointer';
  403. entry.onclick = () => handleEntryClick(post, postId);
  404. document.getElementById('link-checker-list').appendChild(entry);
  405. panelEntries.set(postId + '-broken-' + realUrl, { entry, postId });
  406.  
  407. if (isVisible) {
  408. originalLink.style.border = '2px solid red';
  409. originalLink.style.backgroundColor = '#ffebee';
  410. originalLink.style.padding = '2px';
  411. originalLink.title = 'Битая ссылка (статус: ' + status + ')';
  412. originalLink.setAttribute('data-broken-link', 'true');
  413. }
  414. }
  415.  
  416. function handleEntryClick(post, postId) {
  417. if (!post) {
  418. showNotification(`Пост ${postId} не найден в DOM.`);
  419. return;
  420. }
  421.  
  422. const table = post.closest('table[data-post]');
  423. const isInDOM = document.contains(post);
  424. const hasOffsetParent = post.offsetParent !== null;
  425. const style = table ? window.getComputedStyle(table) : window.getComputedStyle(post);
  426. const isHiddenByClass = table && (table.classList.contains('hidepin') || table.classList.contains('deletedpost'));
  427. const isHiddenByStyle = style.display === 'none' || style.visibility === 'hidden';
  428. const content = getAllTextContent(post);
  429. const isHiddenByText = content.includes('[HIDE]');
  430. const isDeletedByText = content.includes('[DELETE]');
  431.  
  432. // Прокрутка непосредственно к целевому посту
  433. post.scrollIntoView({ behavior: 'smooth', block: 'center' });
  434.  
  435. // Уведомление о статусе поста
  436. if (!isInDOM) {
  437. showNotification(`Пост ${postId} отсутствует в DOM.`);
  438. } else if (!hasOffsetParent) {
  439. showNotification(`Пост ${postId} не отображается (нет offsetParent).`);
  440. } else if (isHiddenByClass) {
  441. showNotification(`Пост ${postId} скрыт/удалён (класс ${table.classList.contains('hidepin') ? 'hidepin' : 'deletedpost'}).`);
  442. } else if (isHiddenByStyle) {
  443. showNotification(`Пост ${postId} скрыт стилями (display: ${style.display}, visibility: ${style.visibility}).`);
  444. } else if (isHiddenByText) {
  445. showNotification(`Пост ${postId} скрыт текстом [HIDE].`);
  446. } else if (isDeletedByText) {
  447. showNotification(`Пост ${postId} удалён текстом [DELETE].`);
  448. } else {
  449. showNotification(`Пост ${postId} отображен.`);
  450. }
  451. }
  452.  
  453. function updatePanel() {
  454. title.innerHTML = `<b>Проверка ссылок... (${pendingRequests} осталось)</b>`;
  455. if (pendingRequests === 0) {
  456. title.innerHTML = `<b>Все ссылки проверены. Найдено ${brokenLinksCount} битых ссылок.</b>`;
  457. }
  458. }
  459.  
  460. function checkLink(realUrl, linkText, originalLink, method = 'HEAD', attempt = 1) {
  461. const urlObj = new URL(realUrl);
  462. const isIgnoredHost = ignoredDomains.some(host => urlObj.hostname.includes(host));
  463.  
  464. GM_xmlhttpRequest({
  465. method: method,
  466. url: realUrl,
  467. headers: {
  468. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
  469. },
  470. timeout: 10000,
  471. onload: function (response) {
  472. console.log(`Ссылка: ${realUrl}, Метод: ${method}, Статус: ${response.status}, Final URL: ${response.finalUrl || 'N/A'}, Response Headers: ${response.responseHeaders || 'N/A'}`);
  473.  
  474. // Обработка редиректов
  475. if ([301, 302, 303, 307, 308].includes(response.status) && response.finalUrl && attempt < 3) {
  476. console.log(`Редирект: ${realUrl} -> ${response.finalUrl}`);
  477. checkLink(response.finalUrl, linkText, originalLink, method, attempt + 1);
  478. return;
  479. }
  480.  
  481. // Пропускаем 403 для игнорируемых хостов
  482. if (response.status === 403 && isIgnoredHost) {
  483. console.log(`Игнорируем 403 для ${realUrl} (игнорируемый хост)`);
  484. pendingRequests--;
  485. updatePanel();
  486. return;
  487. }
  488.  
  489. // Пробуем GET для проблемных хостов при 403
  490. if (response.status === 403 && !isIgnoredHost && method === 'HEAD' && attempt < 3) {
  491. console.log(`Статус 403 для проблемного хоста ${urlObj.hostname}, пробуем GET`);
  492. checkLink(realUrl, linkText, originalLink, 'GET', attempt + 1);
  493. return;
  494. }
  495.  
  496. // Проверяем другие коды ошибок
  497. if ([404, 410].includes(response.status)) {
  498. addBrokenLink(realUrl, response.status, linkText, originalLink);
  499. } else if (response.status >= 200 && response.status < 300) {
  500. console.log(`Ссылка ${realUrl} рабочая (статус: ${response.status})`);
  501. } else {
  502. console.warn(`Неопределённый статус для ${realUrl}: ${response.status}`);
  503. }
  504.  
  505. pendingRequests--;
  506. updatePanel();
  507. },
  508. onerror: function () {
  509. console.log(`Ошибка проверки ссылки: ${realUrl}`);
  510. addBrokenLink(realUrl, 'Ошибка', linkText, originalLink);
  511. pendingRequests--;
  512. updatePanel();
  513. },
  514. ontimeout: function () {
  515. console.log(`Таймаут проверки ссылки: ${realUrl}`);
  516. if (!isIgnoredHost && method === 'HEAD' && attempt < 3) {
  517. console.log(`Таймаут для проблемного хоста ${urlObj.hostname}, пробуем GET`);
  518. checkLink(realUrl, linkText, originalLink, 'GET', attempt + 1);
  519. } else {
  520. addBrokenLink(realUrl, 'Таймаут', linkText, originalLink);
  521. pendingRequests--;
  522. updatePanel();
  523. }
  524. }
  525. });
  526. }
  527.  
  528. links.forEach((a, i) => {
  529. const urlParams = new URLSearchParams(a.href.split('?')[1]);
  530. const realUrl = decodeURIComponent(urlParams.get('u') || '');
  531. const linkText = a.innerText || 'Без текста';
  532.  
  533. if (!realUrl) {
  534. console.warn('Пустой URL найден для ссылки:', a.href);
  535. pendingRequests--;
  536. updatePanel();
  537. return;
  538. }
  539.  
  540. if (skipUrls.includes(realUrl) || skipHosts.some(host => realUrl.startsWith(host))) {
  541. console.log(`⏩ Пропущена ссылка: ${realUrl}`);
  542. pendingRequests--;
  543. updatePanel();
  544. return;
  545. }
  546.  
  547. if (rule39Hosts.some(host => realUrl.includes(host))) {
  548. addRule39Link(realUrl, linkText, a);
  549. }
  550.  
  551. setTimeout(() => {
  552. checkLink(realUrl, linkText, a);
  553. }, i * 100);
  554. });
  555.  
  556. if (links.length === 0) {
  557. title.innerHTML = `<b>Ссылок для проверки не найдено.</b>`;
  558. }
  559.  
  560. function getVisibleText(element) {
  561. const clone = element.cloneNode(true);
  562. clone.querySelectorAll('script, style').forEach(el => el.remove());
  563. let text = '';
  564. const walker = document.createTreeWalker(clone, NodeFilter.SHOW_TEXT, null, false);
  565. while (walker.nextNode()) {
  566. text += walker.currentNode.nodeValue + ' ';
  567. }
  568. const links = clone.querySelectorAll('a[href]');
  569. links.forEach(link => {
  570. const href = link.getAttribute('href');
  571. if (href) text += ' ' + href;
  572. const linkText = link.textContent.trim();
  573. if (linkText) text += ' ' + linkText;
  574. });
  575. text = text.replace(/\[.\]/g, '.').replace(/[-_]/g, '.');
  576. return text.replace(/[\n\r\s]+/g, ' ').toLowerCase().trim();
  577. }
  578.  
  579. window.addEventListener('load', () => {
  580. setTimeout(() => {
  581. const posts = document.querySelectorAll('div.postcolor[id^="post-"]');
  582. for (const post of posts) {
  583. const postId = post.id;
  584. const text = getVisibleText(post);
  585. console.log("VPN-тест: пост", post.id, "| текст:", text);
  586. if (text.includes('vpn') || text.includes('квн') || text.includes('впн')) {
  587. const isVisible = isPostVisible(post);
  588. const entry = document.createElement('div');
  589. entry.className = 'link-checker-entry';
  590. entry.setAttribute('data-post-id', postId);
  591. const entryHTML = `⚠️ Обсуждение VPN (пост: ${postId})`;
  592. entry.setAttribute('data-original-html', entryHTML);
  593. entry.innerHTML = isVisible ? entryHTML : `${entryHTML} <i>(скрыт/удалён)</i>`;
  594. entry.style.cursor = 'pointer';
  595. entry.onclick = () => handleEntryClick(post, postId);
  596. document.getElementById('link-checker-list').appendChild(entry);
  597. panelEntries.set(postId + '-vpn', { entry, postId });
  598. if (isVisible) {
  599. post.style.backgroundColor = '#fff3e0';
  600. post.style.border = '2px solid orange';
  601. post.title = 'Обсуждение VPN';
  602. post.setAttribute('data-rule-39', 'true');
  603. }
  604. }
  605. }
  606. }, 1000);
  607. });
  608.  
  609. const postContainer = document.querySelector('.ipsForum_topic') || document.body;
  610. const observer = new MutationObserver((mutations) => {
  611. let needsUpdate = false;
  612. for (const mutation of mutations) {
  613. if (mutation.type === 'childList') {
  614. for (const node of mutation.removedNodes) {
  615. const post = node.querySelector('div.postcolor[id^="post-"]') || node.closest('div.postcolor[id^="post-"]');
  616. if (post) {
  617. needsUpdate = true;
  618. console.log(`Обнаружено удаление поста: ${post.id}`);
  619. break;
  620. }
  621. }
  622. for (const node of mutation.addedNodes) {
  623. const post = node.querySelector('div.postcolor[id^="post-"]') || node.closest('div.postcolor[id^="post-"]');
  624. if (post) {
  625. needsUpdate = true;
  626. console.log(`Обнаружено добавление поста: ${post.id}`);
  627. }
  628. }
  629. } else if (mutation.type === 'characterData') {
  630. const post = mutation.target.parentNode.closest('div.postcolor[id^="post-"]');
  631. if (post) {
  632. needsUpdate = true;
  633. console.log(`Изменение текста в посте: ${post.id}`);
  634. }
  635. } else if (mutation.type === 'attributes') {
  636. const post = mutation.target.closest('div.postcolor[id^="post-"]');
  637. if (post || mutation.target.classList.contains('hidepin') || mutation.target.classList.contains('deletedpost')) {
  638. needsUpdate = true;
  639. console.log(`Изменение атрибутов или классов (hidepin/deletedpost): ${post ? post.id : 'неизвестно'}`);
  640. }
  641. }
  642. }
  643. if (needsUpdate) {
  644. setTimeout(() => {
  645. panelEntries.forEach(({ entry, postId }) => {
  646. const post = document.querySelector(`div.postcolor[id="${postId}"]`);
  647. const isVisible = post ? isPostVisible(post) : false;
  648. const originalHTML = entry.getAttribute('data-original-html');
  649. entry.innerHTML = isVisible ? originalHTML : `${originalHTML} <i>(скрыт/удалён)</i>`;
  650. console.log(`Обновление статуса поста: ${postId}, visible: ${isVisible}`);
  651. });
  652. }, 500);
  653. }
  654. });
  655.  
  656. observer.observe(postContainer, {
  657. childList: true,
  658. subtree: true,
  659. attributes: true,
  660. attributeFilter: ['style', 'class', 'hidden'],
  661. characterData: true
  662. });
  663.  
  664. setInterval(() => {
  665. let needsUpdate = false;
  666. panelEntries.forEach(({ entry, postId }) => {
  667. const post = document.querySelector(`div.postcolor[id="${postId}"]`);
  668. const isVisible = post ? isPostVisible(post) : false;
  669. const originalHTML = entry.getAttribute('data-original-html');
  670. const currentHTML = entry.innerHTML;
  671. const expectedHTML = isVisible ? originalHTML : `${originalHTML} <i>(скрыт/удалён)</i>`;
  672. if (currentHTML !== expectedHTML) {
  673. entry.innerHTML = expectedHTML;
  674. console.log(`Периодическое обновление статуса поста: ${postId}, visible: ${isVisible}`);
  675. needsUpdate = true;
  676. }
  677. });
  678. if (needsUpdate) {
  679. console.log('Периодическая проверка: обновлены статусы постов');
  680. }
  681. }, 2000);
  682. })();