/
guselnikov
/
inst
Обзор
Документация
Войти
/
guselnikov
/
inst
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
back/scripts/probe-post.ts
411 строк
14 KB
Viktor Guselnikov
first_commit
07 июл 2026, 11:54
07 июл 2026, 11:54
098126a
Код
Авторство
О чём код?
/** * Empirical probe: Instagram create-post flow. * Dumps HTML + interactive elements at each step. * * Usage: * npx ts-node -r tsconfig-paths/register scripts/probe-post.ts [--publish] */ import { chromium, Page, Locator } from 'playwright'; import { mkdir, writeFile } from 'fs/promises'; import { join } from 'path'; import { readFileSync } from 'fs'; const IMAGE_PATH = join(__dirname, '../image.png'); const POST_TEXT = readFileSync(join(__dirname, 'probe-post-text.txt'), 'utf8').trim(); const PUBLISH = process.argv.includes('--publish'); async function dumpStep(page: Page, debugDir: string, step: string): Promise<void> { await mkdir(debugDir, { recursive: true }); await writeFile(join(debugDir, `${step}.html`), await page.content(), 'utf8'); await page.screenshot({ path: join(debugDir, `${step}.png`), fullPage: true }).catch(() => undefined); console.log(`[dump] ${step} url=${page.url()}`); } async function dumpInteractive(page: Page, scope: Locator | Page, label: string): Promise<void> { const items = await (scope as Locator).evaluate((el) => { const out: Array<{ tag: string; role: string | null; aria: string | null; href: string | null; text: string; type: string | null; }> = []; const root = el instanceof Element ? el : document.body; for (const node of root.querySelectorAll( 'a, button, input, textarea, [role="button"], [role="link"], [role="tab"], svg[aria-label]', )) { out.push({ tag: node.tagName, role: node.getAttribute('role'), aria: node.getAttribute('aria-label'), href: node.getAttribute('href'), text: (node.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 60), type: node.getAttribute('type'), }); } return out.slice(0, 80); }).catch(async () => { return page.evaluate(() => { const out: Array<Record<string, string | null>> = []; for (const node of document.querySelectorAll( 'a, button, input, textarea, [role="button"], svg[aria-label]', )) { out.push({ tag: node.tagName, role: node.getAttribute('role'), aria: node.getAttribute('aria-label'), href: node.getAttribute('href'), text: (node.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 60), }); } return out.slice(0, 80); }); }); console.log(`[interactive] ${label}:`, JSON.stringify(items, null, 2)); } async function dismissPopups(page: Page): Promise<void> { for (const sel of [ 'button:has-text("Not Now")', 'button:has-text("Не сейчас")', 'button:has-text("Allow all cookies")', 'button:has-text("Accept")', 'button:has-text("Принять")', ]) { const btn = page.locator(sel).first(); if ((await btn.count()) > 0) await btn.click().catch(() => undefined); await page.waitForTimeout(400); } } async function dialogScope(page: Page): Promise<Locator> { const dialog = page.locator('[role="dialog"]').first(); if ((await dialog.count()) > 0) return dialog; return page.locator('body'); } async function clickDialogButton(page: Page, texts: string[], label: string): Promise<boolean> { const dialog = await dialogScope(page); for (const text of texts) { const loc = dialog.locator(`div[role="button"]:has-text("${text}")`).first(); const n = await loc.count(); console.log(`[click try] ${label} dialog div "${text}": count=${n}`); if (n === 0) continue; await loc.click({ timeout: 8000 }).catch((e) => { console.log(`[click fail] ${text}: ${(e as Error).message}`); }); await page.waitForTimeout(1500); return true; } return false; } async function clickFirst(page: Page, selectors: string[], label: string): Promise<boolean> { for (const sel of selectors) { const loc = page.locator(sel).first(); const n = await loc.count(); console.log(`[click try] ${label} ${sel}: count=${n}`); if (n === 0) continue; await loc.click({ timeout: 8000 }).catch((e) => { console.log(`[click fail] ${sel}: ${(e as Error).message}`); }); await page.waitForTimeout(1500); return true; } return false; } async function findCreateButton(page: Page): Promise<void> { // Sidebar / nav create buttons const createSelectors = [ 'a[href="#"]:has(svg[aria-label="New post"])', 'a[href="#"]:has(svg[aria-label="Новая публикация"])', 'a[href="#"]:has(svg[aria-label="Create"])', 'a[href="#"]:has(svg[aria-label="Создать"])', 'svg[aria-label="New post"]', 'svg[aria-label="Новая публикация"]', 'svg[aria-label="Create"]', 'svg[aria-label="Создать"]', '[aria-label="New post"]', '[aria-label="Новая публикация"]', '[aria-label="Create"]', '[aria-label="Создать"]', 'a[href="/create/select/"]', 'a[href*="/create/"]', ]; console.log('\n=== NAV CREATE BUTTONS ==='); for (const sel of createSelectors) { const loc = page.locator(sel); const n = await loc.count(); if (n > 0) console.log(` ${sel}: ${n}`); } await dumpInteractive(page, page.locator('nav, [role="navigation"]').first(), 'nav'); } async function main() { const storageState = JSON.parse(readFileSync('/tmp/ig-storage.json', 'utf8')); const storagePath = join(__dirname, '../../storage'); const debugDir = join(storagePath, 'debug', 'post-probe'); console.log('Image:', IMAGE_PATH); console.log('Publish mode:', PUBLISH); console.log('Text length:', POST_TEXT.length); const browser = await chromium.launch({ headless: false, slowMo: 60 }); const context = await browser.newContext({ storageState, viewport: { width: 1280, height: 900 }, locale: 'ru-RU', }); const page = await context.newPage(); page.setDefaultTimeout(25000); // Step 0: home await page.goto('https://www.instagram.com/', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2500); await dismissPopups(page); await dumpStep(page, debugDir, '00-home'); await findCreateButton(page); // Step 1: Create menu → Публикация (RU UI opens dropdown first) await clickFirst( page, [ 'svg[aria-label="New post"]', 'svg[aria-label="Новая публикация"]', 'svg[aria-label="Create"]', 'svg[aria-label="Создать"]', ], 'create-menu-trigger', ); await page.waitForTimeout(800); await dumpStep(page, debugDir, '01-create-menu'); const postOpened = (await clickFirst( page, [ 'svg[aria-label="Публикация"]', 'svg[aria-label="Post"]', 'a:has(svg[aria-label="Публикация"])', 'a:has(svg[aria-label="Post"])', 'span:has-text("Публикация")', ], 'create-post-item', )) || (await page.goto('https://www.instagram.com/create/select/', { waitUntil: 'domcontentloaded' }).then(() => true)); console.log('[flow] post dialog opened:', postOpened); await page.waitForTimeout(2000); await dumpStep(page, debugDir, '02-create-dialog'); await dumpInteractive(page, page, 'create-dialog'); // Step 2: file input — upload image console.log('\n=== FILE INPUTS ==='); const fileInputs = page.locator('input[type="file"]'); const fileCount = await fileInputs.count(); console.log('file inputs:', fileCount); for (let i = 0; i < fileCount; i++) { const input = fileInputs.nth(i); const accept = await input.getAttribute('accept'); const multiple = await input.getAttribute('multiple'); console.log(` input[${i}] accept=${accept} multiple=${multiple}`); } if (fileCount > 0) { await fileInputs.first().setInputFiles(IMAGE_PATH); console.log('[upload] setInputFiles done'); await page.waitForTimeout(4000); await dumpStep(page, debugDir, '03-after-upload'); await dumpInteractive(page, page, 'after-upload'); } else { // Maybe need to click "Select from computer" first await clickFirst( page, [ 'button:has-text("Select from computer")', 'button:has-text("Выбрать с компьютера")', 'button:has-text("Select from Computer")', 'div[role="button"]:has-text("Select from computer")', 'div[role="button"]:has-text("Выбрать с компьютера")', ], 'select-computer', ); await page.waitForTimeout(1000); const inputsAfter = page.locator('input[type="file"]'); if ((await inputsAfter.count()) > 0) { await inputsAfter.first().setInputFiles(IMAGE_PATH); console.log('[upload] setInputFiles after select-computer'); await page.waitForTimeout(4000); await dumpStep(page, debugDir, '03-after-upload'); } else { console.log('[upload] ERROR: no file input found'); await dumpStep(page, debugDir, '03-no-file-input'); } } // Step 3: crop/advance — "Next" / "Далее" console.log('\n=== NEXT BUTTONS (crop) ==='); for (const sel of [ 'button:has-text("Next")', 'button:has-text("Далее")', 'div[role="button"]:has-text("Next")', 'div[role="button"]:has-text("Далее")', ]) { const n = await page.locator(sel).count(); if (n > 0) console.log(` ${sel}: ${n}`); } await clickDialogButton(page, ['Next', 'Далее'], 'next-crop'); await page.waitForTimeout(2000); await dumpStep(page, debugDir, '04-after-crop-next'); // Step 4: filters — skip with Next await clickDialogButton(page, ['Next', 'Далее'], 'next-filter'); await page.waitForTimeout(2000); await dumpStep(page, debugDir, '05-caption-screen'); await dumpInteractive(page, page, 'caption-screen'); // Step 5: caption textarea console.log('\n=== CAPTION INPUTS ==='); const captionSelectors = [ 'div[contenteditable="true"][aria-label="Write a caption..."]', 'div[contenteditable="true"][aria-label="Добавьте подпись…"]', 'div[contenteditable="true"][aria-label*="подпис"]', 'textarea[aria-label="Write a caption..."]', 'textarea[aria-label="Напишите подпись..."]', 'textarea[aria-label*="подпис"]', 'div[contenteditable="true"][role="textbox"]', 'div[contenteditable="true"]', 'textarea', ]; for (const sel of captionSelectors) { const loc = page.locator(sel); const n = await loc.count(); if (n > 0) { const aria = await loc.first().getAttribute('aria-label'); const ph = await loc.first().getAttribute('placeholder'); console.log(` ${sel}: count=${n} aria=${aria} placeholder=${ph}`); } } let captionFilled = false; const captionText = PUBLISH ? POST_TEXT : POST_TEXT.slice(0, 500); for (const sel of captionSelectors) { const loc = page.locator(sel).first(); if ((await loc.count()) === 0) continue; await loc.click(); if (sel.includes('contenteditable')) { await page.keyboard.press('Meta+A'); await page.keyboard.press('Backspace'); await page.keyboard.type(captionText, { delay: 5 }); } else { await loc.fill(captionText); } captionFilled = true; console.log(`[caption] filled via ${sel} (${captionText.length} chars)`); break; } await page.waitForTimeout(1500); await dumpStep(page, debugDir, '06-caption-filled'); // Explore extra options on caption screen console.log('\n=== CAPTION SCREEN OPTIONS ==='); for (const sel of [ 'button:has-text("Add location")', 'button:has-text("Добавить место")', 'button:has-text("Tag people")', 'button:has-text("Отметить людей")', 'button:has-text("Accessibility")', 'button:has-text("Специальные возможности")', 'button:has-text("Advanced settings")', 'button:has-text("Расширенные настройки")', 'svg[aria-label="Add location"]', 'svg[aria-label="Tag people"]', ]) { const n = await page.locator(sel).count(); if (n > 0) console.log(` ${sel}: ${n}`); } // Step 6: Share / Поделиться console.log('\n=== SHARE BUTTONS ==='); for (const sel of [ 'button:has-text("Share")', 'button:has-text("Поделиться")', 'div[role="button"]:has-text("Share")', 'div[role="button"]:has-text("Поделиться")', ]) { const n = await page.locator(sel).count(); if (n > 0) console.log(` ${sel}: ${n}`); } if (PUBLISH && captionFilled) { const shared = await clickDialogButton(page, ['Share', 'Поделиться'], 'share'); if (shared) { console.log('[publish] clicked Share, waiting...'); await page.waitForTimeout(12000); await dumpStep(page, debugDir, '07-after-share'); const dialogGone = (await page.locator('[role="dialog"]').count()) === 0; console.log('[publish] dialog closed:', dialogGone); for (const t of ['Your post has been shared', 'Публикация опубликована', 'Post shared', 'Reel shared']) { if ((await page.locator(`text=${t}`).count()) > 0) { console.log('[publish] SUCCESS:', t); } } } } else { console.log('\n[probe] Skipping publish (pass --publish to actually post)'); } // Explore Create menu (plus icon dropdown) for other options console.log('\n=== CREATE MENU OPTIONS ==='); await page.goto('https://www.instagram.com/', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2000); await dismissPopups(page); const createMenuOpened = await clickFirst( page, ['svg[aria-label="Create"]', 'svg[aria-label="Создать"]', 'svg[aria-label="New post"]', 'svg[aria-label="Новая публикация"]'], 'create-menu', ); if (createMenuOpened) { await page.waitForTimeout(1500); await dumpStep(page, debugDir, '07-create-menu'); await dumpInteractive(page, page, 'create-menu-items'); for (const sel of [ 'a:has-text("Post")', 'a:has-text("Публикация")', 'a:has-text("Reel")', 'a:has-text("Live")', 'a:has-text("Эфир")', 'span:has-text("Post")', 'span:has-text("Reel")', 'span:has-text("Story")', 'span:has-text("История")', ]) { const n = await page.locator(sel).count(); if (n > 0) console.log(` menu ${sel}: ${n}`); } } console.log('\n[done] Debug dumps in', debugDir); await page.waitForTimeout(3000); await browser.close(); } main().catch((e) => { console.error(e); process.exit(1); });