How to scrape Instagram data in 2026

How to scrape Instagram data in 2026
Artur Hvalei's Profile Image
Artur Hvalei

Technical Support Specialist, Octo Browser

In our previous article, we covered scraping Google search results using Octo Browser and Puppeteer. Now, let's raise the bar and take on Instagram, a platform with one of the toughest anti-bot systems in 2026. We'll look at the types of data people commonly scrape and why they do it. We'll explain why standard bots quickly run into restrictions and bans. We'll also examine the architecture of a working solution, the issues we encountered during testing, and ways to scale data collection without constantly dealing with account problems.

Contents

Stay anonymous, take advantage of multi-accounting, and achieve your goals with the highest-quality anti-detect browser on the market.

What people scrape from Instagram and why

After analyzing the market for Instagram tools and services in 2025–2026, we can identify six main types of data that are commonly scraped:

Data

Fields

Purpose

Account followers

username, full_name, verified, is_private, bio

Competitor research, finding high-quality leads, audience analysis for Meta Ads

Account follows

username, full_name, verified, is_private, bio

Competitor and influencer target audience analysis

Users who liked a post

username, full_name

Finding users with higher engagement than average followers

Commenters

username, comment text

DM outreach and sentiment analysis

Posts by hashtag/location

post_url, likes, caption

Content analysis and UGC discovery

Full profile

bio, posts, ER

Influencer evaluation before collaboration

Main use cases:

  1. Competitor research. Scrape the followers of 3–5 competitors, remove duplicates, and identify overlaps. The highest-quality leads are often users who follow several competitors at the same time.

  2. Influencer/blogger evaluation. Before paying for a collaboration, collect follower data and assess how genuine the audience is.

  3. DM outreach and cold email. In SaaS, marketing, and e-commerce niches, 15–35% of profiles include a public email address in their bio.

  4. Lookalike Audiences in Meta Ads. Usernames can be converted into a Custom Audience seed for creating Lookalike Audiences.

  5. Account prep through mass liking. Used to increase reach and establish a pattern of normal account activity.

Why ordinary bots get detected in 2026

The biggest change in recent years is that Instagram now hides content from unauthenticated users. Previously, you could query public endpoints such as /?__a=1 without a session. In 2026, even the followers of a public account are unavailable unless you're logged in.

What you'll need for your script:

1. A real logged-in account. Preferably a well-prepared account rather than one that was just created. Octo Browser helps here: you can create an account, log in manually, let it age naturally, and only then automate it.

2. One account means one fingerprint. Running 20 Instagram accounts from a single Chrome instance is a guaranteed way to get banned. With Octo, you can use isolated profiles with different fingerprints (WebGL parameters, screen resolution, fonts, user-agent, etc.).

3. A proxy for every profile. Mobile or residential proxies are ideal. Using datacenter IP addresses is extremely risky.

However, even with a proper browser fingerprint and a reliable proxy, a Puppeteer bot will not survive without some level of human behavior emulation. Anti-bot systems analyze:

  • Frequency patterns. How many likes per hour, how many follows per day, and the intervals between actions. Server-side analytics will quickly flag a bot that likes a post every 30 seconds.

  • Browser fingerprinting. This is why using an anti-detect browser is critical.

  • TLS/network fingerprinting. This is why quality proxies and a non-custom TLS stack matter.

  • Automation framework vulnerabilities (navigator.webdriver, CDP presence, distinctive traces in Error.stack).

Practical takeaway: in 2026, the main reasons accounts get banned are behavioral frequency patterns (too many actions in a short period of time, overly consistent intervals), browser fingerprints, and proxies. Emulating mouse movements, scrolling, or typing is not strictly necessary yet. In practice, however, these signals may still be used to analyze behavior and increase an account's fraud score.

That’s why our script architecture consists of:

  1. Octo Browser: isolated fingerprint, proxy, and a profile with a real Instagram session.

  2. Rebrowser-Puppeteer: a Puppeteer fork without the common leaks that are relatively easy to detect.

  3. WindMouse: a physics-based mouse movement model.

  4. A behavioral layer: random story views and post likes before scraping, log-normal pause distributions, overshoots (missing a target and correcting), and reactions to checkpoints.

WindMouse: why it's better than cubic Bézier curves

In our previous article, we used cubic Bézier curves to simulate mouse movement. This is a basic approach, but it has weaknesses: with only two control points, the curve is smooth and predictable. The shape is always similar, speed is controlled separately through an easing parameter, which creates overly uniform movement, and micro-jitter is added as a separate layer, making it obvious that the movement consists of a smooth curve plus artificial noise.

WindMouse simulates real physics instead. Gravity pulls the cursor toward the target, while wind introduces random deviations that accumulate with inertia. As a result, the movement path is smooth rather than jerky. Speed is limited by a maximum step size and naturally decreases near the destination, just like a real person's cursor when aiming at an object.

The result is a trajectory with variable speed, natural deceleration, and subtle micro-movements that are indistinguishable from real human behavior. Most importantly, because of the random wind component, the path is different every time.

We add two more human-like behaviors:

  • Overshoot: in 30% of cases, the cursor intentionally misses the button slightly, then moves back to it.

  • Click jitter: between mouse.down() and mouse.up(), the cursor shifts by 1–2 pixels in a random direction.

The WindMouse implementation in the script is the wind_mouse_trajectory function. It generates an array of trajectory points that are later used by page.mouse.move().

Ready-made script for scraping target account followers and prepping accounts simultaneously

Let’s assume you already have an array of Octo profile UUIDs logged into Instagram accounts. Our script will launch each profile, prep the account by liking posts in the feed and viewing Stories, and then proceed to the Instagram profiles specified as scraping targets. There, it will like posts, follow accounts, and finally scrape the follower list into both JSON and CSV formats. All of this is done with natural mouse movement, randomized delays, and checkpoint detection. As a result, Instagram cannot distinguish your automation from normal user activity.

Preparing profiles

  1. Create one or more profiles in Octo and assign a proxy to each. Residential or mobile proxies are recommended.

  2. Open each profile in Octo manually. Log in to or register an Instagram account. For two to three days, use the account like a regular person: like posts, watch Stories, and follow some users.

  3. Copy the profile UUIDs from Octo. You will need them when filling out the script configuration.

Getting started

  1. Download and install VS Code.

  2. Download and install Node.js.

  3. Create a folder anywhere on your device and name it, for example, octo_instagram_scraper.

  4. Open the folder in VS Code.

  5. Create a .js file. It is best to name it after the action the script performs to avoid confusion. For example, octo_instagram_scraper.js.

  6. Paste the script code into the file.

  7. Fill in the configuration in the config variable.

UUID — Octo profile IDs.

target_accounts — the accounts you want to scrape.

followers_per_target — the number of followers you want to collect from each target account during a single run.

The remaining parameters in config control realistic scraper behavior. You can experiment with them or leave them unchanged.

Fill in the configuration in the config variable
  1. Open your terminal and run the following command to install the required Node.js dependencies: npm i rebrowser-puppeteer axios

Open your terminal and run the following command to install the required Node.js dependencies: npm i rebrowser-puppeteer axios

If VS Code displays an error, open Windows PowerShell as Administrator, run the following command, and confirm the change:

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

Then repeat the previous step.

  1. Launch Octo Browser.

  2. . Run the script in Visual Studio Code (Ctrl/Cmd + F5) and wait for it to finish.

The scraper will sequentially launch the profiles specified in your configuration. Then, just like a regular user, it will watch Stories, scroll the feed, like posts, follow accounts, and scrape followers from the target accounts. You can monitor the process in the debug console.

Parser results summary. In this example, two target accounts were processed from each profile: 3 out of 4 runs completed successfully.

Parser results summary. In this example, two target accounts were processed from each profile: 3 out of 4 runs completed successfully.

Keep in mind that Instagram may limit the number of followers returned, depending on various factors such as account prep level, the number of mutual connections, whether the account follows the target account, and more. If something does not work as expected, experiment with additional prep activity or switch accounts and proxies.

Scraper results in CSV format

Scraper results in CSV format

Scraper results in JSON format

Scraper results in JSON format

JSON is more convenient for further processing in code, while CSV is ideal for importing into Excel, Google Sheets, CRM systems, or uploading directly to Meta Ads as a Custom Audience.

Script code

const axios = require('axios');
const puppeteer = require('rebrowser-puppeteer');
const fs = require('fs').promises;
const path = require('path');

const config = {
    octo_local_api_base_url: `http://localhost:58888/api/profiles`,
    headless_mode: false,
    profiles: [
        {
            uuid: "f7ac08ecae1b4a528b843bc4706ef3dd",
            target_accounts: ["phd_balance", "microbialecology"]
        },
        {
            uuid: "22be57d5c6f44e368258dc5ad6b425d3",
            target_accounts: ["the_brain_scientist", "drkaranrajan"]
        }
    ],
    followers_per_target: 100,
    follow_targets_before_parsing: true,
    likes_per_session: { min: 1, max: 3 },
    likes_on_target: { min: 1, max: 4 },
    stories_probability: 0.3,
    stories_per_session: { min: 1, max: 3 },
    delay_between_likes: { min: 5, max: 10 },
    delay_between_targets: { min: 60, max: 120 },
    delay_between_profiles: { min: 60, max: 120 },
    results_dir: 'instagram_results'
};

async function find_unliked_like(scope_handle) {
    const handle = await scope_handle.evaluateHandle(root => {
        const sections = root.querySelectorAll('section');
        for (const sec of sections) {
            const clickables = sec.querySelectorAll('button, div[role="button"], a[role="button"], a[role="link"]');
            const icon_buttons = [];
            for (const el of clickables) {
                if (el.querySelectorAll('svg').length !== 1) continue;
                const r = el.getBoundingClientRect();
                if (r.width < 16 || r.height < 16) continue;
                icon_buttons.push(el);
            }
            const top_level = icon_buttons.filter(b =>
                !icon_buttons.some(other => other !== b && other.contains(b))
            );
            if (top_level.length < 3 || top_level.length > 5) continue;

            const heart_btn = top_level[0];
            const heart_svg = heart_btn.querySelector('svg');
            if (!heart_svg) continue;

            const path = heart_svg.querySelector('path');
            if (path) {
                const fill = window.getComputedStyle(path).fill || '';
                const m = fill.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
                if (m && +m[1] > 200 && +m[2] < 100 && +m[3] < 100) continue;
            }
            return heart_svg;
        }
        return null;
    });
    return handle.asElement();
}

function random_range(min, max) {
    return min + Math.random() * (max - min);
}

function random_int(min, max) {
    return Math.floor(random_range(min, max + 1));
}

function pick_random(arr) {
    return arr[Math.floor(Math.random() * arr.length)];
}

async function sleep(seconds) {
    return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}

async function human_delay(min_ms = 50, max_ms = 200) {
    const mu = Math.log((min_ms + max_ms) / 2);
    const sigma = random_range(0.3, 0.6);
    let delay = Math.exp(mu + sigma * (Math.random() - 0.5) * 2);
    delay = Math.min(max_ms, Math.max(min_ms, delay));
    await new Promise(resolve => setTimeout(resolve, delay));
}

async function ensure_dir(dir) {
    await fs.mkdir(dir, { recursive: true });
}

async function dump_state(page, label) {
    try {
        const dir = path.join(__dirname, 'debug');
        await ensure_dir(dir);
        const stamp = new Date().toISOString().replace(/[:.]/g, '-');
        const png = path.join(dir, `${stamp}_${label}.png`);
        const html = path.join(dir, `${stamp}_${label}.html`);
        await page.screenshot({ path: png, fullPage: false });
        const body = await page.evaluate(() => document.documentElement.outerHTML);
        await fs.writeFile(html, body);
        console.log(`📸 dump [${label}] → ${png}`);
    } catch (e) {
        console.warn(`📸 dump [${label}] failed: ${e.message}`);
    }
}

function csv_escape(value) {
    if (value === null || value === undefined) return '';
    const s = String(value);
    if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
    return s;
}

function wind_mouse_trajectory(start, end, options = {}) {
    const {
        gravity = 9,
        wind = 3,
        max_step = 15,
        target_area = 12,
        min_wait_ms = 5,
        max_wait_ms = 12
    } = options;

    let x = start.x, y = start.y;
    let v_x = 0, v_y = 0;
    let w_x = 0, w_y = 0;
    let M = max_step;

    const points = [];
    let prev_x = Math.round(x), prev_y = Math.round(y);

    let safety = 0;
    while (safety++ < 10000) {
        const dist = Math.hypot(end.x - x, end.y - y);
        if (dist < 1) break;

        const w_mag = Math.min(wind, dist);
        if (dist >= target_area) {
            w_x = w_x / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
            w_y = w_y / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
        } else {
            w_x /= Math.sqrt(2);
            w_y /= Math.sqrt(2);
            if (M < 3) M = Math.random() * 3 + 3;
            else M /= Math.sqrt(5);
        }

        v_x += w_x + gravity * (end.x - x) / dist;
        v_y += w_y + gravity * (end.y - y) / dist;

        const v_mag = Math.hypot(v_x, v_y);
        if (v_mag > M) {
            const v_clip = M / 2 + Math.random() * M / 2;
            v_x = (v_x / v_mag) * v_clip;
            v_y = (v_y / v_mag) * v_clip;
        }

        x += v_x;
        y += v_y;

        const rx = Math.round(x);
        const ry = Math.round(y);
        if (rx !== prev_x || ry !== prev_y) {
            const wait = min_wait_ms + Math.random() * (max_wait_ms - min_wait_ms);
            points.push({ x: rx, y: ry, wait });
            prev_x = rx; prev_y = ry;
        }
    }

    points.push({ x: Math.round(end.x), y: Math.round(end.y), wait: 5 });
    return points;
}

async function move_mouse_human(page, target, options = {}) {
    const current = await page.evaluate(() => ({
        x: window.__mouseX ?? window.innerWidth / 2,
        y: window.__mouseY ?? window.innerHeight / 2
    }));

    const trajectory = wind_mouse_trajectory(current, target, options);
    for (const p of trajectory) {
        await page.mouse.move(p.x, p.y);
        if (p.wait > 0) await new Promise(r => setTimeout(r, p.wait));
    }

    await page.evaluate(({ x, y }) => {
        window.__mouseX = x;
        window.__mouseY = y;
    }, target);
}

async function human_click(page, selector_or_handle, options = {}) {
    const {
        overshoot_chance = 0.3,
        scroll_into_view = true,
        post_click_delay = [120, 350]
    } = options;

    const handle = typeof selector_or_handle === 'string'
        ? await page.$(selector_or_handle)
        : selector_or_handle;

    if (!handle) throw new Error(`Element not found: ${selector_or_handle}`);

    if (scroll_into_view) {
        await handle.evaluate(el => el.scrollIntoView({ block: 'center', behavior: 'smooth' }));
        await human_delay(500, 1000);
    }

    const box = await handle.boundingBox();
    if (!box) throw new Error('Could not get element coordinates');

    const target = {
        x: box.x + random_range(box.width * 0.25, box.width * 0.75),
        y: box.y + random_range(box.height * 0.25, box.height * 0.75)
    };

    if (Math.random() < overshoot_chance) {
        const overshoot = {
            x: target.x + (Math.random() - 0.5) * random_range(15, 35),
            y: target.y + (Math.random() - 0.5) * random_range(15, 35)
        };
        await move_mouse_human(page, overshoot);
        await human_delay(40, 120);
        await move_mouse_human(page, target);
    } else {
        await move_mouse_human(page, target);
    }

    await human_delay(post_click_delay[0], post_click_delay[1]);

    await page.mouse.down();
    await human_delay(40, 120);

    if (Math.random() < 0.25) {
        await page.mouse.move(
            target.x + (Math.random() - 0.5) * 2,
            target.y + (Math.random() - 0.5) * 2
        );
    }
    await page.mouse.up();

    return { x: target.x, y: target.y };
}

async function human_scroll_window(page, options = {}) {
    const { scrolls = random_int(2, 5), back_chance = 0.2 } = options;

    for (let i = 0; i < scrolls; i++) {
        const distance = random_range(300, 800);
        await page.evaluate(d => window.scrollBy({ top: d, behavior: 'smooth' }), distance);
        await human_delay(900, 2200);

        if (Math.random() < back_chance) {
            const back = random_range(100, 280);
            await page.evaluate(d => window.scrollBy({ top: -d, behavior: 'smooth' }), back);
            await human_delay(500, 1100);
        }
    }
}

async function human_scroll_element(page, element_handle, distance) {
    const before = await element_handle.evaluate(el => el.scrollTop);

    await element_handle.evaluate((el, d) => {
        el.scrollTop = el.scrollTop + d;
        el.dispatchEvent(new WheelEvent('wheel', {
            deltaY: d,
            bubbles: true,
            cancelable: true
        }));
    }, distance);

    await human_delay(700, 1800);
    const after = await element_handle.evaluate(el => el.scrollTop);
    return after - before;
}

async function dismiss_overlays(page, max_passes = 3) {

    const MODAL_SCOPE = [
        'div[role="dialog"]',
        'div[role="alertdialog"]',
        '[aria-modal="true"]',
        'div[data-testid="cookie-policy-manage-dialog"]',
        'div[aria-label*="cookie" i]',
        'div[aria-label*="Cookie"]'
    ].join(', ');

    let dismissed = 0;
    for (let pass = 0; pass < max_passes; pass++) {
        const modals = await page.$$(MODAL_SCOPE);
        if (modals.length === 0) break;

        const before = modals.length;
        await page.keyboard.press('Escape').catch(() => { });
        await human_delay(600, 1400);

        const after_modals = await page.$$(MODAL_SCOPE);
        if (after_modals.length < before) {
            console.log(`🪟 Modal dismissed via ESC`);
            dismissed++;
            continue;
        }
        break;
    }
    return dismissed;
}

