Initial commit: Discord automation tools
This commit is contained in:
0
scripts/__init__.py
Executable file
0
scripts/__init__.py
Executable file
240
scripts/backup_server.py
Executable file
240
scripts/backup_server.py
Executable 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
99
scripts/chat_fetcher.py
Executable 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()
|
||||
267
scripts/discord_message_analyzer.py
Executable file
267
scripts/discord_message_analyzer.py
Executable 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
47
scripts/dm_opener.py
Executable 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
131
scripts/ghost_typer.py
Executable 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
209
scripts/image_fetcher.py
Executable 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
157
scripts/message_deletor.py
Executable 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
204
scripts/pfp_downloader.py
Executable 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
1024
scripts/profile_stalker.py
Executable file
File diff suppressed because it is too large
Load Diff
57
scripts/status_updater.py
Executable file
57
scripts/status_updater.py
Executable 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
209
scripts/token_validator.py
Executable 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
242
scripts/user_history_scraper.py
Executable 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
218
scripts/video_fetcher.py
Executable 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
473
scripts/voice_activity_tracker.py
Executable 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
311
scripts/voice_call_keeper.py
Executable 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()
|
||||
Reference in New Issue
Block a user