Initial commit: Discord automation tools

This commit is contained in:
2026-01-27 10:13:41 +01:00
commit 7f5d3c2ca7
41 changed files with 983407 additions and 0 deletions

0
scripts/__init__.py Executable file
View File

240
scripts/backup_server.py Executable file
View File

@@ -0,0 +1,240 @@
# discord_tools/scripts/server_backup.py
import os
import sys
import json
import asyncio
import aiohttp
from datetime import datetime
import requests
# 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 DiscordServerBackup:
def __init__(self, guild_id):
self.guild_id = guild_id
self.backup_data = {}
self.backup_folder = os.path.join(DATA_DIR, f"server_backup_{guild_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
self.create_folder_structure()
def create_folder_structure(self):
folders = [
self.backup_folder,
os.path.join(self.backup_folder, "channels"),
os.path.join(self.backup_folder, "images"),
os.path.join(self.backup_folder, "images", "emojis"),
os.path.join(self.backup_folder, "images", "stickers"),
]
for folder in folders:
os.makedirs(folder, exist_ok=True)
def download_file(self, url, filepath):
response = requests.get(url)
if response.status_code == 200:
with open(filepath, 'wb') as f:
f.write(response.content)
print(f"Downloaded {os.path.basename(filepath)}")
else:
print(f"Failed to download {os.path.basename(filepath)}")
async def fetch_guild_info(self):
endpoint = f'/guilds/{self.guild_id}'
response = make_discord_request('GET', endpoint)
if response:
guild_info = response.json()
self.save_json("guild_info", guild_info)
print("Guild information fetched successfully.")
# Download icon
if 'icon' in guild_info:
icon_url = f"https://cdn.discordapp.com/icons/{self.guild_id}/{guild_info['icon']}.png"
self.download_file(icon_url, os.path.join(self.backup_folder, "images", "guild_icon.png"))
# Download banner
if 'banner' in guild_info:
banner_url = f"https://cdn.discordapp.com/banners/{self.guild_id}/{guild_info['banner']}.png"
self.download_file(banner_url, os.path.join(self.backup_folder, "images", "guild_banner.png"))
else:
print("Failed to fetch guild information.")
async def fetch_channels(self):
endpoint = f'/guilds/{self.guild_id}/channels'
response = make_discord_request('GET', endpoint)
if response:
channels = response.json()
self.save_json("channels", channels)
# Create channel structure
channel_structure = self.create_channel_structure(channels)
self.save_json("channel_structure", channel_structure)
# Fetch channel-specific settings
await self.fetch_channel_settings(channels)
print("Channel information and structure fetched successfully.")
else:
print("Failed to fetch channel information.")
def create_channel_structure(self, channels):
structure = {}
categories = {c['id']: c for c in channels if c['type'] == 4}
for channel in channels:
if channel['type'] != 4: # Not a category
parent_id = channel.get('parent_id')
if parent_id:
if parent_id not in structure:
structure[parent_id] = []
structure[parent_id].append(channel)
else:
if 'no_category' not in structure:
structure['no_category'] = []
structure['no_category'].append(channel)
return structure
async def fetch_channel_settings(self, channels):
for channel in channels:
endpoint = f'/channels/{channel["id"]}'
response = make_discord_request('GET', endpoint)
if response:
channel_data = response.json()
self.save_json(f"channels/{channel['id']}_settings", channel_data)
else:
print(f"Failed to fetch settings for channel {channel['name']}")
async def fetch_roles(self):
endpoint = f'/guilds/{self.guild_id}/roles'
response = make_discord_request('GET', endpoint)
if response:
roles = response.json()
self.save_json("roles", roles)
print("Role information fetched successfully.")
else:
print("Failed to fetch role information.")
async def fetch_emojis(self):
endpoint = f'/guilds/{self.guild_id}/emojis'
response = make_discord_request('GET', endpoint)
if response:
emojis = response.json()
self.save_json("emojis", emojis)
print("Emoji information fetched successfully.")
# Download custom emojis
for emoji in emojis:
emoji_url = f"https://cdn.discordapp.com/emojis/{emoji['id']}.png"
self.download_file(emoji_url, os.path.join(self.backup_folder, "images", "emojis", f"{emoji['name']}.png"))
else:
print("Failed to fetch emoji information.")
async def fetch_stickers(self):
endpoint = f'/guilds/{self.guild_id}/stickers'
response = make_discord_request('GET', endpoint)
if response:
stickers = response.json()
self.save_json("stickers", stickers)
print("Sticker information fetched successfully.")
# Download custom stickers
for sticker in stickers:
sticker_url = f"https://cdn.discordapp.com/stickers/{sticker['id']}.png"
self.download_file(sticker_url, os.path.join(self.backup_folder, "images", "stickers", f"{sticker['name']}.png"))
else:
print("Failed to fetch sticker information.")
async def fetch_invites(self):
endpoint = f'/guilds/{self.guild_id}/invites'
response = make_discord_request('GET', endpoint)
if response:
invites = response.json()
self.save_json("invites", invites)
print("Server invites fetched successfully.")
else:
print("Failed to fetch server invites.")
async def fetch_messages(self, channel_id, limit=None):
messages = []
last_message_id = None
while True:
endpoint = f'/channels/{channel_id}/messages?limit=100'
if last_message_id:
endpoint += f'&before={last_message_id}'
response = make_discord_request('GET', endpoint)
if response and response.status_code == 200:
new_messages = response.json()
if not new_messages:
break
messages.extend(new_messages)
last_message_id = new_messages[-1]['id']
if limit and len(messages) >= limit:
messages = messages[:limit]
break
else:
print(f"Failed to fetch messages for channel {channel_id}")
break
return messages
async def backup_messages(self):
channels_data = self.load_json("channels")
if not channels_data:
print("Channel information not available. Skipping message backup.")
return
text_channels = [channel for channel in channels_data if channel['type'] == 0]
for channel in text_channels:
print(f"Backing up messages from channel: {channel['name']}")
messages = await self.fetch_messages(channel['id'])
self.save_json(f"channels/{channel['id']}_messages", messages)
print(f"Backed up {len(messages)} messages from {channel['name']}")
async def create_backup(self, include_messages=False):
tasks = [
self.fetch_guild_info(),
self.fetch_channels(),
self.fetch_roles(),
self.fetch_emojis(),
self.fetch_stickers(),
self.fetch_invites(),
]
await asyncio.gather(*tasks)
if include_messages:
await self.backup_messages()
def save_json(self, filename, data):
filepath = os.path.join(self.backup_folder, f"{filename}.json")
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
def load_json(self, filename):
filepath = os.path.join(self.backup_folder, f"{filename}.json")
try:
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
print(f"File not found: {filepath}")
return None
async def main():
guild_id = input("Enter the Discord server ID to backup: ").strip()
if not guild_id.isdigit():
print("Invalid server ID. Please enter a numeric ID.")
return
include_messages = input("Do you want to backup all messages? (y/n): ").strip().lower() == 'y'
backup = DiscordServerBackup(guild_id)
await backup.create_backup(include_messages)
print(f"Backup completed. Files saved in {backup.backup_folder}")
if __name__ == "__main__":
asyncio.run(main())

99
scripts/chat_fetcher.py Executable file
View File

@@ -0,0 +1,99 @@
# discord_tools/scripts/chat_fetcher.py
import os
import sys
import json
import time
from datetime import datetime
# 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, MAX_MESSAGES_PER_REQUEST
from discord_tools.utils.api_utils import make_discord_request
def fetch_all_messages(channel_id):
"""
Fetch all messages from a specific channel.
:param channel_id: The ID of the channel to fetch messages from
:return: List of all fetched messages
"""
all_messages = []
last_message_id = None
while True:
endpoint = f'/channels/{channel_id}/messages?limit={MAX_MESSAGES_PER_REQUEST}'
if last_message_id:
endpoint += f'&before={last_message_id}'
response = make_discord_request('GET', endpoint)
if not response:
print(f"Failed to fetch messages. Stopping.")
break
new_messages = response.json()
if not new_messages:
break
all_messages.extend(new_messages)
print(f"Fetched {len(all_messages)} messages so far...")
last_message_id = new_messages[-1]['id']
time.sleep(1) # To avoid hitting rate limits
return all_messages
def format_messages(messages):
"""
Format the messages for export.
:param messages: List of message objects
:return: List of formatted message dictionaries
"""
return [
{
'id': msg['id'],
'content': msg['content'],
'author': msg['author']['username'],
'timestamp': msg['timestamp'],
'attachments': [att['url'] for att in msg.get('attachments', [])],
'embeds': msg.get('embeds', [])
} for msg in messages
]
def export_to_json(messages, channel_id):
"""
Export the formatted messages to a JSON file.
:param messages: List of formatted message dictionaries
:param channel_id: The ID of the channel the messages are from
"""
formatted_messages = format_messages(messages)
filename = f'channel_{channel_id}_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
filepath = os.path.join(DATA_DIR, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(formatted_messages, f, ensure_ascii=False, indent=4)
print(f"Exported {len(formatted_messages)} messages to {filepath}")
def main():
channel_id = input("Enter the channel ID to fetch messages from: ").strip()
if not channel_id.isdigit():
print("Invalid channel ID. Please enter a numeric ID.")
return
print("Starting to fetch messages...")
messages = fetch_all_messages(channel_id)
print(f"Finished fetching messages. Total messages: {len(messages)}")
export_to_json(messages, channel_id)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,267 @@
import os
import sys
import json
from datetime import datetime
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
from collections import Counter
import nltk
from nltk.tokenize import word_tokenize
import emoji
from sklearn.linear_model import LinearRegression
import warnings
warnings.filterwarnings("ignore", category=UserWarning, message="Converting to PeriodArray/Index representation will drop timezone information.")
# 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 DATA_DIR
# Download required NLTK resources
nltk.download('punkt', quiet=True)
nltk.download('punkt_tab', quiet=True)
# Set up plot style
plt.style.use('ggplot')
plt.rcParams['font.family'] = 'DejaVu Sans'
class DiscordMessageAnalyzer:
def __init__(self, file_name):
self.file_path = os.path.join(DATA_DIR, file_name)
self.df = self.load_and_process_messages()
self.output_dir = os.path.join(DATA_DIR, 'message_analysis')
self.combined_dir = os.path.join(self.output_dir, 'combined')
self.users = self.df['author'].unique()
self.user_dirs = {user: os.path.join(self.output_dir, user) for user in self.users}
self.create_output_directories()
def load_and_process_messages(self):
with open(self.file_path, 'r', encoding='utf-8') as file:
messages = json.load(file)
df = pd.DataFrame(messages)
df['timestamp'] = pd.to_datetime(df['timestamp'], format='ISO8601')
df['timestamp'] = df['timestamp'].dt.tz_convert('Europe/Stockholm')
df['date'] = df['timestamp'].dt.date
df['hour'] = df['timestamp'].dt.hour
df['day_of_week'] = df['timestamp'].dt.dayofweek
df['month'] = df['timestamp'].dt.month
df['year'] = df['timestamp'].dt.year
df['word_count'] = df['content'].astype(str).apply(lambda x: len(x.split()))
return df
def create_output_directories(self):
os.makedirs(self.output_dir, exist_ok=True)
os.makedirs(self.combined_dir, exist_ok=True)
for user_dir in self.user_dirs.values():
os.makedirs(user_dir, exist_ok=True)
def analyze_message_frequency(self, df):
daily_messages = df.groupby('date').size().reset_index(name='message_count')
peak_day = daily_messages.loc[daily_messages['message_count'].idxmax()]
avg_messages = daily_messages['message_count'].mean()
weekly_messages = df.groupby(df['timestamp'].dt.to_period('W').astype(str)).size()
monthly_messages = df.groupby(df['timestamp'].dt.to_period('M').astype(str)).size()
return {
'daily_messages': daily_messages,
'peak_day': peak_day,
'avg_messages': avg_messages,
'weekly_messages': weekly_messages,
'monthly_messages': monthly_messages
}
def visualize_message_frequency(self, data, output_dir):
plt.figure(figsize=(12, 6))
sns.lineplot(x='date', y='message_count', data=data['daily_messages'])
plt.title('Daily Message Frequency')
plt.xlabel('Date')
plt.ylabel('Number of Messages')
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig(os.path.join(output_dir, 'daily_message_frequency.png'))
plt.close()
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 12))
sns.lineplot(data=data['weekly_messages'], ax=ax1)
ax1.set_title('Weekly Message Trend')
ax1.set_xlabel('Week')
ax1.set_ylabel('Number of Messages')
ax1.tick_params(axis='x', rotation=45)
sns.lineplot(data=data['monthly_messages'], ax=ax2)
ax2.set_title('Monthly Message Trend')
ax2.set_xlabel('Month')
ax2.set_ylabel('Number of Messages')
ax2.tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.savefig(os.path.join(output_dir, 'message_trends.png'))
plt.close()
def analyze_timing(self, df):
hourly_activity = df.groupby('hour').size()
daily_activity = df.groupby('day_of_week').size()
return {
'hourly_activity': hourly_activity,
'daily_activity': daily_activity,
}
def visualize_timing(self, data, output_dir):
plt.figure(figsize=(12, 6))
sns.barplot(x=data['hourly_activity'].index, y=data['hourly_activity'].values)
plt.title('Hourly Message Activity')
plt.xlabel('Hour of Day')
plt.ylabel('Number of Messages')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, 'hourly_activity.png'))
plt.close()
plt.figure(figsize=(12, 6))
sns.barplot(x=['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], y=data['daily_activity'].values)
plt.title('Daily Message Activity')
plt.xlabel('Day of Week')
plt.ylabel('Number of Messages')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, 'daily_activity.png'))
plt.close()
def analyze_content(self, df):
avg_words = df['word_count'].mean()
all_words = ' '.join(df['content'].astype(str).str.lower())
word_freq = Counter(word_tokenize(all_words))
emoji_freq = Counter(char for char in all_words if char in emoji.EMOJI_DATA)
return {
'avg_words': avg_words,
'word_freq': word_freq,
'emoji_freq': emoji_freq,
}
def visualize_content(self, data, output_dir):
plt.figure(figsize=(12, 6))
word_freq_df = pd.DataFrame(data['word_freq'].most_common(20), columns=['Word', 'Frequency'])
sns.barplot(x='Frequency', y='Word', data=word_freq_df)
plt.title('Top 20 Most Common Words')
plt.xlabel('Frequency')
plt.ylabel('Word')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, 'word_frequency.png'))
plt.close()
plt.figure(figsize=(12, 8))
emoji_freq_df = pd.DataFrame(data['emoji_freq'].most_common(20), columns=['Emoji', 'Frequency'])
bars = plt.barh(emoji_freq_df['Emoji'], emoji_freq_df['Frequency'])
plt.title('Top 20 Most Used Emojis')
plt.xlabel('Frequency')
plt.ylabel('Emoji')
for i, bar in enumerate(bars):
width = bar.get_width()
plt.text(width, bar.get_y() + bar.get_height()/2, f'{width}', ha='left', va='center')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, 'emoji_usage.png'))
plt.close()
def analyze_user_comparison(self):
user_stats = {user: {
'message_count': len(df),
'avg_message_length': df['word_count'].mean(),
'most_active_hour': df['hour'].value_counts().index[0],
'most_used_words': Counter(' '.join(df['content'].astype(str)).split()).most_common(10)
} for user, df in self.df.groupby('author')}
return user_stats
def visualize_user_comparison(self, data, output_dir):
plt.figure(figsize=(12, 6))
sns.barplot(x=[user for user in self.users], y=[data[user]['message_count'] for user in self.users])
plt.title('Message Count by User')
plt.xlabel('User')
plt.ylabel('Number of Messages')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, 'user_message_count.png'))
plt.close()
plt.figure(figsize=(12, 6))
sns.barplot(x=[user for user in self.users], y=[data[user]['avg_message_length'] for user in self.users])
plt.title('Average Message Length by User')
plt.xlabel('User')
plt.ylabel('Average Word Count')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, 'user_avg_message_length.png'))
plt.close()
def analyze_advanced_analytics(self, df):
monthly_activity = df.groupby([df['timestamp'].dt.to_period('M').astype(str), 'author']).size().unstack()
X = np.array(range(len(monthly_activity))).reshape(-1, 1)
y = monthly_activity.sum(axis=1).values
model = LinearRegression().fit(X, y)
future_months = np.array(range(len(monthly_activity), len(monthly_activity) + 6)).reshape(-1, 1)
predicted_activity = model.predict(future_months)
return {
'monthly_activity': monthly_activity,
'predicted_activity': predicted_activity
}
def visualize_advanced_analytics(self, data, output_dir):
plt.figure(figsize=(12, 6))
data['monthly_activity'].plot(kind='area', stacked=True)
plt.title('Communication Patterns Over Time')
plt.xlabel('Month')
plt.ylabel('Number of Messages')
plt.legend(title='User', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, 'communication_patterns.png'))
plt.close()
plt.figure(figsize=(12, 6))
plt.plot(range(len(data['monthly_activity'])), data['monthly_activity'].sum(axis=1), label='Actual')
plt.plot(range(len(data['monthly_activity']), len(data['monthly_activity']) + 6), data['predicted_activity'], label='Predicted')
plt.title('Message Activity Trend and Prediction')
plt.xlabel('Months from Start')
plt.ylabel('Number of Messages')
plt.legend()
plt.tight_layout()
plt.savefig(os.path.join(output_dir, 'activity_prediction.png'))
plt.close()
def run_analysis(self):
print("Running combined analysis...")
self.run_analysis_for_data(self.df, self.combined_dir)
for user in self.users:
print(f"Running analysis for user: {user}")
user_df = self.df[self.df['author'] == user]
self.run_analysis_for_data(user_df, self.user_dirs[user])
def run_analysis_for_data(self, df, output_dir):
freq_data = self.analyze_message_frequency(df)
self.visualize_message_frequency(freq_data, output_dir)
timing_data = self.analyze_timing(df)
self.visualize_timing(timing_data, output_dir)
content_data = self.analyze_content(df)
self.visualize_content(content_data, output_dir)
if output_dir == self.combined_dir:
user_data = self.analyze_user_comparison()
self.visualize_user_comparison(user_data, output_dir)
advanced_data = self.analyze_advanced_analytics(df)
self.visualize_advanced_analytics(advanced_data, output_dir)
def main():
file_name = 'message_data.json'
analyzer = DiscordMessageAnalyzer(file_name)
analyzer.run_analysis()
print(f"Analysis complete. Results saved in '{analyzer.output_dir}'.")
if __name__ == "__main__":
main()

