LinkedIn Cleaner

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

Zanim zainstalujesz, Greasy Fork chce Cię poinformować, że ten skrypt zawiera pewne funkcjonalności mogące działać na pożytek autora, a nie twój. Upewnij się, czy na pewno chcesz kontynuować.

Ten skrypt uzyskuje pełną funkcjonalność po uiszczeniu opłaty. Serwis Greasy Fork nie jest pośrednikiem wykonywanych transakcji i nie jest za nie odpowiedzialny. Autor tego skryptu wyjaśnia: Requires a paid license key ($3.99 AUD/month or $29.99 AUD/year) from linkedinfilter.com. A free trial is not available.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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();
  };

})();