async function check_for_block(page) {
    const url = page.url();
    if (url.includes('/challenge/') || url.includes('/accounts/suspended/') ||
        url.includes('/accounts/disabled/')) {
        return { blocked: true, reason: 'redirect: ' + url };
    }

    const block_text = await page.evaluate(() => {
        const text = document.body.innerText.toLowerCase();
        const markers = [
            'we restricted certain activity',
            'try again later',
            'suspicious login attempt',
            'your account has been temporarily',
            'temporary action restriction',
            'suspicious login'
        ];
        for (const m of markers) if (text.includes(m)) return m;
        return null;
    });

    if (block_text) return { blocked: true, reason: 'text: ' + block_text };
    return { blocked: false };
}

async function check_logged_in(page) {
    const url = page.url();
    if (url.includes('/accounts/login') || url.includes('/accounts/emailsignup')) {
        return false;
    }
    const has_login_form = await page.$('input[name="username"]');
    return !has_login_form;
}

async function watch_random_stories(page, count) {
    console.log(`📺 Trying to watch ${count} stories...`);
    const story_buttons = await page.$$('div[role="menuitem"] button[role="button"], li button[role="button"]');
    const visible_stories = [];
    for (const btn of story_buttons) {
        const box = await btn.boundingBox();
        if (box && box.y < 250 && box.width > 30) visible_stories.push(btn);
    }

    if (visible_stories.length === 0) {
        const fallback = await page.$$('canvas');
        for (const c of fallback) {
            const box = await c.boundingBox();
            if (box && box.y < 250) visible_stories.push(c);
        }
    }

    if (visible_stories.length === 0) {
        console.log('   ⏭ No stories found, skipping');
        return 0;
    }

    let watched = 0;
    const to_watch = Math.min(count, visible_stories.length);
    for (let i = 0; i < to_watch; i++) {
        try {
            const story = pick_random(visible_stories);
            await human_click(page, story);
            await human_delay(3000, 7000);
            await page.keyboard.press('Escape');
            await human_delay(800, 1500);
            watched++;
            console.log(`   👀 Story ${watched}/${to_watch} watched`);
        } catch (e) {
            console.log(`   ⚠️ Error while watching story: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }
    return watched;
}

async function browse_feed_and_like(page, target_likes) {
    console.log(`❤️ Feed warm-up: ${target_likes} likes`);
    await dismiss_overlays(page);

    if (!page.url().match(/instagram\.com\/?(\?|$)/)) {
        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(2000, 3500);
    }

    const articles_appeared = await page.waitForFunction(
        () => document.querySelectorAll('article').length > 0,
        { timeout: 12000 }
    ).then(() => true).catch(() => false);

    if (!articles_appeared) {

        for (let i = 0; i < 3; i++) {
            await human_scroll_window(page, { scrolls: 2 });
            const ok = await page.evaluate(() => document.querySelectorAll('article').length > 0);
            if (ok) break;
        }
    }

    let liked = 0;
    let scrolls_without_progress = 0;

    while (liked < target_likes && scrolls_without_progress < 5) {

        const articles = await page.$$('article');
        let new_like_done = false;

        for (const article of articles) {
            if (liked >= target_likes) break;
            try {

                const like_btn = await find_unliked_like(article);
                if (!like_btn) continue;

                const box = await like_btn.boundingBox();
                if (!box) continue;

                const in_view = await like_btn.evaluate(el => {
                    const r = el.getBoundingClientRect();
                    return r.top > 50 && r.bottom < window.innerHeight - 50;
                });
                if (!in_view) continue;

                if (Math.random() < 0.3) {
                    await human_delay(1500, 4000);
                }

                await human_click(page, like_btn);
                liked++;
                new_like_done = true;
                console.log(`   ❤️ Like ${liked}/${target_likes}`);

                const pause = random_range(
                    config.delay_between_likes.min,
                    config.delay_between_likes.max
                );
                console.log(`   ⏰ Pause ${pause.toFixed(1)} sec`);
                await sleep(pause);

                const block = await check_for_block(page);
                if (block.blocked) {
                    console.log(`   🚫 Blocked after like: ${block.reason}`);
                    return { liked, blocked: true };
                }

                break;
            } catch (e) {
                continue;
            }
        }

        if (!new_like_done) {
            scrolls_without_progress++;
            await human_scroll_window(page, { scrolls: random_int(2, 4), back_chance: 0.05 });
            await human_delay(1500, 3000);
        } else {
            scrolls_without_progress = 0;

            await human_scroll_window(page, { scrolls: random_int(1, 2), back_chance: 0.1 });
            await human_delay(800, 1800);
        }
    }

    console.log(`✅ Warm-up complete, likes: ${liked}`);
    if (liked === 0) await dump_state(page, 'feed_zero_likes');
    return { liked, blocked: false };
}

async function visit_target_profile(page, username) {
    console.log(`🎯 Opening profile @${username}`);
    await page.goto(`https://www.instagram.com/${username}/`, {
        waitUntil: 'domcontentloaded',
        timeout: 30000
    });
    await human_delay(2500, 4500);
    await dismiss_overlays(page);

    const not_found = await page.evaluate(() => {
        const t = document.body.innerText;
        return t.includes("Sorry, this page isn't available")
            || t.includes('Страница недоступна');
    });
    if (not_found) {
        console.log(`   ⚠️ Profile @${username} not found`);
        return false;
    }

    const block = await check_for_block(page);
    if (block.blocked) {
        console.log(`   🚫 Blocked while opening profile: ${block.reason}`);
        return false;
    }

    await human_scroll_window(page, { scrolls: random_int(2, 4) });
    return true;
}

async function like_target_posts(page, count) {
    console.log(`💜 Liking ${count} post(s) on the target account`);

    const post_links = await page.$$('a[href*="/p/"]');
    if (post_links.length === 0) {
        console.log('   ⏭ No posts visible (private account or no publications)');
        return 0;
    }

    let liked = 0;
    const indices = [];
    for (let i = 0; i < count && i < post_links.length; i++) {

        const idx = random_int(0, Math.min(11, post_links.length - 1));
        if (!indices.includes(idx)) indices.push(idx);
    }

    for (const idx of indices) {
        try {
            const post_links_fresh = await page.$$('a[href*="/p/"]');
            if (!post_links_fresh[idx]) continue;

            await human_click(page, post_links_fresh[idx]);
            await human_delay(2500, 4500);

            const post_root = await page.evaluateHandle(() => {
                const dialog = document.querySelector('div[role="dialog"]');
                if (dialog) {
                    const inner_article = dialog.querySelector('article');
                    return inner_article || dialog;
                }
                return document.querySelector('article');
            });
            const post_root_el = post_root.asElement();
            const like_btn = post_root_el ? await find_unliked_like(post_root_el) : null;
            if (like_btn) {
                await human_click(page, like_btn);
                liked++;
                console.log(`   ❤️ Like on post ${liked}/${count}`);
            } else {
                console.log(`   ⏭ Post already liked or like button not found, skipping`);
            }

            if (Math.random() < 0.4) {
                await page.keyboard.press('ArrowRight').catch(() => { });
                await human_delay(800, 1800);
            }

            await human_delay(1500, 3500);
            await page.keyboard.press('Escape');
            await human_delay(1200, 2200);

            const block = await check_for_block(page);
            if (block.blocked) {
                console.log(`   🚫 Blocked after liking a post: ${block.reason}`);
                return liked;
            }

            const pause = random_range(
                config.delay_between_likes.min,
                config.delay_between_likes.max
            );
            await sleep(pause);
        } catch (e) {
            console.log(`   ⚠️ Post-like error: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }

    return liked;
}

function setup_friendship_interceptor(page) {
    const state = {
        last_profile_info: null,
        last_friendship_action: null,
        action_blocked: false
    };

    const handler = async (response) => {
        const url = response.url();

        if (/\/api\/v1\/users\/web_profile_info\//.test(url)) {
            try {
                const json = await response.json();
                const u = json?.data?.user;
                if (u) {
                    state.last_profile_info = {
                        username: u.username,
                        id: u.id,
                        followed_by_viewer: !!u.followed_by_viewer,
                        requested_by_viewer: !!u.requested_by_viewer,
                        is_private: !!u.is_private
                    };
                }
            } catch (e) { }
            return;
        }

        if (/\/api\/v1\/friendships\/(create|destroy)\//.test(url)) {
            try {
                const json = await response.json();
                state.last_friendship_action = { status: response.status(), body: json };
                if (json?.spam || json?.feedback_required || json?.error_type === 'feedback_required') {
                    state.action_blocked = true;
                }
            } catch (e) {
                state.last_friendship_action = { status: response.status(), body: null };
            }
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function fetch_profile_info(page, username) {
    return await page.evaluate(async (u) => {
        try {
            const res = await fetch(`/api/v1/users/web_profile_info/?username=${encodeURIComponent(u)}`, {
                headers: { 'X-IG-App-ID': '936619743392459' },
                credentials: 'include'
            });
            if (!res.ok) return { error: `HTTP ${res.status}` };
            const json = await res.json();
            const user = json?.data?.user;
            if (!user) return { error: 'no user in response body' };
            return {
                username: user.username,
                id: user.id,
                followed_by_viewer: !!user.followed_by_viewer,
                requested_by_viewer: !!user.requested_by_viewer,
                is_private: !!user.is_private
            };
        } catch (e) {
            return { error: e.message };
        }
    }, username);
}

async function ensure_following(page, target_username, friendship_iface) {
    const wait_for = async (predicate, max_ms, step = 200) => {
        const start = Date.now();
        while (Date.now() - start < max_ms) {
            const v = predicate();
            if (v) return v;
            await new Promise(r => setTimeout(r, step));
        }
        return predicate() || null;
    };

    const info = await fetch_profile_info(page, target_username);

    if (!info || info.error || !info.username) {
        console.log(`   ⚠️ Could not fetch profile info via API for @${target_username}: ${info?.error || 'no data'}`);
        return false;
    }

    console.log(`   🔎 API friendship state for @${target_username}: following=${info.followed_by_viewer}, requested=${info.requested_by_viewer}, private=${info.is_private}`);

    if (info.followed_by_viewer) {
        console.log(`   ✓ Already following @${target_username}`);
        return true;
    }
    if (info.requested_by_viewer) {
        console.log(`   ✓ Follow request already pending for @${target_username}`);
        return true;
    }

    await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
    await human_delay(800, 1500);

    const handle = await page.evaluateHandle((username) => {
        const heading_candidates = Array.from(
            document.querySelectorAll('h1, h2, span')
        ).filter(el => {
            const text = (el.textContent || '').trim();
            if (text !== username) return false;
            const r = el.getBoundingClientRect();
            return r.top >= 0 && r.top < 500 && r.width > 30;
        });

        if (heading_candidates.length === 0) return null;
        const anchor = heading_candidates[0];

        let container = anchor;
        for (let depth = 0; depth < 10 && container.parentElement; depth++) {
            container = container.parentElement;
            const buttons = Array.from(container.querySelectorAll('button, div[role="button"]'));
            const candidates = buttons.filter(btn => {
                const r = btn.getBoundingClientRect();
                if (r.width < 70 || r.height < 28 || r.width > 400 || r.height > 80) return false;
                const text = (btn.textContent || '').trim();
                if (text.length === 0 || text.length > 60) return false;
                return btn.querySelectorAll('svg').length <= 1;
            });
            if (candidates.length === 0) continue;
            candidates.sort((a, b) => {
                const ra = a.getBoundingClientRect(), rb = b.getBoundingClientRect();
                if (Math.abs(ra.top - rb.top) > 5) return ra.top - rb.top;
                return ra.left - rb.left;
            });
            return candidates[0];
        }
        return null;
    }, target_username);

    const el = handle.asElement();
    if (!el) {
        console.log(`   ⚠️ Primary action button not found near @${target_username} username anchor`);
        await dump_state(page, `no_follow_btn_${target_username}`);
        return false;
    }

    friendship_iface.state.last_friendship_action = null;
    friendship_iface.state.action_blocked = false;

    console.log(`   ➕ Subscribing to @${target_username}...`);
    await human_click(page, el);
    await human_delay(2500, 4000);

    const after = await fetch_profile_info(page, target_username);
    if (after && !after.error) {
        if (after.followed_by_viewer) {
            console.log(`   ✓ Subscribed (API confirmed)`);
            return true;
        }
        if (after.requested_by_viewer) {
            console.log(`   ✓ Follow request sent (private account)`);
            return true;
        }
    }

    if (friendship_iface.state.action_blocked) {
        const body = friendship_iface.state.last_friendship_action?.body;
        const fb = body?.feedback_message || body?.feedback_title || 'feedback_required';
        console.log(`   🚫 Instagram action-blocked the follow: ${fb}`);
        return false;
    }

    console.log(`   ⚠️ Click sent but API still reports not following — silent block or click missed the toggle`);
    return false;
}

function setup_followers_interceptor(page) {
    const state = {
        has_next_page: true,
        api_seen: false,
        responses: 0,
        users_returned: 0,
        empty_with_next: 0,
        cap_detected: false
    };

    const handler = async (response) => {
        const url = response.url();
        const is_followers_api = /\/friendships\/\d+\/followers\//.test(url);
        const is_graphql = /edge_followed_by/.test(url);
        if (!is_followers_api && !is_graphql) return;

        try {
            const json = await response.json();
            state.api_seen = true;
            state.responses++;

            let batch_size = 0;
            let has_next = state.has_next_page;

            if ('next_max_id' in json) {
                has_next = !!json.next_max_id;
                if (Array.isArray(json.users)) {
                    batch_size = json.users.length;
                    state.users_returned += batch_size;
                }
            } else if (json?.data?.user?.edge_followed_by?.page_info) {
                const pi = json.data.user.edge_followed_by.page_info;
                has_next = !!pi.has_next_page;
                const edges = json.data.user.edge_followed_by.edges;
                if (Array.isArray(edges)) {
                    batch_size = edges.length;
                    state.users_returned += batch_size;
                }
            }

            state.has_next_page = has_next;

            if (has_next && batch_size === 0) {
                state.empty_with_next++;
                if (state.empty_with_next >= 1) state.cap_detected = true;
            } else {
                state.empty_with_next = 0;
            }
        } catch (e) {
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function parse_followers(page, target_username, max_count) {
    console.log(`📋 Parsing followers of @${target_username} (target: ${max_count})`);

    await dismiss_overlays(page);

    const { state: api_state, cleanup: detach_interceptor } = setup_followers_interceptor(page);
    try {
        return await _parse_followers_impl(page, target_username, max_count, api_state);
    } finally {
        detach_interceptor();
    }
}

async function _parse_followers_impl(page, target_username, max_count, api_state) {
    const followers_handle = await page.evaluateHandle((target) => {
        const followers_re = new RegExp(`^/${target}/followers/?(\\?|$)`, 'i');

        const anchors = Array.from(document.querySelectorAll('a[href]'));
        for (const a of anchors) {
            if (followers_re.test(a.getAttribute('href') || '')) return a;
        }

        const all_clickable = Array.from(document.querySelectorAll(
            'a, button, div[role="button"], a[role="link"]'
        ));
        const counters = all_clickable.filter(el => {
            const t = (el.innerText || '').trim();
            if (!t || t.length > 60 || !/\d/.test(t)) return false;
            const r = el.getBoundingClientRect();
            return r.width > 0 && r.height > 0 && r.top < window.innerHeight;
        });

        for (const seed of counters) {
            let node = seed;
            for (let depth = 0; depth < 8 && node.parentElement; depth++) {
                const parent = node.parentElement;
                const row = [];
                for (const child of parent.children) {
                    const hit = counters.find(c => child === c || child.contains(c));
                    if (hit) row.push(hit);
                }
                if (row.length >= 2 && row.length <= 4) {
                    return row[row.length - 2];
                }
                node = parent;
            }
        }
        return null;
    }, target_username);

    const followers_link = followers_handle.asElement();
    if (!followers_link) {
        console.log('   ❌ "Followers" button not found');
        await dump_state(page, `target_${target_username}_no_followers_btn`);
        return [];
    }
    await human_click(page, followers_link);
    await human_delay(1800, 3000);

    await page.waitForSelector('div[role="dialog"]', { timeout: 10000 }).catch(() => { });

    const got_users = await page.waitForFunction(() => {
        const dialog = document.querySelector('div[role="dialog"]');
        if (!dialog) return false;
        return dialog.querySelectorAll('a[role="link"][href^="/"]').length >= 3;
    }, { timeout: 15000 }).then(() => true).catch(() => false);

    if (!got_users) {
        console.log('   ❌ Followers modal did not load the list within 15 sec');
        return [];
    }
    await human_delay(800, 1500);

    const find_scrollable = async () => {
        const handle = await page.evaluateHandle(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return null;
            const all = dialog.querySelectorAll('div');
            let best = null, best_count = 0;
            for (const el of all) {
                const s = window.getComputedStyle(el);
                if (s.overflowY !== 'auto' && s.overflowY !== 'scroll') continue;
                const links = el.querySelectorAll('a[role="link"][href^="/"]').length;
                if (links > best_count) { best = el; best_count = links; }
            }
            return best;
        });
        return handle.asElement();
    };

    let scrollable = await find_scrollable();
    if (!scrollable) {
        console.log('   ❌ Scrollable container of the modal not found');
        return [];
    }
    console.log(`   📜 Container found, initial scrollHeight: ${await scrollable.evaluate(el => el.scrollHeight)}px`);

    const modal_box = await scrollable.boundingBox();
    if (modal_box) {
        await move_mouse_human(page, {
            x: modal_box.x + modal_box.width / 2,
            y: modal_box.y + modal_box.height / 2
        });
        await human_delay(200, 500);
    }

    const collected = new Map();
    let stagnation = 0;
    const max_stagnation = 5;
    let iteration = 0;
    const max_iterations = 200;

    while (collected.size < max_count && stagnation < max_stagnation && iteration < max_iterations) {
        iteration++;

        const batch = await page.evaluate(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return [];
            const links = dialog.querySelectorAll('a[role="link"][href^="/"]');
            const seen = new Map();

            const skip = new Set(['explore', 'reels', 'direct', 'accounts', 'p', 'tv', 'stories', 'about']);

            const button_words = new Set([
                'Follow', 'Following', 'Message', 'Requested', 'Subscribed', 'Subscribe',
                'Подписаться', 'Подписки', 'Подписан', 'Подписана', 'Запрос', 'Сообщение',
                'Отписаться', 'Subscribirse', 'Seguir', 'Suivre'
            ]);

            for (const a of links) {
                const href = a.getAttribute('href');
                const m = href && href.match(/^\/([A-Za-z0-9._]+)\/?$/);
                if (!m) continue;
                const username = m[1];
                if (skip.has(username)) continue;
                if (seen.has(username)) continue;

                let row = a;
                for (let depth = 0; depth < 10 && row.parentElement; depth++) {
                    const parent = row.parentElement;
                    const sibling_links = parent.querySelectorAll('a[role="link"][href^="/"]');
                    let other = 0;
                    for (const l of sibling_links) {
                        const lm = l.getAttribute('href').match(/^\/([A-Za-z0-9._]+)\/?$/);
                        if (lm && lm[1] !== username && !skip.has(lm[1])) { other++; break; }
                    }
                    if (other > 0) break;
                    row = parent;
                }

                const lines = (row.innerText || '')
                    .split(/\n+/).map(s => s.trim()).filter(Boolean);
                let full_name = '';
                for (const line of lines) {
                    if (line === username) continue;
                    if (button_words.has(line)) continue;
                    if (line.length > 150) continue;
                    full_name = line;
                    break;
                }

                const verified = !!row.querySelector('svg[aria-label="Verified"], svg[aria-label*="Verif"], svg[aria-label="Подтвержденный"]');
                const is_private = !!row.querySelector('svg[aria-label="Private"]');

                seen.set(username, { username, full_name, verified, is_private });
            }
            return Array.from(seen.values());
        });

        const before = collected.size;
        for (const u of batch) {
            if (collected.size >= max_count) break;
            if (!collected.has(u.username)) collected.set(u.username, u);
        }
        const added = collected.size - before;

        if (added === 0) stagnation++;
        else stagnation = 0;

        const api_tail = api_state.api_seen
            ? ` [api: ${api_state.responses} resp, ${api_state.users_returned} users, next=${api_state.has_next_page}${api_state.cap_detected ? ', CAP' : ''}]`
            : '';
        process.stdout.write(`   📊 Collected: ${collected.size}/${max_count} (+${added})${api_tail}\r`);

        if (collected.size >= max_count) break;

        if (api_state.api_seen && (!api_state.has_next_page || api_state.cap_detected)) {
            const reason = api_state.cap_detected
                ? `server cap (empty batch with next_max_id present, returned ${api_state.users_returned} users total)`
                : 'has_next_page=false';
            console.log(`\n   🛑 Pagination closed by Instagram: ${reason}`);
            break;
        }

        const wheel_delta = random_range(400, 700);
        await page.mouse.wheel({ deltaY: wheel_delta });
        await human_delay(900, 1700);

        if (stagnation >= 1) {
            await scrollable.evaluate(el => {
                const links = el.querySelectorAll('a[role="link"][href^="/"]');
                if (links.length > 0) {
                    links[links.length - 1].scrollIntoView({ block: 'end' });
                }
            });
            await human_delay(700, 1300);

            const fresh = await find_scrollable();
            if (fresh) scrollable = fresh;
        }

        if (Math.random() < 0.2) await human_delay(1500, 3500);
    }

    console.log(`\n   ✅ Followers collected: ${collected.size}`);

    await page.keyboard.press('Escape').catch(() => { });
    await human_delay(800, 1500);

    return Array.from(collected.values()).slice(0, max_count);
}

async function save_followers(profile_uuid, target, users) {
    const dir = path.join(__dirname, config.results_dir);
    await ensure_dir(dir);

    const stamp = new Date().toISOString().replace(/[:.]/g, '-');
    const base = `${target}_${profile_uuid.slice(0, 8)}_${stamp}`;

    const json_path = path.join(dir, `${base}.json`);
    const csv_path = path.join(dir, `${base}.csv`);

    const json_payload = {
        target_account: target,
        parsed_by_profile: profile_uuid,
        timestamp: new Date().toISOString(),
        total: users.length,
        followers: users
    };
    await fs.writeFile(json_path, JSON.stringify(json_payload, null, 2), 'utf8');

    const csv_lines = ['username,full_name,verified,is_private'];
    for (const u of users) {
        csv_lines.push([
            csv_escape(u.username),
            csv_escape(u.full_name),
            csv_escape(u.verified),
            csv_escape(u.is_private)
        ].join(','));
    }
    await fs.writeFile(csv_path, csv_lines.join('\n'), 'utf8');

    console.log(`💾 JSON: ${json_path}`);
    console.log(`💾 CSV : ${csv_path}`);
}

async function process_target(page, target, profile_uuid) {
    const friendship_iface = setup_friendship_interceptor(page);
    try {
        const ok = await visit_target_profile(page, target);
        if (!ok) return { target, success: false };

        let followed = false;
        if (config.follow_targets_before_parsing) {
            followed = await ensure_following(page, target, friendship_iface);
            if (followed) await human_delay(3000, 6000);
        }

        const target_likes = random_int(config.likes_on_target.min, config.likes_on_target.max);
        if (target_likes > 0) {
            await like_target_posts(page, target_likes);
        }

        await page.goto(`https://www.instagram.com/${target}/`, {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(1500, 3000);

        const users = await parse_followers(page, target, config.followers_per_target);
        if (users.length > 0) {
            await save_followers(profile_uuid, target, users);
        }

        return { target, success: true, count: users.length, followed };
    } finally {
        friendship_iface.cleanup();
    }
}

async function process_profile(profile_cfg, idx, total) {
    console.log(`\n${'='.repeat(80)}`);
    console.log(`📋 Profile ${idx + 1}/${total} — UUID ${profile_cfg.uuid}`);
    console.log(`   Targets: ${profile_cfg.target_accounts.join(', ')}`);
    console.log(`${'='.repeat(80)}`);

    await octo_stop_profile(profile_cfg.uuid).catch(() => { });
    await sleep(3);

    let ws_data;
    try {
        ws_data = await octo_start_profile(profile_cfg.uuid);
    } catch (e) {
        const body = e.response?.data;
        const body_str = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : '';
        console.error(`❌ Failed to start profile: ${e.message}${body_str ? ' | Octo: ' + body_str : ''}`);
        return { uuid: profile_cfg.uuid, status: 'start_failed', error: body_str || e.message };
    }

    if (!ws_data?.ws_endpoint) {
        console.error('❌ Octo did not return a ws_endpoint');
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'no_ws' };
    }

    let browser;
    try {
        browser = await puppeteer.connect({
            browserWSEndpoint: ws_data.ws_endpoint,
            defaultViewport: null,
            protocolTimeout: 600000
        });
    } catch (e) {
        console.error(`❌ Puppeteer connect: ${e.message}`);
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'connect_failed' };
    }

    try {
        const ctx = browser.defaultBrowserContext();
        await ctx.overridePermissions('https://www.instagram.com', []);
        await ctx.overridePermissions('https://instagram.com', []);
    } catch (e) {
        console.warn(`⚠️ overridePermissions: ${e.message}`);
    }

    let stats = { uuid: profile_cfg.uuid, status: 'ok', targets: [], likes: 0, stories: 0 };

    try {
        const page = await browser.newPage();
        await page.setViewport({ width: 1280, height: 900 });

        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 45000
        });
        await human_delay(2500, 4500);
        await dismiss_overlays(page);

        await human_delay(800, 1500);
        await dismiss_overlays(page);

        if (!await check_logged_in(page)) {
            console.error('❌ Profile is not logged into Instagram');
            stats.status = 'not_logged_in';
            return stats;
        }

        const block = await check_for_block(page);
        if (block.blocked) {
            console.error(`❌ Account is restricted: ${block.reason}`);
            stats.status = 'blocked';
            return stats;
        }

        if (Math.random() < config.stories_probability) {
            const cnt = random_int(config.stories_per_session.min, config.stories_per_session.max);
            stats.stories = await watch_random_stories(page, cnt);
            await human_delay(1500, 3000);
        }

        const feed_likes = random_int(config.likes_per_session.min, config.likes_per_session.max);
        const feed_result = await browse_feed_and_like(page, feed_likes);
        stats.likes += feed_result.liked;
        if (feed_result.blocked) {
            stats.status = 'blocked_during_feed';
            return stats;
        }

        for (let i = 0; i < profile_cfg.target_accounts.length; i++) {
            const target = profile_cfg.target_accounts[i];
            try {
                const r = await process_target(page, target, profile_cfg.uuid);
                stats.targets.push(r);
            } catch (e) {
                console.error(`❌ Target @${target} error: ${e.message}`);
                stats.targets.push({ target, success: false, error: e.message });
            }

            if (i < profile_cfg.target_accounts.length - 1) {
                const pause = random_range(
                    config.delay_between_targets.min,
                    config.delay_between_targets.max
                );
                console.log(`⏰ Pause between targets: ${pause.toFixed(1)} sec`);
                await sleep(pause);
            }
        }

    } catch (e) {
        console.error(`❌ Profile processing error: ${e.message}`);
        stats.status = 'error';
        stats.error = e.message;
    } finally {
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        await sleep(2);
    }

    return stats;
}

