feat(twitter): add accept command to auto-accept DM requests by keyword

Usage:
  opencli twitter accept --keyword '微信' --max 20

Workflow:
1. Navigate to /messages/requests
2. Click into each conversation
3. If message contains keyword, click Accept
4. After accept (auto-redirects to /messages), go back to requests
5. Repeat until --max reached or no more matches
This commit is contained in:
jackwener committed 2026-03-19 21:26:08 +08:00
1 parent 6d3e595d36
commit b01bf6769b
1 file changed
+151
+151
View File
@@ -0,0 +1,151 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
cli({
site: 'twitter',
name: 'accept',
description: 'Auto-accept DM requests containing specific keywords',
domain: 'x.com',
strategy: Strategy.UI,
browser: true,
args: [
{ name: 'keyword', type: 'string', required: true, help: 'Keyword to match in message content (e.g. "微信")' },
{ name: 'max', type: 'int', required: false, default: 20, help: 'Maximum number of requests to accept (default: 20)' },
],
columns: ['index', 'status', 'user', 'message'],
func: async (page: IPage | null, kwargs: any) => {
if (!page) throw new Error('Requires browser');
const keyword: string = kwargs.keyword;
const maxAccepts: number = kwargs.max ?? 20;
const results: Array<{ index: number; status: string; user: string; message: string }> = [];
let acceptCount = 0;
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: Extract conversation URLs from the request list
const urlsResult = await page.evaluate(`(async () => {
try {
let attempts = 0;
let urls = [];
while (attempts < 10) {
const anchors = Array.from(document.querySelectorAll('a[href]'));
urls = anchors
.map(a => a.href)
.filter(href => /\\/messages\\/\\d+-\\d+/.test(href));
urls = [...new Set(urls)];
if (urls.length > 0) break;
await new Promise(r => setTimeout(r, 1000));
attempts++;
}
return { ok: true, urls };
} catch(e) {
return { ok: false, error: String(e), urls: [] };
}
})()`);
if (!urlsResult?.ok || !urlsResult.urls?.length) {
if (results.length === 0) {
results.push({ index: 1, status: 'info', user: 'System', message: 'No message requests found' });
}
break;
}
const urls: string[] = urlsResult.urls;
let foundInThisRound = false;
// Step 3: Try each conversation in this round
for (const url of urls) {
if (acceptCount >= maxAccepts) break;
await page.goto(url);
await page.wait(3);
// Read chat content, check keyword, and accept if matched
const res = await page.evaluate(`(async () => {
try {
const keyword = ${JSON.stringify(keyword)};
// Get username from conversation header
const heading = document.querySelector('[data-testid="conversation-header"]') ||
document.querySelector('h2');
const username = heading ? heading.innerText.trim().split('\\n')[0] : 'Unknown';
// Read full chat area text
const chatArea = document.querySelector('[data-testid="DmScrollerContainer"]') ||
document.querySelector('main');
const text = chatArea ? chatArea.innerText : '';
// Check if keyword is present
if (!text.includes(keyword)) {
return { status: 'skipped', user: username, message: 'No keyword match' };
}
// 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, 1500));
// Check if there's a 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, 800));
}
return { status: 'accepted', user: username, message: 'Accepted! Keyword: ' + keyword };
} catch(e) {
return { status: 'error', user: 'system', message: String(e) };
}
})()`);
if (res) {
if (res.status === 'accepted') {
acceptCount++;
foundInThisRound = true;
results.push({
index: acceptCount,
status: res.status,
user: res.user || 'Unknown',
message: res.message || 'Accepted',
});
// After accept, Twitter redirects to /messages — break out to re-navigate to /messages/requests
await page.wait(2);
break;
}
// Don't add skipped items to output to keep it clean
}
}
// If no match found in this round, we've exhausted all requests
if (!foundInThisRound) {
break;
}
}
if (results.length === 0) {
results.push({ index: 0, status: 'info', user: 'System', message: `No requests matched keyword "${keyword}"` });
}
return results;
}
});