/** * Gemini WebView Injection Scripts * * Extraction-only JavaScript injected into the Gemini WebView. * No branded UI, no debug chatter, no upload calls. * * Extraction strategy: * 1. Discover conversation links from the sidebar (/app/{id}) * 2. Attempt RPC fetch via batchexecute for each conversation * 3. Fall back to DOM extraction for the currently visible conversation * 4. Send extracted payload back to React Native via postMessage * * @reference Web SDK: onairos-npm/src/components/ConnectGeminiModal.jsx */ /** * Export Script — extracts conversations from Gemini. * * Gemini does not expose a simple REST API like ChatGPT. Instead it uses * Google's internal batchexecute RPC which requires a session token * (SNlM0e) from WIZ_global_data. If the RPC path fails, we fall back to * extracting the currently visible conversation from the DOM. */ export declare const GEMINI_EXPORT_SCRIPT = "\n(function() {\n try {\n if (!document.body) {\n window.ReactNativeWebView.postMessage(JSON.stringify({\n type: 'CONVERSATION_EXPORT_ERROR',\n message: 'Page not ready'\n }));\n return;\n }\n\n (async () => {\n try {\n window.ReactNativeWebView.postMessage(JSON.stringify({\n type: 'EXPORT_PROGRESS',\n stage: 'discovering',\n progress: 2,\n message: 'Discovering conversations...'\n }));\n\n // --- Helper utilities ---\n function cleanText(t) {\n t = (t || '').replace(/\\r/g, '').trim();\n if (!t) return '';\n return t.replace(/\\n{3,}/g, '\\n\\n').trim();\n }\n\n function inBadContainer(el) {\n try {\n return !!(el.closest('nav,aside,mat-sidenav,[role=\"navigation\"],[data-test-id*=\"sidebar\" i],[aria-label*=\"Chats\" i]'));\n } catch (_) { return false; }\n }\n\n // --- Step 1: Discover sidebar conversation links ---\n function extractChatLinks(max) {\n try {\n var links = Array.prototype.slice.call(\n document.querySelectorAll('a[href*=\"/app/\"]')\n );\n var out = [];\n var seen = {};\n for (var i = 0; i < links.length; i++) {\n var a = links[i];\n var href = a.getAttribute('href') || '';\n var mm = href.match(/\\/app\\/([a-zA-Z0-9_-]{8,64})/);\n if (!mm || !mm[1]) continue;\n if (seen[mm[1]]) continue;\n seen[mm[1]] = true;\n var title = (a.innerText || a.textContent || '').replace(/\\s+/g, ' ').trim();\n if (title.length > 140) title = title.slice(0, 140);\n out.push({ id: mm[1], title: title || 'Untitled', href: href });\n }\n if (typeof max === 'number') out = out.slice(0, max);\n return out;\n } catch (_) { return []; }\n }\n\n // --- Step 2: Read WIZ_global_data session fields ---\n function readWizGlobalData() {\n var snlm0e = null;\n var fSid = null;\n var bl = null;\n try {\n var wiz = window.WIZ_global_data || {};\n snlm0e = wiz.SNlM0e || null;\n fSid = wiz.FdrFJe || null;\n bl = wiz.cfb2h || null;\n } catch (_) {}\n\n // Fallback: scan script tags for SNlM0e / FdrFJe / cfb2h\n if (!snlm0e || !fSid || !bl) {\n try {\n var scripts = document.querySelectorAll('script');\n for (var si = 0; si < scripts.length; si++) {\n var txt = scripts[si].textContent || '';\n if (!snlm0e) {\n var m = txt.match(/['\"\\s]SNlM0e['\"\\s]*:['\"\\s]*['\"]([^'\"]+)['\"]/);\n if (m) snlm0e = m[1];\n }\n if (!fSid) {\n var m2 = txt.match(/['\"\\s]FdrFJe['\"\\s]*:['\"\\s]*['\"]([^'\"]+)['\"]/);\n if (m2) fSid = m2[1];\n }\n if (!bl) {\n var m3 = txt.match(/['\"\\s]cfb2h['\"\\s]*:['\"\\s]*['\"]([^'\"]+)['\"]/);\n if (m3) bl = m3[1];\n }\n if (snlm0e && fSid && bl) break;\n }\n } catch (_) {}\n }\n return { snlm0e: snlm0e, fSid: fSid, bl: bl };\n }\n\n // --- Step 3: Attempt RPC fetch for a conversation ---\n async function fetchConversationRpc(convId, wizData) {\n try {\n var snlm0e = wizData.snlm0e;\n var fSid = wizData.fSid;\n var bl = wizData.bl;\n\n if (!snlm0e) return null;\n\n // Build query string matching what Gemini actually sends\n var qs = 'rpcids=hNvQHb&source-path=%2Fapp&hl=en&rt=c';\n if (fSid) qs += '&f.sid=' + encodeURIComponent(fSid);\n if (bl) qs += '&bl=' + encodeURIComponent(bl);\n qs += '&_reqid=' + (Math.floor(Math.random() * 9000000) + 1000000);\n\n // Build RPC request\n var reqData = JSON.stringify(['c_' + convId, 100, null, 1, [0], [4], null, 1]);\n var body = 'f.req=' + encodeURIComponent(\n JSON.stringify([[[\n 'hNvQHb',\n reqData,\n null,\n 'generic'\n ]]])\n ) + '&at=' + encodeURIComponent(snlm0e);\n\n var resp = await fetch('/_/BardChatUi/data/batchexecute?' + qs, {\n method: 'POST',\n credentials: 'include',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'\n },\n body: body\n });\n\n if (!resp.ok) return null;\n\n var text = await resp.text();\n // Parse the envelope: lines start with )]}' then JSON arrays\n var lines = text.split('\\n');\n var messages = [];\n\n for (var li = 0; li < lines.length; li++) {\n var line = lines[li].trim();\n if (!line || line === \")]}'\") continue;\n try {\n var parsed = JSON.parse(line);\n // The response is a nested array; conversation turns\n // are buried several levels deep.\n if (Array.isArray(parsed) && parsed[0] && Array.isArray(parsed[0])) {\n var inner = parsed[0];\n if (inner[2]) {\n try {\n var payload = JSON.parse(inner[2]);\n // Navigate nested structure to find turns\n if (Array.isArray(payload)) {\n extractTurnsFromPayload(payload, messages);\n }\n } catch (_) {}\n }\n }\n } catch (_) {}\n }\n\n return messages.length > 0 ? messages : null;\n } catch (_) {\n return null;\n }\n }\n\n function extractTurnsFromPayload(arr, messages) {\n // Gemini RPC responses have deeply nested conversation turns.\n // Walk the structure looking for string arrays that look like turns.\n try {\n if (!Array.isArray(arr)) return;\n for (var i = 0; i < arr.length; i++) {\n var item = arr[i];\n if (!item) continue;\n // A turn is typically [null, content_text, ...]\n // or deeply nested. Look for arrays containing strings > 20 chars.\n if (Array.isArray(item)) {\n // Check if this looks like a conversation turn array\n if (item.length >= 2) {\n for (var j = 0; j < item.length; j++) {\n if (typeof item[j] === 'string' && item[j].length > 10) {\n var cleaned = cleanText(item[j]);\n if (cleaned.length > 10) {\n messages.push(cleaned);\n }\n }\n }\n }\n // Recurse into sub-arrays (max depth control via call stack)\n extractTurnsFromPayload(item, messages);\n }\n }\n } catch (_) {}\n }\n\n // --- Step 3: DOM fallback extraction ---\n function extractCurrentConversationDOM() {\n var root = document.querySelector('main') ||\n document.querySelector('[role=\"main\"]') ||\n document.body;\n\n var selectors = [\n '[role=\"list\"] [role=\"listitem\"]',\n '[role=\"listitem\"]',\n 'article',\n '.query-text',\n '.markdown',\n '[data-test-id*=\"turn\" i]',\n '[data-test-id*=\"message\" i]'\n ];\n\n var texts = [];\n var seen = {};\n\n for (var si = 0; si < selectors.length; si++) {\n var nodes = Array.prototype.slice.call(root.querySelectorAll(selectors[si]));\n for (var ni = 0; ni < nodes.length; ni++) {\n var el = nodes[ni];\n if (!el || inBadContainer(el)) continue;\n var t = cleanText(el.innerText || el.textContent || '');\n if (!t || t.length < 2) continue;\n if (/^Chats\\b/i.test(t)) continue;\n if (t.length > 8000) t = t.slice(0, 8000);\n if (!seen[t]) {\n seen[t] = true;\n texts.push(t);\n }\n }\n if (texts.length >= 6) break;\n }\n\n return texts;\n }\n\n // --- Main extraction flow ---\n var wizData = readWizGlobalData();\n var sidebarLinks = extractChatLinks(10);\n var allConversations = [];\n\n if (sidebarLinks.length > 0) {\n window.ReactNativeWebView.postMessage(JSON.stringify({\n type: 'EXPORT_PROGRESS',\n progress: 10,\n message: 'Found ' + sidebarLinks.length + ' conversations'\n }));\n\n for (var ci = 0; ci < sidebarLinks.length; ci++) {\n var meta = sidebarLinks[ci];\n var progressPercent = 10 + ((ci / sidebarLinks.length) * 75);\n\n window.ReactNativeWebView.postMessage(JSON.stringify({\n type: 'EXPORT_PROGRESS',\n stage: 'processing_conversation',\n current: ci + 1,\n total: sidebarLinks.length,\n progress: Math.round(progressPercent),\n message: 'Processing ' + (ci + 1) + '/' + sidebarLinks.length + ' conversations'\n }));\n\n // Random delay 500-1500ms between requests\n if (ci > 0) {\n await new Promise(function(r) {\n setTimeout(r, 500 + Math.floor(Math.random() * 1000));\n });\n }\n\n var rpcMessages = await fetchConversationRpc(meta.id, wizData);\n if (rpcMessages && rpcMessages.length > 0) {\n allConversations.push({\n conversation_id: meta.id,\n title: meta.title,\n user_messages: rpcMessages\n });\n }\n }\n }\n\n // DOM fallback: extract the currently visible conversation\n if (allConversations.length === 0) {\n window.ReactNativeWebView.postMessage(JSON.stringify({\n type: 'EXPORT_PROGRESS',\n progress: 85,\n message: 'Extracting visible conversation...'\n }));\n\n var domTexts = extractCurrentConversationDOM();\n if (domTexts.length > 0) {\n // Get current chat ID from URL if available\n var chatId = null;\n try {\n var pathMatch = (location.pathname || '').match(\n /^\\/app\\/([a-zA-Z0-9_-]{8,64})/\n );\n if (pathMatch) chatId = pathMatch[1];\n } catch (_) {}\n\n allConversations.push({\n conversation_id: chatId || ('gemini_' + Date.now()),\n title: document.title || 'Gemini Conversation',\n user_messages: domTexts\n });\n }\n }\n\n var totalMessages = 0;\n for (var ti = 0; ti < allConversations.length; ti++) {\n totalMessages += allConversations[ti].user_messages.length;\n }\n\n window.ReactNativeWebView.postMessage(JSON.stringify({\n type: 'CONVERSATION_EXPORT_COMPLETE',\n status: 'success',\n data: {\n conversations: allConversations,\n total_conversations: allConversations.length,\n total_messages: totalMessages\n },\n success: true,\n message: allConversations.length > 0\n ? 'Extracted ' + allConversations.length + ' conversations'\n : 'Connected! No conversations found.'\n }));\n\n } catch (error) {\n window.ReactNativeWebView.postMessage(JSON.stringify({\n type: 'CONVERSATION_EXPORT_ERROR',\n status: 'error',\n message: 'Fetch failed: ' + error.message,\n success: false\n }));\n }\n })();\n\n } catch (error) {\n window.ReactNativeWebView.postMessage(JSON.stringify({\n type: 'CONVERSATION_EXPORT_ERROR',\n status: 'error',\n message: 'Script error: ' + error.message,\n success: false\n }));\n }\n})();\ntrue;\n"; export declare const GEMINI_SUCCESS_SCRIPT = "\n(function() {})();\ntrue;\n"; export declare const GEMINI_ERROR_SCRIPT = "\n(function() {})();\ntrue;\n"; //# sourceMappingURL=gemini.d.ts.map