mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-23 01:54:52 +02:00
Closes the loop on the Twitter MCP: instead of pointing operators at `python -m backend.apps.twitter.import_cookies`, the Tools page now has a Slack-style "Sign in with X" button that opens an Electron BrowserWindow at x.com, waits for a real sign-in, scrapes auth_token + ct0 from the (HttpOnly) cookie jar, and POSTs them to a new backend route that plugs them into the live pool. - electron/main.js: new `connect-twitter` IPC handler. Mirrors `connect-slack` but reads cookies via session.cookies.get() since auth_token/ct0 are HttpOnly (executeJavaScript on document.cookie returns nothing). Guards against the pre-auth ct0 by requiring the popup URL to have left /i/flow/login and auth_token.length > 20. - electron/preload.js: expose `connectTwitter` on the openswarm bridge. - backend/apps/twitter/models.py: `CookieImportRequest` — auth_token + ct0 with strip/non-empty validator, optional id/handle for in-place re-login, optional label/role. - backend/apps/twitter/twitter.py: `POST /accounts/import`. HTTP sibling of import_cookies.py — same on-disk format via the shared `_write_cookies` helper, but hydrates the live AccountPool and runs an inline `_verify_account` so the UI gets the verified state in one round-trip. Re-login by id or handle reuses the existing record; failed verify returns 200 with a downgraded state (needs_relogin / locked / suspended), never echoes cookies in the response. - frontend/src/app/pages/Tools/Tools.tsx: Twitter integration entry + `handleTwitterAutoConnect`. Generalizes the Slack-only dialog branching into a `browserAuthHandlers` lookup so future in-app auth flows are one entry, not a third hardcoded id === 'slack' check. - tests/test_twitter_routes.py: covers happy path (cookies on disk 0o600, pool size 1), idempotent re-login by id, dedupe by handle, 422 on empty/whitespace tokens, the no-cookie-echo contract, and the verify-failure → state=needs_relogin degraded path. Uses a patched twikit.Client so nothing actually hits x.com.
107 lines
5.0 KiB
JavaScript
107 lines
5.0 KiB
JavaScript
const { contextBridge, ipcRenderer } = require('electron');
|
|
|
|
(async () => {
|
|
const port = await ipcRenderer.invoke('get-backend-port');
|
|
const webviewPreloadPath = await ipcRenderer.invoke('get-webview-preload-path');
|
|
|
|
contextBridge.exposeInMainWorld('__OPENSWARM_PORT__', port);
|
|
|
|
contextBridge.exposeInMainWorld('openswarm', {
|
|
getBackendPort: () => port,
|
|
getWebviewPreloadPath: () => webviewPreloadPath,
|
|
|
|
// Per-install auth token required for WS + HTTP calls to the
|
|
// localhost backend. Returns a Promise<string>. The renderer should
|
|
// await this on startup and include the token on every WS URL
|
|
// (`?token=...`) and HTTP request (`Authorization: Bearer ...`).
|
|
// We deliberately do NOT expose the token as a plain window global
|
|
// or a sync getter — contextBridge + IPC keeps it off the
|
|
// renderer's global object so third-party scripts (including any
|
|
// code that leaks through <webview>) can't scrape it.
|
|
getAuthToken: () => ipcRenderer.invoke('get-auth-token'),
|
|
|
|
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
|
openExternal: (url) => ipcRenderer.invoke('open-external', url),
|
|
connectSlack: () => ipcRenderer.invoke('connect-slack'),
|
|
connectTwitter: () => ipcRenderer.invoke('connect-twitter'),
|
|
sendCdpCommand: (wcId, method, params) => ipcRenderer.invoke('send-cdp-command', wcId, method, params),
|
|
cdpCacheSet: (wcId, indexMap) => ipcRenderer.invoke('cdp-cache-set', wcId, indexMap),
|
|
cdpCacheGet: (wcId) => ipcRenderer.invoke('cdp-cache-get', wcId),
|
|
cdpCacheClear: (wcId) => ipcRenderer.invoke('cdp-cache-clear', wcId),
|
|
capturePage: (rect) => ipcRenderer.invoke('capture-page', rect),
|
|
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
|
|
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
|
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
|
installUpdate: () => ipcRenderer.invoke('install-update'),
|
|
|
|
onUpdateAvailable: (cb) => {
|
|
const listener = (_event, info) => cb(info);
|
|
ipcRenderer.on('update-available', listener);
|
|
return () => ipcRenderer.removeListener('update-available', listener);
|
|
},
|
|
onUpdateNotAvailable: (cb) => {
|
|
const listener = (_event, info) => cb(info);
|
|
ipcRenderer.on('update-not-available', listener);
|
|
return () => ipcRenderer.removeListener('update-not-available', listener);
|
|
},
|
|
onDownloadProgress: (cb) => {
|
|
const listener = (_event, progress) => cb(progress);
|
|
ipcRenderer.on('download-progress', listener);
|
|
return () => ipcRenderer.removeListener('download-progress', listener);
|
|
},
|
|
onUpdateDownloaded: (cb) => {
|
|
const listener = (_event, info) => cb(info);
|
|
ipcRenderer.on('update-downloaded', listener);
|
|
return () => ipcRenderer.removeListener('update-downloaded', listener);
|
|
},
|
|
onUpdateError: (cb) => {
|
|
const listener = (_event, message) => cb(message);
|
|
ipcRenderer.on('update-error', listener);
|
|
return () => ipcRenderer.removeListener('update-error', listener);
|
|
},
|
|
|
|
onWebviewNewWindow: (cb) => {
|
|
const listener = (_event, url, webContentsId) => cb(url, webContentsId);
|
|
ipcRenderer.on('webview-new-window', listener);
|
|
return () => ipcRenderer.removeListener('webview-new-window', listener);
|
|
},
|
|
|
|
// Deep-link callback: fires when the OS opens the app with an
|
|
// openswarm://auth?token=... URL (after Stripe-hosted checkout).
|
|
onAuthUrl: (cb) => {
|
|
const listener = (_event, url) => cb(url);
|
|
ipcRenderer.on('openswarm:auth-url', listener);
|
|
return () => ipcRenderer.removeListener('openswarm:auth-url', listener);
|
|
},
|
|
|
|
// OAuth claim deep-link channel. Receives openswarm://oauth/{provider}/complete
|
|
// after the user finishes an OAuth flow in their browser.
|
|
onOauthClaim: (cb) => {
|
|
const listener = (_event, url) => cb(url);
|
|
ipcRenderer.on('openswarm:oauth-claim', listener);
|
|
return () => ipcRenderer.removeListener('openswarm:oauth-claim', listener);
|
|
},
|
|
|
|
// Window blur/focus events — analytics signal for "user switched
|
|
// to another app" (temp-churn measurement). Throttled in main.js to
|
|
// at most once per 2s per direction so OS-level focus storms don't
|
|
// pollute the event stream.
|
|
onWindowFocus: (cb) => {
|
|
const listener = (_event, payload) => cb(payload);
|
|
ipcRenderer.on('openswarm:window-focus', listener);
|
|
return () => ipcRenderer.removeListener('openswarm:window-focus', listener);
|
|
},
|
|
|
|
// OAuth popup callback. Fires when any child webContents navigates to
|
|
// localhost:20128/callback?code=... — main.js watches for this and
|
|
// forwards the parsed params here. Used as a belt-and-suspenders
|
|
// alongside window.opener.postMessage (which silently fails on some
|
|
// Anthropic flows that reset the opener chain during redirect).
|
|
onOauthCallback: (cb) => {
|
|
const listener = (_event, data) => cb(data);
|
|
ipcRenderer.on('openswarm:oauth-callback', listener);
|
|
return () => ipcRenderer.removeListener('openswarm:oauth-callback', listener);
|
|
},
|
|
});
|
|
})();
|