diff --git a/.idea/.gitignore b/.gitignore similarity index 88% rename from .idea/.gitignore rename to .gitignore index 81bd6d5..863ef2c 100644 --- a/.idea/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ details.py Collection __pycache__ *.session -*.session-journal \ No newline at end of file +*.session-journal +*.txt +*.csv \ No newline at end of file diff --git a/assessment.py b/assessment.py index 55e562d..7f91b83 100644 --- a/assessment.py +++ b/assessment.py @@ -92,7 +92,8 @@ def extract_sentences(username, input_csv, output_pdf, target_phrase_list1, targ doc.build(story) print(f"Key phrase extraction report saved to {output_pdf_path}") -if __name__ == "__main__": + +def main(): # Define target phrase lists target_phrase_list1 = ["my", "buy", "buying", "get", "getting", "got", "have", "acquire", "acquiring", "obtain", "obtaining", "procure", "procuring", "wanting", "want to", "want a" "going to", "own", "owning", "license", "a", "with", "certified", "need", "stolen", "steal a", "3d print", "3d-print", "3d printed", "3d-printed", "borrow", "take",] target_phrase_list2 = ["gun", "rifle", "pistol", "knife", "shotgun", "revolver", "firearm", "firearms", "SMG", "AR", "sawnoff", "sawn off", "sawn-off", "machine gun", "doublebarrel", "doublebarreled", "double-barrel", "double-barreled", "bolt-action", "bolt-action", "lever-action", "lever action", "pump-action", "semi-automatic", "semiautomatic", "fully automatic", "ar-15", "ar15", "AK-47", "M4", "M16", "remington", "glock", "sig", "springfield", "ruger", "Smith & Wesson", "S&W", "M&P", "Colt", "Winchester", "benelli", "M&P15", "kel-tec", "KSG", "590", "870", "LE6920", "AR-556", "G19", "85", "taurus", "629", ".85", ".45", ".22", "22", "9mm", "9 mm", ".30", "beretta", ".50", "50cal", "50 cal", "bushmaster", "M1911", "12 gauge", "12gauge", "12ga","geissele", "chamber a round" , "ammo", "ammunition", "caliber", "gauge", "magazine", "buck shot", "buckshot", "armor-piercing", "hollow point", "hollow points","birdshot", "bird shot", "gun range", "rifle club", "shooting pracitice", "shooting range", "firearms training ", "hunting", "duck shooting", "target shooting", "target pracitice", "scope", "silencer", "suppressor", "compensator","stock", "barrel", "muzzle", "bipod", "firing pin", "optics", "crossbow", "compound bow", "pipe bomb", "pipebomb", "pipe-bomb", "pipebombs", "pipe-bombs", "grenade", "grenades", "IED", "improvised explosive device", "ball bearings", "molotov", "nitrate", "TNT", "landmine", "firebomb", "tannerite", "semtex", "fertilizer", "detonater", "detornaters", "nitroglycerin", "ammonium nitrate", "propellant", "thermalite", "thermite", "blasting cap", "det cord", "detcord", "boom stick", "fire bomb", "explosives", "kevlar", "body armour", "stab-proof vest", "stabproof vest",] @@ -104,8 +105,5 @@ def extract_sentences(username, input_csv, output_pdf, target_phrase_list1, targ # Run the extraction and PDF generation extract_sentences(target_username, "input.csv", target_username + "_threat_assessment.pdf", target_phrase_list1, target_phrase_list2, target_phrase_list3) - # Ask if the user wants to return to the launcher - launcher = input('Do you want to return to the launcher? (y/n)') - if launcher == 'y': - print('Restarting...') - exec(open("launcher.py").read()) +if __name__ == "__main__": + main() diff --git a/channellist.py b/channellist.py index 7fd9846..9c6b4b6 100644 --- a/channellist.py +++ b/channellist.py @@ -15,7 +15,7 @@ async def scrape_forwards(channel_name): - l = [] + channel_info = [] source_urls = [] count = 0 @@ -29,7 +29,7 @@ async def scrape_forwards(channel_name): ent = await client.get_entity(id) target_channel_entity = await client.get_entity(message.to_id.channel_id) target_channel_title = target_channel_entity.title - l.append([ent.title, target_channel_title]) + channel_info.append([ent.title, target_channel_title]) source_url = f"https://t.me/{ent.username}" source_urls.append(source_url) count += 1 @@ -42,7 +42,7 @@ async def scrape_forwards(channel_name): except Exception as e: print(f"{Fore.RED}Skipping forward: Private/Inaccessible{Style.RESET_ALL}") - df = pd.DataFrame(l, columns=['From', 'To']) + df = pd.DataFrame(channel_info, columns=['From', 'To']) source_df = pd.DataFrame(source_urls, columns=['SourceURL']) os.makedirs('Adjacency List', exist_ok=True) @@ -75,9 +75,10 @@ async def main(): await scrape_forwards(channel) print("CSV files created for", channel) print() + print('Forwards scraped successfully.') if __name__ == '__main__': asyncio.run(main()) -print('Forwards scraped successfully.') + diff --git a/channels.py b/channels.py index f4dee81..8a05020 100644 --- a/channels.py +++ b/channels.py @@ -12,76 +12,73 @@ async def main(): - client = TelegramClient(phone, api_id, api_hash) - await client.start() + while True: + client = TelegramClient(phone, api_id, api_hash) + await client.start() - print(' ') - print('This tool will scrape a Telegram channel for all forwarded messages and their original source.') - print(' ') + print(' ') + print('This tool will scrape a Telegram channel for all forwarded messages and their original source.') + print(' ') - while True: - try: - channel_name = input("Please enter a Telegram channel name:\n") - print(f'You entered "{channel_name}"') - answer = input('Is this correct? (y/n)') - if answer == 'y': - print('Scraping forwards from', channel_name, '...') - break - except Exception: - continue - - l = [] - source_urls = [] - count = 0 - - async for message in client.iter_messages(channel_name): - if message.forward is not None: + while True: try: - id = message.forward.original_fwd.from_id - if id is not None: - try: - ent = await client.get_entity(id) - target_channel_entity = await client.get_entity(message.to_id.channel_id) - target_channel_title = target_channel_entity.title - l.append([ent.title, target_channel_title]) - source_url = f"https://t.me/{ent.username}" - source_urls.append(source_url) - count += 1 - print( - f"From {Fore.CYAN + ent.title + Style.RESET_ALL} to {Fore.YELLOW + target_channel_title + Style.RESET_ALL}") - except ValueError as e: - print("Skipping forward:", e) - except Exception as e: - print(f"{Fore.RED}Skipping forward: Private/Inaccessible{Style.RESET_ALL}") - - # Create the folders if they don't exist - adjacency_folder = 'Adjacency List' - urls_folder = 'Source URLs' - os.makedirs(adjacency_folder, exist_ok=True) - os.makedirs(urls_folder, exist_ok=True) - - df = pd.DataFrame(l, columns=['From', 'To']) - df.to_csv(os.path.join(adjacency_folder, f'{channel_name}.csv'), header=False, index=False) - - source_df = pd.DataFrame(source_urls, columns=['SourceURL']) - source_df.to_csv(os.path.join(urls_folder, f'{channel_name}SourceURLs.csv'), header=False, index=False) - - await client.disconnect() - + channel_name = input("Please enter a Telegram channel name:\n") + print(f'You entered "{channel_name}"') + answer = input('Is this correct? (y/n)') + if answer.lower() == 'y': + print('Scraping forwards from', channel_name, '...') + break + except Exception: + continue + + forwarded_messages_info = [] + source_urls = [] + count = 0 + + async for message in client.iter_messages(channel_name): + if message.forward is not None: + try: + id = message.forward.original_fwd.from_id + if id is not None: + try: + ent = await client.get_entity(id) + target_channel_entity = await client.get_entity(message.to_id.channel_id) + target_channel_title = target_channel_entity.title + forwarded_messages_info.append([ent.title, target_channel_title]) + source_url = f"https://t.me/{ent.username}" + source_urls.append(source_url) + count += 1 + print( + f"From {Fore.CYAN + ent.title + Style.RESET_ALL} to {Fore.YELLOW + target_channel_title + Style.RESET_ALL}") + except ValueError as e: + print("Skipping forward:", e) + except Exception as e: + print(f"{Fore.RED}Skipping forward: Private/Inaccessible{Style.RESET_ALL}") + + # Create the folders if they don't exist + adjacency_folder = 'Adjacency List' + urls_folder = 'Source URLs' + os.makedirs(adjacency_folder, exist_ok=True) + os.makedirs(urls_folder, exist_ok=True) + + df = pd.DataFrame(forwarded_messages_info, columns=['From', 'To']) + df.to_csv(os.path.join(adjacency_folder, f'{channel_name}.csv'), header=False, index=False) + + source_df = pd.DataFrame(source_urls, columns=['SourceURL']) + source_df.to_csv(os.path.join(urls_folder, f'{channel_name}SourceURLs.csv'), header=False, index=False) + + await client.disconnect() + + print('Forwards scraped successfully.') + + again = input('Do you want to scrape more channels? (y/n)') + if again.lower() == 'y': + print('Restarting...') + + else: + break if __name__ == '__main__': asyncio.run(main()) -print('Forwards scraped successfully.') - -again = input('Do you want to scrape more channels? (y/n)') -if again == 'y': - print('Restarting...') - exec(open("channels.py").read()) -else: - pass -launcher = input('Do you want to return to the launcher? (y/n)') -if launcher == 'y': - print('Restarting...') - exec(open("launcher.py").read()) diff --git a/channelscraper.py b/channelscraper.py index 571737c..89bdb16 100644 --- a/channelscraper.py +++ b/channelscraper.py @@ -9,7 +9,6 @@ api_hash = ds.apiHash phone = ds.number - async def scrape_channel_content(channel_name): async with TelegramClient(phone, api_id, api_hash) as client: try: @@ -60,7 +59,7 @@ async def main(): f"{Fore.CYAN}Please enter a target Telegram channel (e.g., https://t.me/{Fore.LIGHTYELLOW_EX}your_channel{Style.RESET_ALL}):\n") print(f'You entered "{Fore.LIGHTYELLOW_EX}{channel_name}{Style.RESET_ALL}"') answer = input('Is this correct? (y/n)') - if answer != 'y': + if answer.lower() != 'y': return output_directory = f"Collection/{channel_name}" diff --git a/frequency.py b/frequency.py index f9d0838..cdf6d6d 100644 --- a/frequency.py +++ b/frequency.py @@ -4,112 +4,110 @@ from pytz import timezone from tzlocal import get_localzone # Added to get the system's local timezone -# Load the CSV file into a pandas DataFrame based on the target username -target_username = input("Enter the target username: ") -csv_file = f'Collection/{target_username.strip("@")}/{target_username.strip("@")}_messages.csv' -df = pd.read_csv(csv_file) - -# Input for custom timezone -custom_timezone = input("Enter a custom timezone (e.g., 'Pacific/Auckland'): ") - -# Convert the 'Date' column to a datetime object and convert to the custom timezone -df['Date'] = pd.to_datetime(df['Date'], format='%Y-%m-%d %H:%M:%S%z') -try: - custom_tz = timezone(custom_timezone) - df['Date'] = df['Date'].dt.tz_convert(custom_tz) -except Exception as e: - print(f"Error: {e}. Using the system's local timezone instead.") - local_tz = get_localzone() - df['Date'] = df['Date'].dt.tz_convert(local_tz) - -# Group data by user and hour, and count the frequency of posts -df_grouped_hourly = df.groupby([df['Date'].dt.hour, 'Username']).size().reset_index(name='post_count') - -# Pivot the data for the first graph (hourly posting patterns) -pivot_hourly = df_grouped_hourly.pivot(index='Date', columns='Username', values='post_count').fillna(0) - -# Group data by user and date, and sum the daily post counts -df_grouped_daily = df.groupby([df['Date'].dt.date, 'Username']).size().reset_index(name='post_count') - -# Pivot the data for the second graph (daily posting patterns) -pivot_daily = df_grouped_daily.pivot(index='Date', columns='Username', values='post_count').fillna(0) - -# Calculate posting frequency based on the day of the week -df['DayOfWeek'] = df['Date'].dt.day_name() -df_grouped_dayofweek = df.groupby(['DayOfWeek', 'Username']).size().reset_index(name='post_count_dayofweek') -desired_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] -pivot_dayofweek = df_grouped_dayofweek.pivot(index='DayOfWeek', columns='Username', - values='post_count_dayofweek').fillna(0) - -# Reorder the days of the week -pivot_dayofweek = pivot_dayofweek.reindex(desired_order, axis=0) - -# Calculate posting frequency based on the day of the month -df['DayOfMonth'] = df['Date'].dt.day -df_grouped_dayofmonth = df.groupby(['DayOfMonth', 'Username']).size().reset_index(name='post_count_dayofmonth') - -# Pivot the data for the third graph (posting frequency by day of the month) -pivot_dayofmonth = df_grouped_dayofmonth.pivot(index='DayOfMonth', columns='Username', - values='post_count_dayofmonth').fillna(0) - -# Convert non-string values in 'Text' column to string -df['Text'] = df['Text'].astype(str) - -# Save the visualizations to a single PDF in the target user's directory -output_pdf = f'Collection/{target_username.strip("@")}/visualization_report_{target_username.strip("@")}.pdf' -with PdfPages(output_pdf) as pdf: - # First graph (hourly posting patterns) - plt.figure(figsize=(10, 5)) - pivot_hourly.plot(kind='bar', stacked=True) - plt.title('Users Posting Patterns Over a 24-Hour Period') - plt.xlabel('Hour of the Day') - plt.ylabel('Post Count') - plt.legend(title='Users') - plt.grid(True) - plt.xticks(rotation=0) - plt.tight_layout() - pdf.savefig() - - # Second graph (daily posting patterns) - plt.figure(figsize=(10, 5)) - pivot_daily.plot(kind='line') - plt.title('Users Posting Patterns Over Time (Summed Daily)') - plt.xlabel('Date') - plt.ylabel('Post Count') - plt.legend(title='Users') - plt.grid(True) - plt.tight_layout() - pdf.savefig() - - # Third graph (posting frequency by day of the week) - plt.figure(figsize=(10, 5)) - pivot_dayofweek.plot(kind='bar', stacked=True) - plt.title('Users Posting Patterns by Day of the Week') - plt.xlabel('Day of the Week') - plt.ylabel('Post Count') - plt.legend(title='Users') - plt.grid(True) - plt.xticks(rotation=0) - plt.tight_layout() - pdf.savefig() - - # Fourth graph (posting frequency by day of the month) - plt.figure(figsize=(10, 5)) - pivot_dayofmonth.plot(kind='bar', stacked=True) - plt.title('Users Posting Patterns by Day of the Month') - plt.xlabel('Day of the Month') - plt.ylabel('Post Count') - plt.legend(title='Users') - plt.grid(True) - plt.xticks(rotation=0) - plt.tight_layout() - pdf.savefig() - -print(f"PDF report created: {output_pdf}") - -# Ask if the user wants to return to the launcher -launcher = input('Do you want to return to the launcher? (y/n)') - -if launcher == 'y': - print('Restarting...') - exec(open("launcher.py").read()) + +def main(): + # Load the CSV file into a pandas DataFrame based on the target username + target_username = input("Enter the target username: ") + csv_file = f'Collection/{target_username.strip("@")}/{target_username.strip("@")}_messages.csv' + df = pd.read_csv(csv_file) + + # Input for custom timezone + custom_timezone = input("Enter a custom timezone (e.g., 'Pacific/Auckland'): ") + + # Convert the 'Date' column to a datetime object and convert to the custom timezone + df['Date'] = pd.to_datetime(df['Date'], format='%Y-%m-%d %H:%M:%S%z') + try: + custom_tz = timezone(custom_timezone) + df['Date'] = df['Date'].dt.tz_convert(custom_tz) + except Exception as e: + print(f"Error: {e}. Using the system's local timezone instead.") + local_tz = get_localzone() + df['Date'] = df['Date'].dt.tz_convert(local_tz) + + # Group data by user and hour, and count the frequency of posts + df_grouped_hourly = df.groupby([df['Date'].dt.hour, 'Username']).size().reset_index(name='post_count') + + # Pivot the data for the first graph (hourly posting patterns) + pivot_hourly = df_grouped_hourly.pivot(index='Date', columns='Username', values='post_count').fillna(0) + + # Group data by user and date, and sum the daily post counts + df_grouped_daily = df.groupby([df['Date'].dt.date, 'Username']).size().reset_index(name='post_count') + + # Pivot the data for the second graph (daily posting patterns) + pivot_daily = df_grouped_daily.pivot(index='Date', columns='Username', values='post_count').fillna(0) + + # Calculate posting frequency based on the day of the week + df['DayOfWeek'] = df['Date'].dt.day_name() + df_grouped_dayofweek = df.groupby(['DayOfWeek', 'Username']).size().reset_index(name='post_count_dayofweek') + desired_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + pivot_dayofweek = df_grouped_dayofweek.pivot(index='DayOfWeek', columns='Username', + values='post_count_dayofweek').fillna(0) + + # Reorder the days of the week + pivot_dayofweek = pivot_dayofweek.reindex(desired_order, axis=0) + + # Calculate posting frequency based on the day of the month + df['DayOfMonth'] = df['Date'].dt.day + df_grouped_dayofmonth = df.groupby(['DayOfMonth', 'Username']).size().reset_index(name='post_count_dayofmonth') + + # Pivot the data for the third graph (posting frequency by day of the month) + pivot_dayofmonth = df_grouped_dayofmonth.pivot(index='DayOfMonth', columns='Username', + values='post_count_dayofmonth').fillna(0) + + # Convert non-string values in 'Text' column to string + df['Text'] = df['Text'].astype(str) + + # Save the visualizations to a single PDF in the target user's directory + output_pdf = f'Collection/{target_username.strip("@")}/visualization_report_{target_username.strip("@")}.pdf' + with PdfPages(output_pdf) as pdf: + # First graph (hourly posting patterns) + plt.figure(figsize=(10, 5)) + pivot_hourly.plot(kind='bar', stacked=True) + plt.title('Users Posting Patterns Over a 24-Hour Period') + plt.xlabel('Hour of the Day') + plt.ylabel('Post Count') + plt.legend(title='Users') + plt.grid(True) + plt.xticks(rotation=0) + plt.tight_layout() + pdf.savefig() + + # Second graph (daily posting patterns) + plt.figure(figsize=(10, 5)) + pivot_daily.plot(kind='line') + plt.title('Users Posting Patterns Over Time (Summed Daily)') + plt.xlabel('Date') + plt.ylabel('Post Count') + plt.legend(title='Users') + plt.grid(True) + plt.tight_layout() + pdf.savefig() + + # Third graph (posting frequency by day of the week) + plt.figure(figsize=(10, 5)) + pivot_dayofweek.plot(kind='bar', stacked=True) + plt.title('Users Posting Patterns by Day of the Week') + plt.xlabel('Day of the Week') + plt.ylabel('Post Count') + plt.legend(title='Users') + plt.grid(True) + plt.xticks(rotation=0) + plt.tight_layout() + pdf.savefig() + + # Fourth graph (posting frequency by day of the month) + plt.figure(figsize=(10, 5)) + pivot_dayofmonth.plot(kind='bar', stacked=True) + plt.title('Users Posting Patterns by Day of the Month') + plt.xlabel('Day of the Month') + plt.ylabel('Post Count') + plt.legend(title='Users') + plt.grid(True) + plt.xticks(rotation=0) + plt.tight_layout() + pdf.savefig() + + print(f"PDF report created: {output_pdf}") + +if __name__ == "__main__": + main() diff --git a/indicators.py b/indicators.py index 953516c..a1ec639 100644 --- a/indicators.py +++ b/indicators.py @@ -1,19 +1,11 @@ import os - import re - import pandas as pd - from openpyxl import Workbook - from reportlab.lib.pagesizes import letter - from reportlab.lib import colors - from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer - from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle - from reportlab.platypus import PageBreak @@ -108,7 +100,7 @@ def extract_sentences(username, input_csv, output_pdf, target_phrase_sections): print(f"Key phrase extraction report saved to {output_pdf_path}") -if __name__ == "__main__": +def main(): # Define five different target phrase sections with sub-headings target_phrase_sections = [ @@ -240,11 +232,5 @@ def extract_sentences(username, input_csv, output_pdf, target_phrase_sections): extract_sentences(target_username, "input.csv", target_username + "_ideologicalindicators_report.pdf", target_phrase_sections) -# Ask if the user wants to return to the launcher - -launcher = input('Do you want to return to the launcher? (y/n)') - -if launcher == 'y': - print('Restarting...') - - exec(open("launcher.py").read()) +if __name__ == "__main__": + main() diff --git a/launcher.py b/launcher.py index 1f9f76e..9e4a52b 100644 --- a/launcher.py +++ b/launcher.py @@ -1,127 +1,86 @@ import asyncio - -from telethon.sync import TelegramClient - +import importlib from colorama import init, Fore, Style -# Initialize colorama - -init(autoreset=True) - - -# Function to stop any existing asyncio event loop - -def stop_event_loop(): - try: - - loop = asyncio.get_event_loop() - - loop.stop() - - except Exception as e: - - pass - - -# Launcher code - -print( - f'{Fore.CYAN} __________________________________________________________________' -) - -print( - f'{Fore.CYAN} _______ ______ _ ______ _____ ______ _____ ____ _ _ ' -) - -print(Fore.CYAN + r' |__ __| ____| | | ____| __ \| ____/ ____/ __ \| \ | | ') - -print(Fore.CYAN + r' | | | |__ | | | |__ | |__) | |__ | | | | | | \| | ') - -print( - f'{Fore.CYAN} | | | __| | | | __| | _ /| __|| | | | | | . ` | ' -) - -print(Fore.CYAN + r' | | | |____| |____| |____| | \ \| |___| |___| |__| | |\ | ') - -print(Fore.CYAN + r' |_| |______|______|______|_| \_\______\_____\____/|_| \_| v2.1') - -print( - f'{Fore.CYAN}___________________________________________________________________' -) - -print(Style.RESET_ALL) - -print(f'{Fore.YELLOW} ') - -print( - f'{Fore.YELLOW}Welcome to Telerecon, a scraper and reconnaissance framework for Telegram' -) - -print("") - -print(f'{Fore.YELLOW}Please select an option:') - -print(Style.RESET_ALL) - -options = { - - 'Get user information': 'userdetails.py', - - 'Check user activity across a list of channels': 'recon.py', - - 'Collect user messages from a target channel': 'userscraper.py', - - 'Collect user messages from a list of target channels': 'usermultiscraper.py', - - 'Scrape all messages within a channel': 'channelscraper.py', - - 'Scrape all t.me URL’s from within a channel': 'urlscraper.py', - - 'Scrape forwarding relationships into target channel': 'channels.py', - - 'Scrape forwarding relationships into a list of target channel': 'channellist.py', - - 'Identify possible user associates via interaction network map': 'network.py', - - 'Parse user messages to extract selectors/intel': 'selector.py', - - 'Extract GPS data from collected user media': 'metadata.py', - - 'Create visualization report from collected user messages': 'frequency.py', - - 'Extract named entities from collected user messages': 'ner.py', - - 'Conduct a subscriber census across a list of target channels': 'census.py', - - 'Parse user messages to extract ideological indicators': 'indicators.py', - - 'Parse user messages to extract indicators of capability and violent intent': 'assessment.py' -} - +def print_logo(): + # Initialize colorama + init(autoreset=True) + + # Launcher code + print(f'{Fore.CYAN} __________________________________________________________________') + print(f'{Fore.CYAN} _______ ______ _ ______ _____ ______ _____ ____ _ _ ') + print(Fore.CYAN + r' |__ __| ____| | | ____| __ \| ____/ ____/ __ \| \ | | ') + print(Fore.CYAN + r' | | | |__ | | | |__ | |__) | |__ | | | | | | \| | ') + print(f'{Fore.CYAN} | | | __| | | | __| | _ /| __|| | | | | | . ` | ') + print(Fore.CYAN + r' | | | |____| |____| |____| | \ \| |___| |___| |__| | |\ | ') + print(Fore.CYAN + r' |_| |______|______|______|_| \_\______\_____\____/|_| \_| v2.1') + print(f'{Fore.CYAN}___________________________________________________________________') + print(Style.RESET_ALL) + print(f'{Fore.YELLOW} ') + print(f'{Fore.YELLOW}Welcome to Telerecon, a scraper and reconnaissance framework for Telegram') + print("") + print(f'{Fore.YELLOW}Please select an option:') + print(Style.RESET_ALL) def display(options): - for idx, option in enumerate(options.keys(), start=1): - print(f"{idx}. {option}") - + for idx, option in enumerate(options.keys(), start=1): + print(f"{idx}. {option}") def get_choice(options): - choose = int(input("\nPick a number: ")) - 1 - - if choose < 0 or choose >= len(options): - print('Invalid choice') - - return None - - return list(options.values())[choose] - - -display(options) - -if choice := get_choice(options): - # Stop any existing event loop - - stop_event_loop() - - print(f'Loading {choice}...') - - exec(open(choice).read()) \ No newline at end of file + while True: + display(options) + choose = int(input("\nPick a number: ")) - 1 + + if choose < 0 or choose >= len(options): + print('Invalid choice') + else: + return list(options.values())[choose] + +def load_and_run_module(choice): + try: + print(choice) + module = importlib.import_module(choice) + if choice in ('channels', 'channellist', 'userscraper', 'usermultiscraper', 'userdetails', 'urlscraper', 'recon', 'channelscraper'): + asyncio.run(module.main()) + else: + module.main() + + # Ask if the user wants to return to launcher + launcher = input('Do you want to return to the launcher? (y/n)') + if launcher.lower() == 'n': + return False + + except ImportError: + print(f'Failed to load {choice}') + + return True + +def main(): + options = { + 'Get user information': 'userdetails', + 'Check user activity across a list of channels': 'recon', + 'Collect user messages from a target channel': 'userscraper', + 'Collect user messages from a list of target channels': 'usermultiscraper', + 'Scrape all messages within a channel': 'channelscraper', + 'Scrape all t.me URL’s from within a channel': 'urlscraper', + 'Scrape forwarding relationships into a target channel': 'channels', + 'Scrape forwarding relationships into a list of target channel': 'channellist', + 'Identify possible user associates via interaction network map': 'network', + 'Parse user messages to extract selectors/intel': 'selector', + 'Extract GPS data from collected user media': 'metadata', + 'Create visualization report from collected user messages': 'frequency', + 'Extract named entities from collected user messages': 'ner', + 'Conduct a subscriber census across a list of target channels': 'census', + 'Parse user messages to extract ideological indicators': 'indicators', + 'Parse user messages to extract indicators of capability and violent intent': 'assessment' + } + + while True: + print_logo() + + if choice := get_choice(options): + print(f'Loading {choice}...') + if not load_and_run_module(choice): + break + +main() \ No newline at end of file diff --git a/metadata.py b/metadata.py index da86cb4..6b83690 100644 --- a/metadata.py +++ b/metadata.py @@ -48,8 +48,7 @@ def parse_gps_info(gps_info): return gps_data -# Main function -if __name__ == "__main__": +def main(): # Ask the user for a target @Username target_username = input("Enter the target @Username: ").strip() @@ -153,9 +152,6 @@ def parse_gps_info(gps_info): print(f"GPS metadata analysis completed. Results saved to {output_csv_file}") - # Ask if the user wants to return to the launcher - launcher = input('Do you want to return to the launcher? (y/n)') - if launcher == 'y': - print('Restarting...') - exec(open("launcher.py").read()) +if __name__ == "__main__": + main() diff --git a/ner.py b/ner.py index becf565..218e4f3 100644 --- a/ner.py +++ b/ner.py @@ -1,11 +1,12 @@ import pandas as pd import spacy import re +import os +import contextlib from reportlab.lib.pagesizes import letter from reportlab.platypus import SimpleDocTemplate, Paragraph from reportlab.lib.styles import getSampleStyleSheet from collections import Counter -import os # Function to create the target directory if it doesn't exist @@ -14,21 +15,6 @@ def create_target_directory(target_username): if not os.path.exists(target_dir): os.makedirs(target_dir) - -# Load the CSV file into a pandas DataFrame based on the target username -target_username = input("Enter the target username: ") -target_username = target_username.strip("@") # Remove @ symbol if present -csv_file = f'Collection/{target_username}/{target_username}_messages.csv' -if not os.path.exists(csv_file): - print(f"Error: CSV file not found for {target_username}. Make sure the directory structure is correct.") - exit() - -df = pd.read_csv(csv_file) - -# Load the spaCy NER model -nlp = spacy.load('en_core_web_sm') - - # Preprocessing function def preprocess_text(text): if isinstance(text, str): @@ -38,35 +24,8 @@ def preprocess_text(text): text) # Replace punctuation between alphanumeric characters with spaces return text - -# Dictionary to store named entities of each category with counts -import contextlib -entity_categories = { - 'PERSON': Counter(), - 'ORG': Counter(), - 'GPE': Counter(), - 'DATE': Counter(), - # You can add more categories here -} - -# Process each text and extract named entities -for index, row in df.iterrows(): - text = row['Text'] - - # Check if the text is a string - if isinstance(text, str): - preprocessed_text = preprocess_text(text) - - with contextlib.suppress(Exception): - doc = nlp(preprocessed_text) - entities = [(ent.text, ent.label_) for ent in doc.ents] - for entity, label in entities: - if label in entity_categories: - entity_categories[label][entity] += 1 - - # Create and export PDF with sorted entity tags -def export_entities_to_pdf(entity_categories, filename='entity_tags.pdf'): +def export_entities_to_pdf(entity_categories, target_username, filename='entity_tags.pdf'): doc = SimpleDocTemplate(f'Collection/{target_username}/{filename}', pagesize=letter) styles = getSampleStyleSheet() story = [] @@ -92,13 +51,47 @@ def export_entities_to_pdf(entity_categories, filename='entity_tags.pdf'): doc.build(story) -# Export entities to PDF -export_entities_to_pdf(entity_categories) -print(f"PDF report created: Collection/{target_username}/entity_tags.pdf") - -# Ask if the user wants to return to the launcher -launcher = input('Do you want to return to the launcher? (y/n)') - -if launcher == 'y': - print('Restarting...') - exec(open("launcher.py").read()) +def main(): + # Load the CSV file into a pandas DataFrame based on the target username + target_username = input("Enter the target username: ") + target_username = target_username.strip("@") # Remove @ symbol if present + csv_file = f'Collection/{target_username}/{target_username}_messages.csv' + if not os.path.exists(csv_file): + print(f"Error: CSV file not found for {target_username}. Make sure the directory structure is correct.") + exit() + + df = pd.read_csv(csv_file) + + # Load the spaCy NER model + nlp = spacy.load('en_core_web_sm') + + # Dictionary to store named entities of each category with counts + entity_categories = { + 'PERSON': Counter(), + 'ORG': Counter(), + 'GPE': Counter(), + 'DATE': Counter(), + # You can add more categories here + } + + # Process each text and extract named entities + for index, row in df.iterrows(): + text = row['Text'] + + # Check if the text is a string + if isinstance(text, str): + preprocessed_text = preprocess_text(text) + + with contextlib.suppress(Exception): + doc = nlp(preprocessed_text) + entities = [(ent.text, ent.label_) for ent in doc.ents] + for entity, label in entities: + if label in entity_categories: + entity_categories[label][entity] += 1 + + # Export entities to PDF + export_entities_to_pdf(entity_categories, target_username) + print(f"PDF report created: Collection/{target_username}/entity_tags.pdf") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/network.py b/network.py index a6ecec8..679e287 100644 --- a/network.py +++ b/network.py @@ -4,98 +4,103 @@ import matplotlib.pyplot as plt import numpy as np -# Ask the user for the target username -target_username = input("Enter the target username (e.g., @Johnsmith): ") -target_username = target_username.lstrip('@') # Remove leading '@' if present - -# Directory path -directory_path = f"Collection/{target_username}" - -# Load the network data CSV file -network_csv_path = os.path.join(directory_path, f"{target_username}_network.csv") -if not os.path.exists(network_csv_path): - print(f"Network data file not found: {network_csv_path}") -else: - # Read the CSV file into a DataFrame - network_data = pd.read_csv(network_csv_path) - - # Replace NaN values with blank spaces in the DataFrame - network_data = network_data.fillna(' ') - - # Create a directed graph - G = nx.DiGraph() - - # Add nodes (UserIDs) with formatted labels - for _, row in network_data.iterrows(): - sender_user_id = row['Sender_UserID'] - sender_username = row['Sender_Username'] - sender_first_name = row['Sender_FirstName'] - sender_last_name = row['Sender_LastName'] - - receiver_user_id = row['Receiver_UserID'] - receiver_username = row['Receiver_Username'] - receiver_first_name = row['Receiver_FirstName'] - receiver_last_name = row['Receiver_LastName'] - - # Create formatted labels with NaN values replaced by blank spaces - sender_label = f"{sender_first_name} {sender_last_name} (@{sender_username})\nUserID: {sender_user_id}" - receiver_label = f"{receiver_first_name} {receiver_last_name} (@{receiver_username})\nUserID: {receiver_user_id}" - - # Add nodes with formatted labels - G.add_node(sender_user_id, label=sender_label) - G.add_node(receiver_user_id, label=receiver_label) - - # Add edges (interactions) and count the number of interactions - interaction_count = {} - for _, row in network_data.iterrows(): - sender_user_id = row['Sender_UserID'] - receiver_user_id = row['Receiver_UserID'] - - # Create or update the interaction count - if (sender_user_id, receiver_user_id) in interaction_count: - interaction_count[(sender_user_id, receiver_user_id)] += 1 - else: - interaction_count[(sender_user_id, receiver_user_id)] = 1 - - # Add edges with interaction count as labels - G.add_edge(sender_user_id, receiver_user_id, interactions=interaction_count[(sender_user_id, receiver_user_id)]) - - # Create the network visualization with improved styling and dynamic layout - plt.figure(figsize=(14, 10)) - - # Customize the spring layout with better spacing - pos = nx.spring_layout(G, seed=42, k=0.15, iterations=50) # Adjust 'k' and 'iterations' as needed - - labels = nx.get_node_attributes(G, 'label') - interactions = nx.get_edge_attributes(G, 'interactions') - edge_labels = {(u, v): str(interactions[(u, v)]) for u, v in G.edges} - - # Node and edge styling - node_size = 300 # Increase node size to accommodate larger labels - node_color = 'lightblue' - edge_width = 1 - edge_color = 'gray' - font_size = 10 - font_color = 'black' - - # Draw nodes, edges, labels, and edge labels - nx.draw_networkx_nodes(G, pos, node_size=node_size, node_color=node_color) - nx.draw_networkx_edges(G, pos, width=edge_width, edge_color=edge_color) - - # Calculate the label positions to avoid cutoff - label_positions = {node: (pos[node][0], pos[node][1] - 0.02) for node in G.nodes()} - - nx.draw_networkx_labels(G, label_positions, labels, font_size=font_size, font_color=font_color, font_weight='bold') - - # Position edge labels to avoid overlap with nodes - for (u, v), label in edge_labels.items(): - x = (pos[u][0] + pos[v][0]) / 2 # Calculate x-coordinate - y = (pos[u][1] + pos[v][1]) / 2 # Calculate y-coordinate - plt.text(x, y, label, size=font_size, color=font_color, ha='center', va='center') - - plt.title(f"User Interaction Network for {target_username}") - - # Save the visualization to a file - network_viz_path = os.path.join(directory_path, f"{target_username}_network_visualization.png") - plt.savefig(network_viz_path, bbox_inches='tight', pad_inches=0.1, dpi=400) # Adjust DPI for higher resolution - print(f"Network visualization saved to: {network_viz_path}") + +def main(): + # Ask the user for the target username + target_username = input("Enter the target username (e.g., @Johnsmith): ") + target_username = target_username.lstrip('@') # Remove leading '@' if present + + # Directory path + directory_path = f"Collection/{target_username}" + + # Load the network data CSV file + network_csv_path = os.path.join(directory_path, f"{target_username}_network.csv") + if not os.path.exists(network_csv_path): + print(f"Network data file not found: {network_csv_path}") + else: + # Read the CSV file into a DataFrame + network_data = pd.read_csv(network_csv_path) + + # Replace NaN values with blank spaces in the DataFrame + network_data = network_data.fillna(' ') + + # Create a directed graph + G = nx.DiGraph() + + # Add nodes (UserIDs) with formatted labels + for _, row in network_data.iterrows(): + sender_user_id = row['Sender_UserID'] + sender_username = row['Sender_Username'] + sender_first_name = row['Sender_FirstName'] + sender_last_name = row['Sender_LastName'] + + receiver_user_id = row['Receiver_UserID'] + receiver_username = row['Receiver_Username'] + receiver_first_name = row['Receiver_FirstName'] + receiver_last_name = row['Receiver_LastName'] + + # Create formatted labels with NaN values replaced by blank spaces + sender_label = f"{sender_first_name} {sender_last_name} (@{sender_username})\nUserID: {sender_user_id}" + receiver_label = f"{receiver_first_name} {receiver_last_name} (@{receiver_username})\nUserID: {receiver_user_id}" + + # Add nodes with formatted labels + G.add_node(sender_user_id, label=sender_label) + G.add_node(receiver_user_id, label=receiver_label) + + # Add edges (interactions) and count the number of interactions + interaction_count = {} + for _, row in network_data.iterrows(): + sender_user_id = row['Sender_UserID'] + receiver_user_id = row['Receiver_UserID'] + + # Create or update the interaction count + if (sender_user_id, receiver_user_id) in interaction_count: + interaction_count[(sender_user_id, receiver_user_id)] += 1 + else: + interaction_count[(sender_user_id, receiver_user_id)] = 1 + + # Add edges with interaction count as labels + G.add_edge(sender_user_id, receiver_user_id, interactions=interaction_count[(sender_user_id, receiver_user_id)]) + + # Create the network visualization with improved styling and dynamic layout + plt.figure(figsize=(14, 10)) + + # Customize the spring layout with better spacing + pos = nx.spring_layout(G, seed=42, k=0.15, iterations=50) # Adjust 'k' and 'iterations' as needed + + labels = nx.get_node_attributes(G, 'label') + interactions = nx.get_edge_attributes(G, 'interactions') + edge_labels = {(u, v): str(interactions[(u, v)]) for u, v in G.edges} + + # Node and edge styling + node_size = 300 # Increase node size to accommodate larger labels + node_color = 'lightblue' + edge_width = 1 + edge_color = 'gray' + font_size = 10 + font_color = 'black' + + # Draw nodes, edges, labels, and edge labels + nx.draw_networkx_nodes(G, pos, node_size=node_size, node_color=node_color) + nx.draw_networkx_edges(G, pos, width=edge_width, edge_color=edge_color) + + # Calculate the label positions to avoid cutoff + label_positions = {node: (pos[node][0], pos[node][1] - 0.02) for node in G.nodes()} + + nx.draw_networkx_labels(G, label_positions, labels, font_size=font_size, font_color=font_color, font_weight='bold') + + # Position edge labels to avoid overlap with nodes + for (u, v), label in edge_labels.items(): + x = (pos[u][0] + pos[v][0]) / 2 # Calculate x-coordinate + y = (pos[u][1] + pos[v][1]) / 2 # Calculate y-coordinate + plt.text(x, y, label, size=font_size, color=font_color, ha='center', va='center') + + plt.title(f"User Interaction Network for {target_username}") + + # Save the visualization to a file + network_viz_path = os.path.join(directory_path, f"{target_username}_network_visualization.png") + plt.savefig(network_viz_path, bbox_inches='tight', pad_inches=0.1, dpi=400) # Adjust DPI for higher resolution + print(f"Network visualization saved to: {network_viz_path}") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/recon.py b/recon.py index 55dec7f..cfb7ad1 100644 --- a/recon.py +++ b/recon.py @@ -1,11 +1,11 @@ +import importlib import os import asyncio import details as ds from telethon import TelegramClient, errors, types import pandas as pd from colorama import Fore, Style -import subprocess -import sys + # API details api_id = ds.apiID @@ -15,7 +15,6 @@ # Rate limiting settings REQUEST_DELAY = 1 # Delay in seconds between requests - async def count_user_posts(client, channel_name, target_user): try: entity = await client.get_entity(channel_name) @@ -73,21 +72,24 @@ async def process_target_channels(target_user, target_list_filename): print() print(f"{Fore.GREEN}Success: Post counts saved to {csv_filename}{Style.RESET_ALL}") - -if __name__ == '__main__': +async def main(): target_user = input(f"{Fore.CYAN}Please enter the target user's @username or User ID:{Style.RESET_ALL} ") target_user = target_user.replace("@", "") # Remove "@" symbol target_list_filename = input( f"{Fore.CYAN}Please enter the filename of the target channel list (csv/txt):{Style.RESET_ALL} ") - asyncio.run(process_target_channels(target_user, target_list_filename)) + await process_target_channels(target_user, target_list_filename) # Ask the user if they want to scrape posts print() scrape_choice = input( f"{Fore.CYAN}Do you want to scrape posts from target channels? (y/n): {Style.RESET_ALL} ").strip().lower() - if scrape_choice == "y": - # Launch usermultiscraper.py using subprocess and sys.executable - subprocess.run([sys.executable, "usermultiscraper.py", target_user]) + if scrape_choice.lower() == "y": + model = importlib.import_module("usermultiscraper") + await model.main(target_user, target_list_filename) + +if __name__ == '__main__': + asyncio.run(main()) + diff --git a/selector.py b/selector.py index b0440f7..504d4a1 100644 --- a/selector.py +++ b/selector.py @@ -1,21 +1,13 @@ +from ast import main import os - import re - import pandas as pd - from openpyxl import Workbook - from reportlab.lib.pagesizes import letter - from reportlab.lib import colors - from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer - from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle - from reportlab.platypus import PageBreak - from validate_email_address import validate_email @@ -211,8 +203,7 @@ def extract_sentences(username, input_csv, output_pdf, target_phrases): print(f"Key phrase extraction report saved to {output_pdf_path}") - -if __name__ == "__main__": +def main(): target_phrases = [ # List of target phrases here @@ -245,18 +236,12 @@ def extract_sentences(username, input_csv, output_pdf, target_phrases): ] # Get the target username from the user - target_username = input("Enter the target username (with @): ") # Run the extraction and PDF generation - extract_sentences(target_username, "input.csv", target_username + "_keyphrase_report.pdf", target_phrases) -# Ask if the user wants to return to the launcher - -launcher = input('Do you want to return to the launcher? (y/n)') - -if launcher == 'y': - print('Restarting...') +if __name__ == "__main__": + main() + - exec(open("launcher.py").read()) diff --git a/urlscraper.py b/urlscraper.py index 4c060c2..71f680e 100644 --- a/urlscraper.py +++ b/urlscraper.py @@ -14,50 +14,51 @@ async def main(): - client = TelegramClient(phone, api_id, api_hash) + async with TelegramClient(phone, api_id, api_hash) as client: - await client.start() - if not await client.is_user_authorized(): - await client.send_code_request(phone) - await client.sign_in(phone, input('Enter the code: ')) + if not await client.is_user_authorized(): + await client.send_code_request(phone) + await client.sign_in(phone, input('Enter the code: ')) - print( - f'{Fore.CYAN}Please enter a target Telegram channel (e.g. https://t.me/{Fore.LIGHTYELLOW_EX}your_channel{Fore.CYAN}):{Style.RESET_ALL}\n') - print() + print( + f'{Fore.CYAN}Please enter a target Telegram channel (e.g. {Fore.LIGHTYELLOW_EX}@your_channel{Fore.CYAN}):{Style.RESET_ALL}\n') + print() - while True: - try: - channel_name = input("Please enter a Telegram channel name: ") - print(f'You entered "{channel_name}"') - answer = input('Is this correct? (y/n) ') - if answer == 'y': - print(f'Scraping URLs from {channel_name}...') - break - except Exception: - continue + while True: + try: + channel_name = input("Please enter a Telegram channel name: ") + print(f'You entered "{channel_name}"') + answer = input('Is this correct? (y/n) ') + if answer.lower() == 'y': + channel_name = channel_name.replace('@', '') + print(f'Scraping URLs from {channel_name}...') + break + except Exception: + continue - urls = set() # Use a set to deduplicate URLs + urls = set() # Use a set to deduplicate URLs - async for message in client.iter_messages(channel_name): - if message.entities is not None: - for entity in message.entities: - if isinstance(entity, MessageEntityTextUrl): - url = entity.url - if 'https://t.me/' in url: - if match := re.match( - r'https?://t\.me/([^/\s]+)/?', url - ): - channel_link = f'https://t.me/{match[1]}' - urls.add(channel_link) - print(f"URL - {Fore.CYAN}https://t.me/{match[1]}{Style.RESET_ALL}") - elif message.text and isinstance(message.text, str): - matches = re.findall(r'https?://t\.me/([^/\s]+)/?', message.text) - for match in matches: - channel_link = f'https://t.me/{match}' - urls.add(channel_link) - print(f"URL - {Fore.CYAN}https://t.me/{match}{Style.RESET_ALL}") + async for message in client.iter_messages(channel_name): + if message.entities is not None: + for entity in message.entities: + if isinstance(entity, MessageEntityTextUrl): + url = entity.url + if 'https://t.me/' in url: + if match := re.match( + r'https?://t\.me/([^/\s]+)/?', url + ): + channel_link = f'https://t.me/{match[1]}' + urls.add(channel_link) + print(f"URL - {Fore.CYAN}https://t.me/{match[1]}{Style.RESET_ALL}") + elif message.text and isinstance(message.text, str): + matches = re.findall(r'https?://t\.me/([^/\s]+)/?', message.text) + for match in matches: + channel_link = f'https://t.me/{match}' + urls.add(channel_link) + print(f"URL - {Fore.CYAN}https://t.me/{match}{Style.RESET_ALL}") + urls_folder = 'URLs' os.makedirs(urls_folder, exist_ok=True) output_filename = os.path.join(urls_folder, f'{channel_name}.csv') diff --git a/userdetails.py b/userdetails.py index c50a7e6..f7d861c 100644 --- a/userdetails.py +++ b/userdetails.py @@ -1,17 +1,12 @@ import os - +import asyncio import datetime - from telethon import TelegramClient - from telethon.tl.types import UserStatusOffline - from colorama import init, Fore, Style - from details import apiID, apiHash, number # Initialize colorama for colored console output - init(autoreset=True) @@ -26,7 +21,6 @@ async def get_user_information(client, identifier, username): user = await client.get_entity(identifier) # Create a user-specific directory - user_directory = os.path.join("Collection", username) if not os.path.exists(user_directory): @@ -135,31 +129,18 @@ async def main(): identifier = input(f"{Fore.CYAN}Enter target @username{Fore.RESET}: ") # Create a 'Collection' directory if it doesn't exist - if not os.path.exists("Collection"): os.makedirs("Collection") async with TelegramClient('session_name', apiID, apiHash) as client: # Remove "@" symbol from the username if present - username = identifier.replace('@', '') # Call the function with the modified username - await get_user_information(client, identifier, username) - # Ask if the user wants to return to the launcher - - launcher = input('Do you want to return to the launcher? (y/n)') - - if launcher == 'y': - print('Exiting...') - - # You can use 'os.system' to run the launcher script - - os.system('python3 launcher.py') - if __name__ == "__main__": asyncio.run(main()) + diff --git a/usermultiscraper.py b/usermultiscraper.py index 7e3b2f2..e28b4e1 100644 --- a/usermultiscraper.py +++ b/usermultiscraper.py @@ -134,10 +134,11 @@ async def scrape_user_messages(channel_name, target_user, user_directory, downlo return [], [] -async def main(): +async def main(target_user=None, target_list_filename=None): print() - target_user = input(f"{Fore.CYAN}Please enter the target user's @username: {Style.RESET_ALL}") - target_list_filename = input( + if target_user is None: + target_user = input(f"{Fore.CYAN}Please enter the target user's @username: {Style.RESET_ALL}") + target_list_filename = input( f"{Fore.CYAN}Please enter the filename of the target channel list (csv/txt): {Style.RESET_ALL}") download_media_option = input(f"{Fore.CYAN}Would you like to download the target's media (y/n)? {Style.RESET_ALL}") download_media = download_media_option.lower() == 'y' @@ -202,13 +203,6 @@ async def main(): except Exception as e: print(f"{Fore.RED}An error occurred while saving network data to CSV: {e}{Style.RESET_ALL}") - # Ask if the user wants to return to the launcher - launcher = input('Do you want to return to the launcher? (y/n)') - - if launcher == 'y': - print('Restarting...') - exec(open("launcher.py").read()) - if __name__ == '__main__': asyncio.run(main()) diff --git a/userscraper.py b/userscraper.py index 6d45ade..0cf4f7b 100644 --- a/userscraper.py +++ b/userscraper.py @@ -18,7 +18,6 @@ # Define the REQUEST_DELAY REQUEST_DELAY = 1 # Delay in seconds between requests - async def scrape_user_messages(channel_name, target_user, user_directory, download_media, sanitized_target_user): media_directory = os.path.join(user_directory, f"{sanitized_target_user.lstrip('@')}_media") # Sub-directory for media @@ -173,13 +172,6 @@ async def main(): except Exception as e: print(f"{Fore.RED}An error occurred while saving network data to CSV: {e}{Style.RESET_ALL}") - # Ask if the user wants to return to launcher - launcher = input('Do you want to return to the launcher? (y/n)') - - if launcher == 'y': - print('Restarting...') - exec(open("launcher.py").read()) - - if __name__ == '__main__': asyncio.run(main()) +