-
-
Notifications
You must be signed in to change notification settings - Fork 86
Expand file tree
/
Copy pathpostThread.js
More file actions
119 lines (99 loc) · 3.59 KB
/
Copy pathpostThread.js
File metadata and controls
119 lines (99 loc) · 3.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// Copyright (c) 2024-2026 nich (@nichxbt). Licensed under the Apache License, Version 2.0.
// scripts/postThread.js
// Browser console script for posting a thread of connected tweets on X/Twitter
// Paste in DevTools console on x.com
// by nichxbt
(() => {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// =============================================
// CONFIGURATION
// =============================================
const CONFIG = {
thread: [
'Tweet 1 🧵',
'Tweet 2',
'Final tweet!',
],
dryRun: true, // SET FALSE TO ACTUALLY POST
delayBetweenTweets: 2000, // ms between adding each tweet
};
// =============================================
const typeIntoInput = async (input, text) => {
input.focus();
await sleep(100);
document.execCommand('insertText', false, text);
await sleep(200);
};
const run = async () => {
console.log('🧵 POST THREAD — XActions by nichxbt');
if (CONFIG.thread.length === 0) {
console.log('❌ No thread content! Edit CONFIG.thread array.');
return;
}
// Validate character counts
const overLimit = CONFIG.thread.filter((t, i) => {
if (t.length > 280) {
console.error(`❌ Tweet ${i + 1} exceeds 280 chars (${t.length})`);
return true;
}
return false;
});
if (overLimit.length > 0) {
console.error('🛑 Fix the above tweets before posting.');
return;
}
console.log(`📋 Thread: ${CONFIG.thread.length} tweets`);
CONFIG.thread.forEach((t, i) => {
console.log(` ${i + 1}. (${t.length}/280) ${t.substring(0, 60)}${t.length > 60 ? '...' : ''}`);
});
if (CONFIG.dryRun) {
console.log('\n⚠️ DRY RUN MODE — Set CONFIG.dryRun = false to actually post');
return;
}
// Open compose dialog
const composeBtn = document.querySelector('a[data-testid="SideNav_NewTweet_Button"]');
if (composeBtn) {
composeBtn.click();
await sleep(1500);
}
// Type first tweet
const firstInput = document.querySelector('[data-testid="tweetTextarea_0"]');
if (!firstInput) {
console.error('❌ Compose box not found! Try clicking the compose button manually.');
return;
}
await typeIntoInput(firstInput, CONFIG.thread[0]);
console.log(`✅ Tweet 1/${CONFIG.thread.length} typed`);
// Add remaining tweets via the "+" button
for (let i = 1; i < CONFIG.thread.length; i++) {
await sleep(CONFIG.delayBetweenTweets);
const addBtn = document.querySelector('[data-testid="addButton"]');
if (!addBtn) {
console.error(`❌ Add-tweet button not found for tweet ${i + 1}`);
break;
}
addBtn.click();
await sleep(1000);
// Grab the newest textarea
const inputs = document.querySelectorAll('[data-testid^="tweetTextarea_"]');
const newInput = inputs[inputs.length - 1];
if (!newInput) {
console.error(`❌ Input field for tweet ${i + 1} not found`);
break;
}
await typeIntoInput(newInput, CONFIG.thread[i]);
console.log(`✅ Tweet ${i + 1}/${CONFIG.thread.length} typed`);
}
// Post the entire thread
await sleep(1000);
const postBtn = document.querySelector('[data-testid="tweetButton"]') ||
document.querySelector('[data-testid="tweetButtonInline"]');
if (postBtn) {
postBtn.click();
console.log(`\n🧵 Thread posted! ${CONFIG.thread.length} tweets sent.`);
} else {
console.error('❌ Post button not found. Thread is typed — click "Post all" manually.');
}
};
run();
})();