async function check_limits(response) {
    const header = response.headers.ratelimit;
    if (!header) return;
    const entries = header.split(',').map(s => s.trim());
    for (const e of entries) {
        const r_match = e.match(/;r=(\d+)/);
        const t_match = e.match(/;t=(\d+)/);
        if (!r_match || !t_match) continue;
        const remaining = parseInt(r_match[1], 10);
        const window_s = parseInt(t_match[1], 10);
        if (remaining < 5) {
            console.log(`⏳ Octo rate-limit, waiting ${window_s + 1} sec`);
            await sleep(window_s + 1);
        }
    }
}

async function octo_start_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/start`,
        headers: { 'Content-Type': 'application/json' },
        data: {
            uuid,
            headless: config.headless_mode,
            debug_port: true,
            timeout: 60
        }
    });
    await check_limits(res);
    return res.data;
}

async function octo_stop_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/stop`,
        headers: { 'Content-Type': 'application/json' },
        data: { uuid }
    });
    await check_limits(res);
    return res.data;
}

(async () => {
    console.log('🚀 Octo Instagram Parser & Liker');
    console.log(`   Profiles: ${config.profiles.length}`);
    console.log(`   Followers per target: ${config.followers_per_target}`);
    console.log(`   Feed likes: ${config.likes_per_session.min}${config.likes_per_session.max}`);
    console.log('');

    await ensure_dir(path.join(__dirname, config.results_dir));

    const all_stats = [];
    for (let i = 0; i < config.profiles.length; i++) {
        const stats = await process_profile(config.profiles[i], i, config.profiles.length);
        all_stats.push(stats);

        if (i < config.profiles.length - 1) {
            const pause = random_range(
                config.delay_between_profiles.min,
                config.delay_between_profiles.max
            );
            console.log(`\n⏰ Pause before the next profile: ${pause.toFixed(1)} sec`);
            await sleep(pause);
        }
    }

    console.log(`\n${'='.repeat(80)}`);
    console.log('📊 SUMMARY');
    console.log('='.repeat(80));
    for (const s of all_stats) {
        console.log(`\n${s.uuid}${s.status}`);
        console.log(`   Likes: ${s.likes ?? 0}, stories: ${s.stories ?? 0}`);
        if (s.targets) {
            for (const t of s.targets) {
                if (t.success) console.log(`   ✅ @${t.target}: ${t.count} followers`);
                else console.log(`   ❌ @${t.target}: ${t.error || 'fail'}`);
            }
        }
    }

    const summary_path = path.join(__dirname, config.results_dir, `_summary_${Date.now()}.json`);
    await fs.writeFile(summary_path, JSON.stringify(all_stats, null, 2));
    console.log(`\n📄 Summary report: ${summary_path}`);
    console.log('🎉 Done.');
})();

const axios = require('axios');
const puppeteer = require('rebrowser-puppeteer');
const fs = require('fs').promises;
const path = require('path');

const config = {
    octo_local_api_base_url: `http://localhost:58888/api/profiles`,
    headless_mode: false,
    profiles: [
        {
            uuid: "f7ac08ecae1b4a528b843bc4706ef3dd",
            target_accounts: ["phd_balance", "microbialecology"]
        },
        {
            uuid: "22be57d5c6f44e368258dc5ad6b425d3",
            target_accounts: ["the_brain_scientist", "drkaranrajan"]
        }
    ],
    followers_per_target: 100,
    follow_targets_before_parsing: true,
    likes_per_session: { min: 1, max: 3 },
    likes_on_target: { min: 1, max: 4 },
    stories_probability: 0.3,
    stories_per_session: { min: 1, max: 3 },
    delay_between_likes: { min: 5, max: 10 },
    delay_between_targets: { min: 60, max: 120 },
    delay_between_profiles: { min: 60, max: 120 },
    results_dir: 'instagram_results'
};

async function find_unliked_like(scope_handle) {
    const handle = await scope_handle.evaluateHandle(root => {
        const sections = root.querySelectorAll('section');
        for (const sec of sections) {
            const clickables = sec.querySelectorAll('button, div[role="button"], a[role="button"], a[role="link"]');
            const icon_buttons = [];
            for (const el of clickables) {
                if (el.querySelectorAll('svg').length !== 1) continue;
                const r = el.getBoundingClientRect();
                if (r.width < 16 || r.height < 16) continue;
                icon_buttons.push(el);
            }
            const top_level = icon_buttons.filter(b =>
                !icon_buttons.some(other => other !== b && other.contains(b))
            );
            if (top_level.length < 3 || top_level.length > 5) continue;

            const heart_btn = top_level[0];
            const heart_svg = heart_btn.querySelector('svg');
            if (!heart_svg) continue;

            const path = heart_svg.querySelector('path');
            if (path) {
                const fill = window.getComputedStyle(path).fill || '';
                const m = fill.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
                if (m && +m[1] > 200 && +m[2] < 100 && +m[3] < 100) continue;
            }
            return heart_svg;
        }
        return null;
    });
    return handle.asElement();
}

function random_range(min, max) {
    return min + Math.random() * (max - min);
}

function random_int(min, max) {
    return Math.floor(random_range(min, max + 1));
}

function pick_random(arr) {
    return arr[Math.floor(Math.random() * arr.length)];
}

async function sleep(seconds) {
    return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}

async function human_delay(min_ms = 50, max_ms = 200) {
    const mu = Math.log((min_ms + max_ms) / 2);
    const sigma = random_range(0.3, 0.6);
    let delay = Math.exp(mu + sigma * (Math.random() - 0.5) * 2);
    delay = Math.min(max_ms, Math.max(min_ms, delay));
    await new Promise(resolve => setTimeout(resolve, delay));
}

async function ensure_dir(dir) {
    await fs.mkdir(dir, { recursive: true });
}

async function dump_state(page, label) {
    try {
        const dir = path.join(__dirname, 'debug');
        await ensure_dir(dir);
        const stamp = new Date().toISOString().replace(/[:.]/g, '-');
        const png = path.join(dir, `${stamp}_${label}.png`);
        const html = path.join(dir, `${stamp}_${label}.html`);
        await page.screenshot({ path: png, fullPage: false });
        const body = await page.evaluate(() => document.documentElement.outerHTML);
        await fs.writeFile(html, body);
        console.log(`📸 dump [${label}] → ${png}`);
    } catch (e) {
        console.warn(`📸 dump [${label}] failed: ${e.message}`);
    }
}

function csv_escape(value) {
    if (value === null || value === undefined) return '';
    const s = String(value);
    if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
    return s;
}

