LinkedIn Cleaner

Remove all ads, promoted posts, suggested people, and sidebar noise from LinkedIn. Requires a valid license key.

Преди да инсталирате, Greasy Fork иска да ви уведоми, че този скрипт съдържа антиособености, които са в полза на автора, а не за вас.

Скриптът ще е напълно функционален само след като извършите плащане. Greasy Fork не е обвързан с плащането, така че няма как да се провери дали ще получите нещо полезно или да ви помогне да си върнете парите.. Обяснение от автора на скрипта: Requires a paid license key ($3.99 AUD/month or $29.99 AUD/year) from linkedinfilter.com. A free trial is not available.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

Advertisement:

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

Advertisement:

// ==UserScript==
// @name         LinkedIn Cleaner
// @namespace    https://linkedinfilter.com
// @version      2.1.0
// @description  Remove all ads, promoted posts, suggested people, and sidebar noise from LinkedIn. Requires a valid license key.
// @author       LinkedIn Cleaner
// @homepageURL  https://linkedinfilter.com
// @supportURL   https://linkedinfilter.com
// @license      Proprietary
// @antifeature  payment Requires a paid license key ($3.99 AUD/month or $29.99 AUD/year) from linkedinfilter.com. A free trial is not available.
// @match        *://*.linkedin.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @connect      linkedincleaner.replit.app
// @connect      linkedinfilter.com
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const VERIFY_URL = 'https://linkedincleaner.replit.app/api/licenses/verify';

  /* ----------------------------------------------------------
     LICENSE KEY STORAGE
     Uses GM_getValue/GM_setValue if available (Tampermonkey),
     falls back to localStorage for Safari Userscripts.
     ---------------------------------------------------------- */
  const KEY_STORAGE_KEY    = 'linkedin_cleaner_license_key';
  const LAST_CHECK_KEY     = 'linkedin_cleaner_last_check';
  const LAST_VALID_KEY     = 'linkedin_cleaner_last_valid';
  const DEVICE_ID_KEY      = 'linkedin_cleaner_device_id';

  function getStoredKey() {
    try { return GM_getValue(KEY_STORAGE_KEY, null); } catch (_) {}
    try { return localStorage.getItem(KEY_STORAGE_KEY); } catch (_) {}
    return null;
  }

  function setStoredKey(key) {
    try { GM_setValue(KEY_STORAGE_KEY, key); return; } catch (_) {}
    try { localStorage.setItem(KEY_STORAGE_KEY, key); } catch (_) {}
  }

  function deleteStoredKey() {
    try { GM_deleteValue(KEY_STORAGE_KEY); } catch (_) {}
    try { GM_deleteValue(LAST_CHECK_KEY); } catch (_) {}
    try { GM_deleteValue(LAST_VALID_KEY); } catch (_) {}
    try { localStorage.removeItem(KEY_STORAGE_KEY); } catch (_) {}
    // Note: device ID is intentionally kept so the same device can re-register after a new key is entered
  }

  function getOrCreateDeviceId() {
    let id;
    try { id = GM_getValue(DEVICE_ID_KEY, null); } catch (_) {}
    if (!id) { try { id = localStorage.getItem(DEVICE_ID_KEY); } catch (_) {} }
    if (!id) {
      id = crypto.randomUUID();
      try { GM_setValue(DEVICE_ID_KEY, id); } catch (_) {}
      try { localStorage.setItem(DEVICE_ID_KEY, id); } catch (_) {}
    }
    return id;
  }

  function getLastCheck() {
    try { return Number(GM_getValue(LAST_CHECK_KEY, 0)); } catch (_) {}
    try { return Number(localStorage.getItem(LAST_CHECK_KEY) || 0); } catch (_) {}
    return 0;
  }

  function setLastCheck(ts, valid) {
    try { GM_setValue(LAST_CHECK_KEY, String(ts)); GM_setValue(LAST_VALID_KEY, valid ? '1' : '0'); return; } catch (_) {}
    try { localStorage.setItem(LAST_CHECK_KEY, String(ts)); localStorage.setItem(LAST_VALID_KEY, valid ? '1' : '0'); } catch (_) {}
  }

  function getLastValid() {
    try { return GM_getValue(LAST_VALID_KEY, '0') === '1'; } catch (_) {}
    try { return localStorage.getItem(LAST_VALID_KEY) === '1'; } catch (_) {}
    return false;
  }

  /* ----------------------------------------------------------
     VERIFY LICENSE KEY AGAINST THE SERVER
     Verified once on first entry, then cached indefinitely.
     The user can call linkedinCleanerSetKey() to reset and re-verify.
     ---------------------------------------------------------- */
  const CHECK_INTERVAL_MS = Infinity; // verify once, trust forever

  function verifyKey(key, deviceId) {
    const body = JSON.stringify({ key, deviceId, platform: 'userscript' });
    return new Promise((resolve) => {
      const done = (valid, status) => resolve({ valid, status });

      // Use GM_xmlhttpRequest if available (bypasses CORS in Tampermonkey)
      if (typeof GM_xmlhttpRequest !== 'undefined') {
        GM_xmlhttpRequest({
          method: 'POST',
          url: VERIFY_URL,
          headers: { 'Content-Type': 'application/json' },
          data: body,
          timeout: 8000,
          onload(r) {
            try {
              const data = JSON.parse(r.responseText);
              done(data.valid === true, data.status);
            } catch (_) { done(false, 'error'); }
          },
          onerror() { done(false, 'network_error'); },
          ontimeout() { done(false, 'network_error'); },
        });
        return;
      }

      // Fallback: native fetch (works in Safari Userscripts)
      fetch(VERIFY_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body,
        signal: AbortSignal.timeout(8000),
      })
        .then((r) => r.json())
        .then((data) => done(data.valid === true, data.status))
        .catch(() => done(false, 'network_error'));
    });
  }

  /* ----------------------------------------------------------
     PROMPT FOR LICENSE KEY (shown when key is missing or invalid)
     ---------------------------------------------------------- */
  function promptForKey(message) {
    return new Promise((resolve) => {
      const overlay = document.createElement('div');
      overlay.id = 'lc-overlay';
      overlay.style.cssText = `
        position:fixed;top:0;left:0;width:100%;height:100%;z-index:999999;
        background:rgba(0,0,0,.55);display:flex;align-items:center;
        justify-content:center;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
      `;

      overlay.innerHTML = `
        <div style="background:#fff;border-radius:14px;padding:32px 28px;max-width:420px;width:90%;box-shadow:0 8px 40px rgba(0,0,0,.25)">
          <div style="font-size:20px;font-weight:700;color:#1a1a1a;margin-bottom:6px">LinkedIn Cleaner</div>
          <div style="font-size:13px;color:${message ? '#c0392b' : '#555'};margin-bottom:20px;line-height:1.5">
            ${message || 'Enter your license key to activate ad and suggestion removal.'}
          </div>
          <input id="lc-key-input" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
            style="width:100%;padding:10px 12px;border:2px solid #ddd;border-radius:8px;font-size:13px;font-family:monospace;margin-bottom:16px;outline:none"
          />
          <div style="display:flex;gap:10px">
            <button id="lc-submit" style="flex:1;background:#0a66c2;color:#fff;border:none;border-radius:8px;padding:11px;font-size:14px;font-weight:600;cursor:pointer">
              Activate
            </button>
            <button id="lc-cancel" style="padding:11px 16px;background:#f0f0f0;color:#444;border:none;border-radius:8px;font-size:14px;cursor:pointer">
              Cancel
            </button>
          </div>
          <div style="margin-top:14px;font-size:11px;color:#999;text-align:center">
            No key yet? <a href="#" id="lc-buy" style="color:#0a66c2">Get a subscription</a>
          </div>
        </div>
      `;

      document.body.appendChild(overlay);
      const input = overlay.querySelector('#lc-key-input');
      input.focus();

      overlay.querySelector('#lc-submit').addEventListener('click', () => {
        const val = input.value.trim();
        overlay.remove();
        resolve(val || null);
      });

      overlay.querySelector('#lc-cancel').addEventListener('click', () => {
        overlay.remove();
        resolve(null);
      });

      overlay.querySelector('#lc-buy').addEventListener('click', (e) => {
        e.preventDefault();
        window.open('https://1cffa241-a031-4b1a-ac9a-07a7a32ed051-00-1la2kydxl1j2a.spock.replit.dev/', '_blank');
      });

      input.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') overlay.querySelector('#lc-submit').click();
        if (e.key === 'Escape') overlay.querySelector('#lc-cancel').click();
      });
    });
  }

  /* ----------------------------------------------------------
     SMALL STATUS BANNER (shown briefly after activation)
     ---------------------------------------------------------- */
  function showBanner(message, type) {
    const existing = document.getElementById('lc-banner');
    if (existing) existing.remove();

    const banner = document.createElement('div');
    banner.id = 'lc-banner';
    const bg = type === 'success' ? '#2e7d32' : type === 'error' ? '#c0392b' : '#0a66c2';
    banner.style.cssText = `
      position:fixed;bottom:24px;right:24px;z-index:999999;
      background:${bg};color:#fff;padding:12px 18px;
      border-radius:10px;font-size:13px;font-weight:500;
      font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
      box-shadow:0 4px 16px rgba(0,0,0,.25);
      animation:lc-slide-in .25s ease;
    `;
    banner.textContent = message;
    document.body.appendChild(banner);
    setTimeout(() => banner?.remove(), 3500);
  }

  /* ----------------------------------------------------------
     LINKEDIN AD / SUGGESTION REMOVAL
     ---------------------------------------------------------- */

  const STATIC_SELECTORS = [
    '[data-ad-banner]', '.ad-banner-container', '.ad-banner', '.ad-slot', '.ads-container',
    'section[aria-label="Add to your feed"]', 'section[aria-label="Grow your network"]',
    'section[aria-label*="Learning"]', 'section[aria-label*="Pages you might like"]',
    'section[aria-label*="Discover more"]', 'section[aria-label*="Newsletter"]',
    'section[aria-label*="Events you might like"]',
    'section[aria-label*="Jobs you might be interested in"]',
    '.premium-upsell-link', '.premium-upsell-module', '[data-test-premium-module]',
    'section[aria-label*="Premium"]', '.feed-follows-module',
    '[data-finite-scroll-hotspot="pymk"]', '.discovery-modules',
    /* ---- Messaging sponsored / InMail ads ---- */
    '.msg-sponsored-card', '.msg-overlay-bubble-item--sponsored',
    '.msg-conversation-listitem--sponsored', '.msg-s-message-list__event--sponsored',
    '[data-control-name="sponsored_message"]',
  ];

  const PROMOTED_PATTERNS = [/^promoted$/i, /^sponsored$/i, /^promoted\s*·/i, /^sponsored\s*·/i];

  const SUGGESTION_LABEL_PATTERNS = [
    /people you may know/i, /add to your feed/i, /grow your network/i,
    /pages you might like/i, /events you might like/i, /learning/i, /newsletter/i,
    /jobs you might be interested/i, /premium/i, /suggested/i, /discover more/i,
    /you might also like/i,
  ];

  function hide(el) {
    if (el && el instanceof HTMLElement && el.style.display !== 'none')
      el.style.setProperty('display', 'none', 'important');
  }

  function hideFeedItem(el) {
    let node = el;
    while (node && node !== document.body) {
      if (
        (node.tagName === 'LI' && node.classList.contains('occludable-update')) ||
        node.classList.contains('feed-shared-update-v2') ||
        (node.tagName === 'LI' && node.dataset.id)
      ) { hide(node); return; }
      node = node.parentElement;
    }
    hide(el);
  }

  function cleanLinkedIn() {
    STATIC_SELECTORS.forEach((s) => { try { document.querySelectorAll(s).forEach(hide); } catch (_) {} });

    document.querySelectorAll(
      '.update-components-actor__sub-description span[aria-hidden="true"],' +
      '.feed-shared-actor__sub-description span[aria-hidden="true"],' +
      '.update-components-actor__description span[aria-hidden="true"]'
    ).forEach((span) => {
      if (PROMOTED_PATTERNS.some((re) => re.test(span.textContent.trim()))) hideFeedItem(span);
    });
    document.querySelectorAll(
      '.feed-shared-actor__description--promoted,[data-control-name="promoted_entity"]'
    ).forEach((el) => hideFeedItem(el));

    document.querySelectorAll('section[aria-label],div[aria-label]').forEach((el) => {
      const label = el.getAttribute('aria-label') || '';
      if (SUGGESTION_LABEL_PATTERNS.some((re) => re.test(label))) hide(el);
    });

    document.querySelectorAll(
      'li.occludable-update,.scaffold-finite-scroll__content > div > ul > li'
    ).forEach((item) => {
      if (
        item.querySelector('.discover-entity-type-pill') ||
        item.querySelector('[data-finite-scroll-hotspot="pymk"]') ||
        item.querySelector('.feed-follows-module')
      ) { hide(item); return; }
      const label = item.getAttribute('aria-label') || '';
      if (SUGGESTION_LABEL_PATTERNS.some((re) => re.test(label))) hide(item);
    });

    document.querySelectorAll(
      '.scaffold-layout__aside .artdeco-card,aside .artdeco-card'
    ).forEach((card) => {
      const heading = card.querySelector('h2,h3,[aria-label]');
      if (!heading) return;
      const text = (heading.textContent || heading.getAttribute('aria-label') || '').trim();
      if (SUGGESTION_LABEL_PATTERNS.some((re) => re.test(text))) hide(card);
    });

    // Messaging sponsored items — conversation list + message thread
    ['.msg-conversation-listitem', '.msg-selectable-entity',
     '.msg-overlay-list-bubble-item', '.msg-s-message-list__event'].forEach((sel) => {
      try {
        document.querySelectorAll(sel).forEach((item) => {
          const sponsored = Array.from(item.querySelectorAll('span,p,small,strong')).some(
            (el) => el.children.length === 0 && /^sponsored$/i.test((el.textContent || '').trim()),
          );
          if (sponsored) hide(item);
        });
      } catch (_) {}
    });
  }

  let debounceTimer = null;
  const observer = new MutationObserver(() => {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(cleanLinkedIn, 150);
  });

  function startCleaning() {
    observer.observe(document.body, { childList: true, subtree: true });
    cleanLinkedIn();
    setTimeout(cleanLinkedIn, 1000);
    setTimeout(cleanLinkedIn, 3000);
  }

  /* ----------------------------------------------------------
     MAIN — LICENSE CHECK FLOW
     ---------------------------------------------------------- */

  async function main() {
    let key = getStoredKey();
    const deviceId = getOrCreateDeviceId();

    if (!key) {
      key = await promptForKey(null);
      if (!key) return;
      setStoredKey(key);
    }

    const now = Date.now();
    const lastCheck = getLastCheck();
    const needsCheck = now - lastCheck > CHECK_INTERVAL_MS;

    let valid;

    if (!needsCheck) {
      valid = getLastValid();
    } else {
      const result = await verifyKey(key, deviceId);
      valid = result.valid;
      setLastCheck(now, valid);

      if (!valid && result.status === 'device_limit_reached') {
        showBanner(
          'This key is already activated on 2 devices. Purchase an additional license for more devices.',
          'error'
        );
        return;
      }
    }

    if (valid) {
      startCleaning();
      if (needsCheck) showBanner('LinkedIn Cleaner is active', 'success');
    } else {
      deleteStoredKey();
      const newKey = await promptForKey(
        'Your license key is invalid or your subscription has expired. ' +
        'Please enter a valid key or renew your subscription.'
      );
      if (newKey) {
        setStoredKey(newKey);
        const { valid: recheck, status: recheckStatus } = await verifyKey(newKey, deviceId);
        setLastCheck(Date.now(), recheck);
        if (recheck) {
          startCleaning();
          showBanner('LinkedIn Cleaner is active', 'success');
        } else if (recheckStatus === 'device_limit_reached') {
          deleteStoredKey();
          showBanner('This key is already activated on 2 devices. Purchase an additional license.', 'error');
        } else {
          deleteStoredKey();
          showBanner('Key not recognised. Check your subscription.', 'error');
        }
      }
    }
  }

  main();

  /* ----------------------------------------------------------
     GLOBAL HELPER — lets users re-enter their key from console
     ---------------------------------------------------------- */
  window.linkedinCleanerSetKey = function () {
    deleteStoredKey();
    main();
  };

})();