47
scripts/dm_opener.py Executable file
View File

@@ -0,0 +1,47 @@
# discord_tools/scripts/dm_opener.py
import os
import sys
# 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.utils.api_utils import make_discord_request
from discord_tools.config.settings import ERROR_MESSAGES
def open_dm(recipient_id):
"""
Opens a Discord DM channel using the recipient's user ID.
:param recipient_id: The user ID of the recipient
:return: The channel ID of the created DM, or None if the request failed
"""
url = "/users/@me/channels"
data = {
"recipient_id": recipient_id
}
response = make_discord_request('POST', url, json=data)
if response:
channel_data = response.json()
channel_id = channel_data["id"]
print(f"DM channel created successfully: {channel_id}")
return channel_id
else:
print(ERROR_MESSAGES["api_error"])
return None
def main():
recipient_id = input("Enter the recipient's user ID: ")
channel_id = open_dm(recipient_id)
if channel_id:
print(f"Opened channel: {channel_id}")
else:
print("Failed to open DM channel.")
if __name__ == "__main__":
main()

131
scripts/ghost_typer.py Executable file
View File

@@ -0,0 +1,131 @@
# discord_tools/scripts/ghost_typer.py
import os
import sys
import time
# 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.utils.api_utils import make_discord_request
def send_typing_indicator(channel_id):
# Send typing indicator to a channel
# This makes it look like you're typing
endpoint = f'/channels/{channel_id}/typing'
response = make_discord_request('POST', endpoint)
return response is not None
def ghost_type_continuous(channel_id, duration=None):
# Keep sending typing indicator for a specified duration or indefinitely
print(f"Ghost typing in channel: {channel_id}")
if duration:
print(f"Duration: {duration} seconds")
else:
print("Duration: Infinite (press Ctrl+C to stop)")
print("=" * 60)
start_time = time.time()
count = 0
try:
while True:
success = send_typing_indicator(channel_id)
if success:
count += 1
elapsed = time.time() - start_time
print(f"[{count}] Typing indicator sent ({elapsed:.0f}s elapsed)")
else:
print(f"[!] Failed to send typing indicator")
# Typing indicator lasts ~10 seconds, send every 8 to be safe
time.sleep(8)
# Check if duration is up
if duration and (time.time() - start_time) >= duration:
break
except KeyboardInterrupt:
print("\n\nStopped ghost typing")
total_time = time.time() - start_time
print("\n" + "=" * 60)
print(f"Total indicators sent: {count}")
print(f"Total time: {total_time:.0f} seconds")
print("=" * 60)
def ghost_type_burst(channel_id, count):
# Send typing indicator a specific number of times
print(f"Sending {count} typing indicators to channel: {channel_id}")
print("=" * 60)
success_count = 0
for i in range(count):
success = send_typing_indicator(channel_id)
if success:
success_count += 1
print(f"[{i+1}/{count}] Sent")
else:
print(f"[{i+1}/{count}] Failed")
# Small delay between sends
if i < count - 1:
time.sleep(2)
print("\n" + "=" * 60)
print(f"Successfully sent: {success_count}/{count}")
print("=" * 60)
def main():
print("Ghost Typer")
print("=" * 60)
print("Send typing indicators without actually typing\n")
print("1. Continuous ghost typing")
print("2. Timed ghost typing")
print("3. Burst (send X times)")
print("=" * 60)
choice = input("Choice: ").strip()
channel_id = input("\nChannel ID: ").strip()
if not channel_id.isdigit():
print("Invalid channel ID")
return
if choice == '1':
# Continuous
ghost_type_continuous(channel_id)
elif choice == '2':
# Timed
duration = input("Duration (seconds): ").strip()
if not duration.isdigit():
print("Invalid duration")
return
ghost_type_continuous(channel_id, int(duration))
elif choice == '3':
# Burst
count = input("How many times: ").strip()
if not count.isdigit():
print("Invalid count")
return
ghost_type_burst(channel_id, int(count))
else:
print("Invalid choice")
if __name__ == "__main__":
main()

