/ crow.py
crow.py
   1  import breach_vip_username 
   2  import breach_vip
   3  import sys
   4  import subprocess
   5  import os
   6  import json
   7  import requests
   8  import re
   9  import time  # Add this import
  10  from datetime import datetime
  11  from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
  12                               QLabel, QLineEdit, QPushButton, QTextEdit, QFileDialog, 
  13                               QCheckBox, QGroupBox, QFormLayout, QSpinBox, QMessageBox, QInputDialog)
  14  from PyQt6.QtCore import Qt, QThread, pyqtSignal
  15  # Import the separate save and load functions
  16  from pathlib import Path
  17  from save_settings import save_settings
  18  from load_settings import load_settings
  19  from build_blackbird_command import build_blackbird_command
  20  from tor_spoofing import TORSpoofer
  21  from tor_api_setup import TORAPISetup
  22  
  23  # Worker class that handles executing the Blackbird command in a separate thread
  24  class BlackbirdWorker(QThread):
  25      output_signal = pyqtSignal(str)
  26      
  27      def __init__(self, command, needs_ai_confirmation=False, is_setup_ai=False, tor_spoofer=None):
  28          super().__init__()
  29          self.command = command
  30          self.process = None
  31          self.needs_ai_confirmation = needs_ai_confirmation
  32          self.is_setup_ai = is_setup_ai
  33          self.tor_spoofer = tor_spoofer
  34      
  35      def run(self):
  36          # If TOR is enabled for AI, set environment variable to signal the blackbird.py
  37          # to use TOR for AI requests
  38          if self.tor_spoofer and self.tor_spoofer.tor_enabled:
  39              # Set environment variable that blackbird.py can check
  40              import os
  41              os.environ["BLACKBIRD_USE_TOR"] = "1"
  42              os.environ["TOR_PORT"] = str(self.tor_spoofer.tor_port)
  43          self.process = subprocess.Popen(
  44              self.command, 
  45              stdout=subprocess.PIPE, 
  46              stderr=subprocess.STDOUT, 
  47              stdin=subprocess.PIPE,
  48              text=True, 
  49              shell=True,
  50              bufsize=1
  51          )
  52          
  53          # For setup-ai, send confirmation immediately
  54          if self.is_setup_ai:
  55              try:
  56                  # Wait a bit for the prompt to appear
  57                  import time
  58                  time.sleep(2)
  59                  # Send 'Y' and newline
  60                  self.process.stdin.write('Y\n')
  61                  self.process.stdin.flush()
  62                  self.output_signal.emit("✓ Sent confirmation for API key setup")
  63              except Exception as e:
  64                  self.output_signal.emit(f"Setup confirmation error: {e}")
  65          
  66          # For regular AI analysis, wait for the specific prompt
  67          elif self.needs_ai_confirmation:
  68              confirmation_sent = False
  69              for line in self.process.stdout:
  70                  text = line.strip()
  71                  self.output_signal.emit(text)
  72                  
  73                  # Check for AI analysis prompt
  74                  if not confirmation_sent and ('analyzing with ai' in text.lower() or 'consent' in text.lower()):
  75                      try:
  76                          self.process.stdin.write('Y\n')
  77                          self.process.stdin.flush()
  78                          confirmation_sent = True
  79                          self.output_signal.emit("✓ Automatically confirmed AI analysis")
  80                      except Exception as e:
  81                          self.output_signal.emit(f"AI confirmation error: {e}")
  82              return
  83          
  84          # Read output line by line for non-AI or after confirmation
  85          for line in self.process.stdout:
  86              self.output_signal.emit(line.strip())
  87          
  88          self.process.stdout.close()
  89          self.process.wait()
  90  
  91      def terminate(self):
  92          # Terminate the process if it's running
  93          if self.process:
  94              self.process.terminate()
  95              self.process.wait()
  96  
  97  # Main GUI class for the Blackbird OSINT tool
  98  class BlackbirdGUI(QMainWindow):
  99      def __init__(self):
 100          super().__init__()
 101          # Set window properties
 102          self.setWindowTitle("Crow")
 103          self.setGeometry(100, 100, 1000, 800)
 104          self.worker = None
 105  
 106          # Create the central widget and layout for the main window
 107          central_widget = QWidget()
 108          self.setCentralWidget(central_widget)
 109  
 110          layout = QVBoxLayout()
 111          central_widget.setLayout(layout)
 112  
 113          input_group = QGroupBox("Blackbird Search")
 114          input_layout = QFormLayout()
 115  
 116          # Username row with input field and file button
 117          username_row = QHBoxLayout()
 118          self.username_input = QLineEdit()
 119          self.username_input.setPlaceholderText("Enter username(s) or select file")
 120          username_row.addWidget(self.username_input, 4)  # 80% width
 121  
 122          username_file_button = QPushButton("📁 File")
 123          username_file_button.setToolTip("Select username file")
 124          username_file_button.clicked.connect(self.select_username_file)
 125          username_file_button.setFixedWidth(80)
 126          username_row.addWidget(username_file_button, 1)  # 20% width
 127          input_layout.addRow("Username(s):", username_row)
 128  
 129          # Email row with input field and file button
 130          email_row = QHBoxLayout()
 131          self.email_input = QLineEdit()
 132          self.email_input.setPlaceholderText("Enter email(s) or select file")
 133          email_row.addWidget(self.email_input, 4)  # 80% width
 134  
 135          email_file_button = QPushButton("📁 File")
 136          email_file_button.setToolTip("Select email file")
 137          email_file_button.clicked.connect(self.select_email_file)
 138          email_file_button.setFixedWidth(80)
 139          email_row.addWidget(email_file_button, 1)  # 20% width
 140          input_layout.addRow("Email(s):", email_row)
 141  
 142          input_group.setLayout(input_layout)
 143          layout.addWidget(input_group)
 144  
 145          # Options Group: Various checkboxes for additional configuration
 146          options_group = QGroupBox("Options")
 147          options_layout = QVBoxLayout()
 148  
 149          AI_layout = QHBoxLayout()
 150          self.AI_checkbox = QCheckBox("Extract metadata AI")
 151          AI_layout.addWidget(self.AI_checkbox)
 152  
 153          # Add setup button for AI API key
 154          AI_setup_button = QPushButton("Setup API Key")
 155          AI_setup_button.clicked.connect(self.setup_ai_api_key)
 156          AI_layout.addWidget(AI_setup_button)
 157  
 158          AI_help_button = QPushButton("?")
 159          AI_help_button.setFixedSize(30, 30)
 160          AI_help_button.clicked.connect(self.show_AI_help)
 161          AI_layout.addWidget(AI_help_button)
 162          options_layout.addLayout(AI_layout)
 163          
 164          # TOR Spoofing setup - ADD THIS RIGHT AFTER AI LAYOUT
 165          self.tor_spoofer = TORSpoofer(self)
 166          self.setup_tor_ui(options_layout)  # Pass the options_layout to the method
 167          
 168          # Permute username, Permute all, and Exclude NSFW checkboxes in a horizontal row
 169          permute_row_layout = QHBoxLayout()
 170          
 171          # Permute username checkbox with help button
 172          permute_layout = QHBoxLayout()
 173          self.permute_checkbox = QCheckBox("Permute username")
 174          permute_layout.addWidget(self.permute_checkbox)
 175          permute_help_button = QPushButton("?")
 176          permute_help_button.setFixedSize(30, 30)
 177          permute_help_button.clicked.connect(self.show_permute_help)
 178          permute_layout.addWidget(permute_help_button)
 179          permute_row_layout.addLayout(permute_layout)
 180          
 181          # Permute all elements checkbox with help button
 182          permuteall_layout = QHBoxLayout()
 183          self.permuteall_checkbox = QCheckBox("Permute all")
 184          permuteall_layout.addWidget(self.permuteall_checkbox)
 185          permuteall_help_button = QPushButton("?")
 186          permuteall_help_button.setFixedSize(30, 30)
 187          permuteall_help_button.clicked.connect(self.show_permuteall_help)
 188          permuteall_layout.addWidget(permuteall_help_button)
 189          permute_row_layout.addLayout(permuteall_layout)
 190          
 191          # Exclude NSFW checkbox (no help button for this one)
 192          self.no_nsfw_checkbox = QCheckBox("Exclude NSFW sites")
 193          permute_row_layout.addWidget(self.no_nsfw_checkbox)
 194          
 195          # Add some spacing/stretch
 196          permute_row_layout.addStretch()
 197          
 198          # Add this horizontal row to the main options layout
 199          options_layout.addLayout(permute_row_layout)
 200                  
 201          # Proxy input
 202          proxy_layout = QHBoxLayout()
 203          proxy_layout.addWidget(QLabel("Proxy:"))
 204          self.proxy_input = QLineEdit()
 205          proxy_layout.addWidget(self.proxy_input)
 206          options_layout.addLayout(proxy_layout)
 207          
 208          # Timeout input with spinner for seconds
 209          timeout_layout = QHBoxLayout()
 210          timeout_layout.addWidget(QLabel("Timeout (seconds):"))
 211          self.timeout_spinbox = QSpinBox()
 212          self.timeout_spinbox.setRange(1, 300)  # Limit timeout to 1-300 seconds
 213          self.timeout_spinbox.setValue(30)  # Default timeout is 30 seconds
 214          timeout_layout.addWidget(self.timeout_spinbox)
 215          options_layout.addLayout(timeout_layout)
 216          
 217          # Checkbox to disable update checks
 218          self.no_update_checkbox = QCheckBox("Don't check for updates")
 219          options_layout.addWidget(self.no_update_checkbox)
 220          
 221          # Filter input field with a help button
 222          filter_layout = QHBoxLayout()
 223          filter_layout.addWidget(QLabel("Filter:"))
 224          self.filter_input = QLineEdit()
 225          filter_layout.addWidget(self.filter_input)
 226  
 227          # Create a help button and connect it to the help function
 228          filter_help_button = QPushButton("?")
 229          filter_help_button.clicked.connect(self.show_filter_help)  # Connect the button to the help function
 230          filter_layout.addWidget(filter_help_button)
 231  
 232          options_layout.addLayout(filter_layout)
 233  
 234          options_group.setLayout(options_layout)
 235          layout.addWidget(options_group)
 236  
 237  
 238          # Output Options Group: Allows user to specify output types
 239          output_group = QGroupBox("Output Options")
 240          output_layout = QHBoxLayout()
 241          self.csv_checkbox = QCheckBox("CSV (Results)")
 242          self.pdf_checkbox = QCheckBox("PDF (Results)")
 243          self.json_checkbox = QCheckBox("JSON (Results)")
 244          self.verbose_checkbox = QCheckBox("Verbose (LOGS)")
 245          self.dump_checkbox = QCheckBox("Dump HTML (Results)")
 246          self.enable_breach_username_checkbox = QCheckBox("Enable Breach.vip username search")
 247          self.enable_breach_username_checkbox.setChecked(False)
 248          self.enable_breach_email_checkbox = QCheckBox("Enable Breach.vip email search")
 249          self.enable_breach_email_checkbox.setChecked(False)
 250          output_layout.addWidget(self.csv_checkbox)
 251          output_layout.addWidget(self.pdf_checkbox)
 252          output_layout.addWidget(self.json_checkbox)        
 253          output_layout.addWidget(self.verbose_checkbox)
 254          output_layout.addWidget(self.dump_checkbox)
 255          output_layout.addWidget(self.enable_breach_username_checkbox)
 256          output_layout.addWidget(self.enable_breach_email_checkbox)
 257          output_group.setLayout(output_layout)
 258          layout.addWidget(output_group)
 259  
 260          # Instagram session ID input for enhanced metadata extraction
 261          instagram_group = QGroupBox("Instagram Enhanced Metadata")
 262          instagram_layout = QHBoxLayout()
 263          self.instagram_session_id = QLineEdit()
 264          instagram_layout.addWidget(QLabel("Instagram Session ID:"))
 265          instagram_layout.addWidget(self.instagram_session_id)
 266          instagram_help_button = QPushButton("?")
 267          instagram_help_button.clicked.connect(self.show_instagram_help)  # Show help on click
 268          instagram_layout.addWidget(instagram_help_button)
 269          instagram_group.setLayout(instagram_layout)
 270          layout.addWidget(instagram_group)
 271  
 272          button_layout = QHBoxLayout()
 273          # Save and Load buttons connected to the functions
 274          save_button = QPushButton("Save Settings")
 275          save_button.clicked.connect(self.save_settings)
 276          button_layout.addWidget(save_button)
 277  
 278          load_button = QPushButton("Load Settings")
 279          load_button.clicked.connect(self.load_settings)
 280          button_layout.addWidget(load_button)
 281  
 282          # Add the button layout to the main layout
 283          layout.addLayout(button_layout)
 284  
 285          # Run and Stop buttons
 286          button_layout = QHBoxLayout()
 287          self.run_button = QPushButton("Run Blackbird")
 288          self.run_button.clicked.connect(self.run_blackbird)  # Start Blackbird on click
 289          button_layout.addWidget(self.run_button)
 290          
 291          self.stop_button = QPushButton("Stop Blackbird")
 292          self.stop_button.clicked.connect(self.stop_blackbird)  # Stop Blackbird on click
 293          self.stop_button.setEnabled(False)  # Disable initially
 294          button_layout.addWidget(self.stop_button)
 295          
 296          layout.addLayout(button_layout)
 297  
 298          # Output area for displaying logs and results
 299          self.output_area = QTextEdit()
 300          self.output_area.setReadOnly(True)
 301          layout.addWidget(self.output_area)
 302  
 303          # Easter egg setup
 304          self.key_sequence = ""
 305          self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
 306  
 307      def setup_ai_api_key(self):
 308          """Configure AI API key through TOR for anonymous registration ONLY when TOR is enabled"""
 309          self.output_area.clear()
 310          self.output_area.append("🔧 Starting API Key setup...")
 311          
 312          # Check if TOR is explicitly enabled by the user
 313          tor_enabled = hasattr(self, 'tor_checkbox') and self.tor_checkbox.isChecked()
 314          
 315          if tor_enabled:
 316              self.output_area.append("🕶️  TOR enabled - setting up anonymous registration")
 317              self.output_area.append("This will register your TOR IP instead of your real IP")
 318              
 319              # Use existing TOR connection
 320              tor_port = self.tor_spoofer.tor_port
 321              control_port = self.tor_spoofer.control_port
 322              tor_password = self.tor_spoofer.tor_password
 323              
 324              # ONLY delete existing API key when TOR is explicitly enabled
 325              self.delete_existing_api_key()
 326              
 327              # Start TOR setup
 328              self.tor_setup_worker = TORAPISetup(tor_port, control_port, tor_password)
 329              self.tor_setup_worker.output_signal.connect(self.update_output)
 330              self.tor_setup_worker.finished_signal.connect(self.on_tor_setup_finished)
 331              self.tor_setup_worker.start()
 332              
 333          else:
 334              # TOR NOT enabled - use direct connection and PRESERVE existing API key
 335              self.output_area.append("🔗 Setting up direct connection (TOR not enabled)")
 336              self.output_area.append("Your real IP will be registered with Blackbird AI")
 337              self.setup_ai_api_key_direct()
 338  
 339              return  # Return early since direct setup handles its own flow
 340          
 341          self.run_button.setEnabled(False)
 342          self.stop_button.setEnabled(True)
 343  
 344      def delete_existing_api_key(self):
 345          """Delete existing API key file ONLY when using TOR for fresh registration"""
 346          import os
 347          import json
 348          
 349          # Only proceed if TOR is explicitly enabled
 350          if not (hasattr(self, 'tor_checkbox') and self.tor_checkbox.isChecked()):
 351              return  # Don't delete anything if TOR is not enabled
 352          
 353          config_paths = [
 354              os.path.expanduser("~/.ai_key.json"),
 355              ".ai_key.json"
 356          ]
 357          
 358          for config_path in config_paths:
 359              if os.path.exists(config_path):
 360                  try:
 361                      os.remove(config_path)
 362                      self.output_area.append(f"🗑️  Deleted existing API key for TOR registration: {config_path}")
 363                      break
 364                  except Exception as e:
 365                      self.output_area.append(f"⚠️  Could not delete {config_path}: {e}")
 366  
 367      def setup_ai_api_key_direct(self):
 368          """Fallback direct setup without TOR"""
 369          self.output_area.append("🔄 Starting direct API setup (without TOR)...")
 370          
 371          # Build the setup command
 372          command = ["python", "blackbird.py", "--setup-ai"]
 373          
 374          # Create and start the worker for setup
 375          self.worker = BlackbirdWorker(" ".join(command), is_setup_ai=True)
 376          self.worker.output_signal.connect(self.update_output)
 377          self.worker.finished.connect(lambda: self.on_tor_setup_finished(True))
 378          self.worker.start()
 379          
 380          self.run_button.setEnabled(False)
 381          self.stop_button.setEnabled(True)
 382  
 383      def on_tor_setup_finished(self, success):
 384          """Handle completion of TOR API setup"""
 385          self.run_button.setEnabled(True)
 386          self.stop_button.setEnabled(False)
 387          
 388          if success:
 389              self.output_area.append("✅ API setup completed successfully!")
 390              self.check_api_key_config()
 391          else:
 392              self.output_area.append("❌ API setup failed")
 393              
 394              reply = QMessageBox.question(
 395                  self,
 396                  "Setup Failed",
 397                  "API setup failed.\n\n"
 398                  "Do you want to try again?",
 399                  QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
 400              )
 401              
 402              if reply == QMessageBox.StandardButton.Yes:
 403                  self.setup_ai_api_key()
 404  
 405      def check_api_key_config(self):
 406          """Check if API key was successfully configured"""
 407          import os
 408          import json
 409          
 410          api_key_found = False
 411          config_paths = [
 412              os.path.expanduser("~/.ai_key.json"),
 413              ".ai_key.json"
 414          ]
 415          
 416          for config_path in config_paths:
 417              if os.path.exists(config_path):
 418                  try:
 419                      with open(config_path, 'r') as f:
 420                          config = json.load(f)
 421                          if config.get("ai_api_key"):
 422                              api_key_found = True
 423                              self.ai_api_key = config["ai_api_key"]
 424                              os.environ["BLACKBIRD_AI_API_KEY"] = config["ai_api_key"]
 425                              self.output_area.append("✅ AI API Key configured and loaded!")
 426                              break
 427                          elif config.get("api_key"):
 428                              api_key_found = True
 429                              self.ai_api_key = config["api_key"]
 430                              os.environ["BLACKBIRD_AI_API_KEY"] = config["api_key"]
 431                              self.output_area.append("✅ AI API Key configured and loaded!")
 432                              break
 433                  except (json.JSONDecodeError, KeyError):
 434                      continue
 435          
 436          if not api_key_found:
 437              self.output_area.append("⚠️  API key setup completed, but couldn't automatically detect the key.")
 438              self.output_area.append("You may need to manually check the configuration.")
 439  
 440          def on_setup_finished(self):
 441              """Handle completion of AI setup"""
 442              self.run_button.setEnabled(True)
 443              self.stop_button.setEnabled(False)
 444              
 445              # Check if setup was successful by looking for the API key
 446              import os
 447              import json
 448              import os.path
 449              
 450              api_key_found = False
 451              config_paths = [
 452                  os.path.expanduser("~/.ai_key.json"),
 453                  ".ai_key.json"
 454              ]
 455              
 456              for config_path in config_paths:
 457                  if os.path.exists(config_path):
 458                      try:
 459                          with open(config_path, 'r') as f:
 460                              config = json.load(f)
 461                              if config.get("ai_api_key"):
 462                                  api_key_found = True
 463                                  self.ai_api_key = config["ai_api_key"]
 464                                  os.environ["BLACKBIRD_AI_API_KEY"] = config["ai_api_key"]
 465                                  self.output_area.append("✅ AI API Key setup completed and loaded!")
 466                                  break
 467                              elif config.get("api_key"):
 468                                  api_key_found = True
 469                                  self.ai_api_key = config["api_key"]
 470                                  os.environ["BLACKBIRD_AI_API_KEY"] = config["api_key"]
 471                                  self.output_area.append("✅ AI API Key setup completed and loaded!")
 472                                  break
 473                      except (json.JSONDecodeError, KeyError):
 474                          continue
 475              
 476              if not api_key_found:
 477                  self.output_area.append("⚠️  API key setup completed, but couldn't automatically detect the key.")
 478                  self.output_area.append("You may need to manually enter the API key using the 'Setup API Key' button.")
 479  
 480      def keyPressEvent(self, event):
 481          super().keyPressEvent(event)
 482          self.key_sequence += event.text()
 483          
 484          if 'iddqd' in self.key_sequence.lower():
 485              self.trigger_easter_egg()
 486              self.key_sequence = ''
 487          
 488          if len(self.key_sequence) > 10:
 489              self.key_sequence = self.key_sequence[-10:]
 490  
 491      def trigger_easter_egg(self):
 492          easter_egg = """
 493  Crows mimic, crows are intelligent!
 494          """
 495          self.output_area.append("Easter egg activated!")
 496          self.output_area.append(easter_egg)
 497  
 498      def select_username_file(self):
 499          # Open a file dialog to select a username file and set its path in the input field
 500          file_name, _ = QFileDialog.getOpenFileName(self, "Select Username File")
 501          if file_name:
 502              self.username_input.setText(f"file:{file_name}")  # Prefix to indicate file
 503              # Or if you want to keep both separate:
 504              # self.username_file_input.setText(file_name)
 505  
 506      def select_email_file(self):
 507          # Open a file dialog to select an email file and set its path in the input field
 508          file_name, _ = QFileDialog.getOpenFileName(self, "Select Email File")
 509          if file_name:
 510              self.email_input.setText(f"file:{file_name}")  # Prefix to indicate file
 511              # Or if you want to keep both separate:
 512              # self.email_file_input.setText(file_name)
 513  
 514      def save_settings(self):
 515          # Use the modular save_settings function
 516          save_settings(self)
 517  
 518      def load_settings(self):
 519          # Use the modular load_settings function
 520          load_settings(self)
 521  
 522      def show_instagram_help(self):
 523          QMessageBox.information(self, "Instagram Session ID Help",
 524                                  "To use enhanced Instagram metadata extraction:\n\n"
 525                                  "1. Log in to Instagram in your browser\n"
 526                                  "2. Open developer tools (F12)\n"
 527                                  "3. Go to Application > Cookies\n"
 528                                  "4. Find the 'sessionid' cookie\n"
 529                                  "5. Copy its value and paste it here")
 530  
 531      def show_filter_help(self):
 532          QMessageBox.information(self, "Filter Help",
 533                                  "Create custom search filters using specific properties and operators.\n\n"
 534                                  "Properties: name, cat, uri_check, e_code, e_string, m_string, m_code\n"
 535                                  "Operators: =, ~, >, <, >=, <=, !=\n\n"
 536                                  "Examples:\n"
 537                                  "1. name~Mastodon\n"
 538                                  "2. e_code>200\n"
 539                                  "3. cat=social and uri_check~101010\n"
 540                                  "4. e_string=@101010.pl or m_code<=404\n\n"
 541                                  "Concatenate commands with \"\" for multiple filters.\n\n"
 542                                  "Visit https://p1ngul1n0.gitbook.io/blackbird/advanced-usage")
 543  
 544      def show_permute_help(self):
 545          QMessageBox.information(self, "Permute Username Help",
 546                                  "The '--permute' option generates variations of a username.\n\n"
 547                                  "For 'balestek86', permutations include:\n"
 548                                  "balestek86, _balestek86, balestek86_, balestek_86, balestek-86, balestek.86\n"
 549                                  "86balestek, _86balestek, 86balestek_, 86_balestek, 86-balestek, 86.balestek")
 550  
 551      def show_permuteall_help(self):
 552          QMessageBox.information(self, "Permute All Elements Help",
 553                                  "The '--permuteall' option generates a broader set of permutations.\n\n"
 554                                  "For 'balestek86', permutations include:\n"
 555                                  "balestek, _balestek, balestek_, 86, _86, 86_,\n"
 556                                  "balestek86, _balestek86, balestek86_, balestek_86, balestek-86, balestek.86,\n"
 557                                  "86balestek, _86balestek, 86balestek_, 86_balestek, 86-balestek, 86.balestek")
 558  
 559      def show_AI_help(self):
 560          QMessageBox.information(self, "AI Metadata Help",
 561                                 "The '--ai' option performs AI analysis on found results.\n\n"
 562                                 "Features:\n"
 563                                 "• Generates comprehensive profile summaries\n"
 564                                 "• Identifies profile types and interests\n"
 565                                 "• Provides risk assessment flags\n"
 566                                 "• Adds relevant tags for categorization\n"
 567                                 "• Shows remaining daily AI query quota\n\n"
 568                                 "Results are marked with a robot emoji (🤖) and include:\n"
 569                                 "- Summary of online presence\n"
 570                                 "- Profile type classification\n" 
 571                                 "- Key insights and interests\n"
 572                                 "- Risk flags and warnings\n"
 573                                 "- Relevant tags\n\n"
 574                                 "Note: Uses Blackbird AI API with daily query limits.")
 575      
 576      def setup_tor_ui(self, options_layout):
 577          """Add TOR configuration to the options group"""
 578          # Add TOR section to options group
 579          tor_group = QGroupBox("TOR IP Spoofing (for AI)")
 580          tor_layout = QHBoxLayout()
 581          
 582          self.tor_checkbox = QCheckBox("Enable TOR for AI requests")
 583          self.tor_checkbox.stateChanged.connect(self.toggle_tor_spoofing)
 584          tor_layout.addWidget(self.tor_checkbox)
 585          
 586          tor_setup_button = QPushButton("TOR Settings")
 587          tor_setup_button.clicked.connect(self.configure_tor_settings)
 588          tor_layout.addWidget(tor_setup_button)
 589          
 590          tor_help_button = QPushButton("?")
 591          tor_help_button.setFixedSize(30, 30)
 592          tor_help_button.clicked.connect(self.show_tor_help)
 593          tor_layout.addWidget(tor_help_button)
 594          
 595          tor_group.setLayout(tor_layout)
 596          
 597          # Insert TOR group after AI options but before permute options
 598          # We need to find the correct position in the layout
 599          # Since we're calling this during setup, we can add it directly
 600          options_layout.addWidget(tor_group)
 601      
 602      def toggle_tor_spoofing(self, state):
 603          """Enable/disable TOR spoofing"""
 604          if state == Qt.CheckState.Checked.value:
 605              # Try to enable TOR with default settings
 606              if not self.tor_spoofer.enable_tor_for_ai():
 607                  QMessageBox.warning(self, "TOR Not Available", 
 608                                    "TOR connection failed. Please ensure TOR is running and configured.")
 609                  self.tor_checkbox.setChecked(False)
 610          else:
 611              self.tor_spoofer.disable_tor()
 612      
 613      def configure_tor_settings(self):
 614          """Open dialog to configure TOR settings"""
 615          from PyQt6.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QLineEdit, QDialogButtonBox, QLabel
 616          
 617          dialog = QDialog(self)
 618          dialog.setWindowTitle("TOR Configuration")
 619          layout = QVBoxLayout()
 620          
 621          form_layout = QFormLayout()
 622          
 623          tor_port_input = QLineEdit(str(self.tor_spoofer.tor_port))
 624          control_port_input = QLineEdit(str(self.tor_spoofer.control_port))
 625          
 626          # Show current password (masked) but don't allow changing in this version
 627          password_info = QLabel(f"Current password: {'*' * 20} (hardcoded for testing)")
 628          password_info.setWordWrap(True)
 629          
 630          form_layout.addRow("TOR Port:", tor_port_input)
 631          form_layout.addRow("Control Port:", control_port_input)
 632          form_layout.addRow("Password:", password_info)
 633          
 634          help_label = QLabel("Note: Using hardcoded password for testing. Port changes will be applied.")
 635          help_label.setWordWrap(True)
 636          form_layout.addRow("", help_label)
 637          
 638          layout.addLayout(form_layout)
 639          
 640          buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | 
 641                                   QDialogButtonBox.StandardButton.Cancel)
 642          buttons.accepted.connect(dialog.accept)
 643          buttons.rejected.connect(dialog.reject)
 644          layout.addWidget(buttons)
 645          
 646          dialog.setLayout(layout)
 647          
 648          if dialog.exec() == QDialog.DialogCode.Accepted:
 649              try:
 650                  self.tor_spoofer.tor_port = int(tor_port_input.text())
 651                  self.tor_spoofer.control_port = int(control_port_input.text())
 652                  
 653                  # Test new configuration
 654                  if self.tor_checkbox.isChecked():
 655                      if not self.tor_spoofer.enable_tor_for_ai():
 656                          QMessageBox.warning(self, "TOR Configuration Failed", 
 657                                            "New TOR settings are invalid.")
 658                          self.tor_checkbox.setChecked(False)
 659                      else:
 660                          QMessageBox.information(self, "Success", "TOR configuration updated successfully!")
 661                              
 662              except ValueError:
 663                  QMessageBox.warning(self, "Invalid Input", "Port numbers must be integers.")
 664      
 665      def show_tor_help(self):
 666          """Show TOR help information"""
 667          QMessageBox.information(self, "TOR IP Spoofing Help",
 668                                "TOR IP Spoofing for AI Requests:\n\n"
 669                                "• Routes AI API calls through TOR network\n"
 670                                "• Automatically renews IP for each request\n"
 671                                "• Enhances privacy and anonymity\n"
 672                                "• Bypasses IP-based rate limits\n\n"
 673                                "Requirements:\n"
 674                                "• TOR service must be running locally\n"
 675                                "• Default ports: 9050 (TOR), 9051 (control)\n"
 676                                "• TOR control protocol enabled\n\n"
 677                                "Note: This only affects AI metadata extraction requests.")
 678  
 679  
 680      def run_blackbird(self):
 681          # Check if AI is enabled but no API key is set
 682          if self.AI_checkbox.isChecked():
 683              # If TOR is also enabled, set it up
 684              if hasattr(self, 'tor_checkbox') and self.tor_checkbox.isChecked():
 685                  if not self.tor_spoofer.tor_enabled:
 686                      if not self.tor_spoofer.enable_tor_for_ai():
 687                          self.output_area.append("⚠️  TOR spoofing failed, continuing with direct connection")
 688              
 689              # Check multiple locations for API key
 690              api_key_found = False
 691              
 692              # 1. Check environment variable
 693              if os.environ.get("BLACKBIRD_AI_API_KEY"):
 694                  api_key_found = True
 695              
 696              # 2. Check Blackbird config file
 697              if not api_key_found:
 698                  config_paths = [
 699                      os.path.expanduser(".ai_key.json"),
 700                      ".ai_key.json",  # Current directory
 701                  ]
 702                  
 703                  for config_path in config_paths:
 704                      if os.path.exists(config_path):
 705                          try:
 706                              with open(config_path, 'r') as f:
 707                                  config = json.load(f)
 708                                  if config.get("ai_api_key") or config.get("api_key"):
 709                                      api_key_found = True
 710                                      break
 711                          except (json.JSONDecodeError, KeyError):
 712                              continue
 713              
 714              # 3. Check if we have a stored API key in the GUI instance
 715              if not api_key_found and hasattr(self, 'ai_api_key') and self.ai_api_key:
 716                  api_key_found = True
 717              
 718              if not api_key_found:
 719                  reply = QMessageBox.question(
 720                      self, 
 721                      "AI API Key Required",
 722                      "AI analysis requires an API key. Would you like to configure it now?",
 723                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
 724                  )
 725                  if reply == QMessageBox.StandardButton.Yes:
 726                      self.setup_ai_api_key()
 727                      return  # Don't proceed with regular search until setup is complete
 728                  else:
 729                      QMessageBox.warning(self, "Warning", "AI analysis disabled - no API key configured.")
 730                      self.AI_checkbox.setChecked(False)
 731  
 732          # ================================================================
 733          # HOOK: BREACH.VIP USERNAME SEARCH
 734          # ================================================================
 735          if breach_vip_username.is_enabled(self) and self.username_input.text().strip():
 736              
 737              self.output_area.append("\n" + "=" * 60)
 738              self.output_area.append("🔍 BREACH.VIP USERNAME SEARCH HOOK")
 739              self.output_area.append("=" * 60)
 740              
 741              text = self.username_input.text().strip()
 742              
 743              # Check if it's a file reference
 744              if text.startswith("file:"):
 745                  file_path = text[5:]  # Remove "file:" prefix
 746                  if os.path.exists(file_path):
 747                      self.output_area.append(f"Searching Breach.vip for usernames from file: {os.path.basename(file_path)}")
 748                      breach_vip_username.process_username_file(file_path, self.output_area)
 749                  else:
 750                      self.output_area.append(f"❌ File not found: {file_path}")
 751              else:
 752                  # Regular username input (could be single or multiple usernames)
 753                  usernames = [u.strip() for u in text.split(',') if u.strip()]
 754                  if len(usernames) == 1:
 755                      self.output_area.append(f"Searching Breach.vip for username: {usernames[0]}")
 756                      breach_vip_username.process_single_username(usernames[0], self.output_area)
 757                  else:
 758                      self.output_area.append(f"Searching Breach.vip for {len(usernames)} usernames")
 759                      for username in usernames:
 760                          self.output_area.append(f"  • Processing: {username}")
 761                          breach_vip_username.process_single_username(username, self.output_area)
 762              
 763              self.output_area.append("=" * 60 + "\n")
 764  
 765          # ================================================================
 766          # HOOK: BREACH.VIP EMAIL SEARCH
 767          # ================================================================
 768          if breach_vip.is_enabled(self) and self.email_input.text().strip():
 769              
 770              self.output_area.append("\n" + "=" * 60)
 771              self.output_area.append("📧 BREACH.VIP EMAIL SEARCH HOOK")
 772              self.output_area.append("=" * 60)
 773              
 774              text = self.email_input.text().strip()
 775              
 776              # Check if it's a file reference
 777              if text.startswith("file:"):
 778                  file_path = text[5:]  # Remove "file:" prefix
 779                  if os.path.exists(file_path):
 780                      self.output_area.append(f"Searching Breach.vip for emails from file: {os.path.basename(file_path)}")
 781                      breach_vip.process_email_file(file_path, self.output_area)
 782                  else:
 783                      self.output_area.append(f"❌ File not found: {file_path}")
 784              else:
 785                  # Regular email input (could be single or multiple emails)
 786                  emails = [e.strip() for e in text.split(',') if e.strip()]
 787                  if len(emails) == 1:
 788                      self.output_area.append(f"Searching Breach.vip for email: {emails[0]}")
 789                      breach_vip.process_single_email(emails[0], self.output_area)
 790                  else:
 791                      self.output_area.append(f"Searching Breach.vip for {len(emails)} emails")
 792                      for email in emails:
 793                          self.output_area.append(f"  • Processing: {email}")
 794                          breach_vip.process_single_email(email, self.output_area)
 795              
 796              self.output_area.append("=" * 60 + "\n")
 797          
 798          # Rest of the existing run_blackbird method...
 799          if self.worker and self.worker.isRunning():
 800              self.worker.terminate()
 801              self.worker.wait()
 802  
 803          # Get all input values
 804          username_input = self.username_input.text()
 805          email_input = self.email_input.text()
 806          # username_file_input = self.username_file_input.text()
 807          # email_file_input = self.email_file_input.text()
 808          permute_checkbox = self.permute_checkbox.isChecked()
 809          enable_breach_username_checkbox = self.enable_breach_username_checkbox.isChecked()
 810          enable_breach_email_checkbox = self.enable_breach_email_checkbox.isChecked()
 811          permuteall_checkbox = self.permuteall_checkbox.isChecked()
 812          tor_checkbox = self.tor_checkbox.isChecked()
 813          AI_checkbox = self.AI_checkbox.isChecked()
 814          no_nsfw_checkbox = self.no_nsfw_checkbox.isChecked()
 815          no_update_checkbox = self.no_update_checkbox.isChecked()
 816          csv_checkbox = self.csv_checkbox.isChecked()
 817          pdf_checkbox = self.pdf_checkbox.isChecked()
 818          json_checkbox = self.json_checkbox.isChecked()
 819          verbose_checkbox = self.verbose_checkbox.isChecked()
 820          dump_checkbox = self.dump_checkbox.isChecked()
 821          proxy_input = self.proxy_input.text()
 822          timeout_spinbox = self.timeout_spinbox.value()
 823          filter_input = self.filter_input.text()
 824          instagram_session_id = self.instagram_session_id.text()
 825  
 826          # Show AI info if enabled
 827          if AI_checkbox:
 828              self.output_area.append("🤖 AI Analysis Enabled")
 829              self.output_area.append("Note: This will analyze results using Blackbird AI")
 830              self.output_area.append("")
 831  
 832          command = build_blackbird_command(
 833              username_input,           # username(s) - could include "file:" prefix
 834              email_input,              # email(s) - could include "file:" prefix
 835              "",                       # username_file_input (empty string - we're using file: prefix)
 836              "",                       # email_file_input (empty string - we're using file: prefix)
 837              permute_checkbox,
 838              permuteall_checkbox,
 839              AI_checkbox,
 840              no_nsfw_checkbox,
 841              no_update_checkbox,
 842              csv_checkbox,
 843              pdf_checkbox,
 844              json_checkbox,
 845              verbose_checkbox,
 846              dump_checkbox,
 847              proxy_input,
 848              timeout_spinbox,
 849              filter_input,
 850              instagram_session_id
 851          )
 852  
 853          # self.output_area.clear()
 854          # Pass AI_checkbox to determine if we need to auto-confirm
 855          self.worker = BlackbirdWorker(" ".join(command), needs_ai_confirmation=AI_checkbox)
 856          self.worker.output_signal.connect(self.update_output)
 857          self.worker.finished.connect(self.on_worker_finished)
 858          self.worker.start()
 859          
 860          self.run_button.setEnabled(False)
 861          self.stop_button.setEnabled(True)
 862  
 863      def get_output_area(self):
 864          """Get the output area from parent"""
 865          return self.output_area if hasattr(self, 'output_area') else None
 866      
 867      def stop_blackbird(self):
 868          # Stop the Blackbird process if running
 869          if self.worker and self.worker.isRunning():
 870              self.worker.terminate()
 871              self.worker.wait()
 872          self.run_button.setEnabled(True)  # Re-enable Run button
 873          self.stop_button.setEnabled(False)  # Disable Stop button
 874          self.output_area.clear()
 875  
 876      def update_output(self, text):
 877          # Initialize AI results buffer if it doesn't exist
 878          if not hasattr(self, 'ai_results_buffer'):
 879              self.ai_results_buffer = []
 880              self.ai_results_started = False
 881          
 882          # Check if AI analysis is starting
 883          if 'analyzing with ai' in text.lower() or '✨ analyzing with ai' in text.lower():
 884              self.ai_results_started = True
 885              self.ai_results_buffer = [
 886                  "🤖 BLACKBIRD AI ANALYSIS REPORT",
 887                  "=" * 60,
 888                  f"Generated: {self.get_current_timestamp()}",
 889                  "=" * 60,
 890                  ""
 891              ]
 892              
 893              # Add search context
 894              username = self.username_input.text().strip()
 895              email = self.email_input.text().strip()
 896              if username:
 897                  self.ai_results_buffer.append(f"Target Username: {username}")
 898              if email:
 899                  self.ai_results_buffer.append(f"Target Email: {email}")
 900              if username or email:
 901                  self.ai_results_buffer.append("")
 902          
 903          # Buffer AI results
 904          if self.ai_results_started:
 905              # Clean and format the text for file output
 906              clean_text = text.replace('🤖', '').replace('📊', '').strip()
 907              self.ai_results_buffer.append(clean_text)
 908              
 909              # Check if AI analysis is complete
 910              if 'ai queries left' in text.lower():
 911                  self.ai_results_buffer.extend([
 912                      "",
 913                      "=" * 60,
 914                      f"Analysis complete - {self.get_current_timestamp()}",
 915                      "=" * 60
 916                  ])
 917                  self.auto_save_ai_results()  # Auto-save instead of dialog
 918                  self.ai_results_started = False
 919          
 920          # Format for GUI display
 921          formatted_text = self.format_ai_text_for_gui(text)
 922          
 923          # Update GUI
 924          self.append_to_output_area(formatted_text)
 925  
 926      def format_ai_text_for_gui(self, text):
 927          """Format AI text for GUI display with emojis"""
 928          if any(keyword in text.lower() for keyword in ['analyzing with ai', 'ai queries left', '✨']):
 929              return f"🤖 {text}"
 930          elif text.startswith('[Summary]'):
 931              return f"📋 {text}"
 932          elif text.startswith('[Profile Type]'):
 933              return f"🎯 {text}"
 934          elif text.startswith('[Insights]'):
 935              return f"💡 {text}"
 936          elif text.startswith('[Risk Flags]'):
 937              return f"⚠️  {text}"
 938          elif text.startswith('[Tags]'):
 939              return f"🏷️  {text}"
 940          else:
 941              return text
 942  
 943      def get_current_timestamp(self):
 944          """Get current timestamp for file naming and reports"""
 945          from datetime import datetime
 946          return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 947  
 948      def auto_save_ai_results(self):
 949          """Automatically save AI results to file"""
 950          if not self.ai_results_buffer:
 951              return
 952          
 953          try:
 954              # Generate filename with timestamp and target info
 955              from datetime import datetime
 956              timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
 957              
 958              username = self.username_input.text().strip()
 959              email = self.email_input.text().strip()
 960              
 961              # Create descriptive filename
 962              if username:
 963                  base_name = username
 964              elif email:
 965                  base_name = email.split('@')[0]
 966              else:
 967                  base_name = "analysis"
 968  
 969              # Clean filename
 970              safe_name = re.sub(r'[^\w\-_.]', '_', base_name)
 971              filename = f"blackbird_ai_{safe_name}_{timestamp}.txt"
 972  
 973              # Directory path where you want to save the file (e.g., "output_files")
 974              directory_path = "results"
 975  
 976              # Create directory if it doesn't exist
 977              if not os.path.exists(directory_path):
 978                  os.makedirs(directory_path)
 979  
 980              # Full file path with directory
 981              full_path = os.path.join(directory_path, filename)
 982  
 983              # Save to file
 984              with open(full_path, 'w', encoding='utf-8') as f:
 985                  f.write('\n'.join(self.ai_results_buffer))
 986              
 987              # Notify user
 988              self.append_to_output_area(f"💾 AI results auto-saved to: {filename}")
 989              
 990          except Exception as e:
 991              self.append_to_output_area(f"❌ Error auto-saving AI results: {e}")
 992  
 993      def append_to_output_area(self, text):
 994          """Helper method to append text to output area with auto-scroll"""
 995          scrollbar = self.output_area.verticalScrollBar()
 996          was_at_bottom = scrollbar.value() == scrollbar.maximum()
 997          
 998          self.output_area.append(text)
 999          
1000          if was_at_bottom:
1001              scrollbar.setValue(scrollbar.maximum())
1002  
1003      def on_worker_finished(self):
1004          # Re-enable the Run button and disable the Stop button when worker finishes
1005          self.run_button.setEnabled(True)
1006          self.stop_button.setEnabled(False)
1007  
1008  if __name__ == "__main__":
1009      # Create and run the application
1010      app = QApplication(sys.argv)
1011      window = BlackbirdGUI()
1012      window.show()
1013      sys.exit(app.exec())