function wind_mouse_trajectory(start, end, options = {}) {
    const {
        gravity = 9,
        wind = 3,
        max_step = 15,
        target_area = 12,
        min_wait_ms = 5,
        max_wait_ms = 12
    } = options;

    let x = start.x, y = start.y;
    let v_x = 0, v_y = 0;
    let w_x = 0, w_y = 0;
    let M = max_step;

    const points = [];
    let prev_x = Math.round(x), prev_y = Math.round(y);

    let safety = 0;
    while (safety++ < 10000) {
        const dist = Math.hypot(end.x - x, end.y - y);
        if (dist < 1) break;

        const w_mag = Math.min(wind, dist);
        if (dist >= target_area) {
            w_x = w_x / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
            w_y = w_y / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
        } else {
            w_x /= Math.sqrt(2);
            w_y /= Math.sqrt(2);
            if (M < 3) M = Math.random() * 3 + 3;
            else M /= Math.sqrt(5);
        }

        v_x += w_x + gravity * (end.x - x) / dist;
        v_y += w_y + gravity * (end.y - y) / dist;

        const v_mag = Math.hypot(v_x, v_y);
        if (v_mag > M) {
            const v_clip = M / 2 + Math.random() * M / 2;
            v_x = (v_x / v_mag) * v_clip;
            v_y = (v_y / v_mag) * v_clip;
        }

        x += v_x;
        y += v_y;

        const rx = Math.round(x);
        const ry = Math.round(y);
        if (rx !== prev_x || ry !== prev_y) {
            const wait = min_wait_ms + Math.random() * (max_wait_ms - min_wait_ms);
            points.push({ x: rx, y: ry, wait });
            prev_x = rx; prev_y = ry;
        }
    }

    points.push({ x: Math.round(end.x), y: Math.round(end.y), wait: 5 });
    return points;
}

async function move_mouse_human(page, target, options = {}) {
    const current = await page.evaluate(() => ({
        x: window.__mouseX ?? window.innerWidth / 2,
        y: window.__mouseY ?? window.innerHeight / 2
    }));

    const trajectory = wind_mouse_trajectory(current, target, options);
    for (const p of trajectory) {
        await page.mouse.move(p.x, p.y);
        if (p.wait > 0) await new Promise(r => setTimeout(r, p.wait));
    }

    await page.evaluate(({ x, y }) => {
        window.__mouseX = x;
        window.__mouseY = y;
    }, target);
}

async function human_click(page, selector_or_handle, options = {}) {
    const {
        overshoot_chance = 0.3,
        scroll_into_view = true,
        post_click_delay = [120, 350]
    } = options;

    const handle = typeof selector_or_handle === 'string'
        ? await page.$(selector_or_handle)
        : selector_or_handle;

    if (!handle) throw new Error(`Element not found: ${selector_or_handle}`);

    if (scroll_into_view) {
        await handle.evaluate(el => el.scrollIntoView({ block: 'center', behavior: 'smooth' }));
        await human_delay(500, 1000);
    }

    const box = await handle.boundingBox();
    if (!box) throw new Error('Could not get element coordinates');

    const target = {
        x: box.x + random_range(box.width * 0.25, box.width * 0.75),
        y: box.y + random_range(box.height * 0.25, box.height * 0.75)
    };

    if (Math.random() < overshoot_chance) {
        const overshoot = {
            x: target.x + (Math.random() - 0.5) * random_range(15, 35),
            y: target.y + (Math.random() - 0.5) * random_range(15, 35)
        };
        await move_mouse_human(page, overshoot);
        await human_delay(40, 120);
        await move_mouse_human(page, target);
    } else {
        await move_mouse_human(page, target);
    }

    await human_delay(post_click_delay[0], post_click_delay[1]);

    await page.mouse.down();
    await human_delay(40, 120);

    if (Math.random() < 0.25) {
        await page.mouse.move(
            target.x + (Math.random() - 0.5) * 2,
            target.y + (Math.random() - 0.5) * 2
        );
    }
    await page.mouse.up();

    return { x: target.x, y: target.y };
}

async function human_scroll_window(page, options = {}) {
    const { scrolls = random_int(2, 5), back_chance = 0.2 } = options;

    for (let i = 0; i < scrolls; i++) {
        const distance = random_range(300, 800);
        await page.evaluate(d => window.scrollBy({ top: d, behavior: 'smooth' }), distance);
        await human_delay(900, 2200);

        if (Math.random() < back_chance) {
            const back = random_range(100, 280);
            await page.evaluate(d => window.scrollBy({ top: -d, behavior: 'smooth' }), back);
            await human_delay(500, 1100);
        }
    }
}

async function human_scroll_element(page, element_handle, distance) {
    const before = await element_handle.evaluate(el => el.scrollTop);

    await element_handle.evaluate((el, d) => {
        el.scrollTop = el.scrollTop + d;
        el.dispatchEvent(new WheelEvent('wheel', {
            deltaY: d,
            bubbles: true,
            cancelable: true
        }));
    }, distance);

    await human_delay(700, 1800);
    const after = await element_handle.evaluate(el => el.scrollTop);
    return after - before;
}

async function dismiss_overlays(page, max_passes = 3) {

    const MODAL_SCOPE = [
        'div[role="dialog"]',
        'div[role="alertdialog"]',
        '[aria-modal="true"]',
        'div[data-testid="cookie-policy-manage-dialog"]',
        'div[aria-label*="cookie" i]',
        'div[aria-label*="Cookie"]'
    ].join(', ');

    let dismissed = 0;
    for (let pass = 0; pass < max_passes; pass++) {
        const modals = await page.$$(MODAL_SCOPE);
        if (modals.length === 0) break;

        const before = modals.length;
        await page.keyboard.press('Escape').catch(() => { });
        await human_delay(600, 1400);

        const after_modals = await page.$$(MODAL_SCOPE);
        if (after_modals.length < before) {
            console.log(`🪟 Modal dismissed via ESC`);
            dismissed++;
            continue;
        }
        break;
    }
    return dismissed;
}

async function check_for_block(page) {
    const url = page.url();
    if (url.includes('/challenge/') || url.includes('/accounts/suspended/') ||
        url.includes('/accounts/disabled/')) {
        return { blocked: true, reason: 'redirect: ' + url };
    }

    const block_text = await page.evaluate(() => {
        const text = document.body.innerText.toLowerCase();
        const markers = [
            'we restricted certain activity',
            'try again later',
            'suspicious login attempt',
            'your account has been temporarily',
            'temporary action restriction',
            'suspicious login'
        ];
        for (const m of markers) if (text.includes(m)) return m;
        return null;
    });

    if (block_text) return { blocked: true, reason: 'text: ' + block_text };
    return { blocked: false };
}

async function check_logged_in(page) {
    const url = page.url();
    if (url.includes('/accounts/login') || url.includes('/accounts/emailsignup')) {
        return false;
    }
    const has_login_form = await page.$('input[name="username"]');
    return !has_login_form;
}

