Systematically engage with tweets from target
⚠️ Keep rates low. Twitter WILL flag excessive activity.
How to Use
- Go to any timeline, search, or user profile on x.com
- Open DevTools Console (F12)
- Paste and run — a floating panel will appear
- Configure via the panel (or edit CONFIG below)
- Click ▶ Start
Configuration Options
| Option | Default | Description |
|---|---|---|
blockUsers | [] | Never interact with these accounts |
maxLikes | 0 | 0 = no max — helps target smaller accounts |
maxFollowers | 0 | 0 = no max — caps at big accounts |
bioKeywords | [] | Author bio must contain one of these |
intervalMinutes | 30 | Engage every N minutes |
batchSize | 3 | Tweets per batch |
preferSmallAccounts | true | Smaller accounts notice you more |
preferHighEngagement | false | Tweets with lots of engagement (viral) |
preferRecent | true | Most recent tweets first |
Default Configuration
const CONFIG = {
maxInteractions: 15,
dryRun: true,
actions: {
like: true,
reply: false,
retweet: false,
bookmark: false,
follow: false,
},
// Topic-aware reply templates: templates with `topics` match tweets containing those words
replyTemplates: [
{ text: '🔥 Great point!', topics: [] },
{ text: '💯 Couldn\'t agree more.', topics: [] },
{ text: '📌 Bookmarking this.', topics: [] },
{ text: 'Really useful, thanks for sharing! 🙌', topics: [] },
{ text: 'Solid alpha 🧠', topics: ['alpha', 'insight', 'thread', 'deep dive'] },
{ text: 'This is the kind of content I come here for 👏', topics: ['guide', 'tutorial', 'explained', 'breakdown'] },
{ text: 'Great data — appreciate the transparency 📊', topics: ['data', 'stats', 'numbers', 'chart', 'graph'] },
],
targetUsers: [],
blockUsers: [], // Never interact with these accounts
onlyKeywords: [],
skipKeywords: ['promoted', 'ad', 'giveaway', 'nsfw'],
skipLiked: true,
minLikes: 0,
maxLikes: 0, // 0 = no max — helps target smaller accounts
// Author quality filters
onlyVerified: false,
minFollowers: 0,
maxFollowers: 0, // 0 = no max — caps at big accounts
bioKeywords: [], // Author bio must contain one of these
// Speed presets: 'stealth' | 'safe' | 'moderate' | 'fast'
speedPreset: 'safe',
// Drip mode: spread interactions over time
drip: {
enabled: false,
intervalMinutes: 30, // Engage every N minutes
batchSize: 3, // Tweets per batch
},
scrollRounds: 5,
scrollDelay: 2000,
// Smart engagement scoring
scoring: {
enabled: true,
preferSmallAccounts: true, // Smaller accounts notice you more
preferHighEngagement: false, // Tweets with lots of engagement (viral)
preferRecent: true, // Most recent tweets first
},
};
Full Script
Copy and paste this entire script into your browser DevTools console on x.com.
/**
* ============================================================
* 🚀 Engagement Booster v2 — Production Grade
* ============================================================
*
* @name engagementBooster.js
* @description Systematically engage with tweets from target
* accounts to boost mutual engagement. Features a
* floating control panel, smart engagement scoring,
* retweet & bookmark support, drip scheduling, undo
* rollback, persistent session history, and speed
* presets. Works standalone or with core.js.
* @author nichxbt (https://x.com/nichxbt)
* @version 2.0.0
* @date 2026-02-24
* @repository https://github.com/nirholas/XActions
*
* ============================================================
* 📋 USAGE:
*
* 1. Go to any timeline, search, or user profile on x.com
* 2. Open DevTools Console (F12)
* 3. Paste and run — a floating panel will appear
* 4. Configure via the panel (or edit CONFIG below)
* 5. Click ▶ Start
*
* Controls: ⏸ Pause · ▶ Resume · 🛑 Stop · ↩ Undo All
*
* ⚠️ Keep rates low. Twitter WILL flag excessive activity.
* ============================================================
*/
(() => {
'use strict';
// Prevent double-inject
if (document.getElementById('xeb-panel')) {
console.log('⚡ Engagement Booster already running');
return;
}
// ═══════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════
const CONFIG = {
maxInteractions: 15,
dryRun: true,
actions: {
like: true,
reply: false,
retweet: false,
bookmark: false,
follow: false,
},
// Topic-aware reply templates: templates with `topics` match tweets containing those words
replyTemplates: [
{ text: '🔥 Great point!', topics: [] },
{ text: '💯 Couldn\'t agree more.', topics: [] },
{ text: '📌 Bookmarking this.', topics: [] },
{ text: 'Really useful, thanks for sharing! 🙌', topics: [] },
{ text: 'Solid alpha 🧠', topics: ['alpha', 'insight', 'thread', 'deep dive'] },
{ text: 'This is the kind of content I come here for 👏', topics: ['guide', 'tutorial', 'explained', 'breakdown'] },
{ text: 'Great data — appreciate the transparency 📊', topics: ['data', 'stats', 'numbers', 'chart', 'graph'] },
],
targetUsers: [],
blockUsers: [], // Never interact with these accounts
onlyKeywords: [],
skipKeywords: ['promoted', 'ad', 'giveaway', 'nsfw'],
skipLiked: true,
minLikes: 0,
maxLikes: 0, // 0 = no max — helps target smaller accounts
// Author quality filters
onlyVerified: false,
minFollowers: 0,
maxFollowers: 0, // 0 = no max — caps at big accounts
bioKeywords: [], // Author bio must contain one of these
// Speed presets: 'stealth' | 'safe' | 'moderate' | 'fast'
speedPreset: 'safe',
// Drip mode: spread interactions over time
drip: {
enabled: false,
intervalMinutes: 30, // Engage every N minutes
batchSize: 3, // Tweets per batch
},
scrollRounds: 5,
scrollDelay: 2000,
// Smart engagement scoring
scoring: {
enabled: true,
preferSmallAccounts: true, // Smaller accounts notice you more
preferHighEngagement: false, // Tweets with lots of engagement (viral)
preferRecent: true, // Most recent tweets first
},
};
// ═══════════════════════════════════════════════════════════
// SPEED PRESETS
// ═══════════════════════════════════════════════════════════
const SPEED_PRESETS = {
stealth: { delay: [45000, 90000], label: '🐢 Stealth', desc: '45-90s — very safe' },
safe: { delay: [20000, 45000], label: '🛡️ Safe', desc: '20-45s — recommended' },
moderate:{ delay: [8000, 20000], label: '⚡ Moderate', desc: '8-20s — faster' },
fast: { delay: [3000, 8000], label: '🔥 Fast', desc: '3-8s — risky' },
};
// ═══════════════════════════════════════════════════════════
// UTILITIES
// ═══════════════════════════════════════════════════════════
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const gaussian = (a, b) => Math.floor(a + ((Math.random() + Math.random()) / 2) * (b - a));
const fmtNum = (n) => n >= 1000000 ? (n / 1000000).toFixed(1) + 'M' : n >= 1000 ? (n / 1000).toFixed(1) + 'K' : String(n);
const SEL = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
likeBtn: '[data-testid="like"]',
unlikeBtn: '[data-testid="unlike"]',
retweetBtn: '[data-testid="retweet"]',
unretweetBtn: '[data-testid="unretweet"]',
replyBtn: '[data-testid="reply"]',
tweetBox: '[data-testid="tweetTextarea_0"]',
tweetButton: '[data-testid="tweetButton"]',
bookmarkBtn: '[data-testid="bookmark"]',
removeBookmarkBtn: '[data-testid="removeBookmark"]',
confirmBtn: '[data-testid="confirmationSheetConfirm"]',
toast: '[data-testid="toast"]',
userDescription: '[data-testid="UserDescription"]',
};
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
const parseNum = (text) => {
if (!text) return 0;
text = text.trim().replace(/,/g, '');
if (text.endsWith('K')) return Math.round(parseFloat(text) * 1000);
if (text.endsWith('M')) return Math.round(parseFloat(text) * 1000000);
return parseInt(text) || 0;
};
const isRateLimited = () => {
for (const el of document.querySelectorAll(`${SEL.toast}, [role="alert"]`)) {
const text = (el.textContent || '').toLowerCase();
if (/rate limit|try again|too many|slow down/.test(text)) return true;
}
return false;
};
// ═══════════════════════════════════════════════════════════
// PERSISTENT SESSION HISTORY (localStorage)
// ═══════════════════════════════════════════════════════════
const STORAGE_KEY = 'xactions_engagement_history';
const loadHistory = () => {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); }
catch { return []; }
};
const saveHistory = (session) => {
const history = loadHistory();
history.push(session);
// Keep last 50 sessions
if (history.length > 50) history.splice(0, history.length - 50);
localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
};
const getHistoryStats = () => {
const history = loadHistory();
let totalLikes = 0, totalReplies = 0, totalRetweets = 0, totalBookmarks = 0, uniqueAuthors = new Set();
for (const s of history) {
totalLikes += s.liked || 0;
totalReplies += s.replied || 0;
totalRetweets += s.retweeted || 0;
totalBookmarks += s.bookmarked || 0;
(s.authors || []).forEach(a => uniqueAuthors.add(a));
}
return { sessions: history.length, totalLikes, totalReplies, totalRetweets, totalBookmarks, uniqueAuthors: uniqueAuthors.size };
};
// ═══════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════
const STATE = {
status: 'idle', // idle | scanning | running | paused | done | error
aborted: false,
paused: false,
liked: 0,
replied: 0,
retweeted: 0,
bookmarked: 0,
failed: 0,
processed: 0,
total: 0,
startTime: null,
undoStack: [], // { type, article, author } for rollback
results: [],
eligible: [],
dripTimer: null,
logs: [],
};
const addLog = (msg, type = 'info') => {
STATE.logs.push({ msg, type, time: new Date() });
if (STATE.logs.length > 200) STATE.logs.shift();
console.log(msg);
updatePanel();
};
const waitForUnpause = async () => {
while (STATE.paused && !STATE.aborted) await sleep(500);
};
// ═══════════════════════════════════════════════════════════
// FLOATING UI PANEL
// ═══════════════════════════════════════════════════════════
const createPanel = () => {
const panel = document.createElement('div');
panel.id = 'xeb-panel';
panel.innerHTML = `
<style>
#xeb-panel {
position: fixed; top: 60px; right: 16px; z-index: 999999;
width: 320px; background: #16181c; border: 1px solid #2f3336;
border-radius: 16px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #e7e9ea; box-shadow: 0 8px 32px rgba(0,0,0,0.6); user-select: none;
transition: opacity 0.2s;
}
#xeb-panel * { box-sizing: border-box; }
#xeb-panel.xeb-minimized .xeb-body { display: none; }
.xeb-header {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 16px; border-bottom: 1px solid #2f3336; cursor: grab;
}
.xeb-header:active { cursor: grabbing; }
.xeb-title { font-weight: 700; font-size: 14px; }
.xeb-badge {
font-size: 10px; padding: 2px 8px; border-radius: 999px;
font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
}
.xeb-badge.idle { background: #2f3336; color: #71767b; }
.xeb-badge.scanning { background: #1d4ed8; color: #93c5fd; }
.xeb-badge.running { background: #15803d; color: #86efac; }
.xeb-badge.paused { background: #a16207; color: #fde68a; }
.xeb-badge.done { background: #1d9bf0; color: #fff; }
.xeb-badge.error { background: #dc2626; color: #fca5a5; }
.xeb-header-btns { display: flex; gap: 4px; }
.xeb-header-btns button {
background: none; border: none; color: #71767b; cursor: pointer;
font-size: 16px; padding: 2px 6px; border-radius: 4px;
}
.xeb-header-btns button:hover { background: #2f3336; color: #e7e9ea; }
.xeb-body { max-height: 480px; overflow-y: auto; }
.xeb-body::-webkit-scrollbar { width: 4px; }
.xeb-body::-webkit-scrollbar-thumb { background: #2f3336; border-radius: 4px; }
.xeb-tabs {
display: flex; border-bottom: 1px solid #2f3336;
}
.xeb-tab {
flex: 1; padding: 8px 0; text-align: center; font-size: 12px;
font-weight: 600; color: #71767b; cursor: pointer; border: none;
background: none; border-bottom: 2px solid transparent; transition: all 0.15s;
}
.xeb-tab:hover { color: #e7e9ea; }
.xeb-tab.active { color: #1d9bf0; border-bottom-color: #1d9bf0; }
.xeb-pane { display: none; padding: 12px 16px; }
.xeb-pane.active { display: block; }
/* Config pane */
.xeb-field { margin-bottom: 10px; }
.xeb-field label { display: block; font-size: 11px; color: #71767b; margin-bottom: 4px; font-weight: 600; }
.xeb-field input[type="text"], .xeb-field input[type="number"], .xeb-field select {
width: 100%; padding: 6px 10px; background: #000; border: 1px solid #2f3336;
border-radius: 8px; color: #e7e9ea; font-size: 13px; outline: none;
}
.xeb-field input:focus, .xeb-field select:focus { border-color: #1d9bf0; }
.xeb-row { display: flex; gap: 8px; }
.xeb-row .xeb-field { flex: 1; }
.xeb-toggle-row {
display: flex; align-items: center; justify-content: space-between;
padding: 4px 0;
}
.xeb-toggle-row span { font-size: 13px; }
.xeb-toggle {
position: relative; width: 36px; height: 20px; cursor: pointer;
}
.xeb-toggle input { opacity: 0; width: 0; height: 0; }
.xeb-toggle .xeb-slider {
position: absolute; inset: 0; background: #2f3336; border-radius: 10px; transition: 0.2s;
}
.xeb-toggle .xeb-slider::before {
content: ''; position: absolute; left: 2px; top: 2px;
width: 16px; height: 16px; background: #71767b; border-radius: 50%; transition: 0.2s;
}
.xeb-toggle input:checked + .xeb-slider { background: #1d9bf0; }
.xeb-toggle input:checked + .xeb-slider::before { transform: translateX(16px); background: #fff; }
/* Progress pane */
.xeb-progress-bar {
width: 100%; height: 6px; background: #2f3336; border-radius: 3px; overflow: hidden; margin: 8px 0;
}
.xeb-progress-fill { height: 100%; background: #1d9bf0; border-radius: 3px; transition: width 0.3s; }
.xeb-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 8px 0; }
.xeb-stat {
background: #000; border-radius: 10px; padding: 8px 12px; text-align: center;
}
.xeb-stat-val { font-size: 20px; font-weight: 700; color: #e7e9ea; }
.xeb-stat-label { font-size: 10px; color: #71767b; text-transform: uppercase; letter-spacing: 0.5px; }
.xeb-current {
font-size: 12px; color: #71767b; padding: 4px 0; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap;
}
.xeb-log {
max-height: 120px; overflow-y: auto; font-size: 11px; padding: 8px;
background: #000; border-radius: 8px; margin-top: 8px;
}
.xeb-log::-webkit-scrollbar { width: 3px; }
.xeb-log::-webkit-scrollbar-thumb { background: #2f3336; border-radius: 3px; }
.xeb-log-entry { padding: 2px 0; color: #71767b; }
.xeb-log-entry.success { color: #22c55e; }
.xeb-log-entry.warn { color: #eab308; }
.xeb-log-entry.error { color: #ef4444; }
/* History pane */
.xeb-history-stat { display: flex; justify-content: space-between; padding: 4px 0; font-size: 13px; }
.xeb-history-stat span:first-child { color: #71767b; }
.xeb-history-stat span:last-child { font-weight: 600; }
.xeb-empty { color: #71767b; font-size: 12px; text-align: center; padding: 16px 0; }
/* Buttons */
.xeb-actions { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid #2f3336; }
.xeb-btn {
flex: 1; padding: 8px 0; border: none; border-radius: 999px; font-size: 13px;
font-weight: 700; cursor: pointer; transition: all 0.15s; text-align: center;
}
.xeb-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.xeb-btn-primary { background: #1d9bf0; color: #fff; }
.xeb-btn-primary:hover:not(:disabled) { background: #1a8cd8; }
.xeb-btn-warn { background: #2f3336; color: #fde68a; }
.xeb-btn-warn:hover:not(:disabled) { background: #3a3d42; }
.xeb-btn-danger { background: #2f3336; color: #ef4444; }
.xeb-btn-danger:hover:not(:disabled) { background: #3a3d42; }
.xeb-btn-ghost { background: none; color: #71767b; border: 1px solid #2f3336; }
.xeb-btn-ghost:hover:not(:disabled) { background: #2f3336; color: #e7e9ea; }
</style>
<div class="xeb-header" id="xeb-drag">
<span class="xeb-title">🚀 Engagement Booster</span>
<div style="display:flex;align-items:center;gap:8px;">
<span class="xeb-badge idle" id="xeb-status">IDLE</span>
<div class="xeb-header-btns">
<button id="xeb-minimize" title="Minimize">─</button>
<button id="xeb-close" title="Close">✕</button>
</div>
</div>
</div>
<div class="xeb-body">
<div class="xeb-tabs">
<button class="xeb-tab active" data-tab="config">⚙️ Config</button>
<button class="xeb-tab" data-tab="progress">📊 Progress</button>
<button class="xeb-tab" data-tab="history">📜 History</button>
</div>
<!-- CONFIG TAB -->
<div class="xeb-pane active" data-pane="config">
<div class="xeb-field">
<label>TARGET USERS (comma-separated, blank = any)</label>
<input type="text" id="xeb-targetUsers" placeholder="@user1, @user2" />
</div>
<div class="xeb-field">
<label>KEYWORDS (comma-separated, blank = any)</label>
<input type="text" id="xeb-keywords" placeholder="web3, AI, crypto" />
</div>
<div class="xeb-row">
<div class="xeb-field">
<label>MAX INTERACTIONS</label>
<input type="number" id="xeb-maxInteractions" value="15" min="1" max="200" />
</div>
<div class="xeb-field">
<label>SPEED</label>
<select id="xeb-speed">
<option value="stealth">🐢 Stealth (45-90s)</option>
<option value="safe" selected>🛡️ Safe (20-45s)</option>
<option value="moderate">⚡ Moderate (8-20s)</option>
<option value="fast">🔥 Fast (3-8s)</option>
</select>
</div>
</div>
<div class="xeb-row">
<div class="xeb-field">
<label>MIN LIKES</label>
<input type="number" id="xeb-minLikes" value="0" min="0" />
</div>
<div class="xeb-field">
<label>MAX LIKES (0 = no cap)</label>
<input type="number" id="xeb-maxLikes" value="0" min="0" />
</div>
</div>
<div style="border-top:1px solid #2f3336; margin:8px 0; padding-top:8px;">
<label style="font-size:11px;color:#71767b;font-weight:600;margin-bottom:6px;display:block;">ACTIONS</label>
<div class="xeb-toggle-row"><span>❤️ Like</span><label class="xeb-toggle"><input type="checkbox" id="xeb-actLike" checked /><span class="xeb-slider"></span></label></div>
<div class="xeb-toggle-row"><span>🔁 Retweet</span><label class="xeb-toggle"><input type="checkbox" id="xeb-actRetweet" /><span class="xeb-slider"></span></label></div>
<div class="xeb-toggle-row"><span>🔖 Bookmark</span><label class="xeb-toggle"><input type="checkbox" id="xeb-actBookmark" /><span class="xeb-slider"></span></label></div>
<div class="xeb-toggle-row"><span>💬 Reply</span><label class="xeb-toggle"><input type="checkbox" id="xeb-actReply" /><span class="xeb-slider"></span></label></div>
<div class="xeb-toggle-row"><span>👤 Follow</span><label class="xeb-toggle"><input type="checkbox" id="xeb-actFollow" /><span class="xeb-slider"></span></label></div>
</div>
<div style="border-top:1px solid #2f3336; margin:8px 0; padding-top:8px;">
<div class="xeb-field">
<label>BLOCK USERS (never engage, comma-separated)</label>
<input type="text" id="xeb-blockUsers" placeholder="@spammer1, @bot2" />
</div>
<div class="xeb-field">
<label>SKIP KEYWORDS (comma-separated)</label>
<input type="text" id="xeb-skipKeywords" value="promoted, ad, giveaway, nsfw" />
</div>
</div>
<div style="border-top:1px solid #2f3336; margin:8px 0; padding-top:8px;">
<label style="font-size:11px;color:#71767b;font-weight:600;margin-bottom:6px;display:block;">SMART FEATURES</label>
<div class="xeb-toggle-row"><span>🎯 Smart scoring</span><label class="xeb-toggle"><input type="checkbox" id="xeb-scoring" checked /><span class="xeb-slider"></span></label></div>
<div class="xeb-toggle-row"><span>🏃 Dry run</span><label class="xeb-toggle"><input type="checkbox" id="xeb-dryRun" checked /><span class="xeb-slider"></span></label></div>
<div class="xeb-toggle-row"><span>⏰ Drip mode</span><label class="xeb-toggle"><input type="checkbox" id="xeb-drip" /><span class="xeb-slider"></span></label></div>
</div>
<div id="xeb-dripConfig" style="display:none; padding-top:4px;">
<div class="xeb-row">
<div class="xeb-field">
<label>INTERVAL (min)</label>
<input type="number" id="xeb-dripInterval" value="30" min="5" />
</div>
<div class="xeb-field">
<label>BATCH SIZE</label>
<input type="number" id="xeb-dripBatch" value="3" min="1" max="10" />
</div>
</div>
</div>
</div>
<!-- PROGRESS TAB -->
<div class="xeb-pane" data-pane="progress">
<div class="xeb-current" id="xeb-currentAction">Waiting to start...</div>
<div class="xeb-progress-bar"><div class="xeb-progress-fill" id="xeb-progressFill" style="width:0%"></div></div>
<div style="display:flex;justify-content:space-between;font-size:11px;color:#71767b;">
<span id="xeb-progressText">0 / 0</span>
<span id="xeb-eta" style="color:#1d9bf0;"></span>
<span id="xeb-elapsed">0:00</span>
</div>
<div class="xeb-stats">
<div class="xeb-stat"><div class="xeb-stat-val" id="xeb-sLiked">0</div><div class="xeb-stat-label">Liked</div></div>
<div class="xeb-stat"><div class="xeb-stat-val" id="xeb-sRetweeted">0</div><div class="xeb-stat-label">Retweeted</div></div>
<div class="xeb-stat"><div class="xeb-stat-val" id="xeb-sBookmarked">0</div><div class="xeb-stat-label">Bookmarked</div></div>
<div class="xeb-stat"><div class="xeb-stat-val" id="xeb-sReplied">0</div><div class="xeb-stat-label">Replied</div></div>
</div>
<div class="xeb-log" id="xeb-log"></div>
</div>
<!-- HISTORY TAB -->
<div class="xeb-pane" data-pane="history">
<div id="xeb-historyContent"></div>
</div>
</div>
<div class="xeb-actions" id="xeb-actionBar">
<button class="xeb-btn xeb-btn-primary" id="xeb-startBtn">▶ Start</button>
<button class="xeb-btn xeb-btn-warn" id="xeb-pauseBtn" disabled>⏸ Pause</button>
<button class="xeb-btn xeb-btn-danger" id="xeb-stopBtn" disabled>🛑 Stop</button>
</div>
`;
document.body.appendChild(panel);
return panel;
};
// ═══════════════════════════════════════════════════════════
// PANEL LOGIC
// ═══════════════════════════════════════════════════════════
const panel = createPanel();
// Drag support
let dragOffset = { x: 0, y: 0 };
const dragHandle = document.getElementById('xeb-drag');
dragHandle.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
const rect = panel.getBoundingClientRect();
dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
const onMove = (ev) => {
panel.style.left = (ev.clientX - dragOffset.x) + 'px';
panel.style.top = (ev.clientY - dragOffset.y) + 'px';
panel.style.right = 'auto';
};
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
// Minimize / Close
document.getElementById('xeb-minimize').addEventListener('click', () => panel.classList.toggle('xeb-minimized'));
document.getElementById('xeb-close').addEventListener('click', () => {
if (STATE.status === 'running') {
if (!confirm('Engagement Booster is running. Stop and close?')) return;
STATE.aborted = true;
}
clearInterval(STATE.dripTimer);
panel.remove();
});
// Tabs
for (const tab of panel.querySelectorAll('.xeb-tab')) {
tab.addEventListener('click', () => {
panel.querySelectorAll('.xeb-tab').forEach(t => t.classList.remove('active'));
panel.querySelectorAll('.xeb-pane').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
panel.querySelector(`[data-pane="${tab.dataset.tab}"]`).classList.add('active');
if (tab.dataset.tab === 'history') renderHistory();
});
}
// Drip toggle
document.getElementById('xeb-drip').addEventListener('change', (e) => {
document.getElementById('xeb-dripConfig').style.display = e.target.checked ? 'block' : 'none';
});
// ═══════════════════════════════════════════════════════════
// UPDATE PANEL
// ═══════════════════════════════════════════════════════════
let updateTimer = null;
const updatePanel = () => {
const badge = document.getElementById('xeb-status');
if (!badge) return;
badge.textContent = STATE.status.toUpperCase();
badge.className = `xeb-badge ${STATE.status}`;
const pct = STATE.total > 0 ? Math.round((STATE.processed / STATE.total) * 100) : 0;
const fill = document.getElementById('xeb-progressFill');
if (fill) fill.style.width = pct + '%';
const txt = document.getElementById('xeb-progressText');
if (txt) txt.textContent = `${STATE.processed} / ${STATE.total}`;
const el = document.getElementById('xeb-elapsed');
if (el && STATE.startTime) {
const sec = Math.floor((Date.now() - STATE.startTime) / 1000);
el.textContent = `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, '0')}`;
}
// ETA
const etaEl = document.getElementById('xeb-eta');
if (etaEl && STATE.processed > 0 && STATE.total > STATE.processed && STATE.startTime) {
const elapsed = Date.now() - STATE.startTime;
const perItem = elapsed / STATE.processed;
const remaining = (STATE.total - STATE.processed) * perItem;
const mins = Math.ceil(remaining / 60000);
etaEl.textContent = mins > 0 ? `~${mins}m left` : '<1m left';
} else if (etaEl) {
etaEl.textContent = STATE.processed >= STATE.total && STATE.total > 0 ? 'Done' : '';
}
const ids = { 'xeb-sLiked': STATE.liked, 'xeb-sRetweeted': STATE.retweeted, 'xeb-sBookmarked': STATE.bookmarked, 'xeb-sReplied': STATE.replied };
for (const [id, val] of Object.entries(ids)) {
const e = document.getElementById(id);
if (e) e.textContent = val;
}
// Live log
const logEl = document.getElementById('xeb-log');
if (logEl) {
const recent = STATE.logs.slice(-30);
logEl.innerHTML = recent.map(l => {
const t = l.time.toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
return `<div class="xeb-log-entry ${l.type}">${t} ${l.msg}</div>`;
}).join('');
logEl.scrollTop = logEl.scrollHeight;
}
};
const startUpdateTimer = () => {
updateTimer = setInterval(updatePanel, 1000);
};
const stopUpdateTimer = () => {
clearInterval(updateTimer);
updatePanel();
};
// ═══════════════════════════════════════════════════════════
// HISTORY
// ═══════════════════════════════════════════════════════════
const renderHistory = () => {
const container = document.getElementById('xeb-historyContent');
if (!container) return;
const stats = getHistoryStats();
if (stats.sessions === 0) {
container.innerHTML = '<div class="xeb-empty">No sessions yet. Start boosting!</div>';
return;
}
container.innerHTML = `
<div class="xeb-history-stat"><span>Sessions</span><span>${stats.sessions}</span></div>
<div class="xeb-history-stat"><span>❤️ Total liked</span><span>${fmtNum(stats.totalLikes)}</span></div>
<div class="xeb-history-stat"><span>🔁 Total retweeted</span><span>${fmtNum(stats.totalRetweets)}</span></div>
<div class="xeb-history-stat"><span>🔖 Total bookmarked</span><span>${fmtNum(stats.totalBookmarks)}</span></div>
<div class="xeb-history-stat"><span>💬 Total replied</span><span>${fmtNum(stats.totalReplies)}</span></div>
<div class="xeb-history-stat"><span>👥 Unique accounts</span><span>${fmtNum(stats.uniqueAuthors)}</span></div>
<div style="margin-top:12px;">
<button class="xeb-btn xeb-btn-ghost" style="width:100%;padding:6px;" id="xeb-exportHistory">📥 Export all history (JSON)</button>
</div>
<div style="margin-top:6px;">
<button class="xeb-btn xeb-btn-ghost" style="width:100%;padding:6px;color:#ef4444;" id="xeb-clearHistory">🗑️ Clear history</button>
</div>
`;
document.getElementById('xeb-exportHistory')?.addEventListener('click', () => {
const blob = new Blob([JSON.stringify(loadHistory(), null, 2)], { type: 'application/json' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
a.download = `xactions-engagement-history-${new Date().toISOString().slice(0, 10)}.json`;
document.body.appendChild(a); a.click(); a.remove();
});
document.getElementById('xeb-clearHistory')?.addEventListener('click', () => {
if (confirm('Clear all engagement history?')) {
localStorage.removeItem(STORAGE_KEY);
renderHistory();
}
});
};
// ═══════════════════════════════════════════════════════════
// READ CONFIG FROM PANEL
// ═══════════════════════════════════════════════════════════
const readConfigFromPanel = () => {
const val = (id) => document.getElementById(id)?.value?.trim() || '';
const num = (id) => parseInt(document.getElementById(id)?.value) || 0;
const chk = (id) => document.getElementById(id)?.checked || false;
CONFIG.targetUsers = val('xeb-targetUsers').split(',').map(u => u.trim().replace(/^@/, '')).filter(Boolean);
CONFIG.onlyKeywords = val('xeb-keywords').split(',').map(k => k.trim()).filter(Boolean);
CONFIG.maxInteractions = num('xeb-maxInteractions') || 15;
CONFIG.speedPreset = val('xeb-speed') || 'safe';
CONFIG.minLikes = num('xeb-minLikes');
CONFIG.maxLikes = num('xeb-maxLikes');
CONFIG.actions.like = chk('xeb-actLike');
CONFIG.actions.retweet = chk('xeb-actRetweet');
CONFIG.actions.bookmark = chk('xeb-actBookmark');
CONFIG.actions.reply = chk('xeb-actReply');
CONFIG.actions.follow = chk('xeb-actFollow');
CONFIG.blockUsers = val('xeb-blockUsers').split(',').map(u => u.trim().replace(/^@/, '')).filter(Boolean);
CONFIG.skipKeywords = val('xeb-skipKeywords').split(',').map(k => k.trim()).filter(Boolean);
CONFIG.scoring.enabled = chk('xeb-scoring');
CONFIG.dryRun = chk('xeb-dryRun');
CONFIG.drip.enabled = chk('xeb-drip');
CONFIG.drip.intervalMinutes = num('xeb-dripInterval') || 30;
CONFIG.drip.batchSize = num('xeb-dripBatch') || 3;
};
// ═══════════════════════════════════════════════════════════
// SMART ENGAGEMENT SCORING
// ═══════════════════════════════════════════════════════════
const scoreTweet = (tweet) => {
if (!CONFIG.scoring.enabled) return 0;
let score = 0;
// Prefer smaller accounts (higher chance of follow-back / notice)
if (CONFIG.scoring.preferSmallAccounts) {
if (tweet.likes < 5) score += 30;
else if (tweet.likes < 20) score += 20;
else if (tweet.likes < 100) score += 10;
else score += 2;
}
// Prefer high engagement (viral content)
if (CONFIG.scoring.preferHighEngagement) {
if (tweet.likes > 100) score += 25;
else if (tweet.likes > 50) score += 15;
else score += 5;
}
// Prefer recent tweets (higher in DOM = more recent)
if (CONFIG.scoring.preferRecent) {
score += Math.max(0, 20 - (tweet.domIndex * 2));
}
// Bonus: tweet has meaningful text (not just links/media)
if (tweet.text.length > 50) score += 10;
if (tweet.text.length > 140) score += 5;
// Penalty: very short / empty
if (tweet.text.length < 20) score -= 10;
return score;
};
// ═══════════════════════════════════════════════════════════
// COLLECT ELIGIBLE TWEETS
// ═══════════════════════════════════════════════════════════
const collectEligible = async () => {
STATE.status = 'scanning';
updatePanel();
addLog('🔍 Scanning for eligible tweets...', 'info');
const eligible = [];
const seen = new Set();
for (let round = 0; round < CONFIG.scrollRounds; round++) {
if (STATE.aborted) break;
const articles = $$(SEL.tweet);
let domIndex = 0;
for (const article of articles) {
const textEl = $(SEL.tweetText, article);
const text = textEl ? textEl.textContent.trim() : '';
const fingerprint = text.slice(0, 80);
if (seen.has(fingerprint)) continue;
seen.add(fingerprint);
// Author extraction
const authorLink = article.querySelector('a[href^="/"][role="link"]');
const authorMatch = authorLink ? (authorLink.getAttribute('href') || '').match(/^\/([A-Za-z0-9_]+)/) : null;
const author = authorMatch ? authorMatch[1] : null;
if (!author || ['home', 'explore', 'notifications', 'messages', 'i'].includes(author)) continue;
// Filter: blocklist
if (CONFIG.blockUsers.length > 0 && CONFIG.blockUsers.some(u => u.toLowerCase() === author.toLowerCase())) continue;
// Filter: target users
if (CONFIG.targetUsers.length > 0 && !CONFIG.targetUsers.some(u => u.toLowerCase() === author.toLowerCase())) continue;
// Filter: keywords
const textLower = text.toLowerCase();
if (CONFIG.skipKeywords.some(kw => textLower.includes(kw.toLowerCase()))) continue;
if (CONFIG.onlyKeywords.length > 0 && !CONFIG.onlyKeywords.some(kw => textLower.includes(kw.toLowerCase()))) continue;
// Filter: skip already liked
if (CONFIG.skipLiked && $(SEL.unlikeBtn, article)) continue;
// Filter: engagement range
const likeEl = article.querySelector('[data-testid="like"] span');
const likes = likeEl ? parseNum(likeEl.textContent) : 0;
if (likes < CONFIG.minLikes) continue;
if (CONFIG.maxLikes > 0 && likes > CONFIG.maxLikes) continue;
// Filter: verified
if (CONFIG.onlyVerified) {
const verified = article.querySelector('[data-testid="icon-verified"]');
if (!verified) continue;
}
eligible.push({ article, text, author, likes, domIndex: domIndex++ });
}
addLog(`📜 Round ${round + 1}/${CONFIG.scrollRounds}: ${eligible.length} eligible`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
}
// Apply smart scoring and sort
if (CONFIG.scoring.enabled) {
for (const t of eligible) t.score = scoreTweet(t);
eligible.sort((a, b) => b.score - a.score);
addLog(`🎯 Smart scoring applied — top tweet score: ${eligible[0]?.score || 0}`);
}
return eligible;
};
// ═══════════════════════════════════════════════════════════
// ACTIONS
// ═══════════════════════════════════════════════════════════
const likeTweet = async (article) => {
const btn = $(SEL.likeBtn, article);
if (!btn) return false;
if (CONFIG.dryRun) { addLog(' ❤️ [DRY] Would like'); return true; }
btn.click();
await sleep(500);
return !!$(SEL.unlikeBtn, article);
};
const unlikeTweet = async (article) => {
const btn = $(SEL.unlikeBtn, article);
if (!btn) return false;
btn.click();
await sleep(500);
return !!$(SEL.likeBtn, article);
};
const retweetTweet = async (article) => {
const btn = $(SEL.retweetBtn, article);
if (!btn) return false;
if (CONFIG.dryRun) { addLog(' 🔁 [DRY] Would retweet'); return true; }
btn.click();
await sleep(800);
// Confirm retweet in the popup menu
const confirm = document.querySelector('[data-testid="retweetConfirm"]');
if (confirm) { confirm.click(); await sleep(500); }
return !!$(SEL.unretweetBtn, article);
};
const unretweetTweet = async (article) => {
const btn = $(SEL.unretweetBtn, article);
if (!btn) return false;
btn.click();
await sleep(800);
const confirm = document.querySelector('[data-testid="unretweetConfirm"]');
if (confirm) { confirm.click(); await sleep(500); }
return !!$(SEL.retweetBtn, article);
};
const bookmarkTweet = async (article) => {
const btn = $(SEL.bookmarkBtn, article);
if (!btn) return false;
if (CONFIG.dryRun) { addLog(' 🔖 [DRY] Would bookmark'); return true; }
btn.click();
await sleep(500);
return true; // Bookmarks don't visually toggle the same way
};
// Smart template picker — matches reply to tweet topic
const pickReplyTemplate = (tweetText) => {
const lower = tweetText.toLowerCase();
const templates = CONFIG.replyTemplates;
// Try topic-matched templates first
const topicMatches = templates.filter(t =>
t.topics && t.topics.length > 0 && t.topics.some(kw => lower.includes(kw))
);
if (topicMatches.length > 0) {
return topicMatches[Math.floor(Math.random() * topicMatches.length)].text;
}
// Fall back to generic (no topics) templates
const generic = templates.filter(t => !t.topics || t.topics.length === 0);
const pool = generic.length > 0 ? generic : templates;
return pool[Math.floor(Math.random() * pool.length)].text || pool[0];
};
// Follow a user (best-effort: via caret menu on tweet)
const followUserFromTweet = async (article, author) => {
if (CONFIG.dryRun) { addLog(` 👤 [DRY] Would follow @${author}`); return true; }
try {
// Look for a follow button in the tweet's caret menu
const caret = article.querySelector('[data-testid="caret"]');
if (caret) {
caret.click();
await sleep(600);
const menuItems = $$('[role="menuitem"]');
const followItem = menuItems.find(el => /follow @/i.test(el.textContent));
if (followItem && !/unfollow/i.test(followItem.textContent)) {
followItem.click();
await sleep(500);
return true;
}
// Close menu if no follow option
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
await sleep(300);
}
return false;
} catch { return false; }
};
// Tweet visual highlight
let highlightedArticle = null;
const highlightTweet = (article) => {
unhighlightTweet();
highlightedArticle = article;
article.style.outline = '2px solid #1d9bf0';
article.style.outlineOffset = '-2px';
article.style.borderRadius = '16px';
article.style.boxShadow = '0 0 20px rgba(29,155,240,0.25)';
article.style.transition = 'outline 0.3s, box-shadow 0.3s';
};
const unhighlightTweet = () => {
if (!highlightedArticle) return;
highlightedArticle.style.outline = '';
highlightedArticle.style.outlineOffset = '';
highlightedArticle.style.boxShadow = '';
highlightedArticle = null;
};
const replyToTweet = async (article, tweetText, author) => {
const replyBtn = $(SEL.replyBtn, article);
if (!replyBtn) return false;
const replyText = pickReplyTemplate(tweetText).replace('{author}', `@${author}`);
if (CONFIG.dryRun) { addLog(` 💬 [DRY] Would reply: "${replyText}"`); return true; }
replyBtn.click();
await sleep(1500);
const tweetBox = $(SEL.tweetBox);
if (!tweetBox) { addLog(' ⚠️ Reply box not found', 'warn'); return false; }
tweetBox.focus();
document.execCommand('insertText', false, replyText);
await sleep(800);
const sendBtn = $(SEL.tweetButton);
if (!sendBtn) { addLog(' ⚠️ Send button not found', 'warn'); return false; }
sendBtn.click();
await sleep(2000);
return true;
};
// ═══════════════════════════════════════════════════════════
// UNDO / ROLLBACK
// ═══════════════════════════════════════════════════════════
const undoAll = async () => {
if (STATE.undoStack.length === 0) {
addLog('Nothing to undo.', 'warn');
return;
}
const count = STATE.undoStack.length;
addLog(`↩ Undoing ${count} actions...`, 'warn');
let undone = 0;
for (const action of [...STATE.undoStack].reverse()) {
try {
// Scroll the article into view if it's still in the DOM
if (!document.body.contains(action.article)) {
addLog(` ⚠️ @${action.author} tweet no longer in DOM, skipping`, 'warn');
continue;
}
action.article.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(600);
if (action.type === 'like') {
const ok = await unlikeTweet(action.article);
if (ok) undone++;
} else if (action.type === 'retweet') {
const ok = await unretweetTweet(action.article);
if (ok) undone++;
}
// Bookmarks and replies can't be easily undone
await sleep(gaussian(1000, 2500));
} catch { /* swallow */ }
}
STATE.undoStack.length = 0;
addLog(`✅ Undone ${undone}/${count} actions`, 'success');
updatePanel();
};
// ═══════════════════════════════════════════════════════════
// PROCESS ONE TWEET
// ═══════════════════════════════════════════════════════════
const processTweet = async (target, index, total) => {
const preview = target.text.slice(0, 40) + (target.text.length > 40 ? '…' : '');
const el = document.getElementById('xeb-currentAction');
if (el) el.textContent = `@${target.author}: "${preview}"`;
addLog(`[${index + 1}/${total}] @${target.author} (${target.likes}❤️${target.score !== undefined ? ` score:${target.score}` : ''})`);
highlightTweet(target.article);
target.article.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(800);
const result = { author: target.author, text: target.text.slice(0, 120), actions: [], score: target.score };
// Like
if (CONFIG.actions.like) {
const ok = await likeTweet(target.article);
if (ok) {
STATE.liked++;
result.actions.push('liked');
if (!CONFIG.dryRun) STATE.undoStack.push({ type: 'like', article: target.article, author: target.author });
} else STATE.failed++;
await sleep(gaussian(400, 1000));
}
// Retweet
if (CONFIG.actions.retweet) {
const ok = await retweetTweet(target.article);
if (ok) {
STATE.retweeted++;
result.actions.push('retweeted');
if (!CONFIG.dryRun) STATE.undoStack.push({ type: 'retweet', article: target.article, author: target.author });
} else STATE.failed++;
await sleep(gaussian(400, 1000));
}
// Bookmark
if (CONFIG.actions.bookmark) {
const ok = await bookmarkTweet(target.article);
if (ok) {
STATE.bookmarked++;
result.actions.push('bookmarked');
if (!CONFIG.dryRun) STATE.undoStack.push({ type: 'bookmark', article: target.article, author: target.author });
} else STATE.failed++;
await sleep(gaussian(300, 700));
}
// Reply (smart template picker)
if (CONFIG.actions.reply) {
const ok = await replyToTweet(target.article, target.text, target.author);
if (ok) {
STATE.replied++;
result.actions.push('replied');
} else STATE.failed++;
}
// Follow
if (CONFIG.actions.follow) {
const ok = await followUserFromTweet(target.article, target.author);
if (ok) {
STATE.followed = (STATE.followed || 0) + 1;
result.actions.push('followed');
}
// Don't count follow failure as "failed" — option often unavailable
await sleep(gaussian(300, 600));
}
result.timestamp = new Date().toISOString();
STATE.results.push(result);
STATE.processed++;
unhighlightTweet();
updatePanel();
return result;
};
// ═══════════════════════════════════════════════════════════
// MAIN RUN LOOP
// ═══════════════════════════════════════════════════════════
const run = async () => {
readConfigFromPanel();
// Reset state
Object.assign(STATE, {
status: 'scanning', aborted: false, paused: false,
liked: 0, replied: 0, retweeted: 0, bookmarked: 0, failed: 0,
processed: 0, total: 0, startTime: Date.now(), undoStack: [], results: [], logs: [],
});
startUpdateTimer();
const preset = SPEED_PRESETS[CONFIG.speedPreset] || SPEED_PRESETS.safe;
addLog(`🚀 Engagement Booster v2 — by nichxbt`);
addLog(`⚡ Speed: ${preset.label} (${preset.desc})`);
if (CONFIG.dryRun) addLog('🏃 Dry run — no real interactions', 'warn');
const eligible = await collectEligible();
if (eligible.length === 0) {
addLog('❌ No eligible tweets found. Adjust filters.', 'error');
STATE.status = 'error';
stopUpdateTimer();
setButtonStates('idle');
return;
}
const toProcess = eligible.slice(0, CONFIG.maxInteractions);
STATE.total = toProcess.length;
STATE.status = 'running';
addLog(`📊 Found ${eligible.length} eligible → processing ${toProcess.length}`);
updatePanel();
// ── DRIP MODE ──
if (CONFIG.drip.enabled) {
addLog(`⏰ Drip mode: ${CONFIG.drip.batchSize} every ${CONFIG.drip.intervalMinutes}min`);
let batchStart = 0;
const runBatch = async () => {
if (STATE.aborted || batchStart >= toProcess.length) {
clearInterval(STATE.dripTimer);
finalize(toProcess);
return;
}
const batch = toProcess.slice(batchStart, batchStart + CONFIG.drip.batchSize);
addLog(`⏰ Drip batch: ${batchStart + 1}–${batchStart + batch.length}`);
for (let i = 0; i < batch.length; i++) {
if (STATE.aborted) break;
await waitForUnpause();
if (isRateLimited()) { addLog('🚨 Rate limited! Pausing batch.', 'error'); break; }
await processTweet(batch[i], batchStart + i, toProcess.length);
if (i < batch.length - 1) {
const delay = gaussian(preset.delay[0], preset.delay[1]);
addLog(`⏳ ${(delay / 1000).toFixed(0)}s...`);
await sleep(delay);
}
}
batchStart += CONFIG.drip.batchSize;
updatePanel();
};
await runBatch();
STATE.dripTimer = setInterval(runBatch, CONFIG.drip.intervalMinutes * 60 * 1000);
return; // finalize is called inside runBatch when done
}
// ── NORMAL MODE ──
for (let i = 0; i < toProcess.length; i++) {
if (STATE.aborted) break;
await waitForUnpause();
if (isRateLimited()) {
STATE.rateLimitStreak = (STATE.rateLimitStreak || 0) + 1;
const backoff = Math.min(120000 * Math.pow(1.5, STATE.rateLimitStreak - 1), 600000);
const sec = Math.round(backoff / 1000);
addLog(`🚨 Rate limited! Backoff ${sec}s (streak #${STATE.rateLimitStreak})`, 'error');
showToast(`🚨 Rate limited — cooling ${sec}s`, 'error');
await sleep(backoff);
if (isRateLimited()) { addLog('🛑 Still limited. Stopping.', 'error'); break; }
} else {
STATE.rateLimitStreak = 0;
}
await processTweet(toProcess[i], i, toProcess.length);
if (i < toProcess.length - 1 && !STATE.aborted) {
const delay = gaussian(preset.delay[0], preset.delay[1]);
addLog(`⏳ ${(delay / 1000).toFixed(0)}s...`);
await sleep(delay);
}
}
finalize(toProcess);
};
// ═══════════════════════════════════════════════════════════
// FINALIZE
// ═══════════════════════════════════════════════════════════
const finalize = (toProcess) => {
STATE.status = 'done';
stopUpdateTimer();
setButtonStates('done');
const uniqueAuthors = [...new Set(STATE.results.map(r => r.author))];
addLog('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
addLog(`✅ Done! ${STATE.processed} tweets processed`);
addLog(`❤️ ${STATE.liked} liked · 🔁 ${STATE.retweeted} RT · 🔖 ${STATE.bookmarked} saved · 💬 ${STATE.replied} replied`);
addLog(`👥 ${uniqueAuthors.length} unique accounts`);
if (STATE.failed > 0) addLog(`❌ ${STATE.failed} failed`, 'warn');
if (CONFIG.dryRun) addLog('🏃 Dry run — nothing was posted', 'warn');
if (STATE.undoStack.length > 0) addLog(`↩ ${STATE.undoStack.length} actions can be undone`);
// Save to history
saveHistory({
date: new Date().toISOString(),
dryRun: CONFIG.dryRun,
liked: STATE.liked,
replied: STATE.replied,
retweeted: STATE.retweeted,
bookmarked: STATE.bookmarked,
failed: STATE.failed,
total: STATE.processed,
authors: uniqueAuthors,
speed: CONFIG.speedPreset,
});
// Auto-export results (JSON + CSV)
if (STATE.results.length > 0 && !CONFIG.dryRun) {
const dateStr = new Date().toISOString().slice(0, 10);
// JSON
const jsonBlob = new Blob([JSON.stringify(STATE.results, null, 2)], { type: 'application/json' });
downloadBlob(jsonBlob, `xactions-engagement-${dateStr}.json`);
// CSV
const headers = ['timestamp', 'author', 'text', 'score', 'actions'];
const rows = STATE.results.map(r =>
[r.timestamp, r.author, `"${(r.text || '').replace(/"/g, '""')}"`, r.score || '', r.actions.join('+')]
);
const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
const csvBlob = new Blob([csv], { type: 'text/csv' });
downloadBlob(csvBlob, `xactions-engagement-${dateStr}.csv`);
addLog('📥 Results exported (JSON + CSV)');
}
// Completion sound
playCompletionSound();
showToast(`✅ Done! ${STATE.processed} tweets processed`, 'success');
};
const downloadBlob = (blob, filename) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a); a.click(); a.remove();
};
// Completion sound — plays a pleasant C major chord
const playCompletionSound = () => {
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
[523.25, 659.25, 783.99].forEach((freq, i) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.08, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.6);
osc.connect(gain).connect(ctx.destination);
osc.start(ctx.currentTime + i * 0.12);
osc.stop(ctx.currentTime + 0.7);
});
} catch { /* AudioContext unavailable */ }
};
// Toast overlay notifications
let toastContainer = null;
const initToasts = () => {
if (toastContainer) return;
toastContainer = document.createElement('div');
Object.assign(toastContainer.style, {
position: 'fixed', bottom: '80px', left: '50%', transform: 'translateX(-50%)',
zIndex: '2147483646', display: 'flex', flexDirection: 'column-reverse',
gap: '8px', pointerEvents: 'none',
});
document.body.appendChild(toastContainer);
};
const TOAST_COLORS = { success: '#00ba7c', warning: '#ffad1f', error: '#f4212e', info: '#1d9bf0' };
const showToast = (msg, type = 'info') => {
if (!toastContainer) initToasts();
const el = document.createElement('div');
Object.assign(el.style, {
background: TOAST_COLORS[type] || '#1d9bf0', color: '#fff',
padding: '10px 18px', borderRadius: '12px', fontSize: '13px', fontWeight: '600',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
boxShadow: '0 4px 16px rgba(0,0,0,0.4)', opacity: '0', transform: 'translateY(12px)',
transition: 'all 0.3s ease', pointerEvents: 'auto', maxWidth: '320px', textAlign: 'center',
});
el.textContent = msg;
toastContainer.appendChild(el);
requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateY(0)'; });
setTimeout(() => {
el.style.opacity = '0'; el.style.transform = 'translateY(12px)';
setTimeout(() => el.remove(), 300);
}, 3500);
};
// ═══════════════════════════════════════════════════════════
// BUTTON HANDLERS
// ═══════════════════════════════════════════════════════════
const setButtonStates = (mode) => {
const start = document.getElementById('xeb-startBtn');
const pause = document.getElementById('xeb-pauseBtn');
const stop = document.getElementById('xeb-stopBtn');
if (mode === 'running') {
start.disabled = true;
pause.disabled = false;
stop.disabled = false;
} else if (mode === 'paused') {
start.disabled = true;
pause.textContent = '▶ Resume';
pause.disabled = false;
stop.disabled = false;
} else if (mode === 'done' || mode === 'idle') {
start.disabled = false;
start.textContent = mode === 'done' ? '🔄 Restart' : '▶ Start';
pause.disabled = true;
pause.textContent = '⏸ Pause';
stop.disabled = true;
// Show undo button if there are actions to undo
if (STATE.undoStack.length > 0) {
const actionBar = document.getElementById('xeb-actionBar');
if (actionBar && !document.getElementById('xeb-undoBtn')) {
const undoBtn = document.createElement('button');
undoBtn.id = 'xeb-undoBtn';
undoBtn.className = 'xeb-btn xeb-btn-warn';
undoBtn.textContent = `↩ Undo (${STATE.undoStack.length})`;
undoBtn.addEventListener('click', async () => {
undoBtn.disabled = true;
undoBtn.textContent = '↩ Undoing...';
await undoAll();
undoBtn.remove();
});
actionBar.appendChild(undoBtn);
}
}
}
};
document.getElementById('xeb-startBtn').addEventListener('click', () => {
setButtonStates('running');
// Switch to progress tab
panel.querySelectorAll('.xeb-tab').forEach(t => t.classList.remove('active'));
panel.querySelectorAll('.xeb-pane').forEach(p => p.classList.remove('active'));
panel.querySelector('[data-tab="progress"]').classList.add('active');
panel.querySelector('[data-pane="progress"]').classList.add('active');
run();
});
document.getElementById('xeb-pauseBtn').addEventListener('click', () => {
if (STATE.paused) {
STATE.paused = false;
STATE.status = 'running';
setButtonStates('running');
addLog('▶ Resumed', 'success');
} else {
STATE.paused = true;
STATE.status = 'paused';
setButtonStates('paused');
addLog('⏸ Paused', 'warn');
}
});
document.getElementById('xeb-stopBtn').addEventListener('click', () => {
STATE.aborted = true;
addLog('🛑 Stopping...', 'error');
});
// Expose API for console access
window.XActions = window.XActions || {};
window.XActions.engagementBooster = {
pause: () => { STATE.paused = true; STATE.status = 'paused'; addLog('⏸ Paused'); },
resume: () => { STATE.paused = false; STATE.status = 'running'; addLog('▶ Resumed'); },
abort: () => { STATE.aborted = true; addLog('🛑 Aborting...'); },
undo: undoAll,
history: getHistoryStats,
state: STATE,
config: CONFIG,
};
addLog('🚀 Engagement Booster v2 ready');
addLog('Configure settings and click ▶ Start');
console.log('╔══════════════════════════════════════════╗');
console.log('║ 🚀 ENGAGEMENT BOOSTER v2 ║');
console.log('║ by nichxbt — floating panel active ║');
console.log('║ Console: XActions.engagementBooster.* ║');
console.log('╚══════════════════════════════════════════╝');
})();
⚡ More XActions Scripts
Browse 300+ free browser scripts for X/Twitter automation. No API keys, no fees.
Browse All Scripts