# discord_tools/scripts/profile_stalker.py import os import sys import asyncio import websockets import json import aiohttp from datetime import datetime from collections import defaultdict # Add the parent directory to the Python path script_dir = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(os.path.dirname(script_dir)) sys.path.insert(0, project_root) from discord_tools.config.settings import DISCORD_TOKEN, DATA_DIR from discord_tools.utils.api_utils import make_discord_request class ProfileStalker: def __init__(self, user_ids): self.user_ids = user_ids if isinstance(user_ids, list) else [user_ids] self.active = True self.user_data = {} # Stores current state for each user self.change_log = { 'profile': [], # Username, avatar, banner, bio, etc 'activity': [], # Status, games, Spotify, typing 'social': [] # Friends, servers, roles, nicknames } self.message_counts = defaultdict(lambda: defaultdict(int)) # user_id -> channel_id -> count self.spotify_history = defaultdict(list) # user_id -> [songs] self.voice_sessions = {} # Track voice session start times self.voice_history = defaultdict(list) # user_id -> [sessions] self.active_calls = {} # Track active DM calls self.start_time = None self.total_reconnects = 0 # Cache for guild/channel names self.guild_cache = {} self.channel_cache = {} # Initialize user data structure for user_id in self.user_ids: self.user_data[user_id] = { 'username': None, 'global_name': None, 'discriminator': None, 'avatar': None, 'avatar_decoration': None, 'banner': None, 'bio': None, 'pronouns': None, 'clan_tag': None, 'connections': [], 'status': None, 'custom_status': None, 'activities': [], 'current_spotify_song': None, 'voice_channel': None, 'voice_guild': None, 'voice_muted': None, 'voice_deafened': None, 'voice_video': None, 'voice_screenshare': None, 'mutual_guilds': [], 'mutual_friends': [], 'guild_nicknames': {}, 'guild_roles': {}, 'badges': None, 'nitro': None, 'accent_color': None, 'banner_color': None, 'theme_colors': None, 'typing_in': [] } def get_output_folder(self, user_id, subfolder=None): """Get organized output folder path""" base = os.path.join(DATA_DIR, f"stalker_{user_id}") if subfolder: return os.path.join(base, subfolder) return base def get_guild_name(self, guild_id): if guild_id in self.guild_cache: return self.guild_cache[guild_id] response = make_discord_request('GET', f'/guilds/{guild_id}') if response: name = response.json().get('name', f'Guild-{guild_id}') self.guild_cache[guild_id] = name return name return f'Guild-{guild_id}' def get_channel_name(self, channel_id): if channel_id in self.channel_cache: return self.channel_cache[channel_id] response = make_discord_request('GET', f'/channels/{channel_id}') if response: name = response.json().get('name', f'Channel-{channel_id}') self.channel_cache[channel_id] = name return name return f'Channel-{channel_id}' def log_change(self, user_id, category, change_type, old_value, new_value, extra_info=None): """Log a change with timestamp and category""" timestamp = datetime.now() change_entry = { 'timestamp': timestamp.isoformat(), 'user_id': user_id, 'type': change_type, 'old': old_value, 'new': new_value } if extra_info: change_entry['extra'] = extra_info self.change_log[category].append(change_entry) # Print to console with category color coding time_str = timestamp.strftime('%H:%M:%S') category_prefix = { 'profile': 'šŸ‘¤', 'activity': 'šŸŽ®', 'social': 'šŸ‘„' } prefix = category_prefix.get(category, 'šŸ“') print(f"{prefix} [{time_str}] {change_type}: {old_value} → {new_value}") async def download_asset(self, session, url, folder, filename): try: async with session.get(url) as resp: if resp.status == 200: filepath = os.path.join(folder, filename) with open(filepath, 'wb') as f: f.write(await resp.read()) return filepath except Exception as e: print(f"Failed to download {filename}: {e}") return None async def fetch_user_profile(self, user_id): response = make_discord_request('GET', f'/users/{user_id}/profile') if response: return response.json() return None def handle_presence_update(self, data, session): user_id = data.get('user', {}).get('id') if user_id not in self.user_ids: return user_data = self.user_data[user_id] # Status change status = data.get('status') if status and status != user_data['status']: self.log_change(user_id, 'activity', 'Status', user_data['status'], status) user_data['status'] = status # Activities activities = data.get('activities', []) # Custom status custom_status = None for activity in activities: if activity.get('type') == 4: custom_status = activity.get('state', '') emoji = activity.get('emoji') if emoji: emoji_str = emoji.get('name', '') custom_status = f"{emoji_str} {custom_status}".strip() if custom_status != user_data['custom_status']: self.log_change(user_id, 'activity', 'Custom Status', user_data['custom_status'], custom_status) user_data['custom_status'] = custom_status # Other activities activity_names = [] spotify_song = None for activity in activities: if activity.get('type') == 2: # Spotify song_name = activity.get('details', 'Unknown') artist = activity.get('state', 'Unknown') album = activity.get('assets', {}).get('large_text', 'Unknown') spotify_song = f"{song_name} by {artist}" activity_names.append(f"Listening to {spotify_song}") if spotify_song != user_data.get('current_spotify_song'): self.log_change(user_id, 'activity', 'Spotify', user_data.get('current_spotify_song'), spotify_song, {'song': song_name, 'artist': artist, 'album': album}) user_data['current_spotify_song'] = spotify_song # Add to Spotify history self.spotify_history[user_id].append({ 'timestamp': datetime.now().isoformat(), 'song': song_name, 'artist': artist, 'album': album }) elif activity.get('type') in [0, 1]: activity_type = ['Playing', 'Streaming'][activity['type']] activity_name = activity.get('name', 'Unknown') activity_names.append(f"{activity_type} {activity_name}") if not spotify_song and user_data.get('current_spotify_song'): self.log_change(user_id, 'activity', 'Spotify', user_data.get('current_spotify_song'), 'Stopped listening') user_data['current_spotify_song'] = None current_activities = ', '.join(activity_names) if activity_names else None old_activities = ', '.join(user_data['activities']) if user_data['activities'] else None if current_activities != old_activities: self.log_change(user_id, 'activity', 'Activity', old_activities, current_activities) user_data['activities'] = activity_names def handle_call_create(self, data): channel_id = data.get('channel_id') ringing = data.get('ringing', []) for user_id in self.user_ids: if user_id in ringing: timestamp = datetime.now().strftime('%H:%M:%S') print(f"šŸ“ž [{timestamp}] Incoming call in DM {channel_id}") self.active_calls[channel_id] = { 'start_time': datetime.now(), 'participants': ringing } def handle_call_update(self, data): channel_id = data.get('channel_id') ringing = data.get('ringing', []) for user_id in self.user_ids: if user_id in ringing: timestamp = datetime.now().strftime('%H:%M:%S') print(f"šŸ“ž [{timestamp}] User {user_id} in call {channel_id}") def handle_call_delete(self, data): channel_id = data.get('channel_id') if channel_id in self.active_calls: call_info = self.active_calls[channel_id] duration = (datetime.now() - call_info['start_time']).total_seconds() duration_min = duration / 60 timestamp = datetime.now().strftime('%H:%M:%S') print(f"šŸ“ž [{timestamp}] Call ended in DM {channel_id} (duration: {duration_min:.1f} min)") del self.active_calls[channel_id] def handle_guild_member_update(self, data): member_data = data user = member_data.get('user', {}) user_id = user.get('id') if user_id not in self.user_ids: return guild_id = member_data.get('guild_id') user_data = self.user_data[user_id] # Nickname change nick = member_data.get('nick') old_nick = user_data['guild_nicknames'].get(guild_id) if nick != old_nick: guild_name = self.get_guild_name(guild_id) self.log_change(user_id, 'social', 'Nickname', f"{guild_name}: {old_nick or 'None'}", f"{guild_name}: {nick or 'None'}", {'guild_id': guild_id}) if nick: user_data['guild_nicknames'][guild_id] = nick elif guild_id in user_data['guild_nicknames']: del user_data['guild_nicknames'][guild_id] # Role changes new_roles = set(member_data.get('roles', [])) old_roles = set(user_data['guild_roles'].get(guild_id, [])) added_roles = new_roles - old_roles removed_roles = old_roles - new_roles if added_roles or removed_roles: guild_name = self.get_guild_name(guild_id) if added_roles: self.log_change(user_id, 'social', 'Roles Added', f"{guild_name}: {len(old_roles)} roles", f"{guild_name}: {len(new_roles)} roles", {'guild_id': guild_id, 'added': list(added_roles)}) if removed_roles: self.log_change(user_id, 'social', 'Roles Removed', f"{guild_name}: {len(old_roles)} roles", f"{guild_name}: {len(new_roles)} roles", {'guild_id': guild_id, 'removed': list(removed_roles)}) user_data['guild_roles'][guild_id] = list(new_roles) def handle_typing_start(self, data): user_id = data.get('user_id') if user_id not in self.user_ids: return channel_id = data.get('channel_id') user_data = self.user_data[user_id] if channel_id not in user_data['typing_in']: user_data['typing_in'].append(channel_id) timestamp = datetime.now().strftime('%H:%M:%S') print(f"āŒØļø [{timestamp}] Started typing in channel {channel_id}") async def clear_typing(): await asyncio.sleep(10) if channel_id in user_data['typing_in']: user_data['typing_in'].remove(channel_id) asyncio.create_task(clear_typing()) def handle_relationship_add(self, data): user_id = data.get('id') if user_id not in self.user_ids: return friend = data.get('user', {}) friend_name = f"{friend.get('username', 'Unknown')}#{friend.get('discriminator', '0000')}" self.log_change(user_id, 'social', 'Friend Added', None, friend_name, {'friend_id': friend.get('id')}) def handle_relationship_remove(self, data): relationship_id = data.get('id') timestamp = datetime.now().strftime('%H:%M:%S') print(f"šŸ‘„ [{timestamp}] Relationship removed: {relationship_id}") def handle_voice_state_update(self, data): user_id = data.get('user_id') if user_id not in self.user_ids: return user_data = self.user_data[user_id] channel_id = data.get('channel_id') guild_id = data.get('guild_id') # Voice state flags self_mute = data.get('self_mute', False) self_deaf = data.get('self_deaf', False) self_video = data.get('self_video', False) self_stream = data.get('self_stream', False) # Track mute/deaf changes if self_mute != user_data['voice_muted']: self.log_change(user_id, 'activity', 'Voice Muted', user_data['voice_muted'], self_mute) user_data['voice_muted'] = self_mute if self_deaf != user_data['voice_deafened']: self.log_change(user_id, 'activity', 'Voice Deafened', user_data['voice_deafened'], self_deaf) user_data['voice_deafened'] = self_deaf if self_video != user_data['voice_video']: self.log_change(user_id, 'activity', 'Camera', user_data['voice_video'], self_video) user_data['voice_video'] = self_video if self_stream != user_data['voice_screenshare']: self.log_change(user_id, 'activity', 'Screenshare', user_data['voice_screenshare'], self_stream) user_data['voice_screenshare'] = self_stream guild_name = self.get_guild_name(guild_id) if guild_id else 'DM' channel_name = self.get_channel_name(channel_id) if channel_id else None # Check for channel changes if channel_id != user_data['voice_channel']: if channel_id is None: # Left voice session_key = f"{user_id}:{user_data.get('voice_channel')}" if session_key in self.voice_sessions: start_time = self.voice_sessions[session_key] duration = (datetime.now() - start_time).total_seconds() duration_min = duration / 60 old_location = f"{user_data.get('voice_guild', 'Unknown')} > {user_data.get('voice_channel', 'Unknown')}" self.log_change(user_id, 'activity', 'Voice', old_location, 'Left voice', { 'action': 'left', 'duration_seconds': duration, 'duration_minutes': round(duration_min, 1) }) # Add to voice history self.voice_history[user_id].append({ 'start': start_time.isoformat(), 'end': datetime.now().isoformat(), 'duration_minutes': round(duration_min, 1), 'location': old_location }) del self.voice_sessions[session_key] else: old_location = f"{user_data.get('voice_guild', 'Unknown')} > {user_data.get('voice_channel', 'Unknown')}" self.log_change(user_id, 'activity', 'Voice', old_location, 'Left voice', {'action': 'left'}) elif user_data['voice_channel'] is None: # Joined voice new_location = f"{guild_name} > {channel_name}" self.log_change(user_id, 'activity', 'Voice', None, new_location, {'action': 'joined'}) session_key = f"{user_id}:{channel_name}" self.voice_sessions[session_key] = datetime.now() else: # Switched channels old_session_key = f"{user_id}:{user_data.get('voice_channel')}" if old_session_key in self.voice_sessions: start_time = self.voice_sessions[old_session_key] duration = (datetime.now() - start_time).total_seconds() duration_min = duration / 60 # Add to voice history old_location = f"{user_data.get('voice_guild', 'Unknown')} > {user_data.get('voice_channel', 'Unknown')}" self.voice_history[user_id].append({ 'start': start_time.isoformat(), 'end': datetime.now().isoformat(), 'duration_minutes': round(duration_min, 1), 'location': old_location }) del self.voice_sessions[old_session_key] else: duration_min = None old_location = f"{user_data.get('voice_guild', 'Unknown')} > {user_data.get('voice_channel', 'Unknown')}" new_location = f"{guild_name} > {channel_name}" extra_info = {'action': 'moved'} if duration_min: extra_info['prev_duration_minutes'] = round(duration_min, 1) self.log_change(user_id, 'activity', 'Voice', old_location, new_location, extra_info) # Start tracking new session new_session_key = f"{user_id}:{channel_name}" self.voice_sessions[new_session_key] = datetime.now() user_data['voice_channel'] = channel_name if channel_id else None user_data['voice_guild'] = guild_name if guild_id else None def handle_user_update(self, data, session): user_id = data.get('id') if user_id not in self.user_ids: return user_data = self.user_data[user_id] # Username change username = data.get('username') if username and username != user_data['username']: self.log_change(user_id, 'profile', 'Username', user_data['username'], username) user_data['username'] = username # Display name change global_name = data.get('global_name') if global_name != user_data['global_name']: self.log_change(user_id, 'profile', 'Display Name', user_data['global_name'], global_name) user_data['global_name'] = global_name # Discriminator change discriminator = data.get('discriminator') if discriminator and discriminator != user_data['discriminator']: self.log_change(user_id, 'profile', 'Discriminator', user_data['discriminator'], discriminator) user_data['discriminator'] = discriminator # Avatar change avatar = data.get('avatar') if avatar and avatar != user_data['avatar']: self.log_change(user_id, 'profile', 'Avatar', user_data['avatar'], avatar) user_data['avatar'] = avatar avatar_url = f"https://cdn.discordapp.com/avatars/{user_id}/{avatar}.png?size=1024" timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"avatar_{timestamp}.png" folder = self.get_output_folder(user_id, "profile/avatars") os.makedirs(folder, exist_ok=True) asyncio.create_task(self.download_asset(session, avatar_url, folder, filename)) # Banner change banner = data.get('banner') if banner and banner != user_data['banner']: self.log_change(user_id, 'profile', 'Banner', user_data['banner'], banner) user_data['banner'] = banner banner_url = f"https://cdn.discordapp.com/banners/{user_id}/{banner}.png?size=1024" timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"banner_{timestamp}.png" folder = self.get_output_folder(user_id, "profile/banners") os.makedirs(folder, exist_ok=True) asyncio.create_task(self.download_asset(session, banner_url, folder, filename)) async def fetch_initial_data(self, session): print("Fetching initial profile data...") for user_id in self.user_ids: profile = await self.fetch_user_profile(user_id) if profile: user_info = profile.get('user', {}) user_data = self.user_data[user_id] # Set initial values user_data['username'] = user_info.get('username') user_data['global_name'] = user_info.get('global_name') user_data['discriminator'] = user_info.get('discriminator') user_data['avatar'] = user_info.get('avatar') user_data['banner'] = user_info.get('banner') user_data['bio'] = user_info.get('bio', '') user_data['badges'] = user_info.get('public_flags', 0) user_data['accent_color'] = user_info.get('accent_color') user_data['avatar_decoration'] = user_info.get('avatar_decoration_data') user_data['pronouns'] = user_info.get('pronouns', '') user_data['clan_tag'] = user_info.get('clan', {}).get('tag') if user_info.get('clan') else None connected_accounts = profile.get('connected_accounts', []) user_data['connections'] = [ {'type': conn.get('type'), 'name': conn.get('name'), 'visible': conn.get('visibility', 0) == 1} for conn in connected_accounts ] premium_type = user_info.get('premium_type', 0) user_data['nitro'] = ['None', 'Nitro Classic', 'Nitro', 'Nitro Basic'][premium_type] if premium_type < 4 else 'Unknown' mutual_guilds_data = profile.get('mutual_guilds', []) user_data['mutual_guilds'] = [g.get('id') for g in mutual_guilds_data] for guild in mutual_guilds_data: guild_id = guild.get('id') guild_name = guild.get('name') if guild_id and guild_name: self.guild_cache[guild_id] = guild_name # Download initial avatar if user_data['avatar']: avatar_url = f"https://cdn.discordapp.com/avatars/{user_id}/{user_data['avatar']}.png?size=1024" timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"avatar_initial_{timestamp}.png" folder = self.get_output_folder(user_id, "profile/avatars") os.makedirs(folder, exist_ok=True) await self.download_asset(session, avatar_url, folder, filename) # Download initial banner if user_data['banner']: banner_url = f"https://cdn.discordapp.com/banners/{user_id}/{user_data['banner']}.png?size=1024" timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"banner_initial_{timestamp}.png" folder = self.get_output_folder(user_id, "profile/banners") os.makedirs(folder, exist_ok=True) await self.download_asset(session, banner_url, folder, filename) print(f" āœ“ Loaded: {user_data['global_name'] or user_data['username']} ({user_id})") print(f" Mutual servers: {len(user_data['mutual_guilds'])}") print("Initial data loaded\n") async def periodic_profile_check(self, session): while self.active: await asyncio.sleep(300) # Check every 5 minutes for user_id in self.user_ids: profile = await self.fetch_user_profile(user_id) if not profile: continue user_data = self.user_data[user_id] user_info = profile.get('user', {}) # Bio change bio = user_info.get('bio', '') if bio != user_data['bio']: self.log_change(user_id, 'profile', 'Bio', user_data['bio'], bio) user_data['bio'] = bio # Pronouns pronouns = user_info.get('pronouns', '') if pronouns != user_data['pronouns']: self.log_change(user_id, 'profile', 'Pronouns', user_data['pronouns'], pronouns) user_data['pronouns'] = pronouns # Clan tag clan_tag = user_info.get('clan', {}).get('tag') if user_info.get('clan') else None if clan_tag != user_data['clan_tag']: self.log_change(user_id, 'profile', 'Clan Tag', user_data['clan_tag'], clan_tag) user_data['clan_tag'] = clan_tag # Connections connected_accounts = profile.get('connected_accounts', []) new_connections = [ {'type': conn.get('type'), 'name': conn.get('name')} for conn in connected_accounts if conn.get('visibility', 0) == 1 ] old_conn_str = ', '.join([f"{c['type']}:{c['name']}" for c in user_data['connections']]) new_conn_str = ', '.join([f"{c['type']}:{c['name']}" for c in new_connections]) if old_conn_str != new_conn_str: self.log_change(user_id, 'profile', 'Connections', old_conn_str or 'None', new_conn_str or 'None') user_data['connections'] = new_connections # Avatar decoration avatar_decoration = user_info.get('avatar_decoration_data') if avatar_decoration != user_data['avatar_decoration']: self.log_change(user_id, 'profile', 'Avatar Decoration', user_data['avatar_decoration'], avatar_decoration) user_data['avatar_decoration'] = avatar_decoration # Badges flags = user_info.get('public_flags', 0) if flags != user_data['badges']: self.log_change(user_id, 'profile', 'Badges', user_data['badges'], flags) user_data['badges'] = flags # Accent color accent_color = user_info.get('accent_color') if accent_color != user_data['accent_color']: self.log_change(user_id, 'profile', 'Accent Color', user_data['accent_color'], accent_color) user_data['accent_color'] = accent_color # Banner color banner_color = user_info.get('banner_color') if banner_color != user_data['banner_color']: self.log_change(user_id, 'profile', 'Banner Color', user_data['banner_color'], banner_color) user_data['banner_color'] = banner_color # Nitro status premium_type = user_info.get('premium_type', 0) nitro = ['None', 'Nitro Classic', 'Nitro', 'Nitro Basic'][premium_type] if premium_type < 4 else 'Unknown' if nitro != user_data['nitro']: self.log_change(user_id, 'profile', 'Nitro', user_data['nitro'], nitro) user_data['nitro'] = nitro # Mutual servers mutual_guilds_data = profile.get('mutual_guilds', []) new_mutual_guilds = [g.get('id') for g in mutual_guilds_data] old_guilds = set(user_data['mutual_guilds']) new_guilds = set(new_mutual_guilds) joined = new_guilds - old_guilds left = old_guilds - new_guilds for guild_id in joined: guild_name = self.get_guild_name(guild_id) self.log_change(user_id, 'social', 'Mutual Server Joined', None, guild_name, {'guild_id': guild_id}) for guild_id in left: guild_name = self.get_guild_name(guild_id) self.log_change(user_id, 'social', 'Mutual Server Left', guild_name, None, {'guild_id': guild_id}) user_data['mutual_guilds'] = new_mutual_guilds async def stalk(self): """Main stalking loop with auto-reconnect""" gateway_url = "wss://gateway.discord.gg/?v=9&encoding=json" if self.start_time is None: self.start_time = datetime.now() while self.active: try: await self._connect_and_run(gateway_url) except (websockets.exceptions.ConnectionClosed, websockets.exceptions.ConnectionClosedOK, websockets.exceptions.ConnectionClosedError) as e: if self.active: self.total_reconnects += 1 elapsed = (datetime.now() - self.start_time).total_seconds() print(f"\nāš ļø Connection lost after {elapsed:.0f}s: {e.reason if hasattr(e, 'reason') else str(e)}") print(f"šŸ”„ Reconnecting... (attempt #{self.total_reconnects})") await asyncio.sleep(3) else: break except KeyboardInterrupt: print("\n\nā¹ļø Stopping stalker...") self.active = False break except Exception as e: if self.active: self.total_reconnects += 1 print(f"\nāš ļø Unexpected error: {e}") print(f"šŸ”„ Reconnecting... (attempt #{self.total_reconnects})") await asyncio.sleep(3) else: break async def _connect_and_run(self, gateway_url): """Internal method to connect and run gateway loop""" async with aiohttp.ClientSession() as session: async with websockets.connect(gateway_url, max_size=16 * 1024 * 1024) as ws: # Receive Hello hello = json.loads(await ws.recv()) heartbeat_interval = hello['d']['heartbeat_interval'] # Send Identify identify = { "op": 2, "d": { "token": DISCORD_TOKEN, "properties": { "$os": "windows", "$browser": "chrome", "$device": "pc" }, "compress": False } } await ws.send(json.dumps(identify)) # Heartbeat task async def heartbeat(): while self.active: try: await asyncio.sleep(heartbeat_interval / 1000) if self.active: await ws.send(json.dumps({"op": 1, "d": None})) except Exception: break heartbeat_task = asyncio.create_task(heartbeat()) # Periodic profile check profile_check_task = asyncio.create_task(self.periodic_profile_check(session)) try: # Wait for READY ready = False while not ready: msg = json.loads(await ws.recv()) if msg.get('t') == 'READY': ready = True if self.total_reconnects == 0: print("āœ“ Connected to Gateway") else: elapsed = (datetime.now() - self.start_time).total_seconds() print(f"āœ“ Reconnected successfully (total uptime: {elapsed:.0f}s)") # Fetch initial data (only on first connect) if self.total_reconnects == 0: await self.fetch_initial_data(session) print(f"šŸ‘ļø Stalking {len(self.user_ids)} user(s)") print("=" * 60) # Monitor events while self.active: msg = await ws.recv() data = json.loads(msg) event_type = data.get('t') if event_type == 'PRESENCE_UPDATE': self.handle_presence_update(data['d'], session) elif event_type == 'VOICE_STATE_UPDATE': self.handle_voice_state_update(data['d']) elif event_type == 'USER_UPDATE': self.handle_user_update(data['d'], session) elif event_type == 'TYPING_START': self.handle_typing_start(data['d']) elif event_type == 'RELATIONSHIP_ADD': self.handle_relationship_add(data['d']) elif event_type == 'RELATIONSHIP_REMOVE': self.handle_relationship_remove(data['d']) elif event_type == 'CALL_CREATE': self.handle_call_create(data['d']) elif event_type == 'CALL_UPDATE': self.handle_call_update(data['d']) elif event_type == 'CALL_DELETE': self.handle_call_delete(data['d']) elif event_type == 'GUILD_MEMBER_UPDATE': self.handle_guild_member_update(data['d']) elif event_type == 'MESSAGE_CREATE': message_data = data['d'] author_id = message_data.get('author', {}).get('id') if author_id in self.user_ids: channel_id = message_data.get('channel_id') guild_id = message_data.get('guild_id') self.message_counts[author_id][channel_id] += 1 total = self.message_counts[author_id][channel_id] timestamp = datetime.now().strftime('%H:%M:%S') if guild_id: print(f"šŸ’¬ [{timestamp}] Message sent in server channel {channel_id} (#{total} total)") else: print(f"šŸ’¬ [{timestamp}] Message sent in DM {channel_id} (#{total} total)") finally: heartbeat_task.cancel() profile_check_task.cancel() def save_logs(self): """Save all collected data in organized structure""" if not any(self.change_log.values()): print("No changes recorded") return # Log any active voice sessions for session_key, start_time in self.voice_sessions.items(): user_id = session_key.split(':')[0] duration = (datetime.now() - start_time).total_seconds() duration_min = duration / 60 print(f"\nā„¹ļø User {user_id} was still in voice when tracking stopped ({duration_min:.1f} min)") timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S') for user_id in self.user_ids: base_folder = self.get_output_folder(user_id) # Create folder structure folders = { 'profile': self.get_output_folder(user_id, 'profile'), 'activity': self.get_output_folder(user_id, 'activity'), 'social': self.get_output_folder(user_id, 'social'), 'logs': self.get_output_folder(user_id, 'logs') } for folder in folders.values(): os.makedirs(folder, exist_ok=True) # Save categorized change logs for category in ['profile', 'activity', 'social']: user_changes = [c for c in self.change_log[category] if c['user_id'] == user_id] if user_changes: log_file = os.path.join(folders[category], f"changes_{timestamp_str}.json") with open(log_file, 'w', encoding='utf-8') as f: json.dump(user_changes, f, indent=2, ensure_ascii=False) # Save message activity if user_id in self.message_counts and self.message_counts[user_id]: activity_file = os.path.join(folders['activity'], f"messages_{timestamp_str}.txt") with open(activity_file, 'w', encoding='utf-8') as f: f.write(f"Message Activity for {user_id}\n") f.write("=" * 60 + "\n\n") for channel_id, count in sorted(self.message_counts[user_id].items(), key=lambda x: x[1], reverse=True): channel_name = self.get_channel_name(channel_id) f.write(f"{channel_name} ({channel_id}): {count} messages\n") # Save Spotify history if user_id in self.spotify_history and self.spotify_history[user_id]: spotify_file = os.path.join(folders['activity'], f"spotify_{timestamp_str}.txt") with open(spotify_file, 'w', encoding='utf-8') as f: f.write(f"Spotify Listening History\n") f.write("=" * 60 + "\n\n") for entry in self.spotify_history[user_id]: time = entry['timestamp'] f.write(f"[{time}]\n") f.write(f" šŸŽµ {entry['song']}\n") f.write(f" šŸ‘¤ {entry['artist']}\n") f.write(f" šŸ’æ {entry['album']}\n\n") # Save voice history if user_id in self.voice_history and self.voice_history[user_id]: voice_file = os.path.join(folders['activity'], f"voice_{timestamp_str}.txt") with open(voice_file, 'w', encoding='utf-8') as f: f.write(f"Voice Session History\n") f.write("=" * 60 + "\n\n") total_minutes = 0 for session in self.voice_history[user_id]: f.write(f"šŸ“ {session['location']}\n") f.write(f" Started: {session['start']}\n") f.write(f" Ended: {session['end']}\n") f.write(f" Duration: {session['duration_minutes']} minutes\n\n") total_minutes += session['duration_minutes'] f.write(f"\n{'=' * 60}\n") f.write(f"Total voice time: {total_minutes:.1f} minutes ({total_minutes/60:.1f} hours)\n") # Save complete change log all_changes = [] for category in ['profile', 'activity', 'social']: all_changes.extend([c for c in self.change_log[category] if c['user_id'] == user_id]) all_changes.sort(key=lambda x: x['timestamp']) complete_log_file = os.path.join(folders['logs'], f"all_changes_{timestamp_str}.json") with open(complete_log_file, 'w', encoding='utf-8') as f: json.dump(all_changes, f, indent=2, ensure_ascii=False) # Save current state snapshot state_file = os.path.join(base_folder, "current_state.json") with open(state_file, 'w', encoding='utf-8') as f: json.dump(self.user_data[user_id], f, indent=2, ensure_ascii=False) # Generate summary summary_file = os.path.join(folders['logs'], f"summary_{timestamp_str}.txt") with open(summary_file, 'w', encoding='utf-8') as f: f.write(f"Profile Stalker Summary\n") f.write("=" * 60 + "\n\n") f.write(f"User ID: {user_id}\n") f.write(f"Username: {self.user_data[user_id]['username']}\n") f.write(f"Display Name: {self.user_data[user_id]['global_name']}\n") f.write(f"Session Duration: {(datetime.now() - self.start_time).total_seconds() / 3600:.1f} hours\n") f.write(f"Reconnections: {self.total_reconnects}\n") f.write(f"Total Changes: {len(all_changes)}\n\n") # Count changes by category and type f.write("Changes by Category:\n") for category in ['profile', 'activity', 'social']: category_changes = [c for c in self.change_log[category] if c['user_id'] == user_id] f.write(f" {category.capitalize()}: {len(category_changes)}\n") f.write("\nChanges by Type:\n") change_types = defaultdict(int) for change in all_changes: change_types[change['type']] += 1 for change_type, count in sorted(change_types.items()): f.write(f" {change_type}: {count}\n") # Activity stats f.write("\n" + "=" * 60 + "\n") f.write("Activity Statistics:\n\n") if user_id in self.message_counts: total_messages = sum(self.message_counts[user_id].values()) f.write(f"šŸ’¬ Messages Sent: {total_messages}\n") f.write(f" Channels: {len(self.message_counts[user_id])}\n") if user_id in self.spotify_history: f.write(f"šŸŽµ Songs Listened: {len(self.spotify_history[user_id])}\n") if user_id in self.voice_history: total_voice_min = sum(s['duration_minutes'] for s in self.voice_history[user_id]) f.write(f"šŸŽ¤ Voice Sessions: {len(self.voice_history[user_id])}\n") f.write(f" Total Time: {total_voice_min:.1f} min ({total_voice_min/60:.1f} hrs)\n") # Current state f.write("\n" + "=" * 60 + "\n") f.write("Current State:\n\n") f.write(f"Status: {self.user_data[user_id]['status']}\n") f.write(f"Custom Status: {self.user_data[user_id]['custom_status']}\n") f.write(f"Nitro: {self.user_data[user_id]['nitro']}\n") f.write(f"Bio: {self.user_data[user_id]['bio'][:100]}...\n" if self.user_data[user_id]['bio'] else "Bio: None\n") f.write(f"Mutual Servers: {len(self.user_data[user_id]['mutual_guilds'])}\n") if self.user_data[user_id]['voice_channel']: f.write(f"Currently in Voice: {self.user_data[user_id]['voice_guild']} > {self.user_data[user_id]['voice_channel']}\n") print(f"\nšŸ“ Logs saved to: {base_folder}/") print(f" └── profile/") print(f" ā”œā”€ā”€ avatars/") print(f" ā”œā”€ā”€ banners/") print(f" └── changes_{timestamp_str}.json") print(f" └── activity/") print(f" ā”œā”€ā”€ changes_{timestamp_str}.json") print(f" ā”œā”€ā”€ messages_{timestamp_str}.txt") print(f" ā”œā”€ā”€ spotify_{timestamp_str}.txt") print(f" └── voice_{timestamp_str}.txt") print(f" └── social/") print(f" └── changes_{timestamp_str}.json") print(f" └── logs/") print(f" ā”œā”€ā”€ all_changes_{timestamp_str}.json") print(f" └── summary_{timestamp_str}.txt") print(f" └── current_state.json") def main(): print("Profile Stalker") print("=" * 60) user_input = input("User ID(s) to stalk (comma-separated): ").strip() user_ids = [u.strip() for u in user_input.split(',') if u.strip()] if not all(u.isdigit() for u in user_ids): print("Invalid user ID(s)") return stalker = ProfileStalker(user_ids) print(f"\nStarting stalker for {len(user_ids)} user(s)...") print("Press Ctrl+C to stop and save logs\n") try: asyncio.run(stalker.stalk()) except KeyboardInterrupt: print("\n\nā¹ļø Stopping...") finally: stalker.save_logs() if __name__ == "__main__": main()