LinkedIn | Invite to follow Company Page

Adds select all invite connections to follow a LinkedIn company page

当前为 2025-10-01 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         LinkedIn | Invite to follow Company Page
// @namespace    https://github.com/gustavnyberg/tampermonkey
// @version      1.0.1
// @description  Adds select all invite connections to follow a LinkedIn company page
// @author       Gustav Nyberg
// @match        https://www.linkedin.com/company/*/admin/dashboard/?invite=true
// @grant        none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// ==/UserScript==

/* ---------------------------------------------------
   CURSED CODES:
--------------------------------------------------- */

/* ---------------------------------------------------
   DAUNTING DESIGNS:
--------------------------------------------------- */

/* ---------------------------------------------------
   FEARED FEATURES:
--------------------------------------------------- */

/* ---------------------------------------------------
   TERRIFYING TODOS:
   // TODO: Add abort button
   // TODO: Add counter to select a given number of invites/credits
   // TODO: Add break condition for when given/max number of invites reached (also handle 'Show more results')
   // TODO: Add more user feedback (alerts, UI messages) instead of just console.log
   // TODO: Handle unexpected DOM changes or missing elements more gracefully
--------------------------------------------------- */

(function () {
    'use strict';

    // Timing constants
    const waitContainer = 500;
    const waitInvite = 1200;
    const waitScroll = 600;
    const maxIdleChecks = 5;

    class InviteToFollowCompanyPageInviter {
        constructor() {
            this.isRunning = false;
            this.version = this.extractVersion();
        }

        async runSelectAll() {
            if (this.isRunning) {
                this.log("Already running. Ignoring duplicate start.");
                return;
            }
            this.isRunning = true;

            // Show startup alert with version info
            this.showStartupAlert();

            try {
                const creditsAvailable = this.getAvailableCredits();
                if (!creditsAvailable) {
                    this.log("No credits available. Stopping.");
                    return;
                }

                let creditsUsed = 0;
                let creditsRemaining = creditsAvailable;

                let invitesPlanned = creditsAvailable;
                let invitesSelected = 0;
                let invitesRemaining = invitesPlanned;

                let idleChecks = 0;

                while (creditsRemaining > 0 && invitesRemaining > 0) {
                    const container = this.getResultsContainer();
                    if (!container) {
                        this.log("Results container not found yet. Waiting...");
                        await this.wait(waitContainer);
                        continue;
                    }

                    const checkboxes = this.findInviteCheckboxes(container);
                    let invitesThisRound = 0;

                    if (checkboxes.length > 0) {
                        invitesThisRound = Math.min(creditsRemaining, invitesRemaining, checkboxes.length);

                        for (const cb of checkboxes.slice(0, invitesThisRound)) cb.click();

                        // Update invites
                        invitesSelected += invitesThisRound;
                        invitesRemaining -= invitesThisRound;

                        // Update credits
                        creditsUsed += invitesThisRound;
                        creditsRemaining -= invitesThisRound;

                        // Integrity check
                        if (creditsRemaining !== invitesRemaining) {
                            throw new Error(`Mismatch detected: creditsRemaining=${creditsRemaining}, invitesRemaining=${invitesRemaining}`);
                        }

                        this.log(
                            `Invites selected: ${invitesSelected}/${invitesPlanned} | Credits used: ${creditsUsed}/${creditsAvailable}`
                        );

                        await this.wait(waitInvite);
                    } else {
                        await this.scrollToBottom(container);

                        const showMore = this.findShowMoreButton(container);
                        if (showMore) {
                            this.log("Clicking 'Show more results'...");
                            showMore.click();
                            await this.wait(waitInvite);
                        } else {
                            this.log(
                                `No more results. Invites selected: ${invitesSelected}/${invitesPlanned}, Credits used: ${creditsUsed}/${creditsAvailable}`
                            );
                            break;
                        }
                    }

                    if (invitesThisRound === 0) {
                        idleChecks++;
                        if (idleChecks >= maxIdleChecks) {
                            this.log(
                                `Stopping due to repeated idle cycles. Invites selected: ${invitesSelected}/${invitesPlanned}, Credits used: ${creditsUsed}/${creditsAvailable}`
                            );
                            break;
                        }
                    } else {
                        idleChecks = 0;
                    }
                }

                if (creditsRemaining === 0 && invitesRemaining === 0) {
                    this.log(`Done. Credit limit reached: ${creditsUsed}/${creditsAvailable}`);
                }
            } catch (err) {
                console.error("[InviteToFollowCompanyPageInviter] Error:", err);
            } finally {
                this.isRunning = false;
            }
        }

        addButtons() {
            if (document.getElementById('SelectAllBtn')) return;

            const creditsSpan = this.findCreditsElement();
            if (!creditsSpan) return;

            const parent = creditsSpan.closest('div');
            if (!parent) return;

            parent.appendChild(this.createButton("Select All", () => this.runSelectAll()));
            this.log("Button added.");
        }

        /* --- DOM helpers --- */

        getResultsContainer() {
            return document.getElementById('invitee-picker-results-container');
        }

        findInviteCheckboxes(scope) {
            return [...scope.querySelectorAll('input[type="checkbox"]')]
                .filter(cb => !cb.checked && !cb.disabled && cb.offsetParent !== null);
        }

        findCreditsElement() {
            return [...document.querySelectorAll('span')]
                .find(el => /credit/i.test(el.textContent));
        }

        getAvailableCredits() {
            const el = this.findCreditsElement();
            if (!el) return null;
            const text = el.textContent.replace(/\s|,/g, '');
            const match = text.match(/(\d+)\s*\/\s*(\d+)/);
            return match ? parseInt(match[1], 10) : null;
        }

        findShowMoreButton(container) {
            const host = container?.parentElement ?? document;
            return [...host.querySelectorAll('button span.artdeco-button__text')]
                .find(el => el.textContent.trim().toLowerCase() === 'show more results')
                ?.closest('button') || null;
        }

        async scrollToBottom(container) {
            container.scrollTop = container.scrollHeight;
            await this.wait(waitScroll);
        }

        /* --- Utilities --- */

        extractVersion() {
            // Extract version from the script metadata
            const scriptTag = document.querySelector('script[src*="invite-to-follow-company-page"]');
            if (scriptTag) {
                // For Tampermonkey scripts, we can get the version from the script content
                const scriptContent = document.querySelector('script').textContent;
                const versionMatch = scriptContent.match(/@version\s+([\d.]+)/);
                return versionMatch ? versionMatch[1] : 'Unknown';
            }
            
            // Fallback: try to get from the current script
            const scripts = document.querySelectorAll('script');
            for (const script of scripts) {
                if (script.textContent.includes('Invite to follow Company Page')) {
                    const versionMatch = script.textContent.match(/@version\s+([\d.]+)/);
                    if (versionMatch) return versionMatch[1];
                }
            }
            
            return '1.0.0'; // Default fallback
        }

        showStartupAlert() {
            alert(`InviteToFollowCompanyPageInviter is running in version: ${this.version}`);
        }

        createButton(label, onClick) {
            const btn = document.createElement('button');
            btn.id = label.replace(/\s+/g, '') + "Btn";
            btn.innerText = label;
            Object.assign(btn.style, {
                padding: '6px 12px',
                fontWeight: 'bold',
                backgroundColor: '#0073b1',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                cursor: 'pointer'
            });
            btn.addEventListener('click', onClick);
            return btn;
        }

        wait(ms) { return new Promise(r => setTimeout(r, ms)); }
        log(msg) { console.log(`[InviteToFollowCompanyPageInviter] ${msg}`); }
    }

    /* --- Bootstrap --- */

    const inviter = new InviteToFollowCompanyPageInviter();

    // Observe page changes and inject button when credits element appears
    const observer = new MutationObserver(() => inviter.addButtons());
    observer.observe(document.body, { childList: true, subtree: true });

})();