149 lines
4.0 KiB
TypeScript
149 lines
4.0 KiB
TypeScript
/**
|
|
* X Integration - Shared utilities
|
|
* Used by all X scripts
|
|
*/
|
|
|
|
import { chromium, BrowserContext, Page } from 'playwright';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { config } from './config.js';
|
|
|
|
export { config };
|
|
|
|
export interface ScriptResult {
|
|
success: boolean;
|
|
message: string;
|
|
data?: unknown;
|
|
}
|
|
|
|
/**
|
|
* Read input from stdin
|
|
*/
|
|
export async function readInput<T>(): Promise<T> {
|
|
return new Promise((resolve, reject) => {
|
|
let data = '';
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', chunk => { data += chunk; });
|
|
process.stdin.on('end', () => {
|
|
try {
|
|
resolve(JSON.parse(data));
|
|
} catch (err) {
|
|
reject(new Error(`Invalid JSON input: ${err}`));
|
|
}
|
|
});
|
|
process.stdin.on('error', reject);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Write result to stdout
|
|
*/
|
|
export function writeResult(result: ScriptResult): void {
|
|
console.log(JSON.stringify(result));
|
|
}
|
|
|
|
/**
|
|
* Clean up browser lock files
|
|
*/
|
|
export function cleanupLockFiles(): void {
|
|
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
|
|
const lockPath = path.join(config.browserDataDir, lockFile);
|
|
if (fs.existsSync(lockPath)) {
|
|
try { fs.unlinkSync(lockPath); } catch {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate tweet/reply content
|
|
*/
|
|
export function validateContent(content: string | undefined, type = 'Tweet'): ScriptResult | null {
|
|
if (!content || content.length === 0) {
|
|
return { success: false, message: `${type} content cannot be empty` };
|
|
}
|
|
if (content.length > config.limits.tweetMaxLength) {
|
|
return { success: false, message: `${type} exceeds ${config.limits.tweetMaxLength} character limit (current: ${content.length})` };
|
|
}
|
|
return null; // Valid
|
|
}
|
|
|
|
/**
|
|
* Get browser context with persistent profile
|
|
*/
|
|
export async function getBrowserContext(): Promise<BrowserContext> {
|
|
if (!fs.existsSync(config.authPath)) {
|
|
throw new Error('X authentication not configured. Run /x-integration to complete login.');
|
|
}
|
|
|
|
cleanupLockFiles();
|
|
|
|
const context = await chromium.launchPersistentContext(config.browserDataDir, {
|
|
executablePath: config.chromePath,
|
|
headless: false,
|
|
viewport: config.viewport,
|
|
args: config.chromeArgs,
|
|
ignoreDefaultArgs: config.chromeIgnoreDefaultArgs,
|
|
});
|
|
|
|
return context;
|
|
}
|
|
|
|
/**
|
|
* Extract tweet ID from URL or raw ID
|
|
*/
|
|
export function extractTweetId(input: string): string | null {
|
|
const urlMatch = input.match(/(?:x\.com|twitter\.com)\/\w+\/status\/(\d+)/);
|
|
if (urlMatch) return urlMatch[1];
|
|
if (/^\d+$/.test(input.trim())) return input.trim();
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Navigate to a tweet page
|
|
*/
|
|
export async function navigateToTweet(
|
|
context: BrowserContext,
|
|
tweetUrl: string
|
|
): Promise<{ page: Page; success: boolean; error?: string }> {
|
|
const page = context.pages()[0] || await context.newPage();
|
|
|
|
let url = tweetUrl;
|
|
const tweetId = extractTweetId(tweetUrl);
|
|
if (tweetId && !tweetUrl.startsWith('http')) {
|
|
url = `https://x.com/i/status/${tweetId}`;
|
|
}
|
|
|
|
try {
|
|
await page.goto(url, { timeout: config.timeouts.navigation, waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(config.timeouts.pageLoad);
|
|
|
|
const exists = await page.locator('article[data-testid="tweet"]').first().isVisible().catch(() => false);
|
|
if (!exists) {
|
|
return { page, success: false, error: 'Tweet not found. It may have been deleted or the URL is invalid.' };
|
|
}
|
|
|
|
return { page, success: true };
|
|
} catch (err) {
|
|
return { page, success: false, error: `Navigation failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run script with error handling
|
|
*/
|
|
export async function runScript<T>(
|
|
handler: (input: T) => Promise<ScriptResult>
|
|
): Promise<void> {
|
|
try {
|
|
const input = await readInput<T>();
|
|
const result = await handler(input);
|
|
writeResult(result);
|
|
} catch (err) {
|
|
writeResult({
|
|
success: false,
|
|
message: `Script execution failed: ${err instanceof Error ? err.message : String(err)}`
|
|
});
|
|
process.exit(1);
|
|
}
|
|
}
|