Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"dependencies": {
"dotenv": "^16.4.5",
"otplib": "^12.0.1",
"playwright": "^1.44.1"
"playwright": "^1.50.0"
}
}
86 changes: 56 additions & 30 deletions tweet.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,26 @@ async function saveState(context) {
}

async function openComposer(page) {
// First: click sidebar button (works in X 2026)
const newTweetBtn = await page.$('[data-testid="SideNav_NewTweet_Button"]');
if (newTweetBtn) {
await newTweetBtn.click();
await page.waitForTimeout(1000);
}

// Second: keyboard shortcut as backup
await page.keyboard.press('n');
await page.waitForTimeout(500);

// Check if opened
const textbox = await page.$('[role="textbox"]');
if (textbox) return;

// If not, try sidebar button
const composeButtons = [
'[data-testid="SideNav_NewTweet_Button"]',
'[data-testid="DashButton_Profile_SidebarCompose"]',
'[data-testid="app-bar-new-tweet-button"]',
'[data-testid="AppTabBar_ComposeButton"]',
'[data-testid="toolBarComposeButton"]',
'[data-testid="compositionButton"]'
'[data-testid="app-bar-new-tweet-button"]'
];

for (const selector of composeButtons) {
Expand All @@ -191,9 +204,6 @@ async function openComposer(page) {
return;
}
}

// Keyboard shortcut on desktop.
await page.keyboard.press('n').catch(() => {});
}

async function dismissOverlays(page) {
Expand All @@ -206,7 +216,8 @@ async function dismissOverlays(page) {
'[data-testid="confirmationSheetCancel"]',
'[data-testid="dialog"] button',
'[data-testid="sheetDialog"] button',
'[data-testid="twc-cc-mask"] + div [data-testid]'
'button[aria-label="Cerrar"]',
'button[aria-label="Close"]'
];

for (const sel of closeSelectors) {
Expand All @@ -217,26 +228,37 @@ async function dismissOverlays(page) {
}
}

// Remove intercepting mask if still present.
// Remove intercepting mask if still present (X 2026 usa mask diferente)
await page.evaluate(() => {
document.querySelectorAll('[data-testid="twc-cc-mask"]').forEach((el) => el.remove());
document.querySelectorAll('#layers > div[role="presentation"], #layers [style*="pointer-events"]').forEach((el) => {
el.remove();
// Todos los tipos de masks conocidos
const masks = [
'[data-testid="twc-cc-mask"]',
'[data-testid="mask"]',
'[data-testid="modal-mask"]'
];
masks.forEach(sel => {
document.querySelectorAll(sel).forEach(el => el.remove());
});
const layer = document.getElementById('layers');
if (layer) {
layer.style.pointerEvents = 'none';

// Tambien limpiar layers
const layers = document.getElementById('layers');
if (layers) {
layers.style.pointerEvents = 'auto';
// Remover overlays que interceptan
const children = layers.querySelectorAll(':scope > div');
children.forEach(child => {
if (child.getAttribute('role') === 'presentation' ||
child.getAttribute('data-testid')?.includes('mask')) {
child.remove();
}
});
}
}).catch(() => {});
}

async function writeTweet(page, composer, tweetText) {
await composer.focus().catch(() => {});
// Select all and insert text to avoid partial input.
const selectAllKey = process.platform === 'darwin' ? 'Meta+A' : 'Control+A';
await page.keyboard.press(selectAllKey).catch(() => {});
await page.keyboard.press('Backspace').catch(() => {});
await page.keyboard.insertText(tweetText);
// Use fill() which is more robust in X 2026
await composer.fill(tweetText);
await page.waitForTimeout(300);
}

Expand Down Expand Up @@ -329,7 +351,13 @@ async function discardDraftIfPrompted(page) {
}

async function postTweet(page, tweetText) {
await openComposer(page);
// Primero limpiar overlays
await dismissOverlays(page);

// Abrir compositor con "n" (funciona en X 2026)
await page.keyboard.press('n');
await page.waitForTimeout(1000);
await dismissOverlays(page);

const composer = await getComposerTextbox(page);
await composer.waitFor({ state: 'visible' });
Expand All @@ -355,14 +383,12 @@ async function postTweet(page, tweetText) {
}

async function getComposerTextbox(page) {
// Selectors actualizados para X 2026 - preferimos contenteditable
const selectors = [
'div[data-testid="tweetTextarea_0"] div[role="textbox"]',
'div[role="textbox"][data-testid="tweetTextarea_0"]',
'div[data-testid="tweetTextarea_0"]',
'div[data-testid="tweetTextarea_1"]',
'div[role="textbox"][data-testid^="tweetTextarea_"]',
'div[role="textbox"][aria-label*="What"]',
'div[role="textbox"]'
'[contenteditable="true"][role="textbox"]',
'[contenteditable="true"]',
'[role="textbox"]',
'[data-testid="tweetTextarea_0"]'
];

for (const selector of selectors) {
Expand All @@ -372,7 +398,7 @@ async function getComposerTextbox(page) {
}
}

throw new Error('Could not find tweet composer textbox.');
throw new Error('Could not find tweet composer textbox.');
}

async function ensureLogin(page, context, username, password) {
Expand Down