async function watch_random_stories(page, count) {
    console.log(`📺 Trying to watch ${count} stories...`);
    const story_buttons = await page.$$('div[role="menuitem"] button[role="button"], li button[role="button"]');
    const visible_stories = [];
    for (const btn of story_buttons) {
        const box = await btn.boundingBox();
        if (box && box.y < 250 && box.width > 30) visible_stories.push(btn);
    }

    if (visible_stories.length === 0) {
        const fallback = await page.$$('canvas');
        for (const c of fallback) {
            const box = await c.boundingBox();
            if (box && box.y < 250) visible_stories.push(c);
        }
    }

    if (visible_stories.length === 0) {
        console.log('   ⏭ No stories found, skipping');
        return 0;
    }

    let watched = 0;
    const to_watch = Math.min(count, visible_stories.length);
    for (let i = 0; i < to_watch; i++) {
        try {
            const story = pick_random(visible_stories);
            await human_click(page, story);
            await human_delay(3000, 7000);
            await page.keyboard.press('Escape');
            await human_delay(800, 1500);
            watched++;
            console.log(`   👀 Story ${watched}/${to_watch} watched`);
        } catch (e) {
            console.log(`   ⚠️ Error while watching story: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }
    return watched;
}

async function browse_feed_and_like(page, target_likes) {
    console.log(`❤️ Feed warm-up: ${target_likes} likes`);
    await dismiss_overlays(page);

    if (!page.url().match(/instagram\.com\/?(\?|$)/)) {
        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(2000, 3500);
    }

    const articles_appeared = await page.waitForFunction(
        () => document.querySelectorAll('article').length > 0,
        { timeout: 12000 }
    ).then(() => true).catch(() => false);

    if (!articles_appeared) {

        for (let i = 0; i < 3; i++) {
            await human_scroll_window(page, { scrolls: 2 });
            const ok = await page.evaluate(() => document.querySelectorAll('article').length > 0);
            if (ok) break;
        }
    }

    let liked = 0;
    let scrolls_without_progress = 0;

    while (liked < target_likes && scrolls_without_progress < 5) {

        const articles = await page.$$('article');
        let new_like_done = false;

        for (const article of articles) {
            if (liked >= target_likes) break;
            try {

                const like_btn = await find_unliked_like(article);
                if (!like_btn) continue;

                const box = await like_btn.boundingBox();
                if (!box) continue;

                const in_view = await like_btn.evaluate(el => {
                    const r = el.getBoundingClientRect();
                    return r.top > 50 && r.bottom < window.innerHeight - 50;
                });
                if (!in_view) continue;

                if (Math.random() < 0.3) {
                    await human_delay(1500, 4000);
                }

                await human_click(page, like_btn);
                liked++;
                new_like_done = true;
                console.log(`   ❤️ Like ${liked}/${target_likes}`);

                const pause = random_range(
                    config.delay_between_likes.min,
                    config.delay_between_likes.max
                );
                console.log(`   ⏰ Pause ${pause.toFixed(1)} sec`);
                await sleep(pause);

                const block = await check_for_block(page);
                if (block.blocked) {
                    console.log(`   🚫 Blocked after like: ${block.reason}`);
                    return { liked, blocked: true };
                }

                break;
            } catch (e) {
                continue;
            }
        }

        if (!new_like_done) {
            scrolls_without_progress++;
            await human_scroll_window(page, { scrolls: random_int(2, 4), back_chance: 0.05 });
            await human_delay(1500, 3000);
        } else {
            scrolls_without_progress = 0;

            await human_scroll_window(page, { scrolls: random_int(1, 2), back_chance: 0.1 });
            await human_delay(800, 1800);
        }
    }

    console.log(`✅ Warm-up complete, likes: ${liked}`);
    if (liked === 0) await dump_state(page, 'feed_zero_likes');
    return { liked, blocked: false };
}

async function visit_target_profile(page, username) {
    console.log(`🎯 Opening profile @${username}`);
    await page.goto(`https://www.instagram.com/${username}/`, {
        waitUntil: 'domcontentloaded',
        timeout: 30000
    });
    await human_delay(2500, 4500);
    await dismiss_overlays(page);

    const not_found = await page.evaluate(() => {
        const t = document.body.innerText;
        return t.includes("Sorry, this page isn't available")
            || t.includes('Страница недоступна');
    });
    if (not_found) {
        console.log(`   ⚠️ Profile @${username} not found`);
        return false;
    }

    const block = await check_for_block(page);
    if (block.blocked) {
        console.log(`   🚫 Blocked while opening profile: ${block.reason}`);
        return false;
    }

    await human_scroll_window(page, { scrolls: random_int(2, 4) });
    return true;
}

async function like_target_posts(page, count) {
    console.log(`💜 Liking ${count} post(s) on the target account`);

    const post_links = await page.$$('a[href*="/p/"]');
    if (post_links.length === 0) {
        console.log('   ⏭ No posts visible (private account or no publications)');
        return 0;
    }

    let liked = 0;
    const indices = [];
    for (let i = 0; i < count && i < post_links.length; i++) {

        const idx = random_int(0, Math.min(11, post_links.length - 1));
        if (!indices.includes(idx)) indices.push(idx);
    }

    for (const idx of indices) {
        try {
            const post_links_fresh = await page.$$('a[href*="/p/"]');
            if (!post_links_fresh[idx]) continue;

            await human_click(page, post_links_fresh[idx]);
            await human_delay(2500, 4500);

            const post_root = await page.evaluateHandle(() => {
                const dialog = document.querySelector('div[role="dialog"]');
                if (dialog) {
                    const inner_article = dialog.querySelector('article');
                    return inner_article || dialog;
                }
                return document.querySelector('article');
            });
            const post_root_el = post_root.asElement();
            const like_btn = post_root_el ? await find_unliked_like(post_root_el) : null;
            if (like_btn) {
                await human_click(page, like_btn);
                liked++;
                console.log(`   ❤️ Like on post ${liked}/${count}`);
            } else {
                console.log(`   ⏭ Post already liked or like button not found, skipping`);
            }

            if (Math.random() < 0.4) {
                await page.keyboard.press('ArrowRight').catch(() => { });
                await human_delay(800, 1800);
            }

            await human_delay(1500, 3500);
            await page.keyboard.press('Escape');
            await human_delay(1200, 2200);

            const block = await check_for_block(page);
            if (block.blocked) {
                console.log(`   🚫 Blocked after liking a post: ${block.reason}`);
                return liked;
            }

            const pause = random_range(
                config.delay_between_likes.min,
                config.delay_between_likes.max
            );
            await sleep(pause);
        } catch (e) {
            console.log(`   ⚠️ Post-like error: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }

    return liked;
}

function setup_friendship_interceptor(page) {
    const state = {
        last_profile_info: null,
        last_friendship_action: null,
        action_blocked: false
    };

    const handler = async (response) => {
        const url = response.url();

        if (/\/api\/v1\/users\/web_profile_info\//.test(url)) {
            try {
                const json = await response.json();
                const u = json?.data?.user;
                if (u) {
                    state.last_profile_info = {
                        username: u.username,
                        id: u.id,
                        followed_by_viewer: !!u.followed_by_viewer,
                        requested_by_viewer: !!u.requested_by_viewer,
                        is_private: !!u.is_private
                    };
                }
            } catch (e) { }
            return;
        }

        if (/\/api\/v1\/friendships\/(create|destroy)\//.test(url)) {
            try {
                const json = await response.json();
                state.last_friendship_action = { status: response.status(), body: json };
                if (json?.spam || json?.feedback_required || json?.error_type === 'feedback_required') {
                    state.action_blocked = true;
                }
            } catch (e) {
                state.last_friendship_action = { status: response.status(), body: null };
            }
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function fetch_profile_info(page, username) {
    return await page.evaluate(async (u) => {
        try {
            const res = await fetch(`/api/v1/users/web_profile_info/?username=${encodeURIComponent(u)}`, {
                headers: { 'X-IG-App-ID': '936619743392459' },
                credentials: 'include'
            });
            if (!res.ok) return { error: `HTTP ${res.status}` };
            const json = await res.json();
            const user = json?.data?.user;
            if (!user) return { error: 'no user in response body' };
            return {
                username: user.username,
                id: user.id,
                followed_by_viewer: !!user.followed_by_viewer,
                requested_by_viewer: !!user.requested_by_viewer,
                is_private: !!user.is_private
            };
        } catch (e) {
            return { error: e.message };
        }
    }, username);
}

async function ensure_following(page, target_username, friendship_iface) {
    const wait_for = async (predicate, max_ms, step = 200) => {
        const start = Date.now();
        while (Date.now() - start < max_ms) {
            const v = predicate();
            if (v) return v;
            await new Promise(r => setTimeout(r, step));
        }
        return predicate() || null;
    };

    const info = await fetch_profile_info(page, target_username);

    if (!info || info.error || !info.username) {
        console.log(`   ⚠️ Could not fetch profile info via API for @${target_username}: ${info?.error || 'no data'}`);
        return false;
    }

    console.log(`   🔎 API friendship state for @${target_username}: following=${info.followed_by_viewer}, requested=${info.requested_by_viewer}, private=${info.is_private}`);

    if (info.followed_by_viewer) {
        console.log(`   ✓ Already following @${target_username}`);
        return true;
    }
    if (info.requested_by_viewer) {
        console.log(`   ✓ Follow request already pending for @${target_username}`);
        return true;
    }

    await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
    await human_delay(800, 1500);

    const handle = await page.evaluateHandle((username) => {
        const heading_candidates = Array.from(
            document.querySelectorAll('h1, h2, span')
        ).filter(el => {
            const text = (el.textContent || '').trim();
            if (text !== username) return false;
            const r = el.getBoundingClientRect();
            return r.top >= 0 && r.top < 500 && r.width > 30;
        });

        if (heading_candidates.length === 0) return null;
        const anchor = heading_candidates[0];

        let container = anchor;
        for (let depth = 0; depth < 10 && container.parentElement; depth++) {
            container = container.parentElement;
            const buttons = Array.from(container.querySelectorAll('button, div[role="button"]'));
            const candidates = buttons.filter(btn => {
                const r = btn.getBoundingClientRect();
                if (r.width < 70 || r.height < 28 || r.width > 400 || r.height > 80) return false;
                const text = (btn.textContent || '').trim();
                if (text.length === 0 || text.length > 60) return false;
                return btn.querySelectorAll('svg').length <= 1;
            });
            if (candidates.length === 0) continue;
            candidates.sort((a, b) => {
                const ra = a.getBoundingClientRect(), rb = b.getBoundingClientRect();
                if (Math.abs(ra.top - rb.top) > 5) return ra.top - rb.top;
                return ra.left - rb.left;
            });
            return candidates[0];
        }
        return null;
    }, target_username);

    const el = handle.asElement();
    if (!el) {
        console.log(`   ⚠️ Primary action button not found near @${target_username} username anchor`);
        await dump_state(page, `no_follow_btn_${target_username}`);
        return false;
    }

    friendship_iface.state.last_friendship_action = null;
    friendship_iface.state.action_blocked = false;

    console.log(`   ➕ Subscribing to @${target_username}...`);
    await human_click(page, el);
    await human_delay(2500, 4000);

    const after = await fetch_profile_info(page, target_username);
    if (after && !after.error) {
        if (after.followed_by_viewer) {
            console.log(`   ✓ Subscribed (API confirmed)`);
            return true;
        }
        if (after.requested_by_viewer) {
            console.log(`   ✓ Follow request sent (private account)`);
            return true;
        }
    }

    if (friendship_iface.state.action_blocked) {
        const body = friendship_iface.state.last_friendship_action?.body;
        const fb = body?.feedback_message || body?.feedback_title || 'feedback_required';
        console.log(`   🚫 Instagram action-blocked the follow: ${fb}`);
        return false;
    }

    console.log(`   ⚠️ Click sent but API still reports not following — silent block or click missed the toggle`);
    return false;
}

function setup_followers_interceptor(page) {
    const state = {
        has_next_page: true,
        api_seen: false,
        responses: 0,
        users_returned: 0,
        empty_with_next: 0,
        cap_detected: false
    };

    const handler = async (response) => {
        const url = response.url();
        const is_followers_api = /\/friendships\/\d+\/followers\//.test(url);
        const is_graphql = /edge_followed_by/.test(url);
        if (!is_followers_api && !is_graphql) return;

        try {
            const json = await response.json();
            state.api_seen = true;
            state.responses++;

            let batch_size = 0;
            let has_next = state.has_next_page;

            if ('next_max_id' in json) {
                has_next = !!json.next_max_id;
                if (Array.isArray(json.users)) {
                    batch_size = json.users.length;
                    state.users_returned += batch_size;
                }
            } else if (json?.data?.user?.edge_followed_by?.page_info) {
                const pi = json.data.user.edge_followed_by.page_info;
                has_next = !!pi.has_next_page;
                const edges = json.data.user.edge_followed_by.edges;
                if (Array.isArray(edges)) {
                    batch_size = edges.length;
                    state.users_returned += batch_size;
                }
            }

            state.has_next_page = has_next;

            if (has_next && batch_size === 0) {
                state.empty_with_next++;
                if (state.empty_with_next >= 1) state.cap_detected = true;
            } else {
                state.empty_with_next = 0;
            }
        } catch (e) {
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function parse_followers(page, target_username, max_count) {
    console.log(`📋 Parsing followers of @${target_username} (target: ${max_count})`);

    await dismiss_overlays(page);

    const { state: api_state, cleanup: detach_interceptor } = setup_followers_interceptor(page);
    try {
        return await _parse_followers_impl(page, target_username, max_count, api_state);
    } finally {
        detach_interceptor();
    }
}

async function _parse_followers_impl(page, target_username, max_count, api_state) {
    const followers_handle = await page.evaluateHandle((target) => {
        const followers_re = new RegExp(`^/${target}/followers/?(\\?|$)`, 'i');

        const anchors = Array.from(document.querySelectorAll('a[href]'));
        for (const a of anchors) {
            if (followers_re.test(a.getAttribute('href') || '')) return a;
        }

        const all_clickable = Array.from(document.querySelectorAll(
            'a, button, div[role="button"], a[role="link"]'
        ));
        const counters = all_clickable.filter(el => {
            const t = (el.innerText || '').trim();
            if (!t || t.length > 60 || !/\d/.test(t)) return false;
            const r = el.getBoundingClientRect();
            return r.width > 0 && r.height > 0 && r.top < window.innerHeight;
        });

        for (const seed of counters) {
            let node = seed;
            for (let depth = 0; depth < 8 && node.parentElement; depth++) {
                const parent = node.parentElement;
                const row = [];
                for (const child of parent.children) {
                    const hit = counters.find(c => child === c || child.contains(c));
                    if (hit) row.push(hit);
                }
                if (row.length >= 2 && row.length <= 4) {
                    return row[row.length - 2];
                }
                node = parent;
            }
        }
        return null;
    }, target_username);

    const followers_link = followers_handle.asElement();
    if (!followers_link) {
        console.log('   ❌ "Followers" button not found');
        await dump_state(page, `target_${target_username}_no_followers_btn`);
        return [];
    }
    await human_click(page, followers_link);
    await human_delay(1800, 3000);

    await page.waitForSelector('div[role="dialog"]', { timeout: 10000 }).catch(() => { });

    const got_users = await page.waitForFunction(() => {
        const dialog = document.querySelector('div[role="dialog"]');
        if (!dialog) return false;
        return dialog.querySelectorAll('a[role="link"][href^="/"]').length >= 3;
    }, { timeout: 15000 }).then(() => true).catch(() => false);

    if (!got_users) {
        console.log('   ❌ Followers modal did not load the list within 15 sec');
        return [];
    }
    await human_delay(800, 1500);

    const find_scrollable = async () => {
        const handle = await page.evaluateHandle(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return null;
            const all = dialog.querySelectorAll('div');
            let best = null, best_count = 0;
            for (const el of all) {
                const s = window.getComputedStyle(el);
                if (s.overflowY !== 'auto' && s.overflowY !== 'scroll') continue;
                const links = el.querySelectorAll('a[role="link"][href^="/"]').length;
                if (links > best_count) { best = el; best_count = links; }
            }
            return best;
        });
        return handle.asElement();
    };

    let scrollable = await find_scrollable();
    if (!scrollable) {
        console.log('   ❌ Scrollable container of the modal not found');
        return [];
    }
    console.log(`   📜 Container found, initial scrollHeight: ${await scrollable.evaluate(el => el.scrollHeight)}px`);

    const modal_box = await scrollable.boundingBox();
    if (modal_box) {
        await move_mouse_human(page, {
            x: modal_box.x + modal_box.width / 2,
            y: modal_box.y + modal_box.height / 2
        });
        await human_delay(200, 500);
    }

    const collected = new Map();
    let stagnation = 0;
    const max_stagnation = 5;
    let iteration = 0;
    const max_iterations = 200;

    while (collected.size < max_count && stagnation < max_stagnation && iteration < max_iterations) {
        iteration++;

        const batch = await page.evaluate(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return [];
            const links = dialog.querySelectorAll('a[role="link"][href^="/"]');
            const seen = new Map();

            const skip = new Set(['explore', 'reels', 'direct', 'accounts', 'p', 'tv', 'stories', 'about']);

            const button_words = new Set([
                'Follow', 'Following', 'Message', 'Requested', 'Subscribed', 'Subscribe',
                'Подписаться', 'Подписки', 'Подписан', 'Подписана', 'Запрос', 'Сообщение',
                'Отписаться', 'Subscribirse', 'Seguir', 'Suivre'
            ]);

            for (const a of links) {
                const href = a.getAttribute('href');
                const m = href && href.match(/^\/([A-Za-z0-9._]+)\/?$/);
                if (!m) continue;
                const username = m[1];
                if (skip.has(username)) continue;
                if (seen.has(username)) continue;

                let row = a;
                for (let depth = 0; depth < 10 && row.parentElement; depth++) {
                    const parent = row.parentElement;
                    const sibling_links = parent.querySelectorAll('a[role="link"][href^="/"]');
                    let other = 0;
                    for (const l of sibling_links) {
                        const lm = l.getAttribute('href').match(/^\/([A-Za-z0-9._]+)\/?$/);
                        if (lm && lm[1] !== username && !skip.has(lm[1])) { other++; break; }
                    }
                    if (other > 0) break;
                    row = parent;
                }

                const lines = (row.innerText || '')
                    .split(/\n+/).map(s => s.trim()).filter(Boolean);
                let full_name = '';
                for (const line of lines) {
                    if (line === username) continue;
                    if (button_words.has(line)) continue;
                    if (line.length > 150) continue;
                    full_name = line;
                    break;
                }

                const verified = !!row.querySelector('svg[aria-label="Verified"], svg[aria-label*="Verif"], svg[aria-label="Подтвержденный"]');
                const is_private = !!row.querySelector('svg[aria-label="Private"]');

                seen.set(username, { username, full_name, verified, is_private });
            }
            return Array.from(seen.values());
        });

        const before = collected.size;
        for (const u of batch) {
            if (collected.size >= max_count) break;
            if (!collected.has(u.username)) collected.set(u.username, u);
        }
        const added = collected.size - before;

        if (added === 0) stagnation++;
        else stagnation = 0;

        const api_tail = api_state.api_seen
            ? ` [api: ${api_state.responses} resp, ${api_state.users_returned} users, next=${api_state.has_next_page}${api_state.cap_detected ? ', CAP' : ''}]`
            : '';
        process.stdout.write(`   📊 Collected: ${collected.size}/${max_count} (+${added})${api_tail}\r`);

        if (collected.size >= max_count) break;

        if (api_state.api_seen && (!api_state.has_next_page || api_state.cap_detected)) {
            const reason = api_state.cap_detected
                ? `server cap (empty batch with next_max_id present, returned ${api_state.users_returned} users total)`
                : 'has_next_page=false';
            console.log(`\n   🛑 Pagination closed by Instagram: ${reason}`);
            break;
        }

        const wheel_delta = random_range(400, 700);
        await page.mouse.wheel({ deltaY: wheel_delta });
        await human_delay(900, 1700);

        if (stagnation >= 1) {
            await scrollable.evaluate(el => {
                const links = el.querySelectorAll('a[role="link"][href^="/"]');
                if (links.length > 0) {
                    links[links.length - 1].scrollIntoView({ block: 'end' });
                }
            });
            await human_delay(700, 1300);

            const fresh = await find_scrollable();
            if (fresh) scrollable = fresh;
        }

        if (Math.random() < 0.2) await human_delay(1500, 3500);
    }

    console.log(`\n   ✅ Followers collected: ${collected.size}`);

    await page.keyboard.press('Escape').catch(() => { });
    await human_delay(800, 1500);

    return Array.from(collected.values()).slice(0, max_count);
}

async function save_followers(profile_uuid, target, users) {
    const dir = path.join(__dirname, config.results_dir);
    await ensure_dir(dir);

    const stamp = new Date().toISOString().replace(/[:.]/g, '-');
    const base = `${target}_${profile_uuid.slice(0, 8)}_${stamp}`;

    const json_path = path.join(dir, `${base}.json`);
    const csv_path = path.join(dir, `${base}.csv`);

    const json_payload = {
        target_account: target,
        parsed_by_profile: profile_uuid,
        timestamp: new Date().toISOString(),
        total: users.length,
        followers: users
    };
    await fs.writeFile(json_path, JSON.stringify(json_payload, null, 2), 'utf8');

    const csv_lines = ['username,full_name,verified,is_private'];
    for (const u of users) {
        csv_lines.push([
            csv_escape(u.username),
            csv_escape(u.full_name),
            csv_escape(u.verified),
            csv_escape(u.is_private)
        ].join(','));
    }
    await fs.writeFile(csv_path, csv_lines.join('\n'), 'utf8');

    console.log(`💾 JSON: ${json_path}`);
    console.log(`💾 CSV : ${csv_path}`);
}

async function process_target(page, target, profile_uuid) {
    const friendship_iface = setup_friendship_interceptor(page);
    try {
        const ok = await visit_target_profile(page, target);
        if (!ok) return { target, success: false };

        let followed = false;
        if (config.follow_targets_before_parsing) {
            followed = await ensure_following(page, target, friendship_iface);
            if (followed) await human_delay(3000, 6000);
        }

        const target_likes = random_int(config.likes_on_target.min, config.likes_on_target.max);
        if (target_likes > 0) {
            await like_target_posts(page, target_likes);
        }

        await page.goto(`https://www.instagram.com/${target}/`, {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(1500, 3000);

        const users = await parse_followers(page, target, config.followers_per_target);
        if (users.length > 0) {
            await save_followers(profile_uuid, target, users);
        }

        return { target, success: true, count: users.length, followed };
    } finally {
        friendship_iface.cleanup();
    }
}

async function process_profile(profile_cfg, idx, total) {
    console.log(`\n${'='.repeat(80)}`);
    console.log(`📋 Profile ${idx + 1}/${total} — UUID ${profile_cfg.uuid}`);
    console.log(`   Targets: ${profile_cfg.target_accounts.join(', ')}`);
    console.log(`${'='.repeat(80)}`);

    await octo_stop_profile(profile_cfg.uuid).catch(() => { });
    await sleep(3);

    let ws_data;
    try {
        ws_data = await octo_start_profile(profile_cfg.uuid);
    } catch (e) {
        const body = e.response?.data;
        const body_str = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : '';
        console.error(`❌ Failed to start profile: ${e.message}${body_str ? ' | Octo: ' + body_str : ''}`);
        return { uuid: profile_cfg.uuid, status: 'start_failed', error: body_str || e.message };
    }

    if (!ws_data?.ws_endpoint) {
        console.error('❌ Octo did not return a ws_endpoint');
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'no_ws' };
    }

    let browser;
    try {
        browser = await puppeteer.connect({
            browserWSEndpoint: ws_data.ws_endpoint,
            defaultViewport: null,
            protocolTimeout: 600000
        });
    } catch (e) {
        console.error(`❌ Puppeteer connect: ${e.message}`);
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'connect_failed' };
    }

    try {
        const ctx = browser.defaultBrowserContext();
        await ctx.overridePermissions('https://www.instagram.com', []);
        await ctx.overridePermissions('https://instagram.com', []);
    } catch (e) {
        console.warn(`⚠️ overridePermissions: ${e.message}`);
    }

    let stats = { uuid: profile_cfg.uuid, status: 'ok', targets: [], likes: 0, stories: 0 };

    try {
        const page = await browser.newPage();
        await page.setViewport({ width: 1280, height: 900 });

        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 45000
        });
        await human_delay(2500, 4500);
        await dismiss_overlays(page);

        await human_delay(800, 1500);
        await dismiss_overlays(page);

        if (!await check_logged_in(page)) {
            console.error('❌ Profile is not logged into Instagram');
            stats.status = 'not_logged_in';
            return stats;
        }

        const block = await check_for_block(page);
        if (block.blocked) {
            console.error(`❌ Account is restricted: ${block.reason}`);
            stats.status = 'blocked';
            return stats;
        }

        if (Math.random() < config.stories_probability) {
            const cnt = random_int(config.stories_per_session.min, config.stories_per_session.max);
            stats.stories = await watch_random_stories(page, cnt);
            await human_delay(1500, 3000);
        }

        const feed_likes = random_int(config.likes_per_session.min, config.likes_per_session.max);
        const feed_result = await browse_feed_and_like(page, feed_likes);
        stats.likes += feed_result.liked;
        if (feed_result.blocked) {
            stats.status = 'blocked_during_feed';
            return stats;
        }

        for (let i = 0; i < profile_cfg.target_accounts.length; i++) {
            const target = profile_cfg.target_accounts[i];
            try {
                const r = await process_target(page, target, profile_cfg.uuid);
                stats.targets.push(r);
            } catch (e) {
                console.error(`❌ Target @${target} error: ${e.message}`);
                stats.targets.push({ target, success: false, error: e.message });
            }

            if (i < profile_cfg.target_accounts.length - 1) {
                const pause = random_range(
                    config.delay_between_targets.min,
                    config.delay_between_targets.max
                );
                console.log(`⏰ Pause between targets: ${pause.toFixed(1)} sec`);
                await sleep(pause);
            }
        }

    } catch (e) {
        console.error(`❌ Profile processing error: ${e.message}`);
        stats.status = 'error';
        stats.error = e.message;
    } finally {
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        await sleep(2);
    }

    return stats;
}

async function check_limits(response) {
    const header = response.headers.ratelimit;
    if (!header) return;
    const entries = header.split(',').map(s => s.trim());
    for (const e of entries) {
        const r_match = e.match(/;r=(\d+)/);
        const t_match = e.match(/;t=(\d+)/);
        if (!r_match || !t_match) continue;
        const remaining = parseInt(r_match[1], 10);
        const window_s = parseInt(t_match[1], 10);
        if (remaining < 5) {
            console.log(`⏳ Octo rate-limit, waiting ${window_s + 1} sec`);
            await sleep(window_s + 1);
        }
    }
}

async function octo_start_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/start`,
        headers: { 'Content-Type': 'application/json' },
        data: {
            uuid,
            headless: config.headless_mode,
            debug_port: true,
            timeout: 60
        }
    });
    await check_limits(res);
    return res.data;
}

async function octo_stop_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/stop`,
        headers: { 'Content-Type': 'application/json' },
        data: { uuid }
    });
    await check_limits(res);
    return res.data;
}

(async () => {
    console.log('🚀 Octo Instagram Parser & Liker');
    console.log(`   Profiles: ${config.profiles.length}`);
    console.log(`   Followers per target: ${config.followers_per_target}`);
    console.log(`   Feed likes: ${config.likes_per_session.min}${config.likes_per_session.max}`);
    console.log('');

    await ensure_dir(path.join(__dirname, config.results_dir));

    const all_stats = [];
    for (let i = 0; i < config.profiles.length; i++) {
        const stats = await process_profile(config.profiles[i], i, config.profiles.length);
        all_stats.push(stats);

        if (i < config.profiles.length - 1) {
            const pause = random_range(
                config.delay_between_profiles.min,
                config.delay_between_profiles.max
            );
            console.log(`\n⏰ Pause before the next profile: ${pause.toFixed(1)} sec`);
            await sleep(pause);
        }
    }

    console.log(`\n${'='.repeat(80)}`);
    console.log('📊 SUMMARY');
    console.log('='.repeat(80));
    for (const s of all_stats) {
        console.log(`\n${s.uuid}${s.status}`);
        console.log(`   Likes: ${s.likes ?? 0}, stories: ${s.stories ?? 0}`);
        if (s.targets) {
            for (const t of s.targets) {
                if (t.success) console.log(`   ✅ @${t.target}: ${t.count} followers`);
                else console.log(`   ❌ @${t.target}: ${t.error || 'fail'}`);
            }
        }
    }

    const summary_path = path.join(__dirname, config.results_dir, `_summary_${Date.now()}.json`);
    await fs.writeFile(summary_path, JSON.stringify(all_stats, null, 2));
    console.log(`\n📄 Summary report: ${summary_path}`);
    console.log('🎉 Done.');
})();