209
scripts/image_fetcher.py Executable file
View File

@@ -0,0 +1,209 @@
# discord_tools/scripts/image_downloader.py
import os
import sys
import requests
from datetime import datetime
# 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.utils.api_utils import make_discord_request
from discord_tools.config.settings import ERROR_MESSAGES
# Image file extensions to look for
IMAGE_EXTENSIONS = ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')
def fetch_messages(channel_id, before=None, limit=100):
"""
Fetch messages from a Discord channel.
:param channel_id: The channel ID to fetch messages from
:param before: Message ID to fetch messages before (for pagination)
:param limit: Number of messages to fetch (max 100)
:return: List of messages or None if the request failed
"""
endpoint = f"/channels/{channel_id}/messages"
params = {"limit": limit}
if before:
params["before"] = before
response = make_discord_request('GET', endpoint, params=params)
if response:
return response.json()
return None
def extract_images_from_messages(messages, user_id=None):
"""
Extract all image URLs from a list of messages.
:param messages: List of Discord message objects
:param user_id: Optional user ID to filter messages by
:return: List of tuples (image_url, filename, message_id, timestamp)
"""
images = []
for message in messages:
# Filter by user if specified
if user_id and message.get('author', {}).get('id') != user_id:
continue
message_id = message.get('id')
timestamp = message.get('timestamp', '')
# Check attachments
for attachment in message.get('attachments', []):
url = attachment.get('url')
filename = attachment.get('filename', 'unknown')
# Check if it's an image
if url and filename.lower().endswith(IMAGE_EXTENSIONS):
images.append((url, filename, message_id, timestamp))
# Check embeds for images
for embed in message.get('embeds', []):
# Embed image
if embed.get('type') == 'image' and embed.get('thumbnail'):
url = embed['thumbnail'].get('url')
if url:
filename = f"embed_{message_id}_{url.split('/')[-1]}"
images.append((url, filename, message_id, timestamp))
# Embed image field
if embed.get('image'):
url = embed['image'].get('url')
if url:
filename = f"embed_{message_id}_{url.split('/')[-1]}"
images.append((url, filename, message_id, timestamp))
return images
def download_image(url, filepath):
"""
Download an image from a URL to a local file.
:param url: Image URL
:param filepath: Local file path to save the image
:return: True if successful, False otherwise
"""
try:
response = requests.get(url, timeout=30)
response.raise_for_status()
with open(filepath, 'wb') as f:
f.write(response.content)
return True
except Exception as e:
print(f"Failed to download {url}: {e}")
return False
def download_all_images(channel_id, output_dir=None, user_id=None):
"""
Download all images from a Discord channel.
:param channel_id: The channel ID to download images from
:param output_dir: Directory to save images (defaults to project_root/data/images/{channel_id})
:param user_id: Optional user ID to filter images by specific user
:return: Number of images downloaded
"""
# Set up output directory
if output_dir is None:
# Use the project root data folder
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(script_dir)
output_dir = os.path.join(project_root, "data", "images", channel_id)
# Add user ID to path if filtering by user
if user_id:
output_dir = os.path.join(output_dir, f"user_{user_id}")
os.makedirs(output_dir, exist_ok=True)
if user_id:
print(f"Fetching messages from channel {channel_id} (filtering by user {user_id})...")
else:
print(f"Fetching messages from channel {channel_id}...")
all_images = []
before = None
total_messages = 0
# Fetch all messages with pagination
while True:
messages = fetch_messages(channel_id, before=before, limit=100)
if not messages:
if total_messages == 0:
print(ERROR_MESSAGES.get("api_error", "Failed to fetch messages"))
return 0
break
if len(messages) == 0:
break
total_messages += len(messages)
print(f"Fetched {total_messages} messages so far...")
# Extract images from these messages
images = extract_images_from_messages(messages, user_id)
all_images.extend(images)
# Set before to the last message ID for pagination
before = messages[-1]['id']
# If we got fewer than 100 messages, we've reached the end
if len(messages) < 100:
break
print(f"\nFound {len(all_images)} images in {total_messages} messages")
if len(all_images) == 0:
print("No images to download.")
return 0
# Download all images
print(f"\nDownloading images to {output_dir}...\n")
downloaded = 0
for i, (url, filename, message_id, timestamp) in enumerate(all_images, 1):
# Create a unique filename with timestamp and message ID
name, ext = os.path.splitext(filename)
safe_filename = f"{i:04d}_{message_id}_{name}{ext}"
filepath = os.path.join(output_dir, safe_filename)
print(f"[{i}/{len(all_images)}] Downloading {filename}...")
if download_image(url, filepath):
downloaded += 1
print(f"\nSuccessfully downloaded {downloaded}/{len(all_images)} images")
print(f"Images saved to: {os.path.abspath(output_dir)}")
return downloaded
def main():
print("Discord Image Downloader")
print("=" * 50)
channel_id = input("Enter the channel ID: ").strip()
if not channel_id:
print("Error: Channel ID cannot be empty")
return
user_id = input("Enter user ID to filter by (press Enter to download from all users): ").strip()
user_id = user_id if user_id else None
custom_dir = input("Enter output directory (press Enter for default): ").strip()
output_dir = custom_dir if custom_dir else None
print()
download_all_images(channel_id, output_dir, user_id)
if __name__ == "__main__":
main()

157
scripts/message_deletor.py Executable file
View File

