"use server"; // Server actions for the X session broker (common/social/xSessionBroker.ts) // and the X login source (common/social/xCookieSource.ts, release 16 slice XL). // // "Connect X account" opens a HEADED browser on the editor host so the operator // can log in by hand — password, 2FA and captcha included. That is the whole // point: automating an X login is what gets accounts flagged, and a human doing // it once produces a profile that stays valid. The window is the operator's own // Chromium or Chrome when one is installed, without the automation signals // (common/social/xBrowser.ts). // // Because it opens a window on the SERVER's display, this is deliberately an // explicit operator action and never something a fetch triggers on its own. import { revalidatePath } from "next/cache"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { clearXSession, connectXAccount, readXSessionStatus, refreshXCookies, type XSessionStatus, } from "yt-dlp-transcript-common/social/xSessionBroker"; import { readXLoginStatus, resolveXCookieSourceFor, type XLoginStatus, } from "yt-dlp-transcript-common/social/xBrowserLogin"; import { isXCookieSource, isXPostsVisibility, xCookieSourceView, type XCookieSourceView, type XPostsVisibility, type XSocialSettings, } from "yt-dlp-transcript-common/social/xCookieSource"; import { xPostsVisibility } from "yt-dlp-transcript-common/lib/postsVisibility"; import { saveSettings } from "./saveSettings"; // `social.x` is ONE value to saveSettings, whose merge is one level deep: a // patch naming `social: { x }` replaces the whole X block. So each X choice is // written over the block as it is now, with only its own key changed — the // login source keeps the visibility, and the visibility keeps the source. function socialXPatch( change: (x: XSocialSettings) => XSocialSettings, ): { social: { x: XSocialSettings } } { return { social: { x: change({ ...getSettings().social.x }) } }; } // Every session action returns the source in use beside the profile's status: // connecting or forgetting a profile can move the read-time default. export type XSessionActionResult = | { ok: true; status: XSessionStatus; source: XCookieSourceView; browser?: string } | { ok: false; error: string }; async function sourceNow(): Promise { return xCookieSourceView(await resolveXCookieSourceFor(getPaths(), getSettings())); } export async function xSessionStatusAction(): Promise { return readXSessionStatus(getPaths()); } export async function connectXAccountAction(): Promise { try { const { browser, ...status } = await connectXAccount(getPaths(), { // Which browser opened, and any fallback, in the editor's log. onLog: (line) => console.log(`[x-session] ${line}`), }); if (!status.looksAuthenticated) { return { ok: false, error: `The browser (${browser}) closed without a logged-in X session (no ` + "auth_token cookie was exported). Try again and complete the login " + "before closing the window — X's own password login is the reliable " + "path where Google's sign-in refuses.", }; } return { ok: true, status, source: await sourceNow(), browser }; } catch (e) { return { ok: false, error: (e as Error).message }; } } export async function refreshXCookiesAction(): Promise { try { const status = await refreshXCookies(getPaths(), { onLog: (line) => console.log(`[x-session] ${line}`), }); return { ok: true, status, source: await sourceNow() }; } catch (e) { return { ok: false, error: (e as Error).message }; } } export async function clearXSessionAction(): Promise { try { await clearXSession(getPaths()); return { ok: true, status: await readXSessionStatus(getPaths()), source: await sourceNow(), }; } catch (e) { return { ok: false, error: (e as Error).message }; } } // THE CHECK: the source in use, whether an X login (an auth_token for x.com) // is visible in it, and when it was last seen. Reads the browser's cookie store // read-only (a private copy) or the profile's exported jar. export type XLoginCheckResult = | { ok: true; login: XLoginStatus } | { ok: false; error: string }; export async function checkXLoginAction(): Promise { try { return { ok: true, login: await readXLoginStatus(getPaths(), getSettings()) }; } catch (e) { return { ok: false, error: (e as Error).message }; } } // THE CHOICE: "browser", "profile", or "auto" — no stored choice, the default // resolved at read time. Writes `social.x.cookieSource` through the one // settings writer and nothing else. export type XSourceChoiceResult = | { ok: true; source: XCookieSourceView } | { ok: false; error: string }; export async function setXCookieSourceAction( choice: string, ): Promise { if (choice !== "auto" && !isXCookieSource(choice)) { return { ok: false, error: `Unknown X login source "${choice}".` }; } try { await saveSettings( socialXPatch(({ cookieSource: _was, ...rest }) => choice === "auto" ? rest : { ...rest, cookieSource: choice }, ), ); } catch (e) { return { ok: false, error: (e as Error).message }; } revalidatePath("/settings"); return { ok: true, source: await sourceNow() }; } // WHERE X POSTS APPEAR — `social.x.visibility` (release 17 slice XP). "public" // is the default and is written as no key; "private" keeps every X channel's // posts out of every public site build (common/lib/postsVisibility.ts). Nothing // on disk changes and fetching does not: a site already published changes on // its next build and deploy. export type XPostsVisibilityResult = | { ok: true; visibility: XPostsVisibility } | { ok: false; error: string }; export async function setXPostsVisibilityAction( choice: string, ): Promise { if (!isXPostsVisibility(choice)) { return { ok: false, error: `Unknown X post visibility "${choice}".` }; } try { await saveSettings( socialXPatch(({ visibility: _was, ...rest }) => choice === "public" ? rest : { ...rest, visibility: choice }, ), ); } catch (e) { return { ok: false, error: (e as Error).message }; } revalidatePath("/settings"); return { ok: true, visibility: xPostsVisibility(getSettings()) }; }