// Turn whatever people pasted into a spreadsheet into clean Instagram usernames. const HANDLE_COLUMNS = ['instagram', 'handle', 'username', 'ig', 'instagram_url', 'url', 'profile']; const VALID_USERNAME = /^[a-z0-9._]{1,30}$/; const RESERVED_PATHS = new Set(['p', 'reel', 'reels', 'stories', 'explore', 'tv', 'accounts']); export type Rejected = { row: number; raw: string; reason: string }; export type Handle = { row: number; raw: string; username: string }; export function pickHandleColumn(headers: string[]): string { const normalized = headers.map((h) => h.trim().toLowerCase()); for (const candidate of HANDLE_COLUMNS) { const index = normalized.indexOf(candidate); if (index !== -1) return headers[index]; } throw new Error( `No handle column found. Name one of your columns: ${HANDLE_COLUMNS.join(', ')}`, ); } export function normalizeHandle(raw: string): string | { reason: string } { let value = raw.trim(); if (!value) return { reason: 'empty' }; // Profile URLs: instagram.com/name, instagram.com/name/?igsh=..., m.instagram.com/name const url = value.match(/instagram\.com\/([^/?#\s]+)/i); if (url) { if (RESERVED_PATHS.has(url[1].toLowerCase())) { return { reason: 'post or reel link, not a profile' }; } value = url[1]; } else if (/^https?:\/\//i.test(value)) { return { reason: 'not an Instagram link' }; } value = value.replace(/^@+/, '').toLowerCase(); if (!VALID_USERNAME.test(value)) return { reason: 'not a valid Instagram username' }; return value; } export function extractHandles(rows: Record[], column: string) { const handles: Handle[] = []; const rejected: Rejected[] = []; const seen = new Set(); rows.forEach((record, index) => { const row = index + 2; // header is row 1 in the spreadsheet const raw = record[column] ?? ''; const result = normalizeHandle(raw); if (typeof result !== 'string') { rejected.push({ row, raw, reason: result.reason }); } else if (seen.has(result)) { rejected.push({ row, raw, reason: `duplicate of @${result}` }); } else { seen.add(result); handles.push({ row, raw, username: result }); } }); return { handles, rejected }; }