@@ -0,0 +1,157 @@
# discord_tools/scripts/message_deletor.py
import os
import sys
import json
import time
# 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 MAX_MESSAGES_PER_REQUEST, ERROR_MESSAGES
from discord_tools.utils.api_utils import make_discord_request, get_user_id
def search_messages_in_channel(channel_id, user_id):
"""
Search for messages from a specific user in a channel.
:param channel_id: The ID of the channel to search
:param user_id: The ID of the user whose messages to search for
:return: List of message objects
"""
messages = []
offset = 0
while True:
endpoint = f'/channels/{channel_id}/messages/search?author_id={user_id}&offset={offset}'
response = make_discord_request('GET', endpoint)
if not response:
print(f"Failed to search messages in channel {channel_id}")
break
data = response.json()
total_results = data.get('total_results', 0)
messages.extend(msg for msg_group in data.get('messages', []) for msg in msg_group)
print(f"Found {len(messages)} messages. Total: {len(messages)}/{total_results}")
if len(messages) >= total_results or not data.get('messages'):
break
offset += MAX_MESSAGES_PER_REQUEST
time.sleep(1) # Small delay between searches
return messages
def delete_message(channel_id, message_id):
"""
Delete a specific message.
:param channel_id: The ID of the channel containing the message
:param message_id: The ID of the message to delete
:return: True if deletion was successful, False otherwise
"""
endpoint = f'/channels/{channel_id}/messages/{message_id}'
response = make_discord_request('DELETE', endpoint)
if response:
print(f"Successfully deleted message {message_id} in channel {channel_id}")
return True
else:
print(f"Failed to delete message {message_id} in channel {channel_id}")
return False
def delete_messages_search_method(channel_id, user_id):
"""
Delete all messages from a specific user in a channel using the search method.
:param channel_id: The ID of the channel to delete messages from
:param user_id: The ID of the user whose messages to delete
:return: Number of messages deleted
"""
print(f"Deleting messages using search method in channel {channel_id}")
messages = search_messages_in_channel(channel_id, user_id)
messages_deleted = 0
for message in messages:
if delete_message(channel_id, message['id']):
messages_deleted += 1
print(f"Deleted {messages_deleted} messages in channel {channel_id}")
return messages_deleted
def delete_messages_bulk_method(channel_id, user_id):
"""
Delete all messages from a specific user in a channel using the bulk method.
:param channel_id: The ID of the channel to delete messages from
:param user_id: The ID of the user whose messages to delete
:return: Number of messages deleted
"""
print(f"Deleting messages using bulk method in channel {channel_id}")
messages_deleted = 0
last_message_id = None
while True:
endpoint = f'/channels/{channel_id}/messages?limit=100'
if last_message_id:
endpoint += f'&before={last_message_id}'
response = make_discord_request('GET', endpoint)
if not response:
print(f"Failed to fetch messages in channel {channel_id}")
break
messages = response.json()
if not messages:
print(f"No more messages to delete in channel {channel_id}")
break
user_messages = [msg for msg in messages if msg['author']['id'] == user_id]
for message in user_messages:
if delete_message(channel_id, message['id']):
messages_deleted += 1
last_message_id = messages[-1]['id']
time.sleep(1) # Small delay between bulk fetches
print(f"Deleted {messages_deleted} messages in channel {channel_id}")
return messages_deleted
def main():
user_id = get_user_id()
if not user_id:
print(ERROR_MESSAGES["api_error"])
return
print(f"User ID: {user_id}")
while True:
print("\nChoose a deletion method:")
print("1. Search method (more thorough, but slower)")
print("2. Bulk method (faster, but might miss some messages in large channels)")
print("3. Exit")
choice = input("Enter your choice (1, 2, or 3): ").strip()
if choice == '3':
print("Exiting the program.")
break
elif choice not in ['1', '2']:
print("Invalid choice. Please enter 1, 2, or 3.")
continue
delete_method = delete_messages_search_method if choice == '1' else delete_messages_bulk_method
while True:
channel_id = input("Enter a channel ID to delete messages from (or press Enter to go back to method selection): ").strip()
if not channel_id:
break
delete_method(channel_id, user_id)
print("Message deletion process completed.")
if __name__ == "__main__":
main()

204
scripts/pfp_downloader.py Executable file
View File

@@ -0,0 +1,204 @@
# discord_tools/scripts/pfp_downloader.py
import os
import sys
import asyncio
import aiohttp
import websockets
import json
from urllib.parse import urlparse
# 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, DEFAULT_AVATAR_SIZE, DATA_DIR
from discord_tools.utils.api_utils import make_discord_request
async def download_avatar(session, user, folder):
# Download their pfp. Shocking, I know.
if user.get('avatar'):
avatar_url = f"https://cdn.discordapp.com/avatars/{user['id']}/{user['avatar']}.png?size={DEFAULT_AVATAR_SIZE}"
filename = f"{user['username']}_{user['id']}.png"
filepath = os.path.join(folder, filename)
async with session.get(avatar_url) as resp:
if resp.status == 200:
with open(filepath, 'wb') as f:
f.write(await resp.read())
print(f"Downloaded avatar for {user['username']}")
else:
print(f"Failed to download avatar for {user['username']}")
else:
print(f"No avatar found for {user['username']}")
def get_guild_info(guild_id):
# Get guild info so we know how many people we're dealing with
endpoint = f'/guilds/{guild_id}?with_counts=true'
response = make_discord_request('GET', endpoint)
if not response:
print(f"Failed to fetch guild info for {guild_id}")
return None
return response.json()
async def get_guild_members_via_gateway(guild_id):
# Scrape members via gateway because Discord's API is useless for user tokens
members = {}
gateway_url = "wss://gateway.discord.gg/?v=9&encoding=json"
# Figure out how many members exist
guild_info = get_guild_info(guild_id)
if not guild_info:
print("Could not get guild info, using default member count estimate")
approximate_member_count = 200
else:
approximate_member_count = guild_info.get('approximate_member_count', 200)
print(f"Guild has approximately {approximate_member_count} members")
async with websockets.connect(gateway_url, max_size=16 * 1024 * 1024) as ws:
# Get hello, because apparently we need a handshake
hello = json.loads(await ws.recv())
heartbeat_interval = hello['d']['heartbeat_interval']
# Identify ourselves
identify = {
"op": 2,
"d": {
"token": DISCORD_TOKEN,
"properties": {
"$os": "windows",
"$browser": "chrome",
"$device": "pc"
},
"compress": False
}
}
await ws.send(json.dumps(identify))
# Keep the connection alive or it'll die on us
async def heartbeat():
while True:
await asyncio.sleep(heartbeat_interval / 1000)
await ws.send(json.dumps({"op": 1, "d": None}))
heartbeat_task = asyncio.create_task(heartbeat())
# Wait for ready event
ready = False
while not ready:
msg = json.loads(await ws.recv())
if msg.get('t') == 'READY':
ready = True
print("Connected to Discord Gateway")
# Build ranges for member scraping (100 at a time because Discord)
ranges = []
chunk_size = 100
for i in range(0, approximate_member_count + chunk_size, chunk_size):
ranges.append([i, min(i + chunk_size - 1, approximate_member_count)])
print(f"Requesting member list in {len(ranges)} chunks...")
subscribe = {
"op": 14,
"d": {
"guild_id": guild_id,
"typing": False,
"activities": False,
"threads": False,
"members": [],
"channels": {}
}
}
# Need a channel ID for this to work. Any channel will do.
channels_endpoint = f'/guilds/{guild_id}/channels'
channels_response = make_discord_request('GET', channels_endpoint)
if channels_response:
channels = channels_response.json()
if channels:
first_channel_id = channels[0]['id']
subscribe['d']['channels'][first_channel_id] = ranges
print(f"Using channel {first_channel_id} for member scraping")
await ws.send(json.dumps(subscribe))
print(f"Sent subscription request")
# Collect members from the member list updates
timeout = 20
last_member_time = asyncio.get_event_loop().time()
try:
while True:
current_time = asyncio.get_event_loop().time()
remaining_timeout = timeout - (current_time - last_member_time)
if remaining_timeout <= 0:
print("Timeout - no new members received")
break
msg = await asyncio.wait_for(ws.recv(), timeout=remaining_timeout)
data = json.loads(msg)
event_type = data.get('t')
if event_type == 'GUILD_MEMBER_LIST_UPDATE':
ops = data['d'].get('ops', [])
for op in ops:
# SYNC ops contain the bulk of members
if op.get('op') == 'SYNC':
items = op.get('items', [])
for item in items:
if 'member' in item:
member = item['member']
user = member.get('user')
if user:
members[user['id']] = user
last_member_time = asyncio.get_event_loop().time()
# INSERT ops are for individual member additions
elif op.get('op') == 'INSERT':
item = op.get('item', {})
if 'member' in item:
member = item['member']
user = member.get('user')
if user:
members[user['id']] = user
last_member_time = asyncio.get_event_loop().time()
# Progress update so we know it's not frozen
if len(members) % 50 == 0 and len(members) > 0:
print(f"Collected {len(members)} members so far...")
except asyncio.TimeoutError:
print(f"Timeout - collected {len(members)} members total")
heartbeat_task.cancel()
return members
async def download_guild_avatars(guild_id):
# Main function that does the thing
folder = os.path.join(DATA_DIR, f"avatars_{guild_id}")
os.makedirs(folder, exist_ok=True)
print("Fetching guild members via Gateway...")
members = await get_guild_members_via_gateway(guild_id)
print(f"\nTotal members found: {len(members)}")
async with aiohttp.ClientSession() as session:
tasks = [download_avatar(session, user, folder) for user in members.values()]
await asyncio.gather(*tasks)
print(f"All available avatars downloaded for guild {guild_id}!")
def main():
guild_id = input("Enter the guild ID to download avatars from: ").strip()
if not guild_id.isdigit():
print("Invalid guild ID. Please enter a numeric ID.")
return
asyncio.run(download_guild_avatars(guild_id))
if __name__ == "__main__":
main()

1024
scripts/profile_stalker.py Executable file

File diff suppressed because it is too large Load Diff

57
scripts/status_updater.py Executable file
View File

@@ -0,0 +1,57 @@
# discord_tools/scripts/status_updater.py
import os
import sys
import time
import datetime
# 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 DEFAULT_STATUS_UPDATE_INTERVAL
from discord_tools.utils.api_utils import make_discord_request
def update_status(start_datetime):
"""
Update the Discord status with the time elapsed since start_datetime.
:param start_datetime: The datetime to calculate the elapsed time from
:return: True if the status was updated successfully, False otherwise
"""
now = datetime.datetime.now()
time_diff = now - start_datetime
days = time_diff.days
hours, remainder = divmod(time_diff.seconds, 3600)
minutes, _ = divmod(remainder, 60)
status_text = f"{days} days, {hours} hours, {minutes} minutes"
payload = {
"custom_status": {
"text": status_text
}
}
response = make_discord_request('PATCH', '/users/@me/settings', json=payload)
return response is not None
def main():
start_date = datetime.datetime(2024, 2, 24, 3, 43)
update_interval = DEFAULT_STATUS_UPDATE_INTERVAL
print(f"Starting status updater. Press Ctrl+C to exit.")
try:
while True:
if update_status(start_date):
print(f"Status updated successfully at {datetime.datetime.now()}")
else:
print("Failed to update status")
time.sleep(update_interval)
except KeyboardInterrupt:
print("\nStatus updater stopped.")
if __name__ == "__main__":
main()