Scaling and account safety

Checklist before launch:

  1. Profiles are properly prepared. The account should be at least 2–3 weeks old, have a profile picture, posts, and some following activity.

  2. Do not use datacenter proxies. Use residential or mobile proxies.

  3. Verify that selectors are up to date. Before running the script across a large number of accounts, test it on a few accounts and confirm that all selectors work correctly.

  4. Add delays between runs. Our script processes the profiles in config.profiles sequentially, with a 60–120 second pause between them. If you use more accounts, increase this interval.

Conclusion

This single script covers two of the most popular use cases: follower scraping (for competitor research and lookalike audience building) and mass liking (for account prep). It can be easily extended with additional functionality:

  • Scraping users who liked or commented on a specific post for the most engaged audience segment.

  • Collecting bio information and email addresses from commenters. This can be moved into a separate pipeline to avoid overloading the scraper account with extra actions.

  • Comparing follower lists from multiple competitors and identifying overlaps. This is straightforward to implement in pure Node.js using the collected JSON data.

  • Adding a caching layer to avoid scraping the same followers repeatedly across multiple runs.

If you work at scale, you can also adapt the script for parallel execution.

Successful scraping relies on a multi-layered system. Octo Browser provides a consistent fingerprint and profile isolation, rebrowser-puppeteer delivers automation with fixes for common automation-detection leaks, the WindMouse algorithm generates realistic cursor movement, and the behavioral layer introduces distractions and pauses. Ultimately, system resilience depends not on any single component, but on how effectively all of them work together.

Stay anonymous, take advantage of multi-accounting, and achieve your goals with the highest-quality anti-detect browser on the market.

What people scrape from Instagram and why

After analyzing the market for Instagram tools and services in 2025–2026, we can identify six main types of data that are commonly scraped:

Data

Fields

Purpose

Account followers

username, full_name, verified, is_private, bio

Competitor research, finding high-quality leads, audience analysis for Meta Ads

Account follows

username, full_name, verified, is_private, bio

Competitor and influencer target audience analysis

Users who liked a post

username, full_name

Finding users with higher engagement than average followers

Commenters

username, comment text

DM outreach and sentiment analysis

Posts by hashtag/location

post_url, likes, caption

Content analysis and UGC discovery

Full profile

bio, posts, ER

Influencer evaluation before collaboration

Main use cases:

  1. Competitor research. Scrape the followers of 3–5 competitors, remove duplicates, and identify overlaps. The highest-quality leads are often users who follow several competitors at the same time.

  2. Influencer/blogger evaluation. Before paying for a collaboration, collect follower data and assess how genuine the audience is.

  3. DM outreach and cold email. In SaaS, marketing, and e-commerce niches, 15–35% of profiles include a public email address in their bio.

  4. Lookalike Audiences in Meta Ads. Usernames can be converted into a Custom Audience seed for creating Lookalike Audiences.

  5. Account prep through mass liking. Used to increase reach and establish a pattern of normal account activity.

Why ordinary bots get detected in 2026

The biggest change in recent years is that Instagram now hides content from unauthenticated users. Previously, you could query public endpoints such as /?__a=1 without a session. In 2026, even the followers of a public account are unavailable unless you're logged in.

What you'll need for your script:

1. A real logged-in account. Preferably a well-prepared account rather than one that was just created. Octo Browser helps here: you can create an account, log in manually, let it age naturally, and only then automate it.

2. One account means one fingerprint. Running 20 Instagram accounts from a single Chrome instance is a guaranteed way to get banned. With Octo, you can use isolated profiles with different fingerprints (WebGL parameters, screen resolution, fonts, user-agent, etc.).

3. A proxy for every profile. Mobile or residential proxies are ideal. Using datacenter IP addresses is extremely risky.

However, even with a proper browser fingerprint and a reliable proxy, a Puppeteer bot will not survive without some level of human behavior emulation. Anti-bot systems analyze:

  • Frequency patterns. How many likes per hour, how many follows per day, and the intervals between actions. Server-side analytics will quickly flag a bot that likes a post every 30 seconds.

  • Browser fingerprinting. This is why using an anti-detect browser is critical.

  • TLS/network fingerprinting. This is why quality proxies and a non-custom TLS stack matter.

  • Automation framework vulnerabilities (navigator.webdriver, CDP presence, distinctive traces in Error.stack).

Practical takeaway: in 2026, the main reasons accounts get banned are behavioral frequency patterns (too many actions in a short period of time, overly consistent intervals), browser fingerprints, and proxies. Emulating mouse movements, scrolling, or typing is not strictly necessary yet. In practice, however, these signals may still be used to analyze behavior and increase an account's fraud score.

That’s why our script architecture consists of:

  1. Octo Browser: isolated fingerprint, proxy, and a profile with a real Instagram session.

  2. Rebrowser-Puppeteer: a Puppeteer fork without the common leaks that are relatively easy to detect.

  3. WindMouse: a physics-based mouse movement model.

  4. A behavioral layer: random story views and post likes before scraping, log-normal pause distributions, overshoots (missing a target and correcting), and reactions to checkpoints.

WindMouse: why it's better than cubic Bézier curves

In our previous article, we used cubic Bézier curves to simulate mouse movement. This is a basic approach, but it has weaknesses: with only two control points, the curve is smooth and predictable. The shape is always similar, speed is controlled separately through an easing parameter, which creates overly uniform movement, and micro-jitter is added as a separate layer, making it obvious that the movement consists of a smooth curve plus artificial noise.

WindMouse simulates real physics instead. Gravity pulls the cursor toward the target, while wind introduces random deviations that accumulate with inertia. As a result, the movement path is smooth rather than jerky. Speed is limited by a maximum step size and naturally decreases near the destination, just like a real person's cursor when aiming at an object.

The result is a trajectory with variable speed, natural deceleration, and subtle micro-movements that are indistinguishable from real human behavior. Most importantly, because of the random wind component, the path is different every time.

We add two more human-like behaviors:

  • Overshoot: in 30% of cases, the cursor intentionally misses the button slightly, then moves back to it.

  • Click jitter: between mouse.down() and mouse.up(), the cursor shifts by 1–2 pixels in a random direction.

The WindMouse implementation in the script is the wind_mouse_trajectory function. It generates an array of trajectory points that are later used by page.mouse.move().

Ready-made script for scraping target account followers and prepping accounts simultaneously

Let’s assume you already have an array of Octo profile UUIDs logged into Instagram accounts. Our script will launch each profile, prep the account by liking posts in the feed and viewing Stories, and then proceed to the Instagram profiles specified as scraping targets. There, it will like posts, follow accounts, and finally scrape the follower list into both JSON and CSV formats. All of this is done with natural mouse movement, randomized delays, and checkpoint detection. As a result, Instagram cannot distinguish your automation from normal user activity.

Preparing profiles

  1. Create one or more profiles in Octo and assign a proxy to each. Residential or mobile proxies are recommended.

  2. Open each profile in Octo manually. Log in to or register an Instagram account. For two to three days, use the account like a regular person: like posts, watch Stories, and follow some users.

  3. Copy the profile UUIDs from Octo. You will need them when filling out the script configuration.

Getting started

  1. Download and install VS Code.

  2. Download and install Node.js.

  3. Create a folder anywhere on your device and name it, for example, octo_instagram_scraper.

  4. Open the folder in VS Code.

  5. Create a .js file. It is best to name it after the action the script performs to avoid confusion. For example, octo_instagram_scraper.js.

  6. Paste the script code into the file.

  7. Fill in the configuration in the config variable.

UUID — Octo profile IDs.

target_accounts — the accounts you want to scrape.

followers_per_target — the number of followers you want to collect from each target account during a single run.

The remaining parameters in config control realistic scraper behavior. You can experiment with them or leave them unchanged.

Fill in the configuration in the config variable
  1. Open your terminal and run the following command to install the required Node.js dependencies: npm i rebrowser-puppeteer axios

Open your terminal and run the following command to install the required Node.js dependencies: npm i rebrowser-puppeteer axios

If VS Code displays an error, open Windows PowerShell as Administrator, run the following command, and confirm the change:

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

Then repeat the previous step.

  1. Launch Octo Browser.

  2. . Run the script in Visual Studio Code (Ctrl/Cmd + F5) and wait for it to finish.

The scraper will sequentially launch the profiles specified in your configuration. Then, just like a regular user, it will watch Stories, scroll the feed, like posts, follow accounts, and scrape followers from the target accounts. You can monitor the process in the debug console.

Parser results summary. In this example, two target accounts were processed from each profile: 3 out of 4 runs completed successfully.

Parser results summary. In this example, two target accounts were processed from each profile: 3 out of 4 runs completed successfully.

Keep in mind that Instagram may limit the number of followers returned, depending on various factors such as account prep level, the number of mutual connections, whether the account follows the target account, and more. If something does not work as expected, experiment with additional prep activity or switch accounts and proxies.

Scraper results in CSV format

Scraper results in CSV format

Scraper results in JSON format

Scraper results in JSON format

JSON is more convenient for further processing in code, while CSV is ideal for importing into Excel, Google Sheets, CRM systems, or uploading directly to Meta Ads as a Custom Audience.

Script code

const axios = require('axios');
const puppeteer = require('rebrowser-puppeteer');
const fs = require('fs').promises;
const path = require('path');

const config = {
    octo_local_api_base_url: `http://localhost:58888/api/profiles`,
    headless_mode: false,
    profiles: [
        {
            uuid: "f7ac08ecae1b4a528b843bc4706ef3dd",
            target_accounts: ["phd_balance", "microbialecology"]
        },
        {
            uuid: "22be57d5c6f44e368258dc5ad6b425d3",
            target_accounts: ["the_brain_scientist", "drkaranrajan"]
        }
    ],
    followers_per_target: 100,
    follow_targets_before_parsing: true,
    likes_per_session: { min: 1, max: 3 },
    likes_on_target: { min: 1, max: 4 },
    stories_probability: 0.3,
    stories_per_session: { min: 1, max: 3 },
    delay_between_likes: { min: 5, max: 10 },
    delay_between_targets: { min: 60, max: 120 },
    delay_between_profiles: { min: 60, max: 120 },
    results_dir: 'instagram_results'
};

async function find_unliked_like(scope_handle) {
    const handle = await scope_handle.evaluateHandle(root => {
        const sections = root.querySelectorAll('section');
        for (const sec of sections) {
            const clickables = sec.querySelectorAll('button, div[role="button"], a[role="button"], a[role="link"]');
            const icon_buttons = [];
            for (const el of clickables) {
                if (el.querySelectorAll('svg').length !== 1) continue;
                const r = el.getBoundingClientRect();
                if (r.width < 16 || r.height < 16) continue;
                icon_buttons.push(el);
            }
            const top_level = icon_buttons.filter(b =>
                !icon_buttons.some(other => other !== b && other.contains(b))
            );
            if (top_level.length < 3 || top_level.length > 5) continue;

            const heart_btn = top_level[0];
            const heart_svg = heart_btn.querySelector('svg');
            if (!heart_svg) continue;

            const path = heart_svg.querySelector('path');
            if (path) {
                const fill = window.getComputedStyle(path).fill || '';
                const m = fill.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
                if (m && +m[1] > 200 && +m[2] < 100 && +m[3] < 100) continue;
            }
            return heart_svg;
        }
        return null;
    });
    return handle.asElement();
}

function random_range(min, max) {
    return min + Math.random() * (max - min);
}

function random_int(min, max) {
    return Math.floor(random_range(min, max + 1));
}

function pick_random(arr) {
    return arr[Math.floor(Math.random() * arr.length)];
}

async function sleep(seconds) {
    return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}

async function human_delay(min_ms = 50, max_ms = 200) {
    const mu = Math.log((min_ms + max_ms) / 2);
    const sigma = random_range(0.3, 0.6);
    let delay = Math.exp(mu + sigma * (Math.random() - 0.5) * 2);
    delay = Math.min(max_ms, Math.max(min_ms, delay));
    await new Promise(resolve => setTimeout(resolve, delay));
}

async function ensure_dir(dir) {
    await fs.mkdir(dir, { recursive: true });
}

async function dump_state(page, label) {
    try {
        const dir = path.join(__dirname, 'debug');
        await ensure_dir(dir);
        const stamp = new Date().toISOString().replace(/[:.]/g, '-');
        const png = path.join(dir, `${stamp}_${label}.png`);
        const html = path.join(dir, `${stamp}_${label}.html`);
        await page.screenshot({ path: png, fullPage: false });
        const body = await page.evaluate(() => document.documentElement.outerHTML);
        await fs.writeFile(html, body);
        console.log(`📸 dump [${label}] → ${png}`);
    } catch (e) {
        console.warn(`📸 dump [${label}] failed: ${e.message}`);
    }
}

function csv_escape(value) {
    if (value === null || value === undefined) return '';
    const s = String(value);
    if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
    return s;
}

function wind_mouse_trajectory(start, end, options = {}) {
    const {
        gravity = 9,
        wind = 3,
        max_step = 15,
        target_area = 12,
        min_wait_ms = 5,
        max_wait_ms = 12
    } = options;

    let x = start.x, y = start.y;
    let v_x = 0, v_y = 0;
    let w_x = 0, w_y = 0;
    let M = max_step;

    const points = [];
    let prev_x = Math.round(x), prev_y = Math.round(y);

    let safety = 0;
    while (safety++ < 10000) {
        const dist = Math.hypot(end.x - x, end.y - y);
        if (dist < 1) break;

        const w_mag = Math.min(wind, dist);
        if (dist >= target_area) {
            w_x = w_x / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
            w_y = w_y / Math.sqrt(3) + (Math.random() * 2 - 1) * w_mag / Math.sqrt(5);
        } else {
            w_x /= Math.sqrt(2);
            w_y /= Math.sqrt(2);
            if (M < 3) M = Math.random() * 3 + 3;
            else M /= Math.sqrt(5);
        }

        v_x += w_x + gravity * (end.x - x) / dist;
        v_y += w_y + gravity * (end.y - y) / dist;

        const v_mag = Math.hypot(v_x, v_y);
        if (v_mag > M) {
            const v_clip = M / 2 + Math.random() * M / 2;
            v_x = (v_x / v_mag) * v_clip;
            v_y = (v_y / v_mag) * v_clip;
        }

        x += v_x;
        y += v_y;

        const rx = Math.round(x);
        const ry = Math.round(y);
        if (rx !== prev_x || ry !== prev_y) {
            const wait = min_wait_ms + Math.random() * (max_wait_ms - min_wait_ms);
            points.push({ x: rx, y: ry, wait });
            prev_x = rx; prev_y = ry;
        }
    }

    points.push({ x: Math.round(end.x), y: Math.round(end.y), wait: 5 });
    return points;
}

async function move_mouse_human(page, target, options = {}) {
    const current = await page.evaluate(() => ({
        x: window.__mouseX ?? window.innerWidth / 2,
        y: window.__mouseY ?? window.innerHeight / 2
    }));

    const trajectory = wind_mouse_trajectory(current, target, options);
    for (const p of trajectory) {
        await page.mouse.move(p.x, p.y);
        if (p.wait > 0) await new Promise(r => setTimeout(r, p.wait));
    }

    await page.evaluate(({ x, y }) => {
        window.__mouseX = x;
        window.__mouseY = y;
    }, target);
}

async function human_click(page, selector_or_handle, options = {}) {
    const {
        overshoot_chance = 0.3,
        scroll_into_view = true,
        post_click_delay = [120, 350]
    } = options;

    const handle = typeof selector_or_handle === 'string'
        ? await page.$(selector_or_handle)
        : selector_or_handle;

    if (!handle) throw new Error(`Element not found: ${selector_or_handle}`);

    if (scroll_into_view) {
        await handle.evaluate(el => el.scrollIntoView({ block: 'center', behavior: 'smooth' }));
        await human_delay(500, 1000);
    }

    const box = await handle.boundingBox();
    if (!box) throw new Error('Could not get element coordinates');

    const target = {
        x: box.x + random_range(box.width * 0.25, box.width * 0.75),
        y: box.y + random_range(box.height * 0.25, box.height * 0.75)
    };

    if (Math.random() < overshoot_chance) {
        const overshoot = {
            x: target.x + (Math.random() - 0.5) * random_range(15, 35),
            y: target.y + (Math.random() - 0.5) * random_range(15, 35)
        };
        await move_mouse_human(page, overshoot);
        await human_delay(40, 120);
        await move_mouse_human(page, target);
    } else {
        await move_mouse_human(page, target);
    }

    await human_delay(post_click_delay[0], post_click_delay[1]);

    await page.mouse.down();
    await human_delay(40, 120);

    if (Math.random() < 0.25) {
        await page.mouse.move(
            target.x + (Math.random() - 0.5) * 2,
            target.y + (Math.random() - 0.5) * 2
        );
    }
    await page.mouse.up();

    return { x: target.x, y: target.y };
}

