├── requirements.txt ├── .github └── FUNDING.yml ├── LICENSE ├── README.md └── telegram-checker.py /requirements.txt: -------------------------------------------------------------------------------- 1 | telethon 2 | rich 3 | asyncio 4 | click 5 | pathlib 6 | python-dotenv -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: unnohwn 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 𝓾𝓷𝓷𝓸𝓱𝔀𝓷 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📱 Telegram Account Checker 2 | Enhanced version of bellingcat's Telegram Phone Checker! 3 | 4 | A Python script to check Telegram accounts using phone numbers or usernames, now with more detailed user 5 | information and enhanced status display. 6 | 7 | ## ✨ Features 8 | 9 | - 🔍 Check single or multiple phone numbers and usernames. 10 | - 📁 Import numbers and usernames from text files. 11 | - 📸 Auto-download all profile pictures for a user. 12 | - 💾 Save results as detailed JSON files. 13 | - 🔐 Secure credential storage (API ID, hash, and phone number in `config.pkl`). 14 | - 📊 Detailed user information, including: 15 | - Basic info: ID, username, first/last name, phone. 16 | - Account status: Premium, Verified, Bot, Fake. 17 | - Enhanced last seen status: Online, Offline (with exact timestamp), Recently, Last Week, Last Month, 18 | or Unavailable, nothing if privacy is restricted. 19 | - Profile details: Bio, common chats count. 20 | - Interaction status: Blocked by user. 21 | - 📝 Logging of operations to `telegram_checker.log`. 22 | - 🎨 Rich console output for better readability and interaction. 23 | - 💨 Option to clear saved credentials and session. 24 | 25 | ## 🚀 Installation 26 | 27 | 1. Clone the repository: 28 | ```bash 29 | git clone https://github.com/unnohwn/telegram-checker.git 30 | cd telegram-checker 31 | ``` 32 | 33 | 2. Install required packages: 34 | ```bash 35 | pip install -r requirements.txt 36 | ``` 37 | 38 | ## 📦 Requirements 39 | 40 | Contents of `requirements.txt`: 41 | ``` 42 | telethon 43 | rich 44 | click 45 | python-dotenv 46 | ``` 47 | 48 | Or install packages individually: 49 | ```bash 50 | pip install telethon rich click python-dotenv 51 | ``` 52 | 53 | ## ⚙️ Configuration 54 | 55 | First time running the script, you'll need: 56 | - Telegram API credentials (get from https://my.telegram.org/apps) 57 | - Your Telegram phone number including countrycode + 58 | - Verification code (sent to your Telegram) 59 | 60 | ## 💻 Usage 61 | 62 | Run the script: 63 | ```bash 64 | python telegram_checker.py 65 | ``` 66 | 67 | Choose from options: 68 | 1. Check phone numbers from input 69 | 2. Check phone numbers from file 70 | 3. Check usernames from input 71 | 4. Check usernames from file 72 | 5. Clear saved credentials 73 | 6. Exit 74 | 75 | ## 📂 Output 76 | 77 | Results are saved in: 78 | - `results/` - JSON files with detailed information 79 | - `profile_photos/` - Downloaded profile pictures 80 | 81 | ## 📊 Result EXAMPLE 82 | 83 | The script provides both a summary in the console and detailed JSON output. 84 | 85 | **Enhanced Results Summary (Console Output Example):** 86 | ```text 87 | Enhanced Results Summary: 88 | ✓ +12345678900: John Doe (@johndoe_example) 89 | 📅 Exact time: 2025-05-25 10:30:45 UTC 90 | 📝 Bio: Just an example bio here. Living the example life! Follow for more examples... 91 | ⭐ Telegram Premium user 92 | ✅ Verified account 93 | 👥 Common chats: 2 94 | 📸 Profile photos downloaded: 2 95 | 96 | ✓ example_user: Jane Smith (@example_user) 97 | 📅 Status: Last seen recently (1 second - 3 days ago) [yellow](Privacy restricted - exact time hidden)[/yellow] 98 | 📝 Bio: Exploring the digital world. 99 | 🚫 User has blocked you 100 | 📸 Profile photos downloaded: 1 101 | 102 | ❌ +98765432100: No Telegram account found 103 | ``` 104 | 105 | **Detailed Results (JSON Output Example - content of `results/results_YYYYMMDD_HHMMSS.json`):** 106 | ```json 107 | { 108 | "+12345678900": { 109 | "id": 123456789, 110 | "username": "johndoe_example", 111 | "first_name": "John", 112 | "last_name": "Doe", 113 | "phone": "+12345678900", 114 | "premium": true, 115 | "verified": true, 116 | "fake": false, 117 | "bot": false, 118 | "last_seen": "Last seen: 2025-05-25 10:30:45 UTC", 119 | "last_seen_exact": "2025-05-25 10:30:45 UTC", 120 | "status_type": "offline", 121 | "bio": "Just an example bio here. Living the example life! Follow for more examples...", 122 | "common_chats_count": 2, 123 | "blocked": false, 124 | "profile_photos": [ 125 | "profile_photos/123456789_+12345678900_photo_0.jpg", 126 | "profile_photos/123456789_+12345678900_photo_1.jpg" 127 | ], 128 | "privacy_restricted": false 129 | }, 130 | "example_user": { 131 | "id": 987654321, 132 | "username": "example_user", 133 | "first_name": "Jane", 134 | "last_name": "Smith", 135 | "phone": "", 136 | "premium": false, 137 | "verified": false, 138 | "fake": false, 139 | "bot": false, 140 | "last_seen": "Last seen recently (1 second - 3 days ago)", 141 | "last_seen_exact": null, 142 | "status_type": "recently", 143 | "bio": "Exploring the digital world.", 144 | "common_chats_count": 0, 145 | "blocked": true, 146 | "profile_photos": [ 147 | "profile_photos/987654321_example_user_photo_0.jpg" 148 | ], 149 | "privacy_restricted": true 150 | }, 151 | "+98765432100": { 152 | "error": "No Telegram account found" 153 | } 154 | } 155 | ``` 156 | 157 | ## ⚠️ Note 158 | 159 | This tool is for educational purposes only. Please respect Telegram's terms of service and user privacy. 160 | 161 | ## 📄 License 162 | 163 | MIT License 164 | -------------------------------------------------------------------------------- /telegram-checker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | import os 5 | import pickle 6 | from dataclasses import dataclass, asdict 7 | from pathlib import Path 8 | from typing import List, Optional, Dict, Any 9 | from datetime import datetime 10 | import re 11 | from rich.console import Console 12 | from rich.prompt import Prompt, Confirm 13 | from rich import print as rprint 14 | from telethon.sync import TelegramClient, errors 15 | from telethon.tl import types 16 | from telethon.tl.functions.contacts import ImportContactsRequest, DeleteContactsRequest 17 | from telethon.tl.functions.users import GetFullUserRequest 18 | 19 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[logging.FileHandler("telegram_checker.log"), logging.StreamHandler()]) 20 | logger = logging.getLogger(__name__) 21 | console = Console() 22 | CONFIG_FILE = Path("config.pkl") 23 | PROFILE_PHOTOS_DIR = Path("profile_photos") 24 | RESULTS_DIR = Path("results") 25 | 26 | @dataclass 27 | class TelegramUser: 28 | id: int 29 | username: Optional[str] 30 | first_name: Optional[str] 31 | last_name: Optional[str] 32 | phone: str 33 | premium: bool 34 | verified: bool 35 | fake: bool 36 | bot: bool 37 | last_seen: str 38 | last_seen_exact: Optional[str] = None 39 | status_type: Optional[str] = None 40 | bio: Optional[str] = None 41 | common_chats_count: Optional[int] = None 42 | blocked: Optional[bool] = None 43 | profile_photos: List[str] = None 44 | privacy_restricted: bool = False 45 | 46 | @classmethod 47 | async def from_user(cls, client: TelegramClient, user: types.User, phone: str = "") -> 'TelegramUser': 48 | try: 49 | bio = '' 50 | common_chats_count = 0 51 | blocked = False 52 | 53 | try: 54 | full_user = await client(GetFullUserRequest(user.id)) 55 | user_full_info = full_user.full_user 56 | bio = getattr(user_full_info, 'about', '') or '' 57 | common_chats_count = getattr(user_full_info, 'common_chats_count', 0) 58 | blocked = getattr(user_full_info, 'blocked', False) 59 | except: 60 | pass 61 | 62 | status_info = get_enhanced_user_status(user.status) 63 | 64 | return cls( 65 | id=user.id, 66 | username=user.username, 67 | first_name=getattr(user, 'first_name', None) or "", 68 | last_name=getattr(user, 'last_name', None) or "", 69 | phone=phone, 70 | premium=getattr(user, 'premium', False), 71 | verified=getattr(user, 'verified', False), 72 | fake=getattr(user, 'fake', False), 73 | bot=getattr(user, 'bot', False), 74 | last_seen=status_info['display_text'], 75 | last_seen_exact=status_info['exact_time'], 76 | status_type=status_info['status_type'], 77 | bio=bio, 78 | common_chats_count=common_chats_count, 79 | blocked=blocked, 80 | privacy_restricted=status_info['privacy_restricted'], 81 | profile_photos=[] 82 | ) 83 | except Exception as e: 84 | logger.error(f"Error creating TelegramUser: {str(e)}") 85 | status_info = get_enhanced_user_status(getattr(user, 'status', None)) 86 | return cls( 87 | id=user.id, 88 | username=getattr(user, 'username', None), 89 | first_name=getattr(user, 'first_name', None) or "", 90 | last_name=getattr(user, 'last_name', None) or "", 91 | phone=phone, 92 | premium=getattr(user, 'premium', False), 93 | verified=getattr(user, 'verified', False), 94 | fake=getattr(user, 'fake', False), 95 | bot=getattr(user, 'bot', False), 96 | last_seen=status_info['display_text'], 97 | last_seen_exact=status_info['exact_time'], 98 | status_type=status_info['status_type'], 99 | privacy_restricted=status_info['privacy_restricted'], 100 | profile_photos=[] 101 | ) 102 | 103 | def get_enhanced_user_status(status: types.TypeUserStatus) -> Dict[str, Any]: 104 | result = { 105 | 'display_text': 'Unknown', 106 | 'exact_time': None, 107 | 'status_type': 'unknown', 108 | 'privacy_restricted': False 109 | } 110 | 111 | if isinstance(status, types.UserStatusOnline): 112 | result.update({ 113 | 'display_text': "Currently online", 114 | 'status_type': 'online', 115 | 'privacy_restricted': False 116 | }) 117 | elif isinstance(status, types.UserStatusOffline): 118 | exact_time = status.was_online.strftime('%Y-%m-%d %H:%M:%S UTC') 119 | result.update({ 120 | 'display_text': f"Last seen: {exact_time}", 121 | 'exact_time': exact_time, 122 | 'status_type': 'offline', 123 | 'privacy_restricted': False 124 | }) 125 | elif isinstance(status, types.UserStatusRecently): 126 | result.update({ 127 | 'display_text': "Last seen recently (1 second - 3 days ago)", 128 | 'status_type': 'recently', 129 | 'privacy_restricted': True 130 | }) 131 | elif isinstance(status, types.UserStatusLastWeek): 132 | result.update({ 133 | 'display_text': "Last seen within a week (3-7 days ago)", 134 | 'status_type': 'last_week', 135 | 'privacy_restricted': True 136 | }) 137 | elif isinstance(status, types.UserStatusLastMonth): 138 | result.update({ 139 | 'display_text': "Last seen within a month (7-30 days ago)", 140 | 'status_type': 'last_month', 141 | 'privacy_restricted': True 142 | }) 143 | elif status is None: 144 | result.update({ 145 | 'display_text': "Status unavailable", 146 | 'status_type': 'unavailable' 147 | }) 148 | 149 | return result 150 | 151 | def validate_phone_number(phone: str) -> str: 152 | phone = re.sub(r'[^\d+]', '', phone.strip()) 153 | if not phone.startswith('+'): phone = '+' + phone 154 | if not re.match(r'^\+\d{10,15}$', phone): raise ValueError(f"Invalid phone number format: {phone}") 155 | return phone 156 | 157 | def validate_username(username: str) -> str: 158 | username = username.strip().lstrip('@') 159 | if not re.match(r'^[A-Za-z]\w{3,30}[A-Za-z0-9]$', username): raise ValueError(f"Invalid username format: {username}") 160 | return username 161 | 162 | class TelegramChecker: 163 | def __init__(self): 164 | self.config = self.load_config() 165 | self.client = None 166 | PROFILE_PHOTOS_DIR.mkdir(exist_ok=True) 167 | RESULTS_DIR.mkdir(exist_ok=True) 168 | 169 | def load_config(self) -> dict: 170 | if CONFIG_FILE.exists(): 171 | try: 172 | with open(CONFIG_FILE, 'rb') as f: return pickle.load(f) 173 | except Exception as e: 174 | logger.error(f"Error loading config: {e}") 175 | return {} 176 | return {} 177 | 178 | def save_config(self): 179 | with open(CONFIG_FILE, 'wb') as f: pickle.dump(self.config, f) 180 | 181 | async def initialize(self): 182 | if not self.config.get('api_id'): 183 | console.print("[yellow]First time setup - please enter your Telegram API credentials[/yellow]") 184 | console.print("[cyan]You can get these from https://my.telegram.org/apps[/cyan]") 185 | self.config['api_id'] = int(Prompt.ask("Enter your API ID")) 186 | self.config['api_hash'] = Prompt.ask("Enter your API hash", password=True) 187 | self.config['phone'] = validate_phone_number(Prompt.ask("Enter your phone number (with country code)")) 188 | self.save_config() 189 | 190 | self.client = TelegramClient('telegram_checker_session', self.config['api_id'], self.config['api_hash']) 191 | await self.client.connect() 192 | 193 | if not await self.client.is_user_authorized(): 194 | await self.client.send_code_request(self.config['phone']) 195 | code = Prompt.ask("Enter the verification code sent to your Telegram") 196 | try: 197 | await self.client.sign_in(self.config['phone'], code) 198 | except errors.SessionPasswordNeededError: 199 | password = Prompt.ask("Enter your 2FA password", password=True) 200 | await self.client.sign_in(password=password) 201 | 202 | async def download_all_profile_photos(self, user: types.User, user_data: TelegramUser): 203 | try: 204 | photos = await self.client.get_profile_photos(user) 205 | if not photos: return 206 | user_data.profile_photos = [] 207 | for i, photo in enumerate(photos): 208 | identifier = user_data.phone if user_data.phone else user_data.username 209 | photo_path = PROFILE_PHOTOS_DIR / f"{user.id}_{identifier}_photo_{i}.jpg" 210 | await self.client.download_media(photo, file=photo_path) 211 | user_data.profile_photos.append(str(photo_path)) 212 | except Exception as e: 213 | logger.error(f"Error downloading profile photos for {user.id}: {str(e)}") 214 | 215 | async def check_phone_number(self, phone: str) -> Optional[TelegramUser]: 216 | try: 217 | phone = validate_phone_number(phone) 218 | try: 219 | user = await self.client.get_entity(phone) 220 | telegram_user = await TelegramUser.from_user(self.client, user, phone) 221 | await self.download_all_profile_photos(user, telegram_user) 222 | return telegram_user 223 | except: 224 | contact = types.InputPhoneContact(client_id=0, phone=phone, first_name="Test", last_name="User") 225 | result = await self.client(ImportContactsRequest([contact])) 226 | 227 | if not result.users: return None 228 | 229 | user = result.users[0] 230 | try: 231 | full_user = await self.client.get_entity(user.id) 232 | await self.client(DeleteContactsRequest(id=[user.id])) 233 | telegram_user = await TelegramUser.from_user(self.client, full_user, phone) 234 | await self.download_all_profile_photos(full_user, telegram_user) 235 | return telegram_user 236 | finally: 237 | try: 238 | await self.client(DeleteContactsRequest(id=[user.id])) 239 | except: 240 | pass 241 | except Exception as e: 242 | logger.error(f"Error checking {phone}: {str(e)}") 243 | return None 244 | 245 | async def check_username(self, username: str) -> Optional[TelegramUser]: 246 | try: 247 | username = validate_username(username) 248 | user = await self.client.get_entity(username) 249 | if not isinstance(user, types.User): return None 250 | telegram_user = await TelegramUser.from_user(self.client, user, "") 251 | await self.download_all_profile_photos(user, telegram_user) 252 | return telegram_user 253 | except ValueError as e: 254 | logger.error(f"Invalid username {username}: {str(e)}") 255 | return None 256 | except errors.UsernameNotOccupiedError: 257 | logger.error(f"Username {username} not found") 258 | return None 259 | except Exception as e: 260 | logger.error(f"Error checking username {username}: {str(e)}") 261 | return None 262 | 263 | async def process_phones(self, phones: List[str]) -> dict: 264 | results = {} 265 | total_phones = len(phones) 266 | console.print(f"\n[cyan]Processing {total_phones} phone numbers...[/cyan]") 267 | 268 | for i, phone in enumerate(phones, 1): 269 | try: 270 | phone = phone.strip() 271 | if not phone: continue 272 | console.print(f"[cyan]Checking {phone} ({i}/{total_phones})[/cyan]") 273 | user = await self.check_phone_number(phone) 274 | results[phone] = asdict(user) if user else {"error": "No Telegram account found"} 275 | except ValueError as e: 276 | results[phone] = {"error": str(e)} 277 | except Exception as e: 278 | results[phone] = {"error": f"Unexpected error: {str(e)}"} 279 | return results 280 | 281 | async def process_usernames(self, usernames: List[str]) -> dict: 282 | results = {} 283 | total_usernames = len(usernames) 284 | console.print(f"\n[cyan]Processing {total_usernames} usernames...[/cyan]") 285 | 286 | for i, username in enumerate(usernames, 1): 287 | try: 288 | username = username.strip() 289 | if not username: continue 290 | console.print(f"[cyan]Checking {username} ({i}/{total_usernames})[/cyan]") 291 | user = await self.check_username(username) 292 | results[username] = asdict(user) if user else {"error": "No Telegram account found"} 293 | except ValueError as e: 294 | results[username] = {"error": str(e)} 295 | except Exception as e: 296 | results[username] = {"error": f"Unexpected error: {str(e)}"} 297 | return results 298 | 299 | def display_enhanced_results(results: dict): 300 | console.print("\n[bold]Enhanced Results Summary:[/bold]") 301 | 302 | for identifier, data in results.items(): 303 | if "error" in data: 304 | console.print(f"[red]❌ {identifier}: {data['error']}[/red]") 305 | else: 306 | name = f"{data.get('first_name', '')} {data.get('last_name', '')}".strip() 307 | username = f"@{data.get('username', 'no username')}" 308 | 309 | status_line = f"[green]✓ {identifier}: {name} ({username})[/green]" 310 | 311 | if data.get('privacy_restricted'): 312 | status_line += " [yellow](Privacy restricted - exact time hidden)[/yellow]" 313 | 314 | if data.get('last_seen_exact'): 315 | status_line += f"\n 📅 Exact time: {data['last_seen_exact']}" 316 | else: 317 | status_line += f"\n 📅 Status: {data.get('last_seen', 'Unknown')}" 318 | 319 | if data.get('bio'): 320 | status_line += f"\n 📝 Bio: {data['bio'][:100]}{'...' if len(data['bio']) > 100 else ''}" 321 | 322 | if data.get('premium'): 323 | status_line += "\n ⭐ Telegram Premium user" 324 | 325 | if data.get('verified'): 326 | status_line += "\n ✅ Verified account" 327 | 328 | if data.get('blocked'): 329 | status_line += "\n 🚫 User has blocked you" 330 | 331 | if data.get('common_chats_count', 0) > 0: 332 | status_line += f"\n 👥 Common chats: {data['common_chats_count']}" 333 | 334 | if data.get('profile_photos'): 335 | status_line += f"\n 📸 Profile photos downloaded: {len(data['profile_photos'])}" 336 | 337 | console.print(status_line) 338 | console.print() 339 | 340 | async def main(): 341 | checker = TelegramChecker() 342 | await checker.initialize() 343 | 344 | while True: 345 | rprint("\n[bold cyan]Telegram Account Checker[/bold cyan]") 346 | rprint("\n1. Check phone numbers from input") 347 | rprint("2. Check phone numbers from file") 348 | rprint("3. Check usernames from input") 349 | rprint("4. Check usernames from file") 350 | rprint("5. Clear saved credentials") 351 | rprint("6. Exit") 352 | 353 | choice = Prompt.ask("\nSelect an option", choices=["1", "2", "3", "4", "5", "6"]) 354 | 355 | if choice == "1": 356 | phones = [p.strip() for p in Prompt.ask("Enter phone numbers (comma-separated)").split(",")] 357 | results = await checker.process_phones(phones) 358 | elif choice == "2": 359 | file_path = Prompt.ask("Enter the path to your phone numbers file") 360 | try: 361 | with open(file_path, 'r') as f: 362 | phones = [line.strip() for line in f if line.strip()] 363 | results = await checker.process_phones(phones) 364 | except FileNotFoundError: 365 | console.print("[red]File not found![/red]") 366 | continue 367 | elif choice == "3": 368 | usernames = [u.strip() for u in Prompt.ask("Enter usernames (comma-separated)").split(",")] 369 | results = await checker.process_usernames(usernames) 370 | elif choice == "4": 371 | file_path = Prompt.ask("Enter the path to your usernames file") 372 | try: 373 | with open(file_path, 'r') as f: 374 | usernames = [line.strip() for line in f if line.strip()] 375 | results = await checker.process_usernames(usernames) 376 | except FileNotFoundError: 377 | console.print("[red]File not found![/red]") 378 | continue 379 | elif choice == "5": 380 | if Confirm.ask("Are you sure you want to clear saved credentials?"): 381 | if CONFIG_FILE.exists(): CONFIG_FILE.unlink() 382 | if Path('telegram_checker_session.session').exists(): Path('telegram_checker_session.session').unlink() 383 | console.print("[green]Credentials cleared. Please restart the program.[/green]") 384 | break 385 | continue 386 | else: 387 | break 388 | 389 | if 'results' in locals(): 390 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 391 | output_file = RESULTS_DIR / f"results_{timestamp}.json" 392 | 393 | with open(output_file, 'w') as f: 394 | json.dump(results, f, indent=2) 395 | 396 | console.print(f"\n[green]Results saved to {output_file}[/green]") 397 | display_enhanced_results(results) 398 | 399 | console.print("\n[bold cyan]Detailed Results (JSON):[/bold cyan]") 400 | formatted_json = json.dumps(results, indent=2) 401 | console.print(formatted_json) 402 | 403 | if __name__ == "__main__": 404 | try: 405 | asyncio.run(main()) 406 | except KeyboardInterrupt: 407 | console.print("\n[yellow]Program terminated by user[/yellow]") 408 | except Exception as e: 409 | console.print(f"\n[red]An error occurred: {str(e)}[/red]") 410 | logger.exception("Unhandled exception occurred") 411 | --------------------------------------------------------------------------------