209
scripts/token_validator.py Executable file
View File

@@ -0,0 +1,209 @@
# discord_tools/scripts/token_validator.py
import os
import sys
import requests
from datetime import datetime
# 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_API_BASE_URL
def validate_token(token):
# Check if a Discord token is valid
url = f"{DISCORD_API_BASE_URL}/users/@me"
headers = {
"Authorization": token,
"Content-Type": "application/json"
}
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
user_data = response.json()
return {
'valid': True,
'user_id': user_data['id'],
'username': user_data['username'],
'discriminator': user_data.get('discriminator', '0'),
'email': user_data.get('email', 'N/A'),
'verified': user_data.get('verified', False),
'mfa_enabled': user_data.get('mfa_enabled', False),
'flags': user_data.get('flags', 0),
'premium_type': user_data.get('premium_type', 0),
'phone': user_data.get('phone', 'N/A')
}
elif response.status_code == 401:
return {'valid': False, 'error': 'Invalid token'}
elif response.status_code == 403:
return {'valid': False, 'error': 'Token is valid but locked/disabled'}
else:
return {'valid': False, 'error': f'HTTP {response.status_code}'}
except requests.exceptions.RequestException as e:
return {'valid': False, 'error': f'Request failed: {str(e)}'}
def get_token_type(token):
# Determine what kind of token it is
if token.startswith('mfa.'):
return 'MFA Token'
elif token.startswith('Bot '):
return 'Bot Token'
elif '.' in token:
# User tokens typically have format: base64.base64.base64
parts = token.split('.')
if len(parts) == 3:
return 'User Token'
return 'Unknown'
def print_token_info(result, token):
# Print info about a token
print("\n" + "=" * 60)
if result['valid']:
print("✓ TOKEN IS VALID")
print("=" * 60)
print(f"Type: {get_token_type(token)}")
print(f"User ID: {result['user_id']}")
print(f"Username: {result['username']}#{result['discriminator']}")
print(f"Email: {result['email']}")
print(f"Verified: {result['verified']}")
print(f"MFA Enabled: {result['mfa_enabled']}")
print(f"Phone: {result['phone']}")
# Decode premium type
premium_types = {
0: 'None',
1: 'Nitro Classic',
2: 'Nitro',
3: 'Nitro Basic'
}
print(f"Premium: {premium_types.get(result['premium_type'], 'Unknown')}")
else:
print("✗ TOKEN IS INVALID")
print("=" * 60)
print(f"Type: {get_token_type(token)}")
print(f"Error: {result['error']}")
print("=" * 60)
def validate_tokens_from_file(filepath):
# Validate multiple tokens from a file
if not os.path.exists(filepath):
print(f"File not found: {filepath}")
return
with open(filepath, 'r') as f:
tokens = [line.strip() for line in f if line.strip()]
print(f"Validating {len(tokens)} tokens from {filepath}...")
valid_count = 0
invalid_count = 0
results = []
for i, token in enumerate(tokens, 1):
print(f"\n[{i}/{len(tokens)}] Checking token...")
result = validate_token(token)
if result['valid']:
valid_count += 1
status = "VALID"
else:
invalid_count += 1
status = "INVALID"
results.append({
'token': token,
'status': status,
'result': result
})
print_token_info(result, token)
# Summary
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
print(f"Total Tokens: {len(tokens)}")
print(f"Valid: {valid_count}")
print(f"Invalid: {invalid_count}")
print("=" * 60)
# Save results
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
output_file = f"token_validation_{timestamp}.txt"
with open(output_file, 'w') as f:
f.write(f"Token Validation Results\n")
f.write(f"Validated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"Total: {len(tokens)} | Valid: {valid_count} | Invalid: {invalid_count}\n")
f.write("=" * 80 + "\n\n")
for i, item in enumerate(results, 1):
f.write(f"[{i}] {item['status']}\n")
f.write(f"Token: {item['token'][:20]}...{item['token'][-10:]}\n")
if item['result']['valid']:
f.write(f"User: {item['result']['username']}#{item['result']['discriminator']}\n")
f.write(f"ID: {item['result']['user_id']}\n")
else:
f.write(f"Error: {item['result']['error']}\n")
f.write("\n")
print(f"\nResults saved to: {output_file}")
def validate_env_token():
# Check the token from the .env file
try:
from discord_tools.config.settings import DISCORD_TOKEN
if not DISCORD_TOKEN:
print("No token found in .env file")
return
print("\nValidating token from .env file...")
result = validate_token(DISCORD_TOKEN)
print_token_info(result, DISCORD_TOKEN)
except Exception as e:
print(f"Failed to load token from .env: {e}")
def main():
print("Discord Token Validator")
print("=" * 60)
print("1. Validate single token")
print("2. Validate tokens from file")
print("3. Validate token from .env")
print("=" * 60)
choice = input("Choice: ").strip()
if choice == '1':
token = input("\nEnter Discord token: ").strip()
if not token:
print("No token provided")
return
print("\nValidating token...")
result = validate_token(token)
print_token_info(result, token)
elif choice == '2':
filepath = input("\nEnter path to tokens file (one token per line): ").strip()
validate_tokens_from_file(filepath)
elif choice == '3':
validate_env_token()
else:
print("Invalid choice")
if __name__ == "__main__":
main()

242
scripts/user_history_scraper.py Executable file
View File

@@ -0,0 +1,242 @@
# discord_tools/scripts/user_history_scraper.py
import os
import sys
import asyncio
import json
from datetime import datetime
# 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 DATA_DIR
from discord_tools.utils.api_utils import make_discord_request
def get_mutual_guilds():
# Get all servers you're in
response = make_discord_request('GET', '/users/@me/guilds')
if not response:
print("Failed to fetch guilds")
return []
return response.json()
def get_guild_channels(guild_id):
# Get channels in a server
response = make_discord_request('GET', f'/guilds/{guild_id}/channels')
if not response:
return []
return response.json()
def search_user_messages_in_guild(guild_id, user_id):
# Use Discord's search API to find messages from a specific user
# Way faster than manually paginating through every channel
all_messages = []
offset = 0
while True:
endpoint = f'/guilds/{guild_id}/messages/search?author_id={user_id}&offset={offset}'
response = make_discord_request('GET', endpoint)
if not response:
break
data = response.json()
messages = data.get('messages', [])
if not messages:
break
# Discord returns messages in a weird nested format
# Each item is a list containing related messages
for message_group in messages:
if isinstance(message_group, list):
for msg in message_group:
if msg['author']['id'] == user_id:
all_messages.append(msg)
else:
if message_group['author']['id'] == user_id:
all_messages.append(message_group)
# Pagination
offset += 25
total_results = data.get('total_results', 0)
print(f" Found {len(all_messages)} messages so far...")
# If we've gotten all results, stop
if offset >= total_results:
break
return all_messages
def get_user_info_from_guild(guild_id, user_id):
# Try to get user info from guild member endpoint
endpoint = f'/guilds/{guild_id}/members/{user_id}'
response = make_discord_request('GET', endpoint)
if response:
member_data = response.json()
return member_data.get('user')
return None
def format_message_for_export(msg):
# Convert message to a readable format
timestamp = datetime.fromisoformat(msg['timestamp'].replace('Z', '+00:00'))
formatted = {
'id': msg['id'],
'timestamp': timestamp.strftime('%Y-%m-%d %H:%M:%S'),
'content': msg['content'],
'attachments': [att['url'] for att in msg.get('attachments', [])],
'embeds': len(msg.get('embeds', [])),
'mentions': [f"{u['username']}#{u['discriminator']}" for u in msg.get('mentions', [])],
'edited': msg.get('edited_timestamp') is not None
}
return formatted
def main():
print("User History Scraper")
print("=" * 50)
user_id = input("Enter the user ID to scrape: ").strip()
if not user_id.isdigit():
print("Invalid user ID")
return
print(f"\nScraping messages from user ID: {user_id}")
print("=" * 50)
# Get all mutual guilds
guilds = get_mutual_guilds()
print(f"Found {len(guilds)} servers")
all_scraped_messages = {}
total_messages = 0
username = "Unknown"
for guild in guilds:
guild_id = guild['id']
guild_name = guild['name']
print(f"\nSearching {guild_name}...")
# Try to get user info from this guild
if username == "Unknown":
user_info = get_user_info_from_guild(guild_id, user_id)
if user_info:
username = f"{user_info['username']}#{user_info.get('discriminator', '0')}"
print(f" Found user: {username}")
# Use search API to find all messages from this user in this guild
messages = search_user_messages_in_guild(guild_id, user_id)
if messages:
# Organize by channel
channels_dict = {}
for msg in messages:
channel_id = msg['channel_id']
if channel_id not in channels_dict:
channels_dict[channel_id] = []
channels_dict[channel_id].append(msg)
all_scraped_messages[guild_name] = channels_dict
total_messages += len(messages)
print(f" Total in {guild_name}: {len(messages)} messages across {len(channels_dict)} channels")
print("\n" + "=" * 50)
print(f"Scraping complete. Total messages found: {total_messages}")
if total_messages == 0:
print("No messages found for this user.")
return
# If we still don't have username, try to get it from first message
if username == "Unknown":
for guild_messages in all_scraped_messages.values():
for messages in guild_messages.values():
if messages:
first_msg = messages[0]
author = first_msg['author']
username = f"{author['username']}#{author.get('discriminator', '0')}"
break
if username != "Unknown":
break
print(f"User identified as: {username}")
# Save to file
output_folder = os.path.join(DATA_DIR, f"user_history_{user_id}")
os.makedirs(output_folder, exist_ok=True)
# Save raw JSON
raw_file = os.path.join(output_folder, "raw_messages.json")
with open(raw_file, 'w', encoding='utf-8') as f:
json.dump(all_scraped_messages, f, indent=2, ensure_ascii=False)
# Save formatted text file
formatted_file = os.path.join(output_folder, "formatted_messages.txt")
with open(formatted_file, 'w', encoding='utf-8') as f:
f.write(f"Message History for {username}\n")
f.write(f"Scraped on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"Total Messages: {total_messages}\n")
f.write("=" * 80 + "\n\n")
for guild_name, channels in all_scraped_messages.items():
f.write(f"\n{'=' * 80}\n")
f.write(f"SERVER: {guild_name}\n")
f.write(f"{'=' * 80}\n\n")
for channel_id, messages in channels.items():
# Try to get channel name from first message
channel_name = f"channel-{channel_id}"
if messages:
# Channel info might be in message metadata
channel_name = messages[0].get('channel_id', channel_id)
f.write(f"\n--- Channel {channel_name} ({len(messages)} messages) ---\n\n")
# Sort messages by timestamp
sorted_messages = sorted(messages, key=lambda x: x['timestamp'])
for msg in sorted_messages:
formatted = format_message_for_export(msg)
f.write(f"[{formatted['timestamp']}]\n")
f.write(f"{formatted['content']}\n")
if formatted['attachments']:
f.write(f"Attachments: {', '.join(formatted['attachments'])}\n")
if formatted['edited']:
f.write("(edited)\n")
f.write("\n")
# Generate summary stats
stats_file = os.path.join(output_folder, "stats.txt")
with open(stats_file, 'w', encoding='utf-8') as f:
f.write(f"Message Statistics for {username}\n")
f.write("=" * 50 + "\n\n")
f.write(f"Total Messages: {total_messages}\n")
f.write(f"Servers: {len(all_scraped_messages)}\n\n")
f.write("Messages per server:\n")
for guild_name, channels in all_scraped_messages.items():
guild_total = sum(len(msgs) for msgs in channels.values())
f.write(f" {guild_name}: {guild_total}\n")
f.write("\nMessages per channel:\n")
for guild_name, channels in all_scraped_messages.items():
for channel_id, messages in channels.items():
f.write(f" {guild_name} > {channel_id}: {len(messages)}\n")
print(f"\nResults saved to: {output_folder}")
print(f" - raw_messages.json (complete data)")
print(f" - formatted_messages.txt (readable format)")
print(f" - stats.txt (summary)")
if __name__ == "__main__":
main()

218
scripts/video_fetcher.py Executable file
View File

@@ -0,0 +1,218 @@
# discord_tools/scripts/video_downloader.py
import os
import sys
import requests
from datetime import datetime
# 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.utils.api_utils import make_discord_request
from discord_tools.config.settings import ERROR_MESSAGES
# Video file extensions to look for
VIDEO_EXTENSIONS = ('.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv', '.wmv', '.m4v', '.mpeg', '.mpg', '.3gp', '.ogv')
def fetch_messages(channel_id, before=None, limit=100):
"""
Fetch messages from a Discord channel.
:param channel_id: The channel ID to fetch messages from
:param before: Message ID to fetch messages before (for pagination)
:param limit: Number of messages to fetch (max 100)
:return: List of messages or None if the request failed
"""
endpoint = f"/channels/{channel_id}/messages"
params = {"limit": limit}
if before:
params["before"] = before
response = make_discord_request('GET', endpoint, params=params)
if response:
return response.json()
return None
def extract_videos_from_messages(messages, user_id=None):
"""
Extract all video URLs from a list of messages.
:param messages: List of Discord message objects
:param user_id: Optional user ID to filter messages by
:return: List of tuples (video_url, filename, message_id, timestamp)
"""
videos = []
for message in messages:
# Filter by user if specified
if user_id and message.get('author', {}).get('id') != user_id:
continue
message_id = message.get('id')
timestamp = message.get('timestamp', '')
# Check attachments
for attachment in message.get('attachments', []):
url = attachment.get('url')
filename = attachment.get('filename', 'unknown')
# Check if it's a video
if url and filename.lower().endswith(VIDEO_EXTENSIONS):
videos.append((url, filename, message_id, timestamp))
# Check embeds for videos
for embed in message.get('embeds', []):
# Embed video
if embed.get('type') == 'video' and embed.get('video'):
url = embed['video'].get('url')
if url:
filename = f"embed_{message_id}_{url.split('/')[-1]}"
videos.append((url, filename, message_id, timestamp))
return videos
def download_video(url, filepath):
"""
Download a video from a URL to a local file.
:param url: Video URL
:param filepath: Local file path to save the video
:return: True if successful, False otherwise
"""
try:
print(f" Downloading from {url}...")
response = requests.get(url, timeout=60, stream=True)
response.raise_for_status()
# Download in chunks for large files
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
with open(filepath, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
percent = (downloaded / total_size) * 100
print(f" Progress: {percent:.1f}%", end='\r')
if total_size > 0:
print(f" Progress: 100.0%")
return True
except Exception as e:
print(f" Failed to download {url}: {e}")
return False
def download_all_videos(channel_id, output_dir=None, user_id=None):
"""
Download all videos from a Discord channel.
:param channel_id: The channel ID to download videos from
:param output_dir: Directory to save videos (defaults to project_root/data/videos/{channel_id})
:param user_id: Optional user ID to filter videos by specific user
:return: Number of videos downloaded
"""
# Set up output directory
if output_dir is None:
# Use the project root data folder
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(script_dir)
output_dir = os.path.join(project_root, "data", "videos", channel_id)
# Add user ID to path if filtering by user
if user_id:
output_dir = os.path.join(output_dir, f"user_{user_id}")
os.makedirs(output_dir, exist_ok=True)
if user_id:
print(f"Fetching messages from channel {channel_id} (filtering by user {user_id})...")
else:
print(f"Fetching messages from channel {channel_id}...")
all_videos = []
before = None
total_messages = 0
# Fetch all messages with pagination
while True:
messages = fetch_messages(channel_id, before=before, limit=100)
if not messages:
if total_messages == 0:
print(ERROR_MESSAGES.get("api_error", "Failed to fetch messages"))
return 0
break
if len(messages) == 0:
break
total_messages += len(messages)
print(f"Fetched {total_messages} messages so far...")
# Extract videos from these messages
videos = extract_videos_from_messages(messages, user_id)
all_videos.extend(videos)
# Set before to the last message ID for pagination
before = messages[-1]['id']
# If we got fewer than 100 messages, we've reached the end
if len(messages) < 100:
break
print(f"\nFound {len(all_videos)} videos in {total_messages} messages")
if len(all_videos) == 0:
print("No videos to download.")
return 0
# Download all videos
print(f"\nDownloading videos to {output_dir}...\n")
downloaded = 0
for i, (url, filename, message_id, timestamp) in enumerate(all_videos, 1):
# Create a unique filename with timestamp and message ID
name, ext = os.path.splitext(filename)
safe_filename = f"{i:04d}_{message_id}_{name}{ext}"
filepath = os.path.join(output_dir, safe_filename)
print(f"[{i}/{len(all_videos)}] Downloading {filename}...")
if download_video(url, filepath):
downloaded += 1
file_size = os.path.getsize(filepath) / (1024 * 1024) # MB
print(f" Saved: {safe_filename} ({file_size:.2f} MB)\n")
print(f"\nSuccessfully downloaded {downloaded}/{len(all_videos)} videos")
print(f"Videos saved to: {os.path.abspath(output_dir)}")
return downloaded
def main():
print("Discord Video Downloader")
print("=" * 50)
channel_id = input("Enter the channel ID: ").strip()
if not channel_id:
print("Error: Channel ID cannot be empty")
return
user_id = input("Enter user ID to filter by (press Enter to download from all users): ").strip()
user_id = user_id if user_id else None
custom_dir = input("Enter output directory (press Enter for default): ").strip()
output_dir = custom_dir if custom_dir else None
print()
download_all_videos(channel_id, output_dir, user_id)
if __name__ == "__main__":
main()

473
scripts/voice_activity_tracker.py Executable file
View File

@@ -0,0 +1,473 @@
# discord_tools/scripts/voice_activity_tracker.py
import os
import sys
import asyncio
import websockets
import json
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 VoiceActivityTracker:
def __init__(self, guild_ids=None, user_ids=None, exclude_guilds=None):
# Support multiple guilds or specific users
self.guild_ids = guild_ids if isinstance(guild_ids, list) else ([guild_ids] if guild_ids else [])
self.user_ids = user_ids if isinstance(user_ids, list) else ([user_ids] if user_ids else [])
self.exclude_guilds = exclude_guilds if isinstance(exclude_guilds, list) else ([exclude_guilds] if exclude_guilds else [])
self.voice_states = {} # user_id -> {channel_id, joined_at, user_info, guild_id}
self.session_log = [] # List of all voice sessions
self.active = True
self.guild_names = {} # Cache guild names
self.channel_names = {} # Cache channel names
def get_guild_info(self, guild_id):
# Get guild name and cache it
if guild_id in self.guild_names:
return self.guild_names[guild_id]
response = make_discord_request('GET', f'/guilds/{guild_id}')
if response:
guild_data = response.json()
self.guild_names[guild_id] = guild_data['name']
return guild_data['name']
self.guild_names[guild_id] = f'Guild-{guild_id}'
return f'Guild-{guild_id}'
def get_voice_channels(self, guild_id):
# Get all voice channels in a guild
response = make_discord_request('GET', f'/guilds/{guild_id}/channels')
if not response:
return {}
channels = response.json()
voice_channels = {}
for channel in channels:
# Type 2 = voice channel, Type 13 = stage channel
if channel['type'] in [2, 13]:
voice_channels[channel['id']] = channel['name']
self.channel_names[channel['id']] = channel['name']
return voice_channels
def should_track_event(self, guild_id, user_id):
# Check if we should track this event based on filters
# Check excluded guilds
if guild_id in self.exclude_guilds:
return False
# If specific guilds set, only track those
if self.guild_ids and guild_id not in self.guild_ids:
return False
# If specific users set, only track those
if self.user_ids and user_id not in self.user_ids:
return False
return True
def handle_voice_state_update(self, data):
# Process voice state changes
user_id = data['user_id']
channel_id = data.get('channel_id') # None if user left voice
guild_id = data.get('guild_id')
# Check if we should track this event
if not self.should_track_event(guild_id, user_id):
return
timestamp = datetime.now()
# User info
member = data.get('member', {})
user = member.get('user', data.get('user', {}))
username = f"{user.get('username', 'Unknown')}#{user.get('discriminator', '0000')}"
# Get guild and channel names
guild_name = self.get_guild_info(guild_id)
# Build unique key for user+guild combo
state_key = f"{user_id}:{guild_id}"
# Check if user was already in a voice channel in this guild
if state_key in self.voice_states:
old_state = self.voice_states[state_key]
# User left voice completely
if channel_id is None:
duration = (timestamp - old_state['joined_at']).total_seconds()
session = {
'user_id': user_id,
'username': username,
'guild_id': guild_id,
'guild_name': guild_name,
'channel_id': old_state['channel_id'],
'channel_name': old_state.get('channel_name', 'Unknown'),
'joined_at': old_state['joined_at'].isoformat(),
'left_at': timestamp.isoformat(),
'duration_seconds': duration
}
self.session_log.append(session)
del self.voice_states[state_key]
print(f"[-] {username} left {old_state.get('channel_name', 'Unknown')} in {guild_name} (stayed {duration:.0f}s)")
# User switched channels
elif channel_id != old_state['channel_id']:
duration = (timestamp - old_state['joined_at']).total_seconds()
session = {
'user_id': user_id,
'username': username,
'guild_id': guild_id,
'guild_name': guild_name,
'channel_id': old_state['channel_id'],
'channel_name': old_state.get('channel_name', 'Unknown'),
'joined_at': old_state['joined_at'].isoformat(),
'left_at': timestamp.isoformat(),
'duration_seconds': duration
}
self.session_log.append(session)
# Get new channel name
if channel_id not in self.channel_names:
self.get_voice_channels(guild_id)
new_channel_name = self.channel_names.get(channel_id, f'Channel-{channel_id}')
self.voice_states[state_key] = {
'channel_id': channel_id,
'channel_name': new_channel_name,
'guild_id': guild_id,
'guild_name': guild_name,
'joined_at': timestamp,
'username': username
}
print(f"[→] {username} moved from {old_state.get('channel_name', 'Unknown')} to {new_channel_name} in {guild_name}")
# User joined voice (wasn't in voice before)
elif channel_id is not None:
if channel_id not in self.channel_names:
self.get_voice_channels(guild_id)
channel_name = self.channel_names.get(channel_id, f'Channel-{channel_id}')
self.voice_states[state_key] = {
'channel_id': channel_id,
'channel_name': channel_name,
'guild_id': guild_id,
'guild_name': guild_name,
'joined_at': timestamp,
'username': username
}
print(f"[+] {username} joined {channel_name} in {guild_name}")
async def track_voice_activity(self):
# Connect to Discord gateway and track voice state changes
gateway_url = "wss://gateway.discord.gg/?v=9&encoding=json"
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:
await asyncio.sleep(heartbeat_interval / 1000)
if self.active:
await ws.send(json.dumps({"op": 1, "d": None}))
heartbeat_task = asyncio.create_task(heartbeat())
# Wait for READY
ready = False
while not ready:
msg = json.loads(await ws.recv())
if msg.get('t') == 'READY':
ready = True
print("Connected to Discord Gateway")
print("Tracking voice activity...")
print("=" * 60)
# Track voice state updates
try:
while self.active:
msg = await ws.recv()
data = json.loads(msg)
event_type = data.get('t')
if event_type == 'VOICE_STATE_UPDATE':
self.handle_voice_state_update(data['d'])
# Also handle initial guild members in voice
elif event_type == 'GUILD_CREATE':
guild_id = data['d']['id']
# Only process if we should track this guild
if guild_id in self.exclude_guilds:
continue
if self.guild_ids and guild_id not in self.guild_ids:
continue
# Process initial voice states
for voice_state in data['d'].get('voice_states', []):
voice_state['guild_id'] = guild_id
self.handle_voice_state_update(voice_state)
except KeyboardInterrupt:
print("\nStopping tracker...")
self.active = False
finally:
heartbeat_task.cancel()
def save_logs(self):
# Save session logs to file
if not self.session_log and not self.voice_states:
print("No voice activity recorded")
return
# Close any active sessions
timestamp = datetime.now()
for state_key, state in self.voice_states.items():
duration = (timestamp - state['joined_at']).total_seconds()
session = {
'user_id': state_key.split(':')[0],
'username': state['username'],
'guild_id': state['guild_id'],
'guild_name': state.get('guild_name', 'Unknown'),
'channel_id': state['channel_id'],
'channel_name': state.get('channel_name', 'Unknown'),
'joined_at': state['joined_at'].isoformat(),
'left_at': timestamp.isoformat(),
'duration_seconds': duration,
'ongoing': True
}
self.session_log.append(session)
# Create output folder
if self.guild_ids and len(self.guild_ids) == 1:
folder_name = f"voice_activity_{self.guild_ids[0]}"
elif self.user_ids:
folder_name = f"voice_activity_user_{'_'.join(self.user_ids[:3])}"
else:
folder_name = "voice_activity_all"
output_folder = os.path.join(DATA_DIR, folder_name)
os.makedirs(output_folder, exist_ok=True)
timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S')
# Save raw JSON
json_file = os.path.join(output_folder, f"sessions_{timestamp_str}.json")
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(self.session_log, f, indent=2, ensure_ascii=False)
# Generate summary stats
stats_file = os.path.join(output_folder, f"stats_{timestamp_str}.txt")
# Calculate stats
user_stats = defaultdict(lambda: {'total_time': 0, 'sessions': 0, 'channels': set(), 'guilds': set()})
channel_stats = defaultdict(lambda: {'total_time': 0, 'unique_users': set()})
guild_stats = defaultdict(lambda: {'total_time': 0, 'unique_users': set()})
for session in self.session_log:
user_id = session['user_id']
username = session['username']
channel_id = session['channel_id']
channel_name = session['channel_name']
guild_name = session.get('guild_name', 'Unknown')
duration = session['duration_seconds']
user_stats[username]['total_time'] += duration
user_stats[username]['sessions'] += 1
user_stats[username]['channels'].add(channel_name)
user_stats[username]['guilds'].add(guild_name)
channel_stats[f"{guild_name} > {channel_name}"]['total_time'] += duration
channel_stats[f"{guild_name} > {channel_name}"]['unique_users'].add(username)
guild_stats[guild_name]['total_time'] += duration
guild_stats[guild_name]['unique_users'].add(username)
with open(stats_file, 'w', encoding='utf-8') as f:
f.write("Voice Activity Statistics\n")
f.write("=" * 60 + "\n\n")
f.write(f"Total Sessions: {len(self.session_log)}\n")
f.write(f"Unique Users: {len(user_stats)}\n")
f.write(f"Servers Tracked: {len(guild_stats)}\n\n")
if len(guild_stats) > 1:
f.write("\nServer Activity:\n")
f.write("-" * 60 + "\n")
sorted_guilds = sorted(guild_stats.items(), key=lambda x: x[1]['total_time'], reverse=True)
for guild_name, stats in sorted_guilds:
hours = stats['total_time'] / 3600
f.write(f"{guild_name}:\n")
f.write(f" Total Time: {hours:.2f} hours\n")
f.write(f" Unique Users: {len(stats['unique_users'])}\n\n")
f.write("\nMost Active Users (by time):\n")
f.write("-" * 60 + "\n")
sorted_users = sorted(user_stats.items(), key=lambda x: x[1]['total_time'], reverse=True)
for username, stats in sorted_users[:10]:
hours = stats['total_time'] / 3600
f.write(f"{username}:\n")
f.write(f" Total Time: {hours:.2f} hours\n")
f.write(f" Sessions: {stats['sessions']}\n")
if len(stats['guilds']) > 1:
f.write(f" Servers: {', '.join(stats['guilds'])}\n")
f.write(f" Channels: {', '.join(stats['channels'])}\n\n")
f.write("\nChannel Activity:\n")
f.write("-" * 60 + "\n")
sorted_channels = sorted(channel_stats.items(), key=lambda x: x[1]['total_time'], reverse=True)
for channel_name, stats in sorted_channels:
hours = stats['total_time'] / 3600
f.write(f"{channel_name}:\n")
f.write(f" Total Time: {hours:.2f} hours\n")
f.write(f" Unique Users: {len(stats['unique_users'])}\n\n")
# Save detailed log
log_file = os.path.join(output_folder, f"detailed_log_{timestamp_str}.txt")
with open(log_file, 'w', encoding='utf-8') as f:
f.write("Detailed Voice Activity Log\n")
f.write("=" * 80 + "\n\n")
for session in sorted(self.session_log, key=lambda x: x['joined_at']):
ongoing = session.get('ongoing', False)
duration_min = session['duration_seconds'] / 60
f.write(f"User: {session['username']}\n")
f.write(f"Server: {session.get('guild_name', 'Unknown')}\n")
f.write(f"Channel: {session['channel_name']}\n")
f.write(f"Joined: {session['joined_at']}\n")
f.write(f"Left: {session['left_at']}")
if ongoing:
f.write(" (ongoing when tracking stopped)")
f.write("\n")
f.write(f"Duration: {duration_min:.1f} minutes\n")
f.write("-" * 80 + "\n\n")
print("\n" + "=" * 60)
print(f"Logs saved to: {output_folder}")
print(f" - sessions_{timestamp_str}.json (raw data)")
print(f" - stats_{timestamp_str}.txt (summary)")
print(f" - detailed_log_{timestamp_str}.txt (full log)")
def main():
print("Voice Activity Tracker")
print("=" * 60)
print("Track voice activity with filters\n")
print("1. Track specific server(s)")
print("2. Track specific user(s)")
print("3. Track all servers")
print("4. Track all servers except some")
print("=" * 60)
choice = input("Choice: ").strip()
guild_ids = None
user_ids = None
exclude_guilds = None
if choice == '1':
# Track specific guilds
guild_input = input("\nEnter guild ID(s) (comma-separated for multiple): ").strip()
guild_ids = [g.strip() for g in guild_input.split(',') if g.strip()]
if not all(g.isdigit() for g in guild_ids):
print("Invalid guild ID(s)")
return
elif choice == '2':
# Track specific users
user_input = input("\nEnter user ID(s) (comma-separated for multiple): ").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
elif choice == '3':
# Track all servers
print("\nTracking all servers...")
elif choice == '4':
# Exclude specific servers
exclude_input = input("\nEnter guild ID(s) to EXCLUDE (comma-separated): ").strip()
exclude_guilds = [g.strip() for g in exclude_input.split(',') if g.strip()]
if not all(g.isdigit() for g in exclude_guilds):
print("Invalid guild ID(s)")
return
else:
print("Invalid choice")
return
tracker = VoiceActivityTracker(
guild_ids=guild_ids,
user_ids=user_ids,
exclude_guilds=exclude_guilds
)
# Show what we're tracking
print("\n" + "=" * 60)
if guild_ids:
print(f"Tracking {len(guild_ids)} specific server(s)")
for gid in guild_ids:
name = tracker.get_guild_info(gid)
print(f" - {name}")
elif user_ids:
print(f"Tracking {len(user_ids)} specific user(s) across all servers")
elif exclude_guilds:
print(f"Tracking all servers except {len(exclude_guilds)}")
else:
print("Tracking all servers")
print("\nPress Ctrl+C to stop tracking and save logs\n")
print("=" * 60)
try:
asyncio.run(tracker.track_voice_activity())
except KeyboardInterrupt:
print("\n\nStopping tracker...")
finally:
tracker.save_logs()
if __name__ == "__main__":
main()

311
scripts/voice_call_keeper.py Executable file
View File

@@ -0,0 +1,311 @@
# discord_tools/scripts/voice_call_keeper.py
import os
import sys
import asyncio
import websockets
import json
from datetime import datetime
# 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
from discord_tools.utils.api_utils import make_discord_request
class VoiceCallKeeper:
def __init__(self):
self.active = True
self.current_channel_id = None
self.current_guild_id = None
self.session_id = None
self.token = None
self.endpoint = None
self.my_user_id = None
self.start_time = None
self.total_reconnects = 0
async def find_current_voice_channel(self, ws):
# Wait for READY and get current voice state
ready = False
current_voice = None
while not ready:
msg = json.loads(await ws.recv())
if msg.get('t') == 'READY':
ready_data = msg['d']
self.my_user_id = ready_data['user']['id']
# Check private calls
private_calls = ready_data.get('private_calls', [])
for call in private_calls:
# Check if we're in this call
voice_states = call.get('voice_states', [])
for vs in voice_states:
if vs.get('user_id') == self.my_user_id:
current_voice = {
'channel_id': call['channel_id'],
'guild_id': None,
'type': 'DM Call'
}
break
if current_voice:
break
# Check guild voice states
if not current_voice:
guilds = ready_data.get('guilds', [])
for guild in guilds:
voice_states = guild.get('voice_states', [])
for vs in voice_states:
if vs.get('user_id') == self.my_user_id and vs.get('channel_id'):
current_voice = {
'channel_id': vs['channel_id'],
'guild_id': guild['id'],
'type': 'Server VC'
}
break
if current_voice:
break
ready = True
return current_voice
async def maintain_voice_connection(self, channel_id=None, guild_id=None, auto_join=False):
# Connect to gateway and maintain voice connection with auto-reconnect
gateway_url = "wss://gateway.discord.gg/?v=9&encoding=json"
# Set start time on first connection
if self.start_time is None:
self.start_time = datetime.now()
while self.active:
try:
await self._connect_and_run(gateway_url, channel_id, guild_id, auto_join)
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)
# After reconnect, don't auto-join again, use stored IDs
auto_join = False
else:
break
except KeyboardInterrupt:
print("\n\nDisconnecting...")
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)
auto_join = False
else:
break
async def _connect_and_run(self, gateway_url, channel_id, guild_id, auto_join):
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())
try:
# If auto-join, find current voice channel
if auto_join:
print("Detecting current voice channel...")
current_voice = await self.find_current_voice_channel(ws)
if not current_voice:
print("Not currently in any voice channel/call")
return
channel_id = current_voice['channel_id']
guild_id = current_voice['guild_id']
print(f"Found: {current_voice['type']}")
# Store for reconnections
self.current_channel_id = channel_id
self.current_guild_id = guild_id
else:
# Wait for READY
ready = False
while not ready:
msg = json.loads(await ws.recv())
if msg.get('t') == 'READY':
ready = True
self.my_user_id = msg['d']['user']['id']
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)")
# Use stored or provided IDs
if self.current_channel_id is None:
self.current_channel_id = channel_id
self.current_guild_id = guild_id
# Join/maintain voice channel/call
if self.total_reconnects == 0:
print(f"Maintaining voice connection...")
print("Call keeper active - Press Ctrl+C to disconnect")
print("=" * 60)
voice_state_update = {
"op": 4,
"d": {
"guild_id": self.current_guild_id,
"channel_id": self.current_channel_id,
"self_mute": False,
"self_deaf": False
}
}
await ws.send(json.dumps(voice_state_update))
# Monitor voice state and keep connection alive
while self.active:
msg = await ws.recv()
data = json.loads(msg)
event_type = data.get('t')
# Track voice state changes
if event_type == 'VOICE_STATE_UPDATE':
voice_data = data['d']
# Check if it's related to our channel
if voice_data.get('channel_id') == self.current_channel_id or voice_data.get('user_id') == self.my_user_id:
user_id = voice_data.get('user_id')
member = voice_data.get('member', {})
user = member.get('user', {})
username = user.get('username', 'Unknown')
if voice_data.get('channel_id') is None and user_id != self.my_user_id:
print(f"[-] {username} left")
elif voice_data.get('channel_id') == self.current_channel_id and user_id != self.my_user_id:
elapsed = (datetime.now() - self.start_time).total_seconds()
print(f"[+] {username} joined (running {elapsed:.0f}s)")
# Voice server update
elif event_type == 'VOICE_SERVER_UPDATE':
self.token = data['d'].get('token')
self.endpoint = data['d'].get('endpoint')
if self.total_reconnects == 0:
print(f"[✓] Connection established")
else:
print(f"[✓] Voice connection re-established")
elif event_type == 'CALL_CREATE':
print("[📞] Call created")
elif event_type == 'CALL_UPDATE':
ringing = data['d'].get('ringing', [])
if ringing:
print(f"[📞] Ringing: {len(ringing)} user(s)")
finally:
heartbeat_task.cancel()
# Only leave voice if we're truly shutting down
if not self.active:
try:
leave_voice = {
"op": 4,
"d": {
"guild_id": self.current_guild_id,
"channel_id": None,
"self_mute": False,
"self_deaf": False
}
}
await ws.send(json.dumps(leave_voice))
except Exception:
pass
# Stats
total_time = (datetime.now() - self.start_time).total_seconds()
hours = total_time / 3600
minutes = (total_time % 3600) / 60
print("\n" + "=" * 60)
print(f"Duration: {int(hours)}h {int(minutes)}m")
print(f"Total reconnections: {self.total_reconnects}")
print("=" * 60)
def main():
print("Voice Call Keeper")
print("=" * 60)
print("1. Auto-join current call/VC")
print("2. Join specific channel")
print("=" * 60)
choice = input("Choice: ").strip()
if choice == '1':
# Auto-join current voice
keeper = VoiceCallKeeper()
asyncio.run(keeper.maintain_voice_connection(auto_join=True))
elif choice == '2':
# Manual join
channel_id = input("\nChannel ID: ").strip()
if not channel_id.isdigit():
print("Invalid channel ID")
return
# Ask if it's a server VC
is_server = input("Server VC? (y/n): ").strip().lower()
guild_id = None
if is_server == 'y':
guild_id = input("Guild ID: ").strip()
if not guild_id.isdigit():
print("Invalid guild ID")
return
keeper = VoiceCallKeeper()
asyncio.run(keeper.maintain_voice_connection(channel_id, guild_id=guild_id))
else:
print("Invalid choice")
if __name__ == "__main__":
main()