async function human_scroll_window(page, options = {}) {
    const { scrolls = random_int(2, 5), back_chance = 0.2 } = options;

    for (let i = 0; i < scrolls; i++) {
        const distance = random_range(300, 800);
        await page.evaluate(d => window.scrollBy({ top: d, behavior: 'smooth' }), distance);
        await human_delay(900, 2200);

        if (Math.random() < back_chance) {
            const back = random_range(100, 280);
            await page.evaluate(d => window.scrollBy({ top: -d, behavior: 'smooth' }), back);
            await human_delay(500, 1100);
        }
    }
}

async function human_scroll_element(page, element_handle, distance) {
    const before = await element_handle.evaluate(el => el.scrollTop);

    await element_handle.evaluate((el, d) => {
        el.scrollTop = el.scrollTop + d;
        el.dispatchEvent(new WheelEvent('wheel', {
            deltaY: d,
            bubbles: true,
            cancelable: true
        }));
    }, distance);

    await human_delay(700, 1800);
    const after = await element_handle.evaluate(el => el.scrollTop);
    return after - before;
}

async function dismiss_overlays(page, max_passes = 3) {

    const MODAL_SCOPE = [
        'div[role="dialog"]',
        'div[role="alertdialog"]',
        '[aria-modal="true"]',
        'div[data-testid="cookie-policy-manage-dialog"]',
        'div[aria-label*="cookie" i]',
        'div[aria-label*="Cookie"]'
    ].join(', ');

    let dismissed = 0;
    for (let pass = 0; pass < max_passes; pass++) {
        const modals = await page.$$(MODAL_SCOPE);
        if (modals.length === 0) break;

        const before = modals.length;
        await page.keyboard.press('Escape').catch(() => { });
        await human_delay(600, 1400);

        const after_modals = await page.$$(MODAL_SCOPE);
        if (after_modals.length < before) {
            console.log(`🪟 Modal dismissed via ESC`);
            dismissed++;
            continue;
        }
        break;
    }
    return dismissed;
}

async function check_for_block(page) {
    const url = page.url();
    if (url.includes('/challenge/') || url.includes('/accounts/suspended/') ||
        url.includes('/accounts/disabled/')) {
        return { blocked: true, reason: 'redirect: ' + url };
    }

    const block_text = await page.evaluate(() => {
        const text = document.body.innerText.toLowerCase();
        const markers = [
            'we restricted certain activity',
            'try again later',
            'suspicious login attempt',
            'your account has been temporarily',
            'temporary action restriction',
            'suspicious login'
        ];
        for (const m of markers) if (text.includes(m)) return m;
        return null;
    });

    if (block_text) return { blocked: true, reason: 'text: ' + block_text };
    return { blocked: false };
}

async function check_logged_in(page) {
    const url = page.url();
    if (url.includes('/accounts/login') || url.includes('/accounts/emailsignup')) {
        return false;
    }
    const has_login_form = await page.$('input[name="username"]');
    return !has_login_form;
}

