/** * Verify social media handles exist via lightweight API checks. * Each check runs independently — one failure doesn't block others. */ export interface VerifiedChannel { handle: string; verified: boolean; url?: string; channelId?: string; // YouTube channel ID if resolved } export interface VerifiedChannels { instagram: VerifiedChannel[]; youtube: VerifiedChannel | null; facebook: VerifiedChannel | null; naverBlog: VerifiedChannel | null; gangnamUnni: VerifiedChannel | null; tiktok: VerifiedChannel | null; } /** * Verify an Instagram handle exists. * Uses a lightweight fetch to the profile page. */ async function verifyInstagram(handle: string): Promise { try { const url = `https://www.instagram.com/${handle}/`; const res = await fetch(url, { method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0' }, redirect: 'follow', }); // Instagram returns 200 for existing profiles, 404 for missing return { handle, verified: res.status === 200, url, }; } catch { return { handle, verified: false }; } } /** * Verify a YouTube handle/channel exists using YouTube Data API v3. */ async function verifyYouTube(handle: string, apiKey: string): Promise { try { const YT_BASE = 'https://www.googleapis.com/youtube/v3'; const cleanHandle = handle.replace(/^@/, ''); // Try forHandle first, then forUsername for (const param of ['forHandle', 'forUsername']) { const res = await fetch(`${YT_BASE}/channels?part=id,snippet&${param}=${cleanHandle}&key=${apiKey}`); const data = await res.json(); const channel = data.items?.[0]; if (channel) { return { handle, verified: true, channelId: channel.id, url: `https://youtube.com/@${cleanHandle}`, }; } } // Try as channel ID directly (starts with UC) if (cleanHandle.startsWith('UC') && cleanHandle.length === 24) { const res = await fetch(`${YT_BASE}/channels?part=id,snippet&id=${cleanHandle}&key=${apiKey}`); const data = await res.json(); if (data.items?.[0]) { return { handle: cleanHandle, verified: true, channelId: cleanHandle, url: `https://youtube.com/channel/${cleanHandle}` }; } } return { handle, verified: false }; } catch { return { handle, verified: false }; } } /** * Verify a Facebook page exists via HEAD request. */ async function verifyFacebook(handle: string): Promise { try { const url = `https://www.facebook.com/${handle}/`; const res = await fetch(url, { method: 'HEAD', headers: { 'User-Agent': 'Mozilla/5.0' }, redirect: 'follow', }); return { handle, verified: res.status === 200, url }; } catch { return { handle, verified: false }; } } /** * Verify Naver Blog exists. */ async function verifyNaverBlog(blogId: string): Promise { try { const url = `https://blog.naver.com/${blogId}`; const res = await fetch(url, { method: 'HEAD', redirect: 'follow', }); return { handle: blogId, verified: res.status === 200, url }; } catch { return { handle: blogId, verified: false }; } } /** * Find and verify gangnamunni hospital page using Firecrawl search. */ async function verifyGangnamUnni( clinicName: string, firecrawlKey: string, hintUrl?: string, ): Promise { try { // If we already have a URL hint from Perplexity, just verify it if (hintUrl && hintUrl.includes('gangnamunni.com/hospitals/')) { const res = await fetch(hintUrl, { method: 'HEAD', redirect: 'follow' }); if (res.status === 200) { return { handle: clinicName, verified: true, url: hintUrl }; } } // Otherwise, search with multiple fallback queries const shortName = clinicName.replace(/성형외과|의원|병원|클리닉|피부과/g, '').trim(); const queries = [ `${clinicName} site:gangnamunni.com`, `${shortName} 성형외과 site:gangnamunni.com`, `${clinicName} 강남언니`, ]; for (const query of queries) { const searchRes = await fetch('https://api.firecrawl.dev/v1/search', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${firecrawlKey}`, }, body: JSON.stringify({ query, limit: 5 }), }); const data = await searchRes.json(); const url = (data.data || []) .map((r: Record) => r.url) .find((u: string) => u?.includes('gangnamunni.com/hospitals/')); if (url) { return { handle: clinicName, verified: true, url }; } } return { handle: clinicName, verified: false }; } catch { return { handle: clinicName, verified: false }; } } /** * Verify all discovered handles in parallel. */ export async function verifyAllHandles( candidates: { instagram: string[]; youtube: string[]; facebook: string[]; naverBlog: string[]; tiktok: string[]; }, clinicName: string, gangnamUnniHintUrl?: string, ): Promise { const YOUTUBE_API_KEY = Deno.env.get('YOUTUBE_API_KEY') || ''; const FIRECRAWL_API_KEY = Deno.env.get('FIRECRAWL_API_KEY') || ''; const tasks: Promise[] = []; const result: VerifiedChannels = { instagram: [], youtube: null, facebook: null, naverBlog: null, gangnamUnni: null, tiktok: null, }; // Instagram — verify each candidate, keep unverified as fallback for (const handle of candidates.instagram.slice(0, 5)) { tasks.push( verifyInstagram(handle).then(v => { result.instagram.push(v); }) ); } // YouTube — first candidate if (candidates.youtube.length > 0) { tasks.push( verifyYouTube(candidates.youtube[0], YOUTUBE_API_KEY).then(v => { result.youtube = v; }) ); } // Facebook — first candidate if (candidates.facebook.length > 0) { tasks.push( verifyFacebook(candidates.facebook[0]).then(v => { result.facebook = v; }) ); } // Naver Blog — first candidate if (candidates.naverBlog.length > 0) { tasks.push( verifyNaverBlog(candidates.naverBlog[0]).then(v => { result.naverBlog = v; }) ); } // 강남언니 — always try if clinicName exists if (clinicName && FIRECRAWL_API_KEY) { tasks.push( verifyGangnamUnni(clinicName, FIRECRAWL_API_KEY, gangnamUnniHintUrl) .then(v => { result.gangnamUnni = v; }) ); } // TikTok — skip verification for now (TikTok blocks HEAD requests) if (candidates.tiktok.length > 0) { const tkHandle = candidates.tiktok[0].replace(/^@/, ''); result.tiktok = { handle: tkHandle, verified: false, url: `https://tiktok.com/@${tkHandle}` }; } await Promise.allSettled(tasks); return result; }