9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
205 lines
8.9 KiB
JavaScript
205 lines
8.9 KiB
JavaScript
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
cli({
|
|
site: 'twitter',
|
|
name: 'accept',
|
|
access: 'write',
|
|
description: 'Auto-accept DM requests containing specific keywords',
|
|
domain: 'x.com',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [
|
|
{ name: 'query', type: 'string', required: true, positional: true, help: 'Keywords to match (comma-separated for OR, e.g. "群,微信")' },
|
|
{ name: 'max', type: 'int', required: false, default: 20, help: 'Maximum number of requests to accept (default: 20)' },
|
|
{ name: 'timeout', type: 'int', required: false, default: 600, help: 'Max seconds for the overall command (default: 600 — batch op)' },
|
|
],
|
|
columns: ['index', 'status', 'user', 'message'],
|
|
func: async (page, kwargs) => {
|
|
if (!page)
|
|
throw new CommandExecutionError('Browser session required for twitter accept');
|
|
const keywords = kwargs.query.split(',').map((k) => k.trim()).filter(Boolean);
|
|
const maxAccepts = kwargs.max ?? 20;
|
|
const results = [];
|
|
let acceptCount = 0;
|
|
// Track already-visited conversations to avoid infinite loops
|
|
const visited = new Set();
|
|
for (let round = 0; round < maxAccepts + 50; round++) {
|
|
if (acceptCount >= maxAccepts)
|
|
break;
|
|
// Step 1: Navigate to DM requests page
|
|
await page.goto('https://x.com/messages/requests');
|
|
await page.wait(4);
|
|
// Step 2: Get conversations with scroll-to-load
|
|
const convInfo = await page.evaluate(`(async () => {
|
|
try {
|
|
// Wait for initial items
|
|
let attempts = 0;
|
|
while (attempts < 10) {
|
|
const convs = document.querySelectorAll('[data-testid="conversation"]');
|
|
if (convs.length > 0) break;
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
attempts++;
|
|
}
|
|
|
|
// Scroll to load more
|
|
const seenCount = new Set();
|
|
let noNewCount = 0;
|
|
for (let scroll = 0; scroll < 20; scroll++) {
|
|
const convs = Array.from(document.querySelectorAll('[data-testid="conversation"]'));
|
|
const prevSize = seenCount.size;
|
|
convs.forEach((_, i) => seenCount.add(i));
|
|
if (convs.length >= ${maxAccepts + 10}) break;
|
|
|
|
// Scroll last item into view
|
|
if (convs.length > 0) {
|
|
convs[convs.length - 1].scrollIntoView({ behavior: 'instant', block: 'end' });
|
|
}
|
|
await new Promise(r => setTimeout(r, 1500));
|
|
|
|
if (seenCount.size <= prevSize) {
|
|
noNewCount++;
|
|
if (noNewCount >= 3) break;
|
|
} else {
|
|
noNewCount = 0;
|
|
}
|
|
}
|
|
|
|
const convs = Array.from(document.querySelectorAll('[data-testid="conversation"]'));
|
|
if (convs.length === 0) return { ok: false, count: 0, items: [] };
|
|
|
|
const items = convs.map((conv, idx) => {
|
|
const text = conv.innerText || '';
|
|
const link = conv.querySelector('a[href]');
|
|
const href = link ? link.href : '';
|
|
const lines = text.split('\\n').filter(l => l.trim());
|
|
const user = lines[0] || 'Unknown';
|
|
return { idx, text, href, user };
|
|
});
|
|
return { ok: true, count: convs.length, items };
|
|
} catch(e) {
|
|
return { ok: false, error: String(e), count: 0, items: [] };
|
|
}
|
|
})()`);
|
|
if (!convInfo?.ok || convInfo.count === 0) {
|
|
if (results.length === 0) {
|
|
results.push({ index: 1, status: 'info', user: 'System', message: 'No message requests found' });
|
|
}
|
|
break;
|
|
}
|
|
let foundInThisRound = false;
|
|
// Step 3: Find first unvisited conversation with keyword match in preview
|
|
for (const item of convInfo.items) {
|
|
if (acceptCount >= maxAccepts)
|
|
break;
|
|
const convKey = item.href || `conv-${item.idx}`;
|
|
if (visited.has(convKey))
|
|
continue;
|
|
visited.add(convKey);
|
|
// Check if preview text contains any keyword
|
|
const previewMatch = keywords.some((k) => item.text.includes(k));
|
|
if (!previewMatch)
|
|
continue;
|
|
// Step 4: Click this conversation to open it
|
|
const clickResult = await page.evaluate(`(async () => {
|
|
try {
|
|
const convs = Array.from(document.querySelectorAll('[data-testid="conversation"]'));
|
|
const conv = convs[${item.idx}];
|
|
if (!conv) return { ok: false, error: 'Conversation element not found' };
|
|
conv.click();
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
return { ok: true };
|
|
} catch(e) {
|
|
return { ok: false, error: String(e) };
|
|
}
|
|
})()`);
|
|
if (!clickResult?.ok)
|
|
continue;
|
|
// Wait for conversation to load
|
|
await page.wait(2);
|
|
// Step 5: Read full chat content and find Accept button
|
|
const res = await page.evaluate(`(async () => {
|
|
try {
|
|
const keywords = ${JSON.stringify(keywords)};
|
|
|
|
// Get username from conversation header
|
|
const heading = document.querySelector('[data-testid="conversation-header"]') ||
|
|
document.querySelector('[data-testid="DM-conversation-header"]');
|
|
let username = 'Unknown';
|
|
if (heading) {
|
|
username = heading.innerText.trim().split('\\n')[0];
|
|
}
|
|
|
|
// Read full chat area text
|
|
const chatArea = document.querySelector('[data-testid="DmScrollerContainer"]') ||
|
|
document.querySelector('[data-testid="DMConversationBody"]') ||
|
|
document.querySelector('main [data-testid="cellInnerDiv"]')?.closest('section') ||
|
|
document.querySelector('main');
|
|
const text = chatArea ? chatArea.innerText : '';
|
|
|
|
// Verify keyword match in full chat content
|
|
const matchedKw = keywords.filter(k => text.includes(k));
|
|
if (matchedKw.length === 0) {
|
|
return { status: 'skipped', user: username, message: 'No keyword match in full content' };
|
|
}
|
|
|
|
// Find the Accept button
|
|
const allBtns = Array.from(document.querySelectorAll('[role="button"]'));
|
|
const acceptBtn = allBtns.find(btn => {
|
|
const t = btn.innerText.trim().toLowerCase();
|
|
return t === 'accept' || t === '接受';
|
|
});
|
|
|
|
if (!acceptBtn) {
|
|
return { status: 'no_button', user: username, message: 'Keyword matched but no Accept button (already accepted?)' };
|
|
}
|
|
|
|
// Click Accept
|
|
acceptBtn.click();
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
|
|
// Check for confirmation dialog
|
|
const btnsAfter = Array.from(document.querySelectorAll('[role="button"]'));
|
|
const confirmBtn = btnsAfter.find(btn => {
|
|
const t = btn.innerText.trim().toLowerCase();
|
|
return (t === 'accept' || t === '接受') && btn !== acceptBtn;
|
|
});
|
|
if (confirmBtn) {
|
|
confirmBtn.click();
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
}
|
|
|
|
return { status: 'accepted', user: username, message: 'Accepted! Matched: ' + matchedKw.join(', ') };
|
|
} catch(e) {
|
|
return { status: 'error', user: 'system', message: String(e) };
|
|
}
|
|
})()`);
|
|
if (res?.status === 'accepted') {
|
|
acceptCount++;
|
|
foundInThisRound = true;
|
|
results.push({
|
|
index: acceptCount,
|
|
status: 'accepted',
|
|
user: res.user || 'Unknown',
|
|
message: res.message || 'Accepted',
|
|
});
|
|
// After accept, Twitter redirects to /messages — loop back to /messages/requests
|
|
await page.wait(2);
|
|
break; // break inner loop, outer loop will re-navigate to requests
|
|
}
|
|
else if (res?.status === 'no_button') {
|
|
// Already accepted, skip
|
|
continue;
|
|
}
|
|
}
|
|
// If no match found in this round, we've exhausted all visible requests
|
|
if (!foundInThisRound) {
|
|
break;
|
|
}
|
|
}
|
|
if (results.length === 0) {
|
|
results.push({ index: 0, status: 'info', user: 'System', message: `No requests matched keywords "${keywords.join(', ')}"` });
|
|
}
|
|
return results;
|
|
}
|
|
});
|