Remove redundant comments and clean up code formatting
This commit is contained in:
@ -8,8 +8,6 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# Try to import matplotlib for graphing
|
|
||||||
try:
|
try:
|
||||||
import matplotlib
|
import matplotlib
|
||||||
matplotlib.use('Qt5Agg')
|
matplotlib.use('Qt5Agg')
|
||||||
@ -18,39 +16,28 @@ try:
|
|||||||
MATPLOTLIB_AVAILABLE = True
|
MATPLOTLIB_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
MATPLOTLIB_AVAILABLE = False
|
MATPLOTLIB_AVAILABLE = False
|
||||||
|
|
||||||
# WhatsApp Web Thread (Node.js Bridge)
|
|
||||||
class WhatsAppWebThread(QThread):
|
class WhatsAppWebThread(QThread):
|
||||||
"""Thread for interacting with WhatsApp Web Node.js bridge"""
|
"""Thread for interacting with WhatsApp Web Node.js bridge"""
|
||||||
log_signal = pyqtSignal(str, str)
|
log_signal = pyqtSignal(str, str)
|
||||||
qr_signal = pyqtSignal(str)
|
qr_signal = pyqtSignal(str)
|
||||||
status_signal = pyqtSignal(bool)
|
status_signal = pyqtSignal(bool)
|
||||||
spam_data_signal = pyqtSignal(dict) # Signal for spam timing data
|
spam_data_signal = pyqtSignal(dict)
|
||||||
|
|
||||||
def __init__(self, script_dir):
|
def __init__(self, script_dir):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.script_dir = script_dir
|
self.script_dir = script_dir
|
||||||
self.process = None
|
self.process = None
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.running = True
|
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')
|
bridge_script = os.path.join(os.path.dirname(__file__), 'whatsapp_bridge.js')
|
||||||
|
|
||||||
# Debug: Print node version and path
|
|
||||||
try:
|
try:
|
||||||
node_version = subprocess.check_output(['node', '--version'], text=True).strip()
|
node_version = subprocess.check_output(['node', '--version'], text=True).strip()
|
||||||
node_path = subprocess.check_output(['which', 'node'], 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")
|
self.log_signal.emit(f"[*] Node.js found: {node_version} at {node_path}", "system")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_signal.emit(f"[Error] Check node failed: {e}", "error")
|
self.log_signal.emit(f"[Error] Check node failed: {e}", "error")
|
||||||
|
|
||||||
self.log_signal.emit(f"[*] Starting bridge script: {bridge_script}", "system")
|
self.log_signal.emit(f"[*] Starting bridge script: {bridge_script}", "system")
|
||||||
|
|
||||||
# Use absolute path for CWD
|
|
||||||
cwd = os.path.dirname(bridge_script)
|
cwd = os.path.dirname(bridge_script)
|
||||||
|
|
||||||
self.process = subprocess.Popen(
|
self.process = subprocess.Popen(
|
||||||
['node', bridge_script],
|
['node', bridge_script],
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
@ -58,12 +45,10 @@ class WhatsAppWebThread(QThread):
|
|||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=True,
|
text=True,
|
||||||
bufsize=1,
|
bufsize=1,
|
||||||
cwd=cwd # Run from the script's directory
|
cwd=cwd
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for immediate failure
|
|
||||||
try:
|
try:
|
||||||
time.sleep(1) # Wait a bit to see if it crashes
|
time.sleep(1)
|
||||||
gone = self.process.poll()
|
gone = self.process.poll()
|
||||||
if gone is not None:
|
if gone is not None:
|
||||||
stderr_output = self.process.stderr.read()
|
stderr_output = self.process.stderr.read()
|
||||||
@ -72,7 +57,6 @@ class WhatsAppWebThread(QThread):
|
|||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_signal.emit(f"[Error] Monitor failed: {e}", "error")
|
self.log_signal.emit(f"[Error] Monitor failed: {e}", "error")
|
||||||
|
|
||||||
while self.running and self.process.poll() is None:
|
while self.running and self.process.poll() is None:
|
||||||
line = self.process.stdout.readline()
|
line = self.process.stdout.readline()
|
||||||
if not line:
|
if not line:
|
||||||
@ -83,14 +67,11 @@ class WhatsAppWebThread(QThread):
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
if line.strip():
|
if line.strip():
|
||||||
self.log_signal.emit(f"[Bridge] {line.strip()}", "system")
|
self.log_signal.emit(f"[Bridge] {line.strip()}", "system")
|
||||||
|
|
||||||
self.log_signal.emit("[*] WhatsApp Web client stopped.", "system")
|
self.log_signal.emit("[*] WhatsApp Web client stopped.", "system")
|
||||||
self.status_signal.emit(False)
|
self.status_signal.emit(False)
|
||||||
|
|
||||||
def _handle_bridge_message(self, data):
|
def _handle_bridge_message(self, data):
|
||||||
msg_type = data.get('type')
|
msg_type = data.get('type')
|
||||||
message = data.get('message', '')
|
message = data.get('message', '')
|
||||||
|
|
||||||
if msg_type == 'qr':
|
if msg_type == 'qr':
|
||||||
self.log_signal.emit(f"[*] {message}", "system")
|
self.log_signal.emit(f"[*] {message}", "system")
|
||||||
self.qr_signal.emit(data.get('data'))
|
self.qr_signal.emit(data.get('data'))
|
||||||
@ -109,30 +90,27 @@ class WhatsAppWebThread(QThread):
|
|||||||
is_ready = data.get('ready', False)
|
is_ready = data.get('ready', False)
|
||||||
self.status_signal.emit(is_ready)
|
self.status_signal.emit(is_ready)
|
||||||
elif msg_type == 'ack':
|
elif msg_type == 'ack':
|
||||||
# Message ACK status update
|
|
||||||
ack = data.get('ack', 0)
|
ack = data.get('ack', 0)
|
||||||
ack_name = data.get('ackName', 'UNKNOWN')
|
ack_name = data.get('ackName', 'UNKNOWN')
|
||||||
time_formatted = data.get('timeSinceSentFormatted', '?')
|
time_formatted = data.get('timeSinceSentFormatted', '?')
|
||||||
if ack == 1: # Single tick
|
if ack == 1:
|
||||||
self.log_signal.emit(f"[✓] Single tick after {time_formatted}", "system")
|
self.log_signal.emit(f"[✓] Single tick after {time_formatted}", "system")
|
||||||
elif ack == 2: # Double tick
|
elif ack == 2:
|
||||||
self.log_signal.emit(f"[✓✓] Double tick after {time_formatted}", "success")
|
self.log_signal.emit(f"[✓✓] Double tick after {time_formatted}", "success")
|
||||||
elif ack == 3: # Read
|
elif ack == 3:
|
||||||
self.log_signal.emit(f"[✓✓] Read (blue tick) after {time_formatted}", "success")
|
self.log_signal.emit(f"[✓✓] Read (blue tick) after {time_formatted}", "success")
|
||||||
elif ack == 4: # Played
|
elif ack == 4:
|
||||||
self.log_signal.emit(f"[▶] Played after {time_formatted}", "success")
|
self.log_signal.emit(f"[▶] Played after {time_formatted}", "success")
|
||||||
elif ack == -1: # Error
|
elif ack == -1:
|
||||||
self.log_signal.emit(f"[✗] Message error after {time_formatted}", "error")
|
self.log_signal.emit(f"[✗] Message error after {time_formatted}", "error")
|
||||||
else:
|
else:
|
||||||
self.log_signal.emit(f"[*] ACK {ack_name} after {time_formatted}", "system")
|
self.log_signal.emit(f"[*] ACK {ack_name} after {time_formatted}", "system")
|
||||||
elif msg_type == 'ack_timing':
|
elif msg_type == 'ack_timing':
|
||||||
# Timing between single and double tick
|
|
||||||
single_tick_ms = data.get('singleTickMs', 0)
|
single_tick_ms = data.get('singleTickMs', 0)
|
||||||
double_tick_ms = data.get('doubleTickMs', 0)
|
double_tick_ms = data.get('doubleTickMs', 0)
|
||||||
single_to_double_ms = data.get('singleToDoubleMs', 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"[⏱] 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")
|
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({
|
self.spam_data_signal.emit({
|
||||||
'type': 'ack_timing',
|
'type': 'ack_timing',
|
||||||
'singleTickMs': single_tick_ms,
|
'singleTickMs': single_tick_ms,
|
||||||
@ -150,7 +128,6 @@ class WhatsAppWebThread(QThread):
|
|||||||
react_remove = data.get('reactionRemoveTimeMs', 0)
|
react_remove = data.get('reactionRemoveTimeMs', 0)
|
||||||
iteration_time = data.get('iterationTimeMs', 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")
|
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({
|
self.spam_data_signal.emit({
|
||||||
'type': 'spam_iteration',
|
'type': 'spam_iteration',
|
||||||
'index': index,
|
'index': index,
|
||||||
@ -164,7 +141,6 @@ class WhatsAppWebThread(QThread):
|
|||||||
self.spam_data_signal.emit({'type': 'spam_complete'})
|
self.spam_data_signal.emit({'type': 'spam_complete'})
|
||||||
elif msg_type == 'spam_error':
|
elif msg_type == 'spam_error':
|
||||||
self.log_signal.emit(f"[❌] {message}", "error")
|
self.log_signal.emit(f"[❌] {message}", "error")
|
||||||
|
|
||||||
def send_command(self, command):
|
def send_command(self, command):
|
||||||
if self.process and self.process.poll() is None:
|
if self.process and self.process.poll() is None:
|
||||||
try:
|
try:
|
||||||
@ -174,7 +150,6 @@ class WhatsAppWebThread(QThread):
|
|||||||
self.log_signal.emit(f"[Error] Failed to send command: {e}", "error")
|
self.log_signal.emit(f"[Error] Failed to send command: {e}", "error")
|
||||||
else:
|
else:
|
||||||
self.log_signal.emit("[Error] WhatsApp client is not running.", "error")
|
self.log_signal.emit("[Error] WhatsApp client is not running.", "error")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.running = False
|
self.running = False
|
||||||
if self.process:
|
if self.process:
|
||||||
@ -183,11 +158,8 @@ class WhatsAppWebThread(QThread):
|
|||||||
self.process.wait(timeout=2)
|
self.process.wait(timeout=2)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
self.process.kill()
|
self.process.kill()
|
||||||
|
|
||||||
|
|
||||||
class SilentWhispersWidget(QWidget):
|
class SilentWhispersWidget(QWidget):
|
||||||
"""Silent Whispers - Clean WhatsApp Reaction Spam Tool"""
|
"""Silent Whispers - Clean WhatsApp Reaction Spam Tool"""
|
||||||
|
|
||||||
def __init__(self, script_dir, parent=None):
|
def __init__(self, script_dir, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.script_dir = script_dir
|
self.script_dir = script_dir
|
||||||
@ -195,29 +167,19 @@ class SilentWhispersWidget(QWidget):
|
|||||||
self.is_spamming = False
|
self.is_spamming = False
|
||||||
self.spam_data = {'indices': [], 'iteration_times': []}
|
self.spam_data = {'indices': [], 'iteration_times': []}
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
main_layout = QVBoxLayout(self)
|
main_layout = QVBoxLayout(self)
|
||||||
main_layout.setContentsMargins(15, 15, 15, 15)
|
main_layout.setContentsMargins(15, 15, 15, 15)
|
||||||
main_layout.setSpacing(10)
|
main_layout.setSpacing(10)
|
||||||
|
|
||||||
# Fonts matching main app
|
|
||||||
title_font = QFont()
|
title_font = QFont()
|
||||||
title_font.setBold(True)
|
title_font.setBold(True)
|
||||||
title_font.setPointSize(12)
|
title_font.setPointSize(12)
|
||||||
subtitle_font = QFont()
|
subtitle_font = QFont()
|
||||||
subtitle_font.setPointSize(10)
|
subtitle_font.setPointSize(10)
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
|
||||||
# TOP SECTION: Connection + Controls
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
|
||||||
top_section = QHBoxLayout()
|
top_section = QHBoxLayout()
|
||||||
top_section.setSpacing(15)
|
top_section.setSpacing(15)
|
||||||
|
|
||||||
# ── LEFT: QR Code Area ──
|
|
||||||
qr_container = QVBoxLayout()
|
qr_container = QVBoxLayout()
|
||||||
qr_container.setSpacing(8)
|
qr_container.setSpacing(8)
|
||||||
|
|
||||||
self.qr_label = QLabel("Scan QR Code")
|
self.qr_label = QLabel("Scan QR Code")
|
||||||
self.qr_label.setAlignment(Qt.AlignCenter)
|
self.qr_label.setAlignment(Qt.AlignCenter)
|
||||||
self.qr_label.setFixedSize(160, 160)
|
self.qr_label.setFixedSize(160, 160)
|
||||||
@ -231,37 +193,25 @@ class SilentWhispersWidget(QWidget):
|
|||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
qr_container.addWidget(self.qr_label)
|
qr_container.addWidget(self.qr_label)
|
||||||
|
|
||||||
# Connection button
|
|
||||||
self.wa_web_btn = QPushButton("Start Client")
|
self.wa_web_btn = QPushButton("Start Client")
|
||||||
self.wa_web_btn.setFont(subtitle_font)
|
self.wa_web_btn.setFont(subtitle_font)
|
||||||
self.wa_web_btn.setFixedWidth(160)
|
self.wa_web_btn.setFixedWidth(160)
|
||||||
self.wa_web_btn.clicked.connect(self.toggle_whatsapp_client)
|
self.wa_web_btn.clicked.connect(self.toggle_whatsapp_client)
|
||||||
qr_container.addWidget(self.wa_web_btn)
|
qr_container.addWidget(self.wa_web_btn)
|
||||||
|
|
||||||
# Status indicator
|
|
||||||
self.wa_web_status = QLabel("Disconnected")
|
self.wa_web_status = QLabel("Disconnected")
|
||||||
self.wa_web_status.setAlignment(Qt.AlignCenter)
|
self.wa_web_status.setAlignment(Qt.AlignCenter)
|
||||||
self.wa_web_status.setStyleSheet("color: #FF3B30; font-size: 10px;")
|
self.wa_web_status.setStyleSheet("color: #FF3B30; font-size: 10px;")
|
||||||
qr_container.addWidget(self.wa_web_status)
|
qr_container.addWidget(self.wa_web_status)
|
||||||
|
|
||||||
top_section.addLayout(qr_container)
|
top_section.addLayout(qr_container)
|
||||||
|
|
||||||
# ── RIGHT: Controls ──
|
|
||||||
controls_container = QVBoxLayout()
|
controls_container = QVBoxLayout()
|
||||||
controls_container.setSpacing(8)
|
controls_container.setSpacing(8)
|
||||||
|
|
||||||
# Phone input
|
|
||||||
phone_label = QLabel("PHONE NUMBER")
|
phone_label = QLabel("PHONE NUMBER")
|
||||||
phone_label.setFont(subtitle_font)
|
phone_label.setFont(subtitle_font)
|
||||||
controls_container.addWidget(phone_label)
|
controls_container.addWidget(phone_label)
|
||||||
|
|
||||||
self.phone_input = QLineEdit()
|
self.phone_input = QLineEdit()
|
||||||
self.phone_input.setPlaceholderText("923001234567")
|
self.phone_input.setPlaceholderText("923001234567")
|
||||||
self.phone_input.setFont(subtitle_font)
|
self.phone_input.setFont(subtitle_font)
|
||||||
controls_container.addWidget(self.phone_input)
|
controls_container.addWidget(self.phone_input)
|
||||||
|
|
||||||
# Delay input row
|
|
||||||
delay_row = QHBoxLayout()
|
delay_row = QHBoxLayout()
|
||||||
delay_label = QLabel("DELAY")
|
delay_label = QLabel("DELAY")
|
||||||
delay_label.setFont(subtitle_font)
|
delay_label.setFont(subtitle_font)
|
||||||
@ -274,11 +224,8 @@ class SilentWhispersWidget(QWidget):
|
|||||||
delay_row.addWidget(delay_label)
|
delay_row.addWidget(delay_label)
|
||||||
delay_row.addWidget(self.spam_delay_input, 1)
|
delay_row.addWidget(self.spam_delay_input, 1)
|
||||||
controls_container.addLayout(delay_row)
|
controls_container.addLayout(delay_row)
|
||||||
|
|
||||||
# Action buttons
|
|
||||||
btn_row = QHBoxLayout()
|
btn_row = QHBoxLayout()
|
||||||
btn_row.setSpacing(8)
|
btn_row.setSpacing(8)
|
||||||
|
|
||||||
self.spam_btn = QPushButton("START SPAM")
|
self.spam_btn = QPushButton("START SPAM")
|
||||||
self.spam_btn.setFont(subtitle_font)
|
self.spam_btn.setFont(subtitle_font)
|
||||||
self.spam_btn.clicked.connect(self.start_reaction_spam)
|
self.spam_btn.clicked.connect(self.start_reaction_spam)
|
||||||
@ -293,7 +240,6 @@ class SilentWhispersWidget(QWidget):
|
|||||||
QPushButton:disabled { background-color: #1D1D1F; color: #555; }
|
QPushButton:disabled { background-color: #1D1D1F; color: #555; }
|
||||||
""")
|
""")
|
||||||
btn_row.addWidget(self.spam_btn)
|
btn_row.addWidget(self.spam_btn)
|
||||||
|
|
||||||
self.stop_spam_btn = QPushButton("STOP SPAM")
|
self.stop_spam_btn = QPushButton("STOP SPAM")
|
||||||
self.stop_spam_btn.setFont(subtitle_font)
|
self.stop_spam_btn.setFont(subtitle_font)
|
||||||
self.stop_spam_btn.clicked.connect(self.stop_reaction_spam)
|
self.stop_spam_btn.clicked.connect(self.stop_reaction_spam)
|
||||||
@ -309,10 +255,7 @@ class SilentWhispersWidget(QWidget):
|
|||||||
QPushButton:disabled { background-color: #1D1D1F; color: #555; }
|
QPushButton:disabled { background-color: #1D1D1F; color: #555; }
|
||||||
""")
|
""")
|
||||||
btn_row.addWidget(self.stop_spam_btn)
|
btn_row.addWidget(self.stop_spam_btn)
|
||||||
|
|
||||||
controls_container.addLayout(btn_row)
|
controls_container.addLayout(btn_row)
|
||||||
|
|
||||||
# Mini log
|
|
||||||
self.log_output = QTextEdit()
|
self.log_output = QTextEdit()
|
||||||
self.log_output.setReadOnly(True)
|
self.log_output.setReadOnly(True)
|
||||||
self.log_output.setMaximumHeight(60)
|
self.log_output.setMaximumHeight(60)
|
||||||
@ -327,13 +270,8 @@ class SilentWhispersWidget(QWidget):
|
|||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
controls_container.addWidget(self.log_output)
|
controls_container.addWidget(self.log_output)
|
||||||
|
|
||||||
top_section.addLayout(controls_container, 1)
|
top_section.addLayout(controls_container, 1)
|
||||||
main_layout.addLayout(top_section)
|
main_layout.addLayout(top_section)
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
|
||||||
# GRAPH SECTION: Full width
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
|
||||||
if MATPLOTLIB_AVAILABLE:
|
if MATPLOTLIB_AVAILABLE:
|
||||||
self.figure = Figure(figsize=(8, 3), dpi=100, facecolor='#111113')
|
self.figure = Figure(figsize=(8, 3), dpi=100, facecolor='#111113')
|
||||||
self.canvas = FigureCanvas(self.figure)
|
self.canvas = FigureCanvas(self.figure)
|
||||||
@ -341,8 +279,6 @@ class SilentWhispersWidget(QWidget):
|
|||||||
self._style_graph()
|
self._style_graph()
|
||||||
self.figure.tight_layout(pad=2)
|
self.figure.tight_layout(pad=2)
|
||||||
main_layout.addWidget(self.canvas, 1)
|
main_layout.addWidget(self.canvas, 1)
|
||||||
|
|
||||||
# Clear button
|
|
||||||
self.clear_graph_btn = QPushButton("Clear Graph")
|
self.clear_graph_btn = QPushButton("Clear Graph")
|
||||||
self.clear_graph_btn.setFont(subtitle_font)
|
self.clear_graph_btn.setFont(subtitle_font)
|
||||||
self.clear_graph_btn.clicked.connect(self.clear_graph)
|
self.clear_graph_btn.clicked.connect(self.clear_graph)
|
||||||
@ -352,8 +288,6 @@ class SilentWhispersWidget(QWidget):
|
|||||||
no_graph.setStyleSheet("color: #555;")
|
no_graph.setStyleSheet("color: #555;")
|
||||||
no_graph.setAlignment(Qt.AlignCenter)
|
no_graph.setAlignment(Qt.AlignCenter)
|
||||||
main_layout.addWidget(no_graph)
|
main_layout.addWidget(no_graph)
|
||||||
|
|
||||||
# Hidden elements for compatibility
|
|
||||||
self.message_input = QTextEdit()
|
self.message_input = QTextEdit()
|
||||||
self.message_input.hide()
|
self.message_input.hide()
|
||||||
self.add_reaction_check = QComboBox()
|
self.add_reaction_check = QComboBox()
|
||||||
@ -361,9 +295,7 @@ class SilentWhispersWidget(QWidget):
|
|||||||
self.add_reaction_check.hide()
|
self.add_reaction_check.hide()
|
||||||
self.send_btn = QPushButton()
|
self.send_btn = QPushButton()
|
||||||
self.send_btn.hide()
|
self.send_btn.hide()
|
||||||
|
|
||||||
self.log_message("[+] Ready", "success")
|
self.log_message("[+] Ready", "success")
|
||||||
|
|
||||||
def _style_graph(self):
|
def _style_graph(self):
|
||||||
"""Apply dark theme to graph matching main app"""
|
"""Apply dark theme to graph matching main app"""
|
||||||
self.ax.set_facecolor('#1D1D1F')
|
self.ax.set_facecolor('#1D1D1F')
|
||||||
@ -375,7 +307,6 @@ class SilentWhispersWidget(QWidget):
|
|||||||
for spine in self.ax.spines.values():
|
for spine in self.ax.spines.values():
|
||||||
spine.set_color('#2a2a2e')
|
spine.set_color('#2a2a2e')
|
||||||
self.ax.grid(True, alpha=0.1, color='#444')
|
self.ax.grid(True, alpha=0.1, color='#444')
|
||||||
|
|
||||||
def log_message(self, message, msg_type="system"):
|
def log_message(self, message, msg_type="system"):
|
||||||
"""Add a message to the log output"""
|
"""Add a message to the log output"""
|
||||||
color_map = {
|
color_map = {
|
||||||
@ -385,15 +316,9 @@ class SilentWhispersWidget(QWidget):
|
|||||||
}
|
}
|
||||||
color = color_map.get(msg_type, "#e0e0e0")
|
color = color_map.get(msg_type, "#e0e0e0")
|
||||||
self.log_output.append(f'<span style="color: {color};">{message}</span>')
|
self.log_output.append(f'<span style="color: {color};">{message}</span>')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# toggle_api_mode removed
|
|
||||||
|
|
||||||
def toggle_whatsapp_client(self):
|
def toggle_whatsapp_client(self):
|
||||||
"""Start or stop the WhatsApp Web client"""
|
"""Start or stop the WhatsApp Web client"""
|
||||||
if self.whatsapp_thread and isinstance(self.whatsapp_thread, WhatsAppWebThread) and self.whatsapp_thread.isRunning():
|
if self.whatsapp_thread and isinstance(self.whatsapp_thread, WhatsAppWebThread) and self.whatsapp_thread.isRunning():
|
||||||
# Stop client
|
|
||||||
self.whatsapp_thread.stop()
|
self.whatsapp_thread.stop()
|
||||||
self.whatsapp_thread.wait()
|
self.whatsapp_thread.wait()
|
||||||
self.whatsapp_thread = None
|
self.whatsapp_thread = None
|
||||||
@ -401,9 +326,8 @@ class SilentWhispersWidget(QWidget):
|
|||||||
self.wa_web_btn.setText("Start WhatsApp Client")
|
self.wa_web_btn.setText("Start WhatsApp Client")
|
||||||
self.wa_web_btn.setStyleSheet("")
|
self.wa_web_btn.setStyleSheet("")
|
||||||
self.qr_label.setText("Click Start to generate QR Code")
|
self.qr_label.setText("Click Start to generate QR Code")
|
||||||
self.qr_label.setPixmap(QPixmap()) # Clear QR code
|
self.qr_label.setPixmap(QPixmap())
|
||||||
else:
|
else:
|
||||||
# Start client
|
|
||||||
self.whatsapp_thread = WhatsAppWebThread(self.script_dir)
|
self.whatsapp_thread = WhatsAppWebThread(self.script_dir)
|
||||||
self.whatsapp_thread.log_signal.connect(self.log_message)
|
self.whatsapp_thread.log_signal.connect(self.log_message)
|
||||||
self.whatsapp_thread.qr_signal.connect(self.display_qr_code)
|
self.whatsapp_thread.qr_signal.connect(self.display_qr_code)
|
||||||
@ -413,34 +337,25 @@ class SilentWhispersWidget(QWidget):
|
|||||||
self.wa_web_status.setText("Status: Starting...")
|
self.wa_web_status.setText("Status: Starting...")
|
||||||
self.wa_web_btn.setText("Stop WhatsApp Client")
|
self.wa_web_btn.setText("Stop WhatsApp Client")
|
||||||
self.wa_web_btn.setStyleSheet("background-color: #d32f2f; color: white;")
|
self.wa_web_btn.setStyleSheet("background-color: #d32f2f; color: white;")
|
||||||
|
|
||||||
def display_qr_code(self, qr_data):
|
def display_qr_code(self, qr_data):
|
||||||
"""Generate and display QR code from data"""
|
"""Generate and display QR code from data"""
|
||||||
try:
|
try:
|
||||||
import qrcode
|
import qrcode
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
# Generate QR code image
|
|
||||||
qr = qrcode.QRCode(version=1, box_size=10, border=2)
|
qr = qrcode.QRCode(version=1, box_size=10, border=2)
|
||||||
qr.add_data(qr_data)
|
qr.add_data(qr_data)
|
||||||
qr.make(fit=True)
|
qr.make(fit=True)
|
||||||
img = qr.make_image(fill_color="black", back_color="white")
|
img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
|
||||||
# Convert to QPixmap using BytesIO (avoids ImageQt issues)
|
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
img.save(buffer, format="PNG")
|
img.save(buffer, format="PNG")
|
||||||
qim = QPixmap()
|
qim = QPixmap()
|
||||||
qim.loadFromData(buffer.getvalue(), "PNG")
|
qim.loadFromData(buffer.getvalue(), "PNG")
|
||||||
|
|
||||||
# Scale to fit label
|
|
||||||
scaled_pixmap = qim.scaled(self.qr_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
scaled_pixmap = qim.scaled(self.qr_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
self.qr_label.setPixmap(scaled_pixmap)
|
self.qr_label.setPixmap(scaled_pixmap)
|
||||||
self.qr_label.setText("") # Clear text
|
self.qr_label.setText("")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_message(f"Error generating QR: {e}", "error")
|
self.log_message(f"Error generating QR: {e}", "error")
|
||||||
self.qr_label.setText(f"Error: {str(e)}")
|
self.qr_label.setText(f"Error: {str(e)}")
|
||||||
|
|
||||||
def update_client_status(self, is_ready):
|
def update_client_status(self, is_ready):
|
||||||
if is_ready:
|
if is_ready:
|
||||||
self.wa_web_status.setText("Status: Connected & Ready")
|
self.wa_web_status.setText("Status: Connected & Ready")
|
||||||
@ -450,42 +365,29 @@ class SilentWhispersWidget(QWidget):
|
|||||||
else:
|
else:
|
||||||
self.wa_web_status.setText("Status: Disconnected")
|
self.wa_web_status.setText("Status: Disconnected")
|
||||||
self.wa_web_status.setStyleSheet("color: #f44336;")
|
self.wa_web_status.setStyleSheet("color: #f44336;")
|
||||||
|
|
||||||
def send_whatsapp_message(self):
|
def send_whatsapp_message(self):
|
||||||
"""Send WhatsApp message using Node.js bridge"""
|
"""Send WhatsApp message using Node.js bridge"""
|
||||||
phone_number = self.phone_input.text().strip()
|
phone_number = self.phone_input.text().strip()
|
||||||
message = self.message_input.toPlainText().strip()
|
message = self.message_input.toPlainText().strip()
|
||||||
add_reaction = self.add_reaction_check.currentIndex()
|
add_reaction = self.add_reaction_check.currentIndex()
|
||||||
|
|
||||||
# Validate inputs
|
|
||||||
if not phone_number:
|
if not phone_number:
|
||||||
self.log_message("[-] Phone number is required!", "error")
|
self.log_message("[-] Phone number is required!", "error")
|
||||||
return
|
return
|
||||||
if not message:
|
if not message:
|
||||||
self.log_message("[-] Message cannot be empty!", "error")
|
self.log_message("[-] Message cannot be empty!", "error")
|
||||||
return
|
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():
|
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("[-] WhatsApp Web client is not running!", "error")
|
||||||
self.log_message("[*] Click 'Start WhatsApp Client' and scan the QR code first", "system")
|
self.log_message("[*] Click 'Start WhatsApp Client' and scan the QR code first", "system")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.send_btn.setText("SENDING...")
|
self.send_btn.setText("SENDING...")
|
||||||
self.send_btn.setEnabled(False)
|
self.send_btn.setEnabled(False)
|
||||||
|
|
||||||
# Clear previous logs
|
|
||||||
self.log_output.clear()
|
self.log_output.clear()
|
||||||
|
|
||||||
# Format phone number for WhatsApp Web.js
|
|
||||||
formatted_number = phone_number
|
formatted_number = phone_number
|
||||||
if not formatted_number.endswith("@c.us") and not formatted_number.endswith("@g.us"):
|
if not formatted_number.endswith("@c.us") and not formatted_number.endswith("@g.us"):
|
||||||
formatted_number = f"{formatted_number}@c.us"
|
formatted_number = f"{formatted_number}@c.us"
|
||||||
|
|
||||||
self.log_message(f"[*] Sending message to {formatted_number}", "system")
|
self.log_message(f"[*] Sending message to {formatted_number}", "system")
|
||||||
|
if add_reaction == 1:
|
||||||
# Send command to Node.js bridge
|
|
||||||
if add_reaction == 1: # Add reaction
|
|
||||||
self.whatsapp_thread.send_command({
|
self.whatsapp_thread.send_command({
|
||||||
"action": "sendMessageAndReact",
|
"action": "sendMessageAndReact",
|
||||||
"chatId": formatted_number,
|
"chatId": formatted_number,
|
||||||
@ -498,16 +400,11 @@ class SilentWhispersWidget(QWidget):
|
|||||||
"chatId": formatted_number,
|
"chatId": formatted_number,
|
||||||
"message": message
|
"message": message
|
||||||
})
|
})
|
||||||
|
|
||||||
# Re-enable button
|
|
||||||
QTimer.singleShot(1000, lambda: self.send_btn.setEnabled(True))
|
QTimer.singleShot(1000, lambda: self.send_btn.setEnabled(True))
|
||||||
QTimer.singleShot(1000, lambda: self.send_btn.setText("SEND MESSAGE"))
|
QTimer.singleShot(1000, lambda: self.send_btn.setText("SEND MESSAGE"))
|
||||||
|
|
||||||
def on_send_finished(self, success, message):
|
def on_send_finished(self, success, message):
|
||||||
"""Handle completion of WhatsApp send operation"""
|
"""Handle completion of WhatsApp send operation"""
|
||||||
# This might not be needed anymore as feedback comes via bridge logs
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_settings(self):
|
def get_settings(self):
|
||||||
"""Get current settings for saving"""
|
"""Get current settings for saving"""
|
||||||
return {
|
return {
|
||||||
@ -515,7 +412,6 @@ class SilentWhispersWidget(QWidget):
|
|||||||
'message': self.message_input.toPlainText(),
|
'message': self.message_input.toPlainText(),
|
||||||
'add_reaction': self.add_reaction_check.currentIndex()
|
'add_reaction': self.add_reaction_check.currentIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
def load_settings(self, settings):
|
def load_settings(self, settings):
|
||||||
"""Load settings from saved configuration"""
|
"""Load settings from saved configuration"""
|
||||||
if 'phone_number' in settings:
|
if 'phone_number' in settings:
|
||||||
@ -524,120 +420,87 @@ class SilentWhispersWidget(QWidget):
|
|||||||
self.message_input.setPlainText(settings['message'])
|
self.message_input.setPlainText(settings['message'])
|
||||||
if 'add_reaction' in settings:
|
if 'add_reaction' in settings:
|
||||||
self.add_reaction_check.setCurrentIndex(settings['add_reaction'])
|
self.add_reaction_check.setCurrentIndex(settings['add_reaction'])
|
||||||
|
|
||||||
def toggle_reaction_spam(self):
|
def toggle_reaction_spam(self):
|
||||||
"""Start or stop reaction spam"""
|
"""Start or stop reaction spam"""
|
||||||
if self.is_spamming:
|
if self.is_spamming:
|
||||||
# Stop spam
|
|
||||||
self.stop_reaction_spam()
|
self.stop_reaction_spam()
|
||||||
else:
|
else:
|
||||||
# Start spam
|
|
||||||
self.start_reaction_spam()
|
self.start_reaction_spam()
|
||||||
|
|
||||||
def start_reaction_spam(self):
|
def start_reaction_spam(self):
|
||||||
"""Start indefinite reaction spam on last message"""
|
"""Start indefinite reaction spam on last message"""
|
||||||
phone_number = self.phone_input.text().strip()
|
phone_number = self.phone_input.text().strip()
|
||||||
delay = self.spam_delay_input.value()
|
delay = self.spam_delay_input.value()
|
||||||
|
|
||||||
# Validate inputs
|
|
||||||
if not phone_number:
|
if not phone_number:
|
||||||
self.log_message("[-] Phone number is required!", "error")
|
self.log_message("[-] Phone number is required!", "error")
|
||||||
return
|
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():
|
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("[-] WhatsApp Web client is not running!", "error")
|
||||||
self.log_message("[*] Click 'Start Client' and scan QR code first", "system")
|
self.log_message("[*] Click 'Start Client' and scan QR code first", "system")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.is_spamming = True
|
self.is_spamming = True
|
||||||
self.spam_btn.setText("SPAMMING...")
|
self.spam_btn.setText("SPAMMING...")
|
||||||
self.spam_btn.setEnabled(False)
|
self.spam_btn.setEnabled(False)
|
||||||
self.stop_spam_btn.setEnabled(True)
|
self.stop_spam_btn.setEnabled(True)
|
||||||
|
|
||||||
# Clear previous logs and graph data
|
|
||||||
self.log_output.clear()
|
self.log_output.clear()
|
||||||
self.clear_graph()
|
self.clear_graph()
|
||||||
|
|
||||||
# Format phone number for WhatsApp Web.js
|
|
||||||
formatted_number = phone_number
|
formatted_number = phone_number
|
||||||
if not formatted_number.endswith("@c.us") and not formatted_number.endswith("@g.us"):
|
if not formatted_number.endswith("@c.us") and not formatted_number.endswith("@g.us"):
|
||||||
formatted_number = f"{formatted_number}@c.us"
|
formatted_number = f"{formatted_number}@c.us"
|
||||||
|
|
||||||
self.log_message(f"[*] Starting reaction spam on {formatted_number}", "system")
|
self.log_message(f"[*] Starting reaction spam on {formatted_number}", "system")
|
||||||
self.log_message(f"[*] Delay: {delay}ms", "system")
|
self.log_message(f"[*] Delay: {delay}ms", "system")
|
||||||
|
|
||||||
# Send spam command to Node.js bridge
|
|
||||||
self.whatsapp_thread.send_command({
|
self.whatsapp_thread.send_command({
|
||||||
"action": "startReactionSpam",
|
"action": "startReactionSpam",
|
||||||
"chatId": formatted_number,
|
"chatId": formatted_number,
|
||||||
"delayMs": delay,
|
"delayMs": delay,
|
||||||
"emoji": "thumbsup"
|
"emoji": "thumbsup"
|
||||||
})
|
})
|
||||||
|
|
||||||
def stop_reaction_spam(self):
|
def stop_reaction_spam(self):
|
||||||
"""Stop the reaction spam"""
|
"""Stop the reaction spam"""
|
||||||
if self.whatsapp_thread and isinstance(self.whatsapp_thread, WhatsAppWebThread) and self.whatsapp_thread.isRunning():
|
if self.whatsapp_thread and isinstance(self.whatsapp_thread, WhatsAppWebThread) and self.whatsapp_thread.isRunning():
|
||||||
self.whatsapp_thread.send_command({
|
self.whatsapp_thread.send_command({
|
||||||
"action": "stopReactionSpam"
|
"action": "stopReactionSpam"
|
||||||
})
|
})
|
||||||
|
|
||||||
self.is_spamming = False
|
self.is_spamming = False
|
||||||
self.spam_btn.setText("START SPAM")
|
self.spam_btn.setText("START SPAM")
|
||||||
self.spam_btn.setEnabled(True)
|
self.spam_btn.setEnabled(True)
|
||||||
self.spam_btn.setStyleSheet("background-color: #34C759; color: white;")
|
self.spam_btn.setStyleSheet("background-color: #34C759; color: white;")
|
||||||
self.stop_spam_btn.setEnabled(False)
|
self.stop_spam_btn.setEnabled(False)
|
||||||
self.send_btn.setEnabled(True)
|
self.send_btn.setEnabled(True)
|
||||||
|
|
||||||
def handle_spam_data(self, data):
|
def handle_spam_data(self, data):
|
||||||
"""Handle spam data for graphing"""
|
"""Handle spam data for graphing"""
|
||||||
data_type = data.get('type')
|
data_type = data.get('type')
|
||||||
|
|
||||||
if data_type == 'spam_start':
|
if data_type == 'spam_start':
|
||||||
# Reset data for new spam session
|
|
||||||
self.spam_data = {
|
self.spam_data = {
|
||||||
'indices': [],
|
'indices': [],
|
||||||
'iteration_times': []
|
'iteration_times': []
|
||||||
}
|
}
|
||||||
|
|
||||||
elif data_type == 'spam_iteration':
|
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['indices'].append(data.get('index', 0))
|
||||||
self.spam_data['iteration_times'].append(data.get('iterationTimeMs', 0))
|
self.spam_data['iteration_times'].append(data.get('iterationTimeMs', 0))
|
||||||
self.update_graph()
|
self.update_graph()
|
||||||
|
|
||||||
elif data_type == 'spam_stopped' or data_type == 'spam_stopping':
|
elif data_type == 'spam_stopped' or data_type == 'spam_stopping':
|
||||||
# Spam was stopped
|
|
||||||
self.is_spamming = False
|
self.is_spamming = False
|
||||||
self.spam_btn.setText("START SPAM")
|
self.spam_btn.setText("START SPAM")
|
||||||
self.spam_btn.setEnabled(True)
|
self.spam_btn.setEnabled(True)
|
||||||
self.stop_spam_btn.setEnabled(False)
|
self.stop_spam_btn.setEnabled(False)
|
||||||
|
|
||||||
def update_graph(self):
|
def update_graph(self):
|
||||||
"""Update the matplotlib graph with current data"""
|
"""Update the matplotlib graph with current data"""
|
||||||
if not MATPLOTLIB_AVAILABLE:
|
if not MATPLOTLIB_AVAILABLE:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.ax.clear()
|
self.ax.clear()
|
||||||
self._style_graph()
|
self._style_graph()
|
||||||
|
|
||||||
indices = self.spam_data['indices']
|
indices = self.spam_data['indices']
|
||||||
|
|
||||||
if indices and self.spam_data['iteration_times']:
|
if indices and self.spam_data['iteration_times']:
|
||||||
# Plot small dots only
|
|
||||||
self.ax.scatter(indices, self.spam_data['iteration_times'],
|
self.ax.scatter(indices, self.spam_data['iteration_times'],
|
||||||
c='#FF6B9D', s=6, alpha=0.9, edgecolors='none', linewidths=0)
|
c='#FF6B9D', s=6, alpha=0.9, edgecolors='none', linewidths=0)
|
||||||
|
|
||||||
self.canvas.draw()
|
self.canvas.draw()
|
||||||
|
|
||||||
def clear_graph(self):
|
def clear_graph(self):
|
||||||
"""Clear the graph and reset data"""
|
"""Clear the graph and reset data"""
|
||||||
self.spam_data = {
|
self.spam_data = {
|
||||||
'indices': [],
|
'indices': [],
|
||||||
'iteration_times': []
|
'iteration_times': []
|
||||||
}
|
}
|
||||||
|
|
||||||
if MATPLOTLIB_AVAILABLE:
|
if MATPLOTLIB_AVAILABLE:
|
||||||
self.ax.clear()
|
self.ax.clear()
|
||||||
self._style_graph()
|
self._style_graph()
|
||||||
self.canvas.draw()
|
self.canvas.draw()
|
||||||
@ -1,6 +1,5 @@
|
|||||||
const { Client, LocalAuth, MessageAck } = require('whatsapp-web.js');
|
const { Client, LocalAuth, MessageAck } = require('whatsapp-web.js');
|
||||||
const qrcode = require('qrcode-terminal');
|
const qrcode = require('qrcode-terminal');
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
authStrategy: new LocalAuth({
|
authStrategy: new LocalAuth({
|
||||||
dataPath: './.wwebjs_auth'
|
dataPath: './.wwebjs_auth'
|
||||||
@ -10,14 +9,9 @@ const client = new Client({
|
|||||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let isReady = false;
|
let isReady = false;
|
||||||
let isSpamming = false;
|
let isSpamming = false;
|
||||||
|
|
||||||
// Track messages for ACK timing
|
|
||||||
const messageTracking = new Map();
|
const messageTracking = new Map();
|
||||||
|
|
||||||
// ACK status names for logging
|
|
||||||
const ackStatusNames = {
|
const ackStatusNames = {
|
||||||
[-1]: 'ERROR',
|
[-1]: 'ERROR',
|
||||||
[0]: 'PENDING (clock)',
|
[0]: 'PENDING (clock)',
|
||||||
@ -26,21 +20,15 @@ const ackStatusNames = {
|
|||||||
[3]: 'READ (blue tick)',
|
[3]: 'READ (blue tick)',
|
||||||
[4]: 'PLAYED'
|
[4]: 'PLAYED'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen for message ACK updates
|
|
||||||
client.on('message_ack', (message, ack) => {
|
client.on('message_ack', (message, ack) => {
|
||||||
const messageId = message.id._serialized;
|
const messageId = message.id._serialized;
|
||||||
const tracking = messageTracking.get(messageId);
|
const tracking = messageTracking.get(messageId);
|
||||||
|
|
||||||
if (tracking) {
|
if (tracking) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const timeSinceSent = now - tracking.sentAt;
|
const timeSinceSent = now - tracking.sentAt;
|
||||||
const ackName = ackStatusNames[ack] || `UNKNOWN(${ack})`;
|
const ackName = ackStatusNames[ack] || `UNKNOWN(${ack})`;
|
||||||
|
|
||||||
// Store timing for each ACK level
|
|
||||||
if (!tracking.ackTimes) tracking.ackTimes = {};
|
if (!tracking.ackTimes) tracking.ackTimes = {};
|
||||||
tracking.ackTimes[ack] = timeSinceSent;
|
tracking.ackTimes[ack] = timeSinceSent;
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'ack',
|
type: 'ack',
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
@ -50,8 +38,6 @@ client.on('message_ack', (message, ack) => {
|
|||||||
timeSinceSentFormatted: formatTime(timeSinceSent),
|
timeSinceSentFormatted: formatTime(timeSinceSent),
|
||||||
message: `Message ${ackName} after ${formatTime(timeSinceSent)}`
|
message: `Message ${ackName} after ${formatTime(timeSinceSent)}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Calculate time between single and double tick
|
|
||||||
if (ack === 2 && tracking.ackTimes[1]) {
|
if (ack === 2 && tracking.ackTimes[1]) {
|
||||||
const singleToDouble = tracking.ackTimes[2] - tracking.ackTimes[1];
|
const singleToDouble = tracking.ackTimes[2] - tracking.ackTimes[1];
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
@ -63,22 +49,17 @@ client.on('message_ack', (message, ack) => {
|
|||||||
message: `Single→Double tick: ${formatTime(singleToDouble)}`
|
message: `Single→Double tick: ${formatTime(singleToDouble)}`
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up tracking after read/played (or after 5 minutes)
|
|
||||||
if (ack >= 3) {
|
if (ack >= 3) {
|
||||||
messageTracking.delete(messageId);
|
messageTracking.delete(messageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatTime(ms) {
|
function formatTime(ms) {
|
||||||
if (ms < 1000) return `${ms}ms`;
|
if (ms < 1000) return `${ms}ms`;
|
||||||
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
|
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
|
||||||
return `${(ms / 60000).toFixed(2)}min`;
|
return `${(ms / 60000).toFixed(2)}min`;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.on('qr', (qr) => {
|
client.on('qr', (qr) => {
|
||||||
// Send QR code to Python via stdout
|
|
||||||
qrcode.generate(qr, { small: true });
|
qrcode.generate(qr, { small: true });
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'qr',
|
type: 'qr',
|
||||||
@ -86,7 +67,6 @@ client.on('qr', (qr) => {
|
|||||||
message: 'Scan the QR code above with WhatsApp on your phone'
|
message: 'Scan the QR code above with WhatsApp on your phone'
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('ready', () => {
|
client.on('ready', () => {
|
||||||
isReady = true;
|
isReady = true;
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
@ -94,21 +74,18 @@ client.on('ready', () => {
|
|||||||
message: 'WhatsApp Web client is ready!'
|
message: 'WhatsApp Web client is ready!'
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('authenticated', () => {
|
client.on('authenticated', () => {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'authenticated',
|
type: 'authenticated',
|
||||||
message: 'Authentication successful'
|
message: 'Authentication successful'
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('auth_failure', (msg) => {
|
client.on('auth_failure', (msg) => {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: `Authentication failed: ${msg}`
|
message: `Authentication failed: ${msg}`
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('disconnected', (reason) => {
|
client.on('disconnected', (reason) => {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'disconnected',
|
type: 'disconnected',
|
||||||
@ -116,12 +93,9 @@ client.on('disconnected', (reason) => {
|
|||||||
}));
|
}));
|
||||||
isReady = false;
|
isReady = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for commands from Python via stdin
|
|
||||||
process.stdin.on('data', async (data) => {
|
process.stdin.on('data', async (data) => {
|
||||||
try {
|
try {
|
||||||
const cmd = JSON.parse(data.toString().trim());
|
const cmd = JSON.parse(data.toString().trim());
|
||||||
|
|
||||||
if (!isReady && cmd.action !== 'status') {
|
if (!isReady && cmd.action !== 'status') {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@ -130,30 +104,24 @@ process.stdin.on('data', async (data) => {
|
|||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd.action === 'status') {
|
if (cmd.action === 'status') {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'status',
|
type: 'status',
|
||||||
ready: isReady
|
ready: isReady
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (cmd.action === 'sendMessage') {
|
else if (cmd.action === 'sendMessage') {
|
||||||
const chatId = cmd.chatId;
|
const chatId = cmd.chatId;
|
||||||
const message = cmd.message;
|
const message = cmd.message;
|
||||||
|
|
||||||
const chat = await client.getChatById(chatId);
|
const chat = await client.getChatById(chatId);
|
||||||
const sentAt = Date.now();
|
const sentAt = Date.now();
|
||||||
const sentMsg = await chat.sendMessage(message);
|
const sentMsg = await chat.sendMessage(message);
|
||||||
const messageId = sentMsg.id._serialized;
|
const messageId = sentMsg.id._serialized;
|
||||||
|
|
||||||
// Track this message for ACK timing
|
|
||||||
messageTracking.set(messageId, {
|
messageTracking.set(messageId, {
|
||||||
sentAt: sentAt,
|
sentAt: sentAt,
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
type: 'message'
|
type: 'message'
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
action: 'sendMessage',
|
action: 'sendMessage',
|
||||||
@ -162,15 +130,12 @@ process.stdin.on('data', async (data) => {
|
|||||||
message: 'Message sent successfully - tracking ACK status...'
|
message: 'Message sent successfully - tracking ACK status...'
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (cmd.action === 'addReaction') {
|
else if (cmd.action === 'addReaction') {
|
||||||
const chatId = cmd.chatId;
|
const chatId = cmd.chatId;
|
||||||
const emoji = cmd.emoji || '👍';
|
const emoji = cmd.emoji || '👍';
|
||||||
const messageIndex = cmd.messageIndex || 0; // 0 = last message
|
const messageIndex = cmd.messageIndex || 0;
|
||||||
|
|
||||||
const chat = await client.getChatById(chatId);
|
const chat = await client.getChatById(chatId);
|
||||||
const messages = await chat.fetchMessages({ limit: messageIndex + 1 });
|
const messages = await chat.fetchMessages({ limit: messageIndex + 1 });
|
||||||
|
|
||||||
if (messages.length > messageIndex) {
|
if (messages.length > messageIndex) {
|
||||||
await messages[messageIndex].react(emoji);
|
await messages[messageIndex].react(emoji);
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
@ -186,37 +151,24 @@ process.stdin.on('data', async (data) => {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (cmd.action === 'sendMessageAndReact') {
|
else if (cmd.action === 'sendMessageAndReact') {
|
||||||
const chatId = cmd.chatId;
|
const chatId = cmd.chatId;
|
||||||
const message = cmd.message;
|
const message = cmd.message;
|
||||||
const emoji = cmd.emoji || '👍';
|
const emoji = cmd.emoji || '👍';
|
||||||
|
|
||||||
// Send the message first
|
|
||||||
const chat = await client.getChatById(chatId);
|
const chat = await client.getChatById(chatId);
|
||||||
const sentAt = Date.now();
|
const sentAt = Date.now();
|
||||||
const sentMsg = await chat.sendMessage(message);
|
const sentMsg = await chat.sendMessage(message);
|
||||||
const messageId = sentMsg.id._serialized;
|
const messageId = sentMsg.id._serialized;
|
||||||
|
|
||||||
// Track this message for ACK timing
|
|
||||||
messageTracking.set(messageId, {
|
messageTracking.set(messageId, {
|
||||||
sentAt: sentAt,
|
sentAt: sentAt,
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
type: 'message'
|
type: 'message'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait a moment for the message to be registered
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
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 });
|
const messages = await chat.fetchMessages({ limit: 2 });
|
||||||
|
|
||||||
// React to the second-to-last message (skip the one we just sent)
|
|
||||||
if (messages.length >= 2) {
|
if (messages.length >= 2) {
|
||||||
const reactionSentAt = Date.now();
|
const reactionSentAt = Date.now();
|
||||||
await messages[1].react(emoji);
|
await messages[1].react(emoji);
|
||||||
|
|
||||||
// Track reaction timing (reactions don't have ACK but we track when sent)
|
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
action: 'sendMessageAndReact',
|
action: 'sendMessageAndReact',
|
||||||
@ -233,19 +185,13 @@ process.stdin.on('data', async (data) => {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (cmd.action === 'startReactionSpam') {
|
else if (cmd.action === 'startReactionSpam') {
|
||||||
const chatId = cmd.chatId;
|
const chatId = cmd.chatId;
|
||||||
const delayMs = cmd.delayMs || 100;
|
const delayMs = cmd.delayMs || 100;
|
||||||
const emoji = cmd.emoji || '👍';
|
const emoji = cmd.emoji || '👍';
|
||||||
|
|
||||||
// Stop any existing spam
|
|
||||||
isSpamming = false;
|
isSpamming = false;
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
const chat = await client.getChatById(chatId);
|
const chat = await client.getChatById(chatId);
|
||||||
|
|
||||||
// Get the last message to react to
|
|
||||||
const messages = await chat.fetchMessages({ limit: 1 });
|
const messages = await chat.fetchMessages({ limit: 1 });
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
@ -255,10 +201,8 @@ process.stdin.on('data', async (data) => {
|
|||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetMessage = messages[0];
|
const targetMessage = messages[0];
|
||||||
const targetMessageId = targetMessage.id._serialized;
|
const targetMessageId = targetMessage.id._serialized;
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'spam_start',
|
type: 'spam_start',
|
||||||
action: 'startReactionSpam',
|
action: 'startReactionSpam',
|
||||||
@ -266,32 +210,21 @@ process.stdin.on('data', async (data) => {
|
|||||||
delayMs: delayMs,
|
delayMs: delayMs,
|
||||||
message: `Starting reaction spam on message, ${delayMs}ms delay...`
|
message: `Starting reaction spam on message, ${delayMs}ms delay...`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
isSpamming = true;
|
isSpamming = true;
|
||||||
let iteration = 0;
|
let iteration = 0;
|
||||||
|
|
||||||
while (isSpamming) {
|
while (isSpamming) {
|
||||||
try {
|
try {
|
||||||
iteration++;
|
iteration++;
|
||||||
const iterationStart = Date.now();
|
const iterationStart = Date.now();
|
||||||
|
|
||||||
// Add reaction
|
|
||||||
const reactionAddStart = Date.now();
|
const reactionAddStart = Date.now();
|
||||||
await targetMessage.react(emoji);
|
await targetMessage.react(emoji);
|
||||||
const reactionAddTime = Date.now() - reactionAddStart;
|
const reactionAddTime = Date.now() - reactionAddStart;
|
||||||
|
|
||||||
// Wait before removing
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||||
|
|
||||||
if (!isSpamming) break;
|
if (!isSpamming) break;
|
||||||
|
|
||||||
// Remove reaction
|
|
||||||
const reactionRemoveStart = Date.now();
|
const reactionRemoveStart = Date.now();
|
||||||
await targetMessage.react('');
|
await targetMessage.react('');
|
||||||
const reactionRemoveTime = Date.now() - reactionRemoveStart;
|
const reactionRemoveTime = Date.now() - reactionRemoveStart;
|
||||||
|
|
||||||
const iterationTime = Date.now() - iterationStart;
|
const iterationTime = Date.now() - iterationStart;
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'spam_iteration',
|
type: 'spam_iteration',
|
||||||
index: iteration,
|
index: iteration,
|
||||||
@ -300,10 +233,7 @@ process.stdin.on('data', async (data) => {
|
|||||||
iterationTimeMs: iterationTime,
|
iterationTimeMs: iterationTime,
|
||||||
message: `[${iteration}] React+: ${reactionAddTime}ms, React-: ${reactionRemoveTime}ms, Total: ${iterationTime}ms`
|
message: `[${iteration}] React+: ${reactionAddTime}ms, React-: ${reactionRemoveTime}ms, Total: ${iterationTime}ms`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Delay before next iteration
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||||
|
|
||||||
} catch (iterError) {
|
} catch (iterError) {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'spam_error',
|
type: 'spam_error',
|
||||||
@ -311,11 +241,9 @@ process.stdin.on('data', async (data) => {
|
|||||||
error: iterError.message,
|
error: iterError.message,
|
||||||
message: `Error on iteration ${iteration}: ${iterError.message}`
|
message: `Error on iteration ${iteration}: ${iterError.message}`
|
||||||
}));
|
}));
|
||||||
// Small delay before retry
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'spam_stopped',
|
type: 'spam_stopped',
|
||||||
action: 'startReactionSpam',
|
action: 'startReactionSpam',
|
||||||
@ -323,7 +251,6 @@ process.stdin.on('data', async (data) => {
|
|||||||
message: `Reaction spam stopped after ${iteration} iterations.`
|
message: `Reaction spam stopped after ${iteration} iterations.`
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (cmd.action === 'stopReactionSpam') {
|
else if (cmd.action === 'stopReactionSpam') {
|
||||||
isSpamming = false;
|
isSpamming = false;
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
@ -332,14 +259,12 @@ process.stdin.on('data', async (data) => {
|
|||||||
message: 'Stopping reaction spam...'
|
message: 'Stopping reaction spam...'
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: `Unknown action: ${cmd.action}`
|
message: `Unknown action: ${cmd.action}`
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@ -348,8 +273,6 @@ process.stdin.on('data', async (data) => {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle process termination
|
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@ -358,10 +281,8 @@ process.on('SIGINT', async () => {
|
|||||||
await client.destroy();
|
await client.destroy();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize the client
|
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: 'Initializing WhatsApp Web client...'
|
message: 'Initializing WhatsApp Web client...'
|
||||||
}));
|
}));
|
||||||
client.initialize();
|
client.initialize();
|
||||||
Reference in New Issue
Block a user