-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcontent_script.js
More file actions
824 lines (688 loc) · 27.2 KB
/
content_script.js
File metadata and controls
824 lines (688 loc) · 27.2 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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
// Prevent duplicate loading
if (window.xReplyLoaded) {
throw new Error("X Crypto Agent already loaded");
}
window.xReplyLoaded = true;
console.log("X Crypto Agent: Content script loaded (v3).");
let repliedAuthors = new Map(); // Map of username -> timestamp
let targetingSettings = {
minFollowers: 0,
maxFollowers: 0,
nicheKeywords: '',
blacklist: '',
whitelist: ''
};
// Load targeting settings
chrome.storage.sync.get(['minFollowers', 'maxFollowers', 'nicheKeywords', 'blacklist', 'whitelist'], (data) => {
if (data.minFollowers) targetingSettings.minFollowers = data.minFollowers;
if (data.maxFollowers) targetingSettings.maxFollowers = data.maxFollowers;
if (data.nicheKeywords) targetingSettings.nicheKeywords = data.nicheKeywords;
if (data.blacklist) targetingSettings.blacklist = data.blacklist;
if (data.whitelist) targetingSettings.whitelist = data.whitelist;
console.log('Targeting settings loaded:', targetingSettings);
});
// Check if user is in blacklist
function isBlacklisted(username) {
if (!targetingSettings.blacklist) return false;
const blacklist = targetingSettings.blacklist.toLowerCase().split(',').map(u => u.trim().replace('@', ''));
return blacklist.includes(username.toLowerCase());
}
// Check if user is in whitelist
function isWhitelisted(username) {
if (!targetingSettings.whitelist) return false;
const whitelist = targetingSettings.whitelist.toLowerCase().split(',').map(u => u.trim().replace('@', ''));
return whitelist.includes(username.toLowerCase());
}
// Extract follower count from tweet element (if visible)
function getFollowerCount(tweetElement) {
// Try to find follower count from the tweet or user card
// This is best-effort as Twitter doesn't always show this in the timeline
const userCard = tweetElement.querySelector('[data-testid="UserCell"]');
if (userCard) {
const followerText = userCard.textContent;
const match = followerText.match(/(\d+(?:\.\d+)?[KMB]?)\s*(?:followers|takipçi)/i);
if (match) {
return parseFollowerString(match[1]);
}
}
return null; // Unknown follower count
}
// Parse follower string like "10K", "1.5M" to number
function parseFollowerString(str) {
if (!str) return 0;
const num = parseFloat(str);
if (str.toUpperCase().includes('K')) return num * 1000;
if (str.toUpperCase().includes('M')) return num * 1000000;
if (str.toUpperCase().includes('B')) return num * 1000000000;
return num;
}
// Check if follower count is within targeting range
function isWithinFollowerRange(followerCount) {
// If we couldn't determine follower count, allow by default
if (followerCount === null) return true;
const min = targetingSettings.minFollowers || 0;
const max = targetingSettings.maxFollowers || 0;
// Check min followers
if (min > 0 && followerCount < min) {
return false;
}
// Check max followers (0 means no limit)
if (max > 0 && followerCount > max) {
return false;
}
return true;
}
// Robust DOM selector helpers with multiple fallbacks
const DOM = {
// Find tweet element
getTweet: (element) => {
return element.closest('article[data-testid="tweet"]') ||
element.closest('article[role="article"]') ||
element.closest('article');
},
// Find all tweets
getAllTweets: () => {
return document.querySelectorAll('article[data-testid="tweet"], article[role="article"]');
},
// Find tweet text
getTweetText: (tweet) => {
const el = tweet.querySelector('[data-testid="tweetText"]') ||
tweet.querySelector('[lang]') ||
tweet.querySelector('div[dir="auto"]');
return el ? el.innerText : '';
},
// Find user names container
getUserNames: (tweet) => {
return tweet.querySelector('[data-testid="User-Names"]') ||
tweet.querySelector('div[data-testid="User-Name"]') ||
tweet.querySelector('a[role="link"][href^="/"]')?.parentElement;
},
// Find reply textbox
getReplyTextbox: () => {
return document.querySelector('div[data-testid="tweetTextarea_0"]') ||
document.querySelector('div[data-testid="tweetTextarea_1"]') ||
document.querySelector('[role="textbox"][aria-label]') ||
document.querySelector('[contenteditable="true"][role="textbox"]');
},
// Find post/reply button
getPostButton: () => {
return document.querySelector('button[data-testid="tweetButton"]') ||
document.querySelector('button[data-testid="tweetButtonInline"]') ||
document.querySelector('[data-testid="toolBar"] button[type="button"]:not([aria-label])') ||
document.querySelector('button[data-testid="reply"]');
},
// Find native reply button in tweet
getNativeReplyButton: (tweet) => {
return tweet.querySelector('button[data-testid="reply"]') ||
tweet.querySelector('[aria-label*="Reply"]') ||
tweet.querySelector('[aria-label*="reply"]');
},
// Find actions bar in tweet
getActionsBar: (tweet) => {
return tweet.querySelector('[role="group"]') ||
tweet.querySelector('[aria-label*="actions"]')?.parentElement;
},
// Find verified badge
getVerifiedBadge: (tweet) => {
return tweet.querySelector('[data-testid="icon-verified"]') ||
tweet.querySelector('svg[aria-label*="Verified"]') ||
tweet.querySelector('[aria-label*="verified"]');
}
};
// Fetch initial list of replied authors
chrome.storage.local.get({ repliedAuthors: [] }, (data) => {
const authors = data.repliedAuthors || [];
repliedAuthors.clear();
// Handle both old format (strings) and new format (objects with timestamp)
authors.forEach(item => {
if (typeof item === 'string') {
// Old format: just username, set timestamp to 0 (very old)
repliedAuthors.set(item.toLowerCase(), 0);
} else if (item.username && item.timestamp) {
// New format: object with username and timestamp
repliedAuthors.set(item.username.toLowerCase(), item.timestamp);
}
});
console.log("Initial replied authors list loaded:", repliedAuthors);
// Initial scan for tweets already on page
DOM.getAllTweets().forEach(processTweet);
});
// Listen for updates to the replied authors list (e.g., after sending a reply)
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'local' && changes.repliedAuthors) {
const authors = changes.repliedAuthors.newValue || [];
repliedAuthors.clear();
authors.forEach(item => {
if (typeof item === 'string') {
repliedAuthors.set(item.toLowerCase(), 0);
} else if (item.username && item.timestamp) {
repliedAuthors.set(item.username.toLowerCase(), item.timestamp);
}
});
console.log("Replied authors list updated:", repliedAuthors);
// Re-process all visible tweets to update their ticks
DOM.getAllTweets().forEach(processTweet);
}
});
// Main function to process a tweet element
function processTweet(tweetElement) {
addReplyButton(tweetElement);
addAuthorTick(tweetElement);
}
// Function to extract tweet text and author info
function getTweetDetails(tweetElement) {
let tweetText = "";
const textElement = tweetElement.querySelector('[data-testid="tweetText"]');
if (textElement) {
tweetText = textElement.innerText;
}
let authorName = "";
// New robust strategy: Find the span with the @-handle, then find its parent link to get the href.
const allSpans = tweetElement.querySelectorAll('span');
const handleSpan = Array.from(allSpans).find(span => span.textContent.startsWith('@'));
if (handleSpan) {
const authorLink = handleSpan.closest('a[role="link"]');
if (authorLink && authorLink.href) {
const hrefParts = authorLink.href.split('/');
authorName = hrefParts[hrefParts.length - 1];
}
}
// Fallback if the first strategy fails
if (!authorName) {
console.log("Could not find author via @-handle, trying fallback...");
const userNamesContainer = tweetElement.querySelector('[data-testid="User-Names"]');
if (userNamesContainer) {
// Get the first link in the user names container
const authorLink = userNamesContainer.querySelector('a[role="link"]');
if (authorLink && authorLink.href) {
const hrefParts = authorLink.href.split('/');
// The username is typically the last part of the URL
authorName = hrefParts[hrefParts.length - 1];
}
}
}
let authorStatus = "Normal";
const verifiedBadge = DOM.getVerifiedBadge(tweetElement);
if (verifiedBadge) {
authorStatus = "Verified";
}
// Extract link from tweet text
let linkUrl = null;
if (textElement) {
// This regex needs to be improved to not catch parts of the URL
const urlRegex = /https?:\/\/[^\s]+/g;
const matches = textElement.innerText.match(urlRegex);
if (matches) {
// Find the t.co link element that corresponds to the displayed text
const linkElement = Array.from(tweetElement.querySelectorAll('a[href*="t.co"]')).find(a => matches.includes(a.textContent));
if (linkElement && linkElement.href) {
linkUrl = linkElement.href; // Use the actual t.co href
console.log("Found URL in tweet:", linkUrl);
}
}
}
// Extract author's bio if available (from profile hover card or page)
let authorBio = "";
const bioElement = document.querySelector('[data-testid="UserDescription"]');
if (bioElement) {
authorBio = bioElement.innerText.trim();
}
// Extract recent tweets from the author (if on their profile or visible)
let recentTweets = [];
const allTweets = document.querySelectorAll('article[data-testid="tweet"]');
allTweets.forEach(tweet => {
const { tweetText: recentText, authorName: recentAuthor } = getTweetDetailsSimple(tweet);
if (recentAuthor && recentAuthor.toLowerCase() === authorName.toLowerCase() && recentText) {
recentTweets.push(recentText);
}
});
// Limit to 3 most recent tweets for context
recentTweets = recentTweets.slice(0, 3);
return { tweetText, authorName, authorStatus, linkUrl, authorBio, recentTweets };
}
// Simplified version to avoid recursion
function getTweetDetailsSimple(tweetElement) {
let tweetText = "";
const textElement = tweetElement.querySelector('[data-testid="tweetText"]');
if (textElement) {
tweetText = textElement.innerText;
}
let authorName = "";
const allSpans = tweetElement.querySelectorAll('span');
const handleSpan = Array.from(allSpans).find(span => span.textContent.startsWith('@'));
if (handleSpan) {
const authorLink = handleSpan.closest('a[role="link"]');
if (authorLink && authorLink.href) {
const hrefParts = authorLink.href.split('/');
authorName = hrefParts[hrefParts.length - 1];
}
}
return { tweetText, authorName };
}
// Function to add a reply button to a tweet
function addReplyButton(tweetElement) {
// Check if a button already exists to prevent duplicates
if (tweetElement.querySelector('.crypto-agent-reply-button')) {
return;
}
// Get author name for targeting checks
const { authorName } = getTweetDetails(tweetElement);
// Check blacklist - skip this tweet
if (authorName && isBlacklisted(authorName)) {
console.log(`Skipping blacklisted user: @${authorName}`);
return;
}
// NOT: Manuel AI Reply butonunda limit yok - kullanıcı istediği kadar kullanabilir
// Limitler sadece otomatik yanıtlar için geçerli (auto-reply, bulk process)
// Check follower count range (unless whitelisted)
if (!isWhitelisted(authorName)) {
const followerCount = getFollowerCount(tweetElement);
if (!isWithinFollowerRange(followerCount)) {
console.log(`Skipping user @${authorName} - follower count ${followerCount} outside range`);
return;
}
}
const actionsBar = DOM.getActionsBar(tweetElement);
if (actionsBar) {
// Create container for the vibe selector
const container = document.createElement('div');
container.className = 'crypto-agent-reply-button';
container.style.cssText = `
display: flex;
align-items: center;
margin-left: 10px;
background-color: transparent;
border-radius: 9999px;
transition: all 0.2s ease;
position: relative;
`;
// Main "AI Reply" button
const mainButton = document.createElement('button');
mainButton.innerText = "AI Reply";
mainButton.style.cssText = `
background-color: #1DA1F2;
color: white;
border: none;
padding: 5px 12px;
border-radius: 9999px;
font-size: 12px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.2s;
`;
// Main button click
mainButton.onclick = () => handleReplyClick(tweetElement, mainButton, null);
container.appendChild(mainButton);
// Wrap in a div for alignment in the action bar
const wrapper = document.createElement('div');
wrapper.style.display = 'flex';
wrapper.style.alignItems = 'center';
wrapper.appendChild(container);
actionsBar.appendChild(wrapper);
}
}
// Helper function to handle reply generation
async function handleReplyClick(tweetElement, button, intent) {
console.log("🎯 handleReplyClick triggered!");
const originalText = button.innerText;
button.innerText = intent ? `${getIntentIcon(intent)} Thinking...` : "Replying...";
button.disabled = true;
const { tweetText, authorName, authorStatus, linkUrl, authorBio, recentTweets } = getTweetDetails(tweetElement);
if (!authorName) {
alert("Could not determine tweet author");
button.innerText = originalText;
button.disabled = false;
return;
}
console.log('📝 Tweet details for AI Reply:', { tweetText: tweetText?.substring(0, 50), authorName, intent });
// Auto Like - like the ORIGINAL POST before opening reply box (SYNC)
const settings = await new Promise(resolve => {
chrome.storage.sync.get({ autoLike: false }, resolve);
});
if (settings.autoLike) {
const likeButton = tweetElement.querySelector('button[data-testid="like"]') ||
tweetElement.querySelector('[data-testid="like"]');
if (likeButton) {
// Check if not already liked
const isLiked = tweetElement.querySelector('button[data-testid="unlike"]') ||
tweetElement.querySelector('[data-testid="unlike"]');
if (!isLiked) {
console.log('❤️ Auto Like: Liking the original post...');
likeButton.click();
// Wait a bit after liking
await new Promise(r => setTimeout(r, 500 + Math.random() * 500));
console.log('✅ Like completed');
} else {
console.log('ℹ️ Post already liked');
}
}
}
// Open reply box
const nativeReplyButton = DOM.getNativeReplyButton(tweetElement);
if (nativeReplyButton) {
console.log('📝 Opening reply box...');
nativeReplyButton.click();
// Wait for reply box to open
await new Promise(r => setTimeout(r, 800));
}
console.log("📤 Sending message to service worker...");
try {
const response = await chrome.runtime.sendMessage({
action: "processTweet",
tweetText,
authorName,
authorStatus,
linkUrl,
authorBio,
recentTweets,
intent
});
console.log("📥 Response from service worker:", response);
if (response && response.reply) {
console.log("Got reply, injecting...");
injectReply(response.reply);
// Track reply for analytics
chrome.storage.local.get({ replyHistory: [] }, (data) => {
const history = data.replyHistory || [];
history.push({
timestamp: Date.now(),
author: authorName,
tone: intent || 'neutral'
});
// Keep only last 500 entries
if (history.length > 500) history.shift();
chrome.storage.local.set({ replyHistory: history });
});
} else {
console.error("❌ No reply in response:", response);
alert("Failed to get AI reply: " + (response?.error || "Unknown error"));
}
} catch (error) {
console.error("❌ Error sending message:", error);
alert("Error: " + error.message);
}
button.innerText = "AI Reply";
button.disabled = false;
}
function getIntentIcon(intent) {
switch (intent) {
case 'agree': return '👍';
case 'disagree': return '👎';
case 'funny': return '😂';
case 'question': return '🤔';
default: return '';
}
}
// Function to add a green/red tick next to the author's name
// Green = Replied within 24h | Red = Not replied yet (needs reply)
function addAuthorTick(tweetElement) {
// Try multiple selectors to find username element
let userNamesElement = tweetElement.querySelector('[data-testid="User-Name"]') ||
tweetElement.querySelector('[data-testid="User-Names"]');
if (!userNamesElement) {
// Fallback: find the first link that looks like a username
const links = tweetElement.querySelectorAll('a[role="link"]');
for (const link of links) {
if (link.href && link.href.includes('x.com/') && !link.href.includes('/status/')) {
userNamesElement = link.parentElement;
break;
}
}
}
if (!userNamesElement) {
return; // Can't find the name container
}
const { authorName } = getTweetDetails(tweetElement);
if (!authorName) return;
const authorLower = authorName.toLowerCase();
const replyTimestamp = repliedAuthors.get(authorLower);
let tickColor, tickSymbol, tickTitle;
const hasReplied = replyTimestamp !== undefined && replyTimestamp !== null;
if (hasReplied) {
const now = Date.now();
const hoursSinceReply = replyTimestamp === 0 ? 999 : (now - replyTimestamp) / (1000 * 60 * 60);
if (hoursSinceReply < 24) {
// Replied within 24h - GREEN
tickColor = '#00BA7C';
tickSymbol = '●';
tickTitle = 'Replied within 24h';
} else {
// Replied but >24h ago - needs fresh reply - RED
tickColor = '#F4212E';
tickSymbol = '●';
tickTitle = 'Reply older than 24h';
}
} else {
// Never replied - needs reply - RED
tickColor = '#F4212E';
tickSymbol = '●';
tickTitle = 'Not replied yet';
}
// Check if tick already exists in this tweet
let tick = tweetElement.querySelector('.author-reply-tick');
if (!tick) {
tick = document.createElement('span');
tick.className = 'author-reply-tick';
tick.style.cssText = 'margin-left: 4px; font-size: 10px; cursor: help; vertical-align: middle;';
userNamesElement.appendChild(tick);
}
// Update tick properties
tick.innerText = tickSymbol;
tick.style.color = tickColor;
tick.title = tickTitle;
}
// Function to find the reply textbox and inject the text
async function injectReply(text) {
console.log('🚀 injectReply called with text:', text?.substring(0, 50) + '...');
const maxAttempts = 25;
let attempts = 0;
const tryInject = async () => {
attempts++;
console.log(`📝 Injection attempt ${attempts}/${maxAttempts}`);
// Find the DraftJS editor container
let editorContainer = document.querySelector('[data-testid="tweetTextarea_0"]') ||
document.querySelector('[data-testid="tweetTextarea_1"]');
if (!editorContainer) {
if (attempts < maxAttempts) {
setTimeout(tryInject, 400);
} else {
console.error('❌ Reply textbox not found after max attempts');
}
return;
}
// Find the contenteditable element
let editableEl = editorContainer.querySelector('.DraftEditor-editorContainer [contenteditable="true"]') ||
editorContainer.querySelector('[contenteditable="true"]') ||
editorContainer;
console.log('✅ Found editor:', editableEl.className);
// Focus
editableEl.focus();
await new Promise(r => setTimeout(r, 200));
// BEST METHOD: Simulate real typing using DataTransfer
console.log('📋 Using DataTransfer paste simulation...');
try {
// Create a DataTransfer object with the text
const dataTransfer = new DataTransfer();
dataTransfer.setData('text/plain', text);
// Create paste event
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dataTransfer
});
// Focus and dispatch
editableEl.focus();
editableEl.dispatchEvent(pasteEvent);
await new Promise(r => setTimeout(r, 500));
// Check if paste worked
let currentText = editableEl.textContent || '';
console.log('📝 After paste:', currentText?.substring(0, 30));
if (currentText.length >= 3) {
console.log('✅ Paste method worked!');
await new Promise(r => setTimeout(r, 300));
await forceClickPostButton();
return;
}
} catch (e) {
console.log('Paste error:', e.message);
}
// FALLBACK: execCommand with input events
console.log('📝 Trying execCommand with beforeinput...');
editableEl.focus();
await new Promise(r => setTimeout(r, 100));
// Fire beforeinput first - this is what DraftJS listens to
const beforeInputEvent = new InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: text
});
editableEl.dispatchEvent(beforeInputEvent);
// Then execCommand
document.execCommand('selectAll', false, null);
document.execCommand('delete', false, null);
document.execCommand('insertText', false, text);
// Fire input event
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: false,
inputType: 'insertText',
data: text
});
editableEl.dispatchEvent(inputEvent);
await new Promise(r => setTimeout(r, 500));
// Check
let currentText = editableEl.textContent || editableEl.innerText || '';
console.log('📝 After execCommand:', currentText?.substring(0, 30));
// Always proceed to try clicking the button
console.log('🔘 Proceeding to click post button...');
await forceClickPostButton();
};
tryInject();
}
// Force click post button - tries multiple times regardless of disabled state
async function forceClickPostButton() {
console.log('🔘 forceClickPostButton called');
// First, get autoComment setting once
const settings = await new Promise(resolve => {
chrome.storage.sync.get({ autoComment: true, actionDelay: 5 }, resolve);
});
console.log('📋 Settings:', settings);
if (!settings.autoComment) {
console.log('ℹ️ Auto Comment disabled, not clicking');
return false;
}
const maxAttempts = 15;
let clickAttempted = false;
let spaceAdded = false;
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 500)); // Faster checks
// Check if reply box still exists
const replyBox = DOM.getReplyTextbox();
if (!replyBox) {
console.log('✅ Reply box gone - post was successful!');
return true;
}
const postButton = DOM.getPostButton();
console.log(`🔘 Attempt ${i + 1}/${maxAttempts} - Button: ${!!postButton}, disabled: ${postButton?.disabled}`);
// If button is disabled, try adding a space to trigger React state
if (postButton && postButton.disabled && !spaceAdded) {
console.log('⚠️ Button disabled, adding space to trigger React...');
// Find editor and add a space
const editor = document.querySelector('[data-testid="tweetTextarea_0"] [contenteditable="true"]') ||
document.querySelector('[data-testid="tweetTextarea_1"] [contenteditable="true"]');
if (editor) {
editor.focus();
// Try adding a space using execCommand
document.execCommand('insertText', false, ' ');
// Also dispatch events
editor.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, inputType: 'insertText', data: ' ' }));
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: ' ' }));
spaceAdded = true;
await new Promise(r => setTimeout(r, 500));
continue; // Check button again
}
}
if (postButton && !postButton.disabled) {
// Button is enabled!
if (!clickAttempted) {
// Short delay before clicking (1 second)
console.log(`⏳ Clicking in 1s...`);
await new Promise(r => setTimeout(r, 1000));
}
console.log('🖱️ Clicking post button...');
clickAttempted = true;
// Try multiple click methods
try {
postButton.click();
} catch (e) {
console.log('Direct click failed');
}
// Also try MouseEvent
postButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
console.log('✅ Click sent!');
// Wait for Twitter to process
await new Promise(r => setTimeout(r, 1500));
// Check if successful
const replyBoxAfter = DOM.getReplyTextbox();
if (!replyBoxAfter) {
console.log('✅ Reply posted successfully!');
return true;
} else {
console.log('⚠️ Reply box still exists, will retry...');
}
} else if (postButton && postButton.disabled && spaceAdded) {
// Space was added but button still disabled - try more aggressive approach
console.log('⚠️ Button still disabled after space, trying aggressive approach...');
const editor = document.querySelector('[data-testid="tweetTextarea_0"] [contenteditable="true"]') ||
document.querySelector('[data-testid="tweetTextarea_1"] [contenteditable="true"]');
if (editor) {
// Get current text
const currentText = editor.textContent || '';
// Clear and re-type everything
editor.focus();
document.execCommand('selectAll', false, null);
await new Promise(r => setTimeout(r, 50));
// Delete and re-insert
document.execCommand('delete', false, null);
await new Promise(r => setTimeout(r, 50));
// Re-insert character by character (first 20 chars)
const textToType = currentText.substring(0, Math.min(currentText.length, 20));
for (const char of textToType) {
document.execCommand('insertText', false, char);
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: char }));
}
// Insert rest at once
if (currentText.length > 20) {
document.execCommand('insertText', false, currentText.substring(20));
}
await new Promise(r => setTimeout(r, 500));
}
}
}
console.error('❌ Could not post after max attempts');
return false;
}
// Observe for new tweets being added to the DOM
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
// Check if node is a tweet
if (node.matches && (node.matches('article[data-testid="tweet"]') || node.matches('article[role="article"]'))) {
processTweet(node);
}
// Check for tweets inside the added node
if (node.querySelectorAll) {
node.querySelectorAll('article[data-testid="tweet"], article[role="article"]').forEach(processTweet);
}
}
});
});
});
// Start observing - use a more specific container if available
const mainContainer = document.querySelector('main') || document.querySelector('[role="main"]') || document.body;
observer.observe(mainContainer, { childList: true, subtree: true });