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