async function watch_random_stories(page, count) {
    console.log(`📺 Trying to watch ${count} stories...`);
    const story_buttons = await page.$$('div[role="menuitem"] button[role="button"], li button[role="button"]');
    const visible_stories = [];
    for (const btn of story_buttons) {
        const box = await btn.boundingBox();
        if (box && box.y < 250 && box.width > 30) visible_stories.push(btn);
    }

    if (visible_stories.length === 0) {
        const fallback = await page.$$('canvas');
        for (const c of fallback) {
            const box = await c.boundingBox();
            if (box && box.y < 250) visible_stories.push(c);
        }
    }

    if (visible_stories.length === 0) {
        console.log('   ⏭ No stories found, skipping');
        return 0;
    }

    let watched = 0;
    const to_watch = Math.min(count, visible_stories.length);
    for (let i = 0; i < to_watch; i++) {
        try {
            const story = pick_random(visible_stories);
            await human_click(page, story);
            await human_delay(3000, 7000);
            await page.keyboard.press('Escape');
            await human_delay(800, 1500);
            watched++;
            console.log(`   👀 Story ${watched}/${to_watch} watched`);
        } catch (e) {
            console.log(`   ⚠️ Error while watching story: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }
    return watched;
}

async function browse_feed_and_like(page, target_likes) {
    console.log(`❤️ Feed warm-up: ${target_likes} likes`);
    await dismiss_overlays(page);

    if (!page.url().match(/instagram\.com\/?(\?|$)/)) {
        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(2000, 3500);
    }

    const articles_appeared = await page.waitForFunction(
        () => document.querySelectorAll('article').length > 0,
        { timeout: 12000 }
    ).then(() => true).catch(() => false);

    if (!articles_appeared) {

        for (let i = 0; i < 3; i++) {
            await human_scroll_window(page, { scrolls: 2 });
            const ok = await page.evaluate(() => document.querySelectorAll('article').length > 0);
            if (ok) break;
        }
    }

    let liked = 0;
    let scrolls_without_progress = 0;

    while (liked < target_likes && scrolls_without_progress < 5) {

        const articles = await page.$$('article');
        let new_like_done = false;

        for (const article of articles) {
            if (liked >= target_likes) break;
            try {

                const like_btn = await find_unliked_like(article);
                if (!like_btn) continue;

                const box = await like_btn.boundingBox();
                if (!box) continue;

                const in_view = await like_btn.evaluate(el => {
                    const r = el.getBoundingClientRect();
                    return r.top > 50 && r.bottom < window.innerHeight - 50;
                });
                if (!in_view) continue;

                if (Math.random() < 0.3) {
                    await human_delay(1500, 4000);
                }

                await human_click(page, like_btn);
                liked++;
                new_like_done = true;
                console.log(`   ❤️ Like ${liked}/${target_likes}`);

                const pause = random_range(
                    config.delay_between_likes.min,
                    config.delay_between_likes.max
                );
                console.log(`   ⏰ Pause ${pause.toFixed(1)} sec`);
                await sleep(pause);

                const block = await check_for_block(page);
                if (block.blocked) {
                    console.log(`   🚫 Blocked after like: ${block.reason}`);
                    return { liked, blocked: true };
                }

                break;
            } catch (e) {
                continue;
            }
        }

        if (!new_like_done) {
            scrolls_without_progress++;
            await human_scroll_window(page, { scrolls: random_int(2, 4), back_chance: 0.05 });
            await human_delay(1500, 3000);
        } else {
            scrolls_without_progress = 0;

            await human_scroll_window(page, { scrolls: random_int(1, 2), back_chance: 0.1 });
            await human_delay(800, 1800);
        }
    }

    console.log(`✅ Warm-up complete, likes: ${liked}`);
    if (liked === 0) await dump_state(page, 'feed_zero_likes');
    return { liked, blocked: false };
}

async function visit_target_profile(page, username) {
    console.log(`🎯 Opening profile @${username}`);
    await page.goto(`https://www.instagram.com/${username}/`, {
        waitUntil: 'domcontentloaded',
        timeout: 30000
    });
    await human_delay(2500, 4500);
    await dismiss_overlays(page);

    const not_found = await page.evaluate(() => {
        const t = document.body.innerText;
        return t.includes("Sorry, this page isn't available")
            || t.includes('Страница недоступна');
    });
    if (not_found) {
        console.log(`   ⚠️ Profile @${username} not found`);
        return false;
    }

    const block = await check_for_block(page);
    if (block.blocked) {
        console.log(`   🚫 Blocked while opening profile: ${block.reason}`);
        return false;
    }

    await human_scroll_window(page, { scrolls: random_int(2, 4) });
    return true;
}

async function like_target_posts(page, count) {
    console.log(`💜 Liking ${count} post(s) on the target account`);

    const post_links = await page.$$('a[href*="/p/"]');
    if (post_links.length === 0) {
        console.log('   ⏭ No posts visible (private account or no publications)');
        return 0;
    }

    let liked = 0;
    const indices = [];
    for (let i = 0; i < count && i < post_links.length; i++) {

        const idx = random_int(0, Math.min(11, post_links.length - 1));
        if (!indices.includes(idx)) indices.push(idx);
    }

    for (const idx of indices) {
        try {
            const post_links_fresh = await page.$$('a[href*="/p/"]');
            if (!post_links_fresh[idx]) continue;

            await human_click(page, post_links_fresh[idx]);
            await human_delay(2500, 4500);

            const post_root = await page.evaluateHandle(() => {
                const dialog = document.querySelector('div[role="dialog"]');
                if (dialog) {
                    const inner_article = dialog.querySelector('article');
                    return inner_article || dialog;
                }
                return document.querySelector('article');
            });
            const post_root_el = post_root.asElement();
            const like_btn = post_root_el ? await find_unliked_like(post_root_el) : null;
            if (like_btn) {
                await human_click(page, like_btn);
                liked++;
                console.log(`   ❤️ Like on post ${liked}/${count}`);
            } else {
                console.log(`   ⏭ Post already liked or like button not found, skipping`);
            }

            if (Math.random() < 0.4) {
                await page.keyboard.press('ArrowRight').catch(() => { });
                await human_delay(800, 1800);
            }

            await human_delay(1500, 3500);
            await page.keyboard.press('Escape');
            await human_delay(1200, 2200);

            const block = await check_for_block(page);
            if (block.blocked) {
                console.log(`   🚫 Blocked after liking a post: ${block.reason}`);
                return liked;
            }

            const pause = random_range(
                config.delay_between_likes.min,
                config.delay_between_likes.max
            );
            await sleep(pause);
        } catch (e) {
            console.log(`   ⚠️ Post-like error: ${e.message}`);
            await page.keyboard.press('Escape').catch(() => { });
        }
    }

    return liked;
}

function setup_friendship_interceptor(page) {
    const state = {
        last_profile_info: null,
        last_friendship_action: null,
        action_blocked: false
    };

    const handler = async (response) => {
        const url = response.url();

        if (/\/api\/v1\/users\/web_profile_info\//.test(url)) {
            try {
                const json = await response.json();
                const u = json?.data?.user;
                if (u) {
                    state.last_profile_info = {
                        username: u.username,
                        id: u.id,
                        followed_by_viewer: !!u.followed_by_viewer,
                        requested_by_viewer: !!u.requested_by_viewer,
                        is_private: !!u.is_private
                    };
                }
            } catch (e) { }
            return;
        }

        if (/\/api\/v1\/friendships\/(create|destroy)\//.test(url)) {
            try {
                const json = await response.json();
                state.last_friendship_action = { status: response.status(), body: json };
                if (json?.spam || json?.feedback_required || json?.error_type === 'feedback_required') {
                    state.action_blocked = true;
                }
            } catch (e) {
                state.last_friendship_action = { status: response.status(), body: null };
            }
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function fetch_profile_info(page, username) {
    return await page.evaluate(async (u) => {
        try {
            const res = await fetch(`/api/v1/users/web_profile_info/?username=${encodeURIComponent(u)}`, {
                headers: { 'X-IG-App-ID': '936619743392459' },
                credentials: 'include'
            });
            if (!res.ok) return { error: `HTTP ${res.status}` };
            const json = await res.json();
            const user = json?.data?.user;
            if (!user) return { error: 'no user in response body' };
            return {
                username: user.username,
                id: user.id,
                followed_by_viewer: !!user.followed_by_viewer,
                requested_by_viewer: !!user.requested_by_viewer,
                is_private: !!user.is_private
            };
        } catch (e) {
            return { error: e.message };
        }
    }, username);
}

async function ensure_following(page, target_username, friendship_iface) {
    const wait_for = async (predicate, max_ms, step = 200) => {
        const start = Date.now();
        while (Date.now() - start < max_ms) {
            const v = predicate();
            if (v) return v;
            await new Promise(r => setTimeout(r, step));
        }
        return predicate() || null;
    };

    const info = await fetch_profile_info(page, target_username);

    if (!info || info.error || !info.username) {
        console.log(`   ⚠️ Could not fetch profile info via API for @${target_username}: ${info?.error || 'no data'}`);
        return false;
    }

    console.log(`   🔎 API friendship state for @${target_username}: following=${info.followed_by_viewer}, requested=${info.requested_by_viewer}, private=${info.is_private}`);

    if (info.followed_by_viewer) {
        console.log(`   ✓ Already following @${target_username}`);
        return true;
    }
    if (info.requested_by_viewer) {
        console.log(`   ✓ Follow request already pending for @${target_username}`);
        return true;
    }

    await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
    await human_delay(800, 1500);

    const handle = await page.evaluateHandle((username) => {
        const heading_candidates = Array.from(
            document.querySelectorAll('h1, h2, span')
        ).filter(el => {
            const text = (el.textContent || '').trim();
            if (text !== username) return false;
            const r = el.getBoundingClientRect();
            return r.top >= 0 && r.top < 500 && r.width > 30;
        });

        if (heading_candidates.length === 0) return null;
        const anchor = heading_candidates[0];

        let container = anchor;
        for (let depth = 0; depth < 10 && container.parentElement; depth++) {
            container = container.parentElement;
            const buttons = Array.from(container.querySelectorAll('button, div[role="button"]'));
            const candidates = buttons.filter(btn => {
                const r = btn.getBoundingClientRect();
                if (r.width < 70 || r.height < 28 || r.width > 400 || r.height > 80) return false;
                const text = (btn.textContent || '').trim();
                if (text.length === 0 || text.length > 60) return false;
                return btn.querySelectorAll('svg').length <= 1;
            });
            if (candidates.length === 0) continue;
            candidates.sort((a, b) => {
                const ra = a.getBoundingClientRect(), rb = b.getBoundingClientRect();
                if (Math.abs(ra.top - rb.top) > 5) return ra.top - rb.top;
                return ra.left - rb.left;
            });
            return candidates[0];
        }
        return null;
    }, target_username);

    const el = handle.asElement();
    if (!el) {
        console.log(`   ⚠️ Primary action button not found near @${target_username} username anchor`);
        await dump_state(page, `no_follow_btn_${target_username}`);
        return false;
    }

    friendship_iface.state.last_friendship_action = null;
    friendship_iface.state.action_blocked = false;

    console.log(`   ➕ Subscribing to @${target_username}...`);
    await human_click(page, el);
    await human_delay(2500, 4000);

    const after = await fetch_profile_info(page, target_username);
    if (after && !after.error) {
        if (after.followed_by_viewer) {
            console.log(`   ✓ Subscribed (API confirmed)`);
            return true;
        }
        if (after.requested_by_viewer) {
            console.log(`   ✓ Follow request sent (private account)`);
            return true;
        }
    }

    if (friendship_iface.state.action_blocked) {
        const body = friendship_iface.state.last_friendship_action?.body;
        const fb = body?.feedback_message || body?.feedback_title || 'feedback_required';
        console.log(`   🚫 Instagram action-blocked the follow: ${fb}`);
        return false;
    }

    console.log(`   ⚠️ Click sent but API still reports not following — silent block or click missed the toggle`);
    return false;
}

function setup_followers_interceptor(page) {
    const state = {
        has_next_page: true,
        api_seen: false,
        responses: 0,
        users_returned: 0,
        empty_with_next: 0,
        cap_detected: false
    };

    const handler = async (response) => {
        const url = response.url();
        const is_followers_api = /\/friendships\/\d+\/followers\//.test(url);
        const is_graphql = /edge_followed_by/.test(url);
        if (!is_followers_api && !is_graphql) return;

        try {
            const json = await response.json();
            state.api_seen = true;
            state.responses++;

            let batch_size = 0;
            let has_next = state.has_next_page;

            if ('next_max_id' in json) {
                has_next = !!json.next_max_id;
                if (Array.isArray(json.users)) {
                    batch_size = json.users.length;
                    state.users_returned += batch_size;
                }
            } else if (json?.data?.user?.edge_followed_by?.page_info) {
                const pi = json.data.user.edge_followed_by.page_info;
                has_next = !!pi.has_next_page;
                const edges = json.data.user.edge_followed_by.edges;
                if (Array.isArray(edges)) {
                    batch_size = edges.length;
                    state.users_returned += batch_size;
                }
            }

            state.has_next_page = has_next;

            if (has_next && batch_size === 0) {
                state.empty_with_next++;
                if (state.empty_with_next >= 1) state.cap_detected = true;
            } else {
                state.empty_with_next = 0;
            }
        } catch (e) {
        }
    };

    page.on('response', handler);
    return { state, cleanup: () => page.off('response', handler) };
}

async function parse_followers(page, target_username, max_count) {
    console.log(`📋 Parsing followers of @${target_username} (target: ${max_count})`);

    await dismiss_overlays(page);

    const { state: api_state, cleanup: detach_interceptor } = setup_followers_interceptor(page);
    try {
        return await _parse_followers_impl(page, target_username, max_count, api_state);
    } finally {
        detach_interceptor();
    }
}

async function _parse_followers_impl(page, target_username, max_count, api_state) {
    const followers_handle = await page.evaluateHandle((target) => {
        const followers_re = new RegExp(`^/${target}/followers/?(\\?|$)`, 'i');

        const anchors = Array.from(document.querySelectorAll('a[href]'));
        for (const a of anchors) {
            if (followers_re.test(a.getAttribute('href') || '')) return a;
        }

        const all_clickable = Array.from(document.querySelectorAll(
            'a, button, div[role="button"], a[role="link"]'
        ));
        const counters = all_clickable.filter(el => {
            const t = (el.innerText || '').trim();
            if (!t || t.length > 60 || !/\d/.test(t)) return false;
            const r = el.getBoundingClientRect();
            return r.width > 0 && r.height > 0 && r.top < window.innerHeight;
        });

        for (const seed of counters) {
            let node = seed;
            for (let depth = 0; depth < 8 && node.parentElement; depth++) {
                const parent = node.parentElement;
                const row = [];
                for (const child of parent.children) {
                    const hit = counters.find(c => child === c || child.contains(c));
                    if (hit) row.push(hit);
                }
                if (row.length >= 2 && row.length <= 4) {
                    return row[row.length - 2];
                }
                node = parent;
            }
        }
        return null;
    }, target_username);

    const followers_link = followers_handle.asElement();
    if (!followers_link) {
        console.log('   ❌ "Followers" button not found');
        await dump_state(page, `target_${target_username}_no_followers_btn`);
        return [];
    }
    await human_click(page, followers_link);
    await human_delay(1800, 3000);

    await page.waitForSelector('div[role="dialog"]', { timeout: 10000 }).catch(() => { });

    const got_users = await page.waitForFunction(() => {
        const dialog = document.querySelector('div[role="dialog"]');
        if (!dialog) return false;
        return dialog.querySelectorAll('a[role="link"][href^="/"]').length >= 3;
    }, { timeout: 15000 }).then(() => true).catch(() => false);

    if (!got_users) {
        console.log('   ❌ Followers modal did not load the list within 15 sec');
        return [];
    }
    await human_delay(800, 1500);

    const find_scrollable = async () => {
        const handle = await page.evaluateHandle(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return null;
            const all = dialog.querySelectorAll('div');
            let best = null, best_count = 0;
            for (const el of all) {
                const s = window.getComputedStyle(el);
                if (s.overflowY !== 'auto' && s.overflowY !== 'scroll') continue;
                const links = el.querySelectorAll('a[role="link"][href^="/"]').length;
                if (links > best_count) { best = el; best_count = links; }
            }
            return best;
        });
        return handle.asElement();
    };

    let scrollable = await find_scrollable();
    if (!scrollable) {
        console.log('   ❌ Scrollable container of the modal not found');
        return [];
    }
    console.log(`   📜 Container found, initial scrollHeight: ${await scrollable.evaluate(el => el.scrollHeight)}px`);

    const modal_box = await scrollable.boundingBox();
    if (modal_box) {
        await move_mouse_human(page, {
            x: modal_box.x + modal_box.width / 2,
            y: modal_box.y + modal_box.height / 2
        });
        await human_delay(200, 500);
    }

    const collected = new Map();
    let stagnation = 0;
    const max_stagnation = 5;
    let iteration = 0;
    const max_iterations = 200;

    while (collected.size < max_count && stagnation < max_stagnation && iteration < max_iterations) {
        iteration++;

        const batch = await page.evaluate(() => {
            const dialog = document.querySelector('div[role="dialog"]');
            if (!dialog) return [];
            const links = dialog.querySelectorAll('a[role="link"][href^="/"]');
            const seen = new Map();

            const skip = new Set(['explore', 'reels', 'direct', 'accounts', 'p', 'tv', 'stories', 'about']);

            const button_words = new Set([
                'Follow', 'Following', 'Message', 'Requested', 'Subscribed', 'Subscribe',
                'Подписаться', 'Подписки', 'Подписан', 'Подписана', 'Запрос', 'Сообщение',
                'Отписаться', 'Subscribirse', 'Seguir', 'Suivre'
            ]);

            for (const a of links) {
                const href = a.getAttribute('href');
                const m = href && href.match(/^\/([A-Za-z0-9._]+)\/?$/);
                if (!m) continue;
                const username = m[1];
                if (skip.has(username)) continue;
                if (seen.has(username)) continue;

                let row = a;
                for (let depth = 0; depth < 10 && row.parentElement; depth++) {
                    const parent = row.parentElement;
                    const sibling_links = parent.querySelectorAll('a[role="link"][href^="/"]');
                    let other = 0;
                    for (const l of sibling_links) {
                        const lm = l.getAttribute('href').match(/^\/([A-Za-z0-9._]+)\/?$/);
                        if (lm && lm[1] !== username && !skip.has(lm[1])) { other++; break; }
                    }
                    if (other > 0) break;
                    row = parent;
                }

                const lines = (row.innerText || '')
                    .split(/\n+/).map(s => s.trim()).filter(Boolean);
                let full_name = '';
                for (const line of lines) {
                    if (line === username) continue;
                    if (button_words.has(line)) continue;
                    if (line.length > 150) continue;
                    full_name = line;
                    break;
                }

                const verified = !!row.querySelector('svg[aria-label="Verified"], svg[aria-label*="Verif"], svg[aria-label="Подтвержденный"]');
                const is_private = !!row.querySelector('svg[aria-label="Private"]');

                seen.set(username, { username, full_name, verified, is_private });
            }
            return Array.from(seen.values());
        });

        const before = collected.size;
        for (const u of batch) {
            if (collected.size >= max_count) break;
            if (!collected.has(u.username)) collected.set(u.username, u);
        }
        const added = collected.size - before;

        if (added === 0) stagnation++;
        else stagnation = 0;

        const api_tail = api_state.api_seen
            ? ` [api: ${api_state.responses} resp, ${api_state.users_returned} users, next=${api_state.has_next_page}${api_state.cap_detected ? ', CAP' : ''}]`
            : '';
        process.stdout.write(`   📊 Collected: ${collected.size}/${max_count} (+${added})${api_tail}\r`);

        if (collected.size >= max_count) break;

        if (api_state.api_seen && (!api_state.has_next_page || api_state.cap_detected)) {
            const reason = api_state.cap_detected
                ? `server cap (empty batch with next_max_id present, returned ${api_state.users_returned} users total)`
                : 'has_next_page=false';
            console.log(`\n   🛑 Pagination closed by Instagram: ${reason}`);
            break;
        }

        const wheel_delta = random_range(400, 700);
        await page.mouse.wheel({ deltaY: wheel_delta });
        await human_delay(900, 1700);

        if (stagnation >= 1) {
            await scrollable.evaluate(el => {
                const links = el.querySelectorAll('a[role="link"][href^="/"]');
                if (links.length > 0) {
                    links[links.length - 1].scrollIntoView({ block: 'end' });
                }
            });
            await human_delay(700, 1300);

            const fresh = await find_scrollable();
            if (fresh) scrollable = fresh;
        }

        if (Math.random() < 0.2) await human_delay(1500, 3500);
    }

    console.log(`\n   ✅ Followers collected: ${collected.size}`);

    await page.keyboard.press('Escape').catch(() => { });
    await human_delay(800, 1500);

    return Array.from(collected.values()).slice(0, max_count);
}

async function save_followers(profile_uuid, target, users) {
    const dir = path.join(__dirname, config.results_dir);
    await ensure_dir(dir);

    const stamp = new Date().toISOString().replace(/[:.]/g, '-');
    const base = `${target}_${profile_uuid.slice(0, 8)}_${stamp}`;

    const json_path = path.join(dir, `${base}.json`);
    const csv_path = path.join(dir, `${base}.csv`);

    const json_payload = {
        target_account: target,
        parsed_by_profile: profile_uuid,
        timestamp: new Date().toISOString(),
        total: users.length,
        followers: users
    };
    await fs.writeFile(json_path, JSON.stringify(json_payload, null, 2), 'utf8');

    const csv_lines = ['username,full_name,verified,is_private'];
    for (const u of users) {
        csv_lines.push([
            csv_escape(u.username),
            csv_escape(u.full_name),
            csv_escape(u.verified),
            csv_escape(u.is_private)
        ].join(','));
    }
    await fs.writeFile(csv_path, csv_lines.join('\n'), 'utf8');

    console.log(`💾 JSON: ${json_path}`);
    console.log(`💾 CSV : ${csv_path}`);
}

async function process_target(page, target, profile_uuid) {
    const friendship_iface = setup_friendship_interceptor(page);
    try {
        const ok = await visit_target_profile(page, target);
        if (!ok) return { target, success: false };

        let followed = false;
        if (config.follow_targets_before_parsing) {
            followed = await ensure_following(page, target, friendship_iface);
            if (followed) await human_delay(3000, 6000);
        }

        const target_likes = random_int(config.likes_on_target.min, config.likes_on_target.max);
        if (target_likes > 0) {
            await like_target_posts(page, target_likes);
        }

        await page.goto(`https://www.instagram.com/${target}/`, {
            waitUntil: 'domcontentloaded',
            timeout: 30000
        }).catch(() => { });
        await human_delay(1500, 3000);

        const users = await parse_followers(page, target, config.followers_per_target);
        if (users.length > 0) {
            await save_followers(profile_uuid, target, users);
        }

        return { target, success: true, count: users.length, followed };
    } finally {
        friendship_iface.cleanup();
    }
}

async function process_profile(profile_cfg, idx, total) {
    console.log(`\n${'='.repeat(80)}`);
    console.log(`📋 Profile ${idx + 1}/${total} — UUID ${profile_cfg.uuid}`);
    console.log(`   Targets: ${profile_cfg.target_accounts.join(', ')}`);
    console.log(`${'='.repeat(80)}`);

    await octo_stop_profile(profile_cfg.uuid).catch(() => { });
    await sleep(3);

    let ws_data;
    try {
        ws_data = await octo_start_profile(profile_cfg.uuid);
    } catch (e) {
        const body = e.response?.data;
        const body_str = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : '';
        console.error(`❌ Failed to start profile: ${e.message}${body_str ? ' | Octo: ' + body_str : ''}`);
        return { uuid: profile_cfg.uuid, status: 'start_failed', error: body_str || e.message };
    }

    if (!ws_data?.ws_endpoint) {
        console.error('❌ Octo did not return a ws_endpoint');
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'no_ws' };
    }

    let browser;
    try {
        browser = await puppeteer.connect({
            browserWSEndpoint: ws_data.ws_endpoint,
            defaultViewport: null,
            protocolTimeout: 600000
        });
    } catch (e) {
        console.error(`❌ Puppeteer connect: ${e.message}`);
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        return { uuid: profile_cfg.uuid, status: 'connect_failed' };
    }

    try {
        const ctx = browser.defaultBrowserContext();
        await ctx.overridePermissions('https://www.instagram.com', []);
        await ctx.overridePermissions('https://instagram.com', []);
    } catch (e) {
        console.warn(`⚠️ overridePermissions: ${e.message}`);
    }

    let stats = { uuid: profile_cfg.uuid, status: 'ok', targets: [], likes: 0, stories: 0 };

    try {
        const page = await browser.newPage();
        await page.setViewport({ width: 1280, height: 900 });

        await page.goto('https://www.instagram.com/', {
            waitUntil: 'domcontentloaded',
            timeout: 45000
        });
        await human_delay(2500, 4500);
        await dismiss_overlays(page);

        await human_delay(800, 1500);
        await dismiss_overlays(page);

        if (!await check_logged_in(page)) {
            console.error('❌ Profile is not logged into Instagram');
            stats.status = 'not_logged_in';
            return stats;
        }

        const block = await check_for_block(page);
        if (block.blocked) {
            console.error(`❌ Account is restricted: ${block.reason}`);
            stats.status = 'blocked';
            return stats;
        }

        if (Math.random() < config.stories_probability) {
            const cnt = random_int(config.stories_per_session.min, config.stories_per_session.max);
            stats.stories = await watch_random_stories(page, cnt);
            await human_delay(1500, 3000);
        }

        const feed_likes = random_int(config.likes_per_session.min, config.likes_per_session.max);
        const feed_result = await browse_feed_and_like(page, feed_likes);
        stats.likes += feed_result.liked;
        if (feed_result.blocked) {
            stats.status = 'blocked_during_feed';
            return stats;
        }

        for (let i = 0; i < profile_cfg.target_accounts.length; i++) {
            const target = profile_cfg.target_accounts[i];
            try {
                const r = await process_target(page, target, profile_cfg.uuid);
                stats.targets.push(r);
            } catch (e) {
                console.error(`❌ Target @${target} error: ${e.message}`);
                stats.targets.push({ target, success: false, error: e.message });
            }

            if (i < profile_cfg.target_accounts.length - 1) {
                const pause = random_range(
                    config.delay_between_targets.min,
                    config.delay_between_targets.max
                );
                console.log(`⏰ Pause between targets: ${pause.toFixed(1)} sec`);
                await sleep(pause);
            }
        }

    } catch (e) {
        console.error(`❌ Profile processing error: ${e.message}`);
        stats.status = 'error';
        stats.error = e.message;
    } finally {
        await octo_stop_profile(profile_cfg.uuid).catch(() => { });
        await sleep(2);
    }

    return stats;
}

async function check_limits(response) {
    const header = response.headers.ratelimit;
    if (!header) return;
    const entries = header.split(',').map(s => s.trim());
    for (const e of entries) {
        const r_match = e.match(/;r=(\d+)/);
        const t_match = e.match(/;t=(\d+)/);
        if (!r_match || !t_match) continue;
        const remaining = parseInt(r_match[1], 10);
        const window_s = parseInt(t_match[1], 10);
        if (remaining < 5) {
            console.log(`⏳ Octo rate-limit, waiting ${window_s + 1} sec`);
            await sleep(window_s + 1);
        }
    }
}

async function octo_start_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/start`,
        headers: { 'Content-Type': 'application/json' },
        data: {
            uuid,
            headless: config.headless_mode,
            debug_port: true,
            timeout: 60
        }
    });
    await check_limits(res);
    return res.data;
}

async function octo_stop_profile(uuid) {
    const res = await axios({
        method: 'post',
        url: `${config.octo_local_api_base_url}/stop`,
        headers: { 'Content-Type': 'application/json' },
        data: { uuid }
    });
    await check_limits(res);
    return res.data;
}

(async () => {
    console.log('🚀 Octo Instagram Parser & Liker');
    console.log(`   Profiles: ${config.profiles.length}`);
    console.log(`   Followers per target: ${config.followers_per_target}`);
    console.log(`   Feed likes: ${config.likes_per_session.min}${config.likes_per_session.max}`);
    console.log('');

    await ensure_dir(path.join(__dirname, config.results_dir));

    const all_stats = [];
    for (let i = 0; i < config.profiles.length; i++) {
        const stats = await process_profile(config.profiles[i], i, config.profiles.length);
        all_stats.push(stats);

        if (i < config.profiles.length - 1) {
            const pause = random_range(
                config.delay_between_profiles.min,
                config.delay_between_profiles.max
            );
            console.log(`\n⏰ Pause before the next profile: ${pause.toFixed(1)} sec`);
            await sleep(pause);
        }
    }

    console.log(`\n${'='.repeat(80)}`);
    console.log('📊 SUMMARY');
    console.log('='.repeat(80));
    for (const s of all_stats) {
        console.log(`\n${s.uuid}${s.status}`);
        console.log(`   Likes: ${s.likes ?? 0}, stories: ${s.stories ?? 0}`);
        if (s.targets) {
            for (const t of s.targets) {
                if (t.success) console.log(`   ✅ @${t.target}: ${t.count} followers`);
                else console.log(`   ❌ @${t.target}: ${t.error || 'fail'}`);
            }
        }
    }

    const summary_path = path.join(__dirname, config.results_dir, `_summary_${Date.now()}.json`);
    await fs.writeFile(summary_path, JSON.stringify(all_stats, null, 2));
    console.log(`\n📄 Summary report: ${summary_path}`);
    console.log('🎉 Done.');
})();

Scaling and account safety

Checklist before launch:

  1. Profiles are properly prepared. The account should be at least 2–3 weeks old, have a profile picture, posts, and some following activity.

  2. Do not use datacenter proxies. Use residential or mobile proxies.

  3. Verify that selectors are up to date. Before running the script across a large number of accounts, test it on a few accounts and confirm that all selectors work correctly.

  4. Add delays between runs. Our script processes the profiles in config.profiles sequentially, with a 60–120 second pause between them. If you use more accounts, increase this interval.

Conclusion

This single script covers two of the most popular use cases: follower scraping (for competitor research and lookalike audience building) and mass liking (for account prep). It can be easily extended with additional functionality:

  • Scraping users who liked or commented on a specific post for the most engaged audience segment.

  • Collecting bio information and email addresses from commenters. This can be moved into a separate pipeline to avoid overloading the scraper account with extra actions.

  • Comparing follower lists from multiple competitors and identifying overlaps. This is straightforward to implement in pure Node.js using the collected JSON data.

  • Adding a caching layer to avoid scraping the same followers repeatedly across multiple runs.

If you work at scale, you can also adapt the script for parallel execution.

Successful scraping relies on a multi-layered system. Octo Browser provides a consistent fingerprint and profile isolation, rebrowser-puppeteer delivers automation with fixes for common automation-detection leaks, the WindMouse algorithm generates realistic cursor movement, and the behavioral layer introduces distractions and pauses. Ultimately, system resilience depends not on any single component, but on how effectively all of them work together.

Stay up to date with the latest Octo Browser news

By clicking the button you agree to our Privacy Policy.

Stay up to date with the latest Octo Browser news

By clicking the button you agree to our Privacy Policy.

Stay up to date with the latest Octo Browser news

By clicking the button you agree to our Privacy Policy.

Join Octo Browser now

Or contact Customer Service at any time with any questions you might have.

Join Octo Browser now

Or contact Customer Service at any time with any questions you might have.

Join Octo Browser now

Or contact Customer Service at any time with any questions you might have.

©

2026

Octo Browser

©

2026

Octo Browser

©

2026

Octo Browser