diff --git a/bot/bot.py b/bot/bot.py index 9dd7368..b043bb7 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -123,6 +123,7 @@ async def on_message(self, message): await self.add_cog(ScriptCog(self)) console.print_info(f"Loaded script: {script_name}") + self.controller.add_startup_script(script) except Exception as e: console.print_error(f"Error loading script: {script_name} - {e}") @@ -224,7 +225,8 @@ async def on_message_delete(self, message): if message.author.id == self.user.id: return delete_time = time.time() - self.controller.gui.home_page.add_discord_log(message.author, message, delete_time) + if self.controller.gui and hasattr(self.controller.gui, 'tools_page'): + self.controller.gui.tools_page.message_logger_page.add_discord_log(message.author, message, delete_time) def run_bot(self): try: diff --git a/bot/commands/util.py b/bot/commands/util.py index 9fd7d80..64fdd93 100644 --- a/bot/commands/util.py +++ b/bot/commands/util.py @@ -350,7 +350,7 @@ async def spypet(self, ctx, member_id: int): mutual_guilds = [guild for guild in self.bot.guilds if guild.get_member(member_id)] data = {} tasks = [] - sem = asyncio.Semaphore(15) + sem = asyncio.Semaphore(10) stop_event = asyncio.Event() last_saved_count = 0 @@ -363,6 +363,7 @@ def _save_data(): if current_count == last_saved_count: return last_saved_count = current_count + with open(files.get_application_support() + "/data/spypet.json", "w") as f: json.dump(data, f, indent=4) console.print_info(f"Auto-saved {current_count} messages.") @@ -399,15 +400,18 @@ async def _fetch_context_channel(channel): latest_msg = [msg async for msg in channel.history(limit=1)][0] context = await self.bot.get_context(latest_msg) + console.success(f"Got context for {channel.guild.name} - {channel.name}") + return context.channel except Exception as e: if "429" in str(e): + console.error(f"Rate limited while fetching context for {channel.guild.name} - {channel.name}") await asyncio.sleep(5) return await _fetch_context_channel(channel) return None - async def _get_messages(channel, delay=0.25): + async def _get_messages(channel, delay=0.25, oldest_first=False): async with sem: try: await asyncio.sleep(delay) @@ -418,7 +422,8 @@ async def _get_messages(channel, delay=0.25): messages = [] try: - async for msg in channel.history(limit=999999999, oldest_first=False): + async for msg in channel.history(limit=None, oldest_first=oldest_first): + print(channel.name, msg.author.id, msg.content) if msg.author.id == member_id: if len(msg.attachments) > 0: attachments = "\n".join([f"Attachment: {attachment.url}" for attachment in msg.attachments]) @@ -427,13 +432,20 @@ async def _get_messages(channel, delay=0.25): msg_string = f"[{msg.created_at.strftime('%Y-%m-%d %H:%M:%S')}] {msg.content}" messages.append(msg_string) _add_message(guild, channel, msg_string) + console.print_info(f"Found message in {channel.guild.name} - {channel.name}") if len(messages) > 0: console.print_success(f"Found messages in {channel.guild.name} - {channel.name}") else: console.print_error(f"Found no messages in {channel.guild.name} - {channel.name}") - except: - console.print_error(f"Failed to fetch messages in {channel.guild.name} - {channel.name}") + except Exception as e: + if "429" in str(e).lower(): + console.print_error("Rate limited! Waiting for 5 seconds...") + await asyncio.sleep(5) + return await _get_messages(channel, delay) + else: + console.print_error(f"Error in {channel.guild.name} - {channel.name}: {e}") + return _save_data() except asyncio.CancelledError: @@ -446,14 +458,33 @@ async def _get_messages(channel, delay=0.25): finally: _save_data() - delay = 0.5 + async def _attempt_scrape(guild, delay): + tasks = [] + console.info(f"Attempting to scrape {guild.name} - {guild.id}") + + for channel in guild.channels: + if isinstance(channel, discord.TextChannel) and _get_permissions(channel): + tasks.append(_get_messages(channel, delay, oldest_first=True)) + delay += 2 + + if len(tasks) == 0: + console.print_error(f"No valid channels in {guild.name} - {guild.id}") + return + + try: + await asyncio.gather(*tasks) + except asyncio.CancelledError: + console.print_warning("Process was cancelled! Saving progress...") + stop_event.set() + _save_data() + raise + + delay = 1 autosave_task = asyncio.create_task(_autosave(5)) for guild in mutual_guilds: - for channel in guild.text_channels: - if _get_permissions(channel): - tasks.append(asyncio.create_task(_get_messages(channel, delay))) - delay += 0.5 + tasks.append(_attempt_scrape(guild, delay)) + delay += 1.5 await asyncio.gather(*tasks) stop_event.set() @@ -464,7 +495,7 @@ async def _get_messages(channel, delay=0.25): console.print_info(f"Total messages: {_count_messages()}") console.print_info(f"Total guilds: {len(data)}") console.print_info(f"Total channels: {sum(len(channels) for channels in data.values())}") - await ctx.send(file=discord.File("data/spypet.json"), delete_after=self.cfg.get("message_settings")["auto_delete_delay"]) + await ctx.send(file=discord.File(files.get_application_support() + "/data/spypet.json"), delete_after=self.cfg.get("message_settings")["auto_delete_delay"]) def setup(bot): bot.add_cog(Util(bot)) diff --git a/bot/controller.py b/bot/controller.py index 54ef80b..e5189db 100644 --- a/bot/controller.py +++ b/bot/controller.py @@ -15,7 +15,7 @@ from bot.helpers import cmdhelper, imgembed import utils.webhook as webhook_client from gui.helpers.images import resize_and_sharpen -from bot.helpers.spypet import Spypet +from bot.tools import SpyPet if getattr(sys, 'frozen', False): os.chdir(os.path.dirname(sys.executable)) @@ -33,13 +33,42 @@ def __init__(self): self.bot_running = False self.startup_scripts = [] self.presence = self.cfg.get_rich_presence() - self.spypet = Spypet() + self.spypet = SpyPet(self) + + def start_spypet(self): + if not self.spypet.bot: + self.spypet.set_bot(self.bot) + console.success("SpyPet bot set successfully.") + else: + console.warning("SpyPet bot is already set.") + + if not self.spypet.member_id: + console.error("SpyPet member ID is not set. Please set it in the settings.") + return + + asyncio.run_coroutine_threadsafe(self.spypet.start(), self.loop) + console.success("SpyPet started successfully!") + + def stop_spypet(self): + if self.spypet.running: + asyncio.run_coroutine_threadsafe(self.spypet.stop(), self.loop) + console.success("SpyPet stopped successfully!") + else: + console.warning("SpyPet is not running.") + + def get_mutual_guilds_spypet(self): + if self.spypet.running: + return asyncio.run_coroutine_threadsafe(self.spypet.get_mutual_guilds(), self.loop).result() + else: + console.warning("SpyPet is not running. Cannot get mutual guilds.") + return [] def add_startup_script(self, script): self.startup_scripts.append(script) def set_gui(self, gui): self.gui = gui + self.spypet.set_gui(gui.tools_page.spypet_page) def check_token(self): resp = requests.get("https://discord.com/api/v9/users/@me", headers={"Authorization": self.cfg.get("token")}) @@ -190,16 +219,19 @@ def get_user_from_id(self, user_id): return self.bot.get_user(user_id) def get_avatar_from_url(self, url, size=50, radius=5): - url = url.split("?")[0] - if url.endswith(".gif"): - url = url.replace(".gif", ".png") - response = requests.get(url) - if response.status_code == 200: - image = Image.open(BytesIO(response.content)) - # image = image.resize((size, size)) - image = resize_and_sharpen(image, (size, size)) - image = imgembed.add_corners(image, radius) - return ImageTk.PhotoImage(image) + try: + url = url.split("?")[0] + if url.endswith(".gif"): + url = url.replace(".gif", ".png") + response = requests.get(url, timeout=10) + if response.status_code == 200: + image = Image.open(BytesIO(response.content)) + # image = image.resize((size, size)) + image = resize_and_sharpen(image, (size, size)) + image = imgembed.add_corners(image, radius) + return ImageTk.PhotoImage(image) + except Exception as e: + print(f"Error processing avatar from URL {url}: {e}") return None @@ -216,6 +248,18 @@ def get_avatar(self, size=50, radius=5): def set_prefix(self, prefix): self.bot.command_prefix = prefix + + async def get_user_from_id_async(self, user_id): + print(f"[BotController] Getting user from ID: {user_id}") + try: + return await self.bot.fetch_user(user_id) if self.bot else None + except Exception as e: + console.print_error(f"Error getting user from ID {user_id}: {e}") + return None + + def get_user_from_id(self, user_id): + return asyncio.run_coroutine_threadsafe(self.get_user_from_id_async(user_id), self.loop).result() + get_user = lambda self: self.bot.user if self.bot else None get_friends = lambda self: self.bot.friends if self.bot else None get_guilds = lambda self: self.bot.guilds if self.bot else None diff --git a/bot/helpers/cmdhelper.py b/bot/helpers/cmdhelper.py index 86edffb..7b6c218 100644 --- a/bot/helpers/cmdhelper.py +++ b/bot/helpers/cmdhelper.py @@ -100,6 +100,8 @@ async def rich_embed(ctx, embed): webhook_client.send(embed=embed.to_dict()) async for message in webhook_channel.history(limit=1): + await message.ack() + try: resp = requests.post( f"https://discord.com/api/v9/channels/{ctx.channel.id}/messages", diff --git a/bot/helpers/imgembed.py b/bot/helpers/imgembed.py index 6e5767b..bb116e2 100644 --- a/bot/helpers/imgembed.py +++ b/bot/helpers/imgembed.py @@ -285,7 +285,10 @@ def draw(self): def save(self): path = f"embed-{random.randint(1000, 9999)}.png" # comment this out if youre running the script directly # path = "embed.png" # uncomment this if youre running the script directly - self.draw().save(path) + final = self.draw() + final.thumbnail((self.width // 2, self.height // 2), Image.LANCZOS) + # final = final.resize((self.width // 2, self.height // 2), Image.LANCZOS) + final.save(path, optimize=True, quality=20) return path diff --git a/bot/tools/__init__.py b/bot/tools/__init__.py new file mode 100644 index 0000000..b9bfd27 --- /dev/null +++ b/bot/tools/__init__.py @@ -0,0 +1 @@ +from .spypet import SpyPet \ No newline at end of file diff --git a/bot/tools/spypet.py b/bot/tools/spypet.py new file mode 100644 index 0000000..e41b9d4 --- /dev/null +++ b/bot/tools/spypet.py @@ -0,0 +1,319 @@ +import asyncio +import discord +import json +import random +from datetime import datetime +from discord.errors import HTTPException + +from utils import files +from utils.config import Config + +class SpyPetConsole: + def __init__(self, controller): + self.controller = controller + self.gui = None + + def set_gui(self, gui): + self.gui = gui + + def add_log(self, prefix, text): + self.gui.add_log(prefix, text) + + def success(self, message): + self.add_log("success", message) + + def error(self, message): + self.add_log("error", message) + + def info(self, message): + self.add_log("info", message) + + def warning(self, message): + self.add_log("warning", message) + +class SpyPet: + def __init__(self, controller): + self.bot = None + self.gui = None + self.controller = controller + self.cfg = Config() + self.running = False + self.mutual_guilds = [] + self.data = {} + self.semaphore = asyncio.Semaphore(5) + self.member_id = None + self.cache = set() + self.console = SpyPetConsole(controller) + self.tasks = [] + self.total_messages = 0 + self.user_total_messages = 0 + + def set_gui(self, gui): + self.gui = gui + self.console.set_gui(gui) + self.console.success("SpyPet GUI set successfully.") + + def set_bot(self, bot): + self.bot = bot + self.load_cache() + self.console.success("SpyPet bot set successfully.") + + def get_data_path(self): + return files.get_application_support() + f"/data/spypet/{self.member_id}.json" + + def save_data(self): + if not self.data: + return + try: + with open(self.get_data_path(), "w") as f: + json.dump(self.data, f, indent=4) + except Exception as e: + self.console.error(f"Error saving data: {e}") + + def load_cache(self): + try: + with open(self.get_data_path(), "r") as f: + self.data = json.load(f) + for guild in self.data.values(): + for channel_data in guild.values(): + for msg in channel_data.get("messages", []): + if isinstance(msg, dict) and "id" in msg: + self.cache.add(msg["id"]) + self.total_messages += 1 + self.user_total_messages += 1 + self.console.success(f"Loaded {len(self.cache)} cached messages.") + except FileNotFoundError: + self.console.info("No existing cache file found.") + except Exception as e: + self.console.error(f"Error loading cache: {e}") + + self.update_progress_labels() + + async def get_permissions(self, channel): + member = await channel.guild.fetch_member(self.member_id) + member_role = member.top_role + bot_role = channel.guild.me.top_role + + return ( + channel.permissions_for(member).read_messages and + channel.permissions_for(channel.guild.me).read_messages or + channel.overwrites_for(member_role).read_messages and + channel.overwrites_for(bot_role).read_messages + ) + + async def fetch_context_channel(self, channel): + try: + latest_msg = [msg async for msg in channel.history(limit=1)][0] + context = await self.bot.get_context(latest_msg) + if context: + self.console.success(f"Context fetched for channel: {channel.name}") + return context.channel + except Exception as e: + if "429" in str(e): + self.console.error(f"Rate limit exceeded for channel: {channel.name} - Waiting...") + await asyncio.sleep(5) + return await self.fetch_context_channel(channel) + return None + + def update_progress_labels(self): + if not self.gui: + return + + if self.total_messages > 0 or self.user_total_messages > 0: + self.gui._update_progress_labels(self.total_messages, self.user_total_messages) + + def add_message(self, guild, channel, message): + gname, cname = guild.name, channel.name + if gname not in self.data: + self.data[gname] = {} + if cname not in self.data[gname]: + self.data[gname][cname] = { + "oldest_timestamp": message.created_at.isoformat(), + "newest_timestamp": message.created_at.isoformat(), + "messages": [] + } + + entry = self.data[gname][cname] + + msg_data = { + "id": message.id, + "timestamp": message.created_at.isoformat(), + "content": message.content, + "attachments": [a.url for a in message.attachments] + } + entry["messages"].append(msg_data) + self.cache.add(message.id) + + ts = message.created_at.isoformat() + if ts < entry["oldest_timestamp"]: + entry["oldest_timestamp"] = ts + if ts > entry["newest_timestamp"]: + entry["newest_timestamp"] = ts + + self.update_progress_labels() + + async def get_messages(self, channel, delay=.5): + async with self.semaphore: + try: + if not self.running: + return + + await asyncio.sleep(delay) + + self.console.info(f"Fetching messages from channel: {channel.name}") + channel = await self.fetch_context_channel(channel) or channel + guild = channel.guild + + gname, cname = guild.name, channel.name + entry = self.data.get(gname, {}).get(cname, {}) + + oldest = entry.get("oldest_timestamp") + newest = entry.get("newest_timestamp") + + # Convert to datetime + before_dt = datetime.fromisoformat(oldest) if oldest else None + after_dt = datetime.fromisoformat(newest) if newest else None + + # Fetch newer messages + if after_dt: + async for message in channel.history(limit=None, oldest_first=False, after=after_dt): + if int(message.author.id) == int(self.member_id) and message.id not in self.cache: + self.add_message(guild, channel, message) + self.user_total_messages += 1 + self.total_messages += 1 + self.update_progress_labels() + + # Fetch older messages + if before_dt: + async for message in channel.history(limit=None, oldest_first=True, before=before_dt): + if int(message.author.id) == int(self.member_id) and message.id not in self.cache: + self.add_message(guild, channel, message) + self.user_total_messages += 1 + self.total_messages += 1 + self.update_progress_labels() + + # If no known timestamps, fetch all + if not after_dt and not before_dt: + async for message in channel.history(limit=None, oldest_first=False): + if int(message.author.id) == int(self.member_id) and message.id not in self.cache: + self.add_message(guild, channel, message) + self.user_total_messages += 1 + self.total_messages += 1 + self.update_progress_labels() + + except HTTPException as e: + if e.status == 429: + retry_after = getattr(e, "retry_after", 8) + self.console.error(f"Rate limited on {channel.name}. Retrying in {retry_after}s...") + await asyncio.sleep(retry_after) + return await self.get_messages(channel) + else: + self.console.error(f"HTTP error on {channel.name}: {e}") + except Exception as e: + self.console.error(f"Error fetching messages from {channel.guild.name} - {channel.name}: {e}") + + async def attempt_scrape(self, guild, delay=1): + if not self.running: + return + + for channel in guild.channels: + if not self.running: + return + if isinstance(channel, discord.TextChannel) and await self.get_permissions(channel): + print(f"Adding task for channel: {channel.name}") + task = asyncio.create_task(self.get_messages(channel, delay=delay)) + self.tasks.append(task) + delay += 0.85 + + async def get_mutual_guilds(self): + mutual_guilds = [] + + for guild in self.bot.guilds: + try: + member = await guild.fetch_member(int(self.member_id)) + except Exception as e: + member = None + if member: + mutual_guilds.append(guild) + self.console.info(f"Found mutual guild: {guild.name} with {len(guild.channels)} channels.") + + return mutual_guilds + + def organise_guilds(self): + self.mutual_guilds.sort(key=lambda g: len(g.members), reverse=True) + self.mutual_guilds.sort(key=lambda g: g.name.lower()) + + async def start(self): + if self.member_id is None: + self.console.error("Member ID not set.") + return + + self.running = True + self.console.info("SpyPet started.") + self.mutual_guilds = await self.get_mutual_guilds() + self.gui._check_spypet_running() + self.gui.mutual_guilds = self.mutual_guilds + + if not self.tasks: + if not self.mutual_guilds: + self.console.error("No mutual guilds found.") + await self.stop() + return + + self.organise_guilds() + self.load_cache() + + delay = 1 + for guild in self.mutual_guilds: + if not self.running: + return + await self.attempt_scrape(guild, delay=delay) + delay += 0.54 + + if not self.tasks: + self.console.error("No tasks created.") + await self.stop() + return + + for _ in range(3): + random.shuffle(self.tasks) + + self.console.info(f"Starting {len(self.tasks)} tasks...") + await asyncio.gather(*self.tasks) + + self.console.success("All tasks completed.") + await self.stop() + + async def stop(self): + self.running = False + + for task in self.tasks: + if not task.done(): + task.cancel() + self.console.warning("Cancelled a running task.") + self.tasks.clear() + + if self.data: + self.console.info("Saving data...") + self.save_data() + + self.console.success("SpyPet stopped and data saved.") + self.gui._check_spypet_running() + + def reset(self): + self.running = False + self.data = {} + self.member_id = None + self.cache.clear() + self.tasks.clear() + self.total_messages = 0 + self.user_total_messages = 0 + self.console.info("SpyPet reset. All data cleared and tasks cancelled.") + self.gui._check_spypet_running() + + def set_member_id(self, member_id): + self.member_id = member_id + self.data = {} + self.cache.clear() + self.load_cache() + self.console.success(f"Member ID set to {self.member_id}. Cache cleared and data loaded.") \ No newline at end of file diff --git a/data/gui_theme-light.json b/data/gui_theme-light.json new file mode 100644 index 0000000..ed37b3c --- /dev/null +++ b/data/gui_theme-light.json @@ -0,0 +1,27 @@ +{ + "themes": [ + { + "ghost": { + "type": "light", + "colors": { + "primary": "#433dfb", + "secondary": "#f0f0f0", + "success": "#0abf34", + "info": "#2b6eff", + "warning": "#f39c12", + "danger": "#ff341f", + "light": "#f8f9fa", + "dark": "#f0f0f0", + "bg": "#ffffff", + "fg": "#000000", + "selectbg": "#d6d6d6", + "selectfg": "#000000", + "border": "#cccccc", + "inputfg": "#000000", + "inputbg": "#ffffff", + "active": "#e6e6e6" + } + } + } + ] +} diff --git a/data/icons/chevron-right-solid.png b/data/icons/chevron-right-solid.png new file mode 100644 index 0000000..97ec7ca Binary files /dev/null and b/data/icons/chevron-right-solid.png differ diff --git a/data/icons/play-solid.png b/data/icons/play-solid.png new file mode 100644 index 0000000..b97b41c Binary files /dev/null and b/data/icons/play-solid.png differ diff --git a/data/icons/rotate-left-solid.png b/data/icons/rotate-left-solid.png new file mode 100644 index 0000000..7d5c107 Binary files /dev/null and b/data/icons/rotate-left-solid.png differ diff --git a/data/icons/stop-solid.png b/data/icons/stop-solid.png new file mode 100644 index 0000000..ccccacf Binary files /dev/null and b/data/icons/stop-solid.png differ diff --git a/gui/components/__init__.py b/gui/components/__init__.py index 1461595..0bd6d45 100644 --- a/gui/components/__init__.py +++ b/gui/components/__init__.py @@ -4,4 +4,5 @@ from .console import Console from .settings_frame import SettingsFrame from .settings_panel import SettingsPanel -from .titlebar import Titlebar \ No newline at end of file +from .titlebar import Titlebar +from .tool_page import ToolPage \ No newline at end of file diff --git a/gui/components/console.py b/gui/components/console.py index 66573f1..a577dd5 100644 --- a/gui/components/console.py +++ b/gui/components/console.py @@ -96,7 +96,7 @@ def _draw_footer(self, parent): clear_btn.configure(background=self.root.style.colors.get("secondary")) clear_btn.bind("", lambda e: self.clear()) clear_btn.grid(row=0, column=3, padx=(10, 8), pady=5, sticky="e") - + def _draw_main(self, parent): wrapper = RoundedFrame(parent, radius=15, bootstyle="dark.TFrame") wrapper.pack(side="top", fill="both", expand=True) @@ -112,12 +112,11 @@ def _draw_main(self, parent): highlightbackground=self.root.style.colors.get("dark"), state="normal" ) - - # Disable insert/delete actions - self.textarea.bind("", lambda e: "break") - self.textarea.bind("", lambda e: "break") # Middle-click paste (Linux) - self.textarea.bind("", lambda e: "break") - self.textarea.bind("", lambda e: "break") + + self.textarea.bind_all( + "" if sys.platform != "darwin" else "", + lambda _: self.textarea.event_generate("<>") + ) self.textarea.pack(fill="both", expand=True, padx=5, pady=5) self._load_tags() diff --git a/gui/components/rounded_frame.py b/gui/components/rounded_frame.py index dc73bd7..cd3f36a 100644 --- a/gui/components/rounded_frame.py +++ b/gui/components/rounded_frame.py @@ -4,7 +4,7 @@ class RoundedFrame(ttk.Canvas): def __init__(self, parent, radius=(25, 25, 25, 25), **kwargs): canvas_kwargs = {} for key in kwargs: - if key not in ["padx", "pady", "bootstyle", "style", "background", "parent_background"]: + if key not in ["padx", "pady", "bootstyle", "style", "background", "parent_background", "custom_size", "min_width", "min_height"]: canvas_kwargs[key] = kwargs[key] super().__init__(parent, highlightthickness=0, bd=0, **canvas_kwargs) @@ -16,6 +16,13 @@ def __init__(self, parent, radius=(25, 25, 25, 25), **kwargs): self.frame_background = self.style.colors.get(bootstyle.split(".")[0]) if kwargs.get("background") is None else kwargs.get("background") self.parent_background = self._get_parent_background() if kwargs.get("parent_background") is None else kwargs.get("parent_background") + self.min_width = kwargs.get("min_width", 0) + self.min_height = kwargs.get("min_height", 0) + + if kwargs.get("custom_size"): + self.pack_propagate(False) + self.grid_propagate(False) + self.configure(background=self.parent_background) self.inner_frame = ttk.Frame(self) self.create_window(0, 0, window=self.inner_frame, anchor="nw") @@ -43,6 +50,14 @@ def on_resize(self, event=None): if width < 2 or height < 2: return + # Enforce minimum size + if width < self.min_width: + width = self.min_width + self.configure(width=width) + if height < self.min_height: + height = self.min_height + self.configure(height=height) + width -= 1 height -= 1 @@ -66,7 +81,7 @@ def on_resize(self, event=None): try: self.itemconfig(self.inner_frame, width=width, height=height) - except Exception as e: + except Exception: pass def set_corner_radius(self, radius): @@ -75,4 +90,19 @@ def set_corner_radius(self, radius): def set_background(self, background): self.frame_background = background - self.on_resize() \ No newline at end of file + self.on_resize() + + def set_height(self, height): + self.configure(height=height) + self.on_resize() + + def set_width(self, width): + self.configure(width=width) + self.pack_propagate(False) # prevent geometry propagation + self.grid_propagate(False) + self.on_resize() + + def bind(self, sequence=None, func=None, add=None): + if sequence == "": + return super().bind(sequence, self.on_resize, add=add) + return super().bind(sequence, func, add=add) \ No newline at end of file diff --git a/gui/components/sidebar.py b/gui/components/sidebar.py index 1184b0f..22edcc6 100644 --- a/gui/components/sidebar.py +++ b/gui/components/sidebar.py @@ -1,4 +1,4 @@ -import os +import os, sys import ttkbootstrap as ttk from ttkbootstrap.dialogs import Messagebox from gui.helpers import Images @@ -41,12 +41,20 @@ def _create_button(self, image, page_name, command, row): self.tk_buttons.append(button) + def set_button_command(self, page_name, command): + if page_name in self.button_cmds: + self.button_cmds[page_name] = command + else: + raise ValueError(f"Button '{page_name}' not found in sidebar.") + def _quit(self): - if str(Messagebox.yesno("Are you sure you want to quit?", title="Ghost")).lower() == "yes": - if os.name == "nt": - os.kill(os.getpid(), 9) - else: - os._exit(0) + # if str(Messagebox.yesno("Are you sure you want to quit?", title="Ghost")).lower() == "yes": + # if os.name == "nt": + # os.kill(os.getpid(), 9) + # else: + # os._exit(0) + self.root.destroy() + sys.exit(0) def disable(self): allowed = ["home", "console"] @@ -68,7 +76,7 @@ def draw(self): "console": self._create_button(self.images.get("console"), "console", self.button_cmds["console"], 1), "settings": self._create_button(self.images.get("settings"), "settings", self.button_cmds["settings"], 2), "scripts": self._create_button(self.images.get("scripts"), "scripts", self.button_cmds["scripts"], 3), - # "tools": self._create_button(self.images.get("tools"), "tools", self.button_cmds["tools"], 4), + "tools": self._create_button(self.images.get("tools"), "tools", self.button_cmds["tools"], 4), } logout_btn = ttk.Label(self.sidebar, image=self.images.get("logout"), background=self.root.style.colors.get("dark"), anchor="center") diff --git a/gui/components/tool_page.py b/gui/components/tool_page.py new file mode 100644 index 0000000..34dd2bf --- /dev/null +++ b/gui/components/tool_page.py @@ -0,0 +1,52 @@ +import abc +import ttkbootstrap as ttk +from gui.components import RoundedFrame + +class ToolPage(abc.ABC): + def __init__(self, toolspage, root, bot_controller, images, layout, title, frame=True): + self.toolspage = toolspage + self.root = root + self.bot_controller = bot_controller + self.images = images + self.layout = layout + self.title = title + self.frame = frame + + def go_back(self): + self.layout.sidebar.set_current_page("tools") + self.layout.clear() + main = self.layout.main(scrollable=True) + self.toolspage.draw(main) + self.layout.sidebar.set_button_command("tools", self.go_back) + + def draw_navigation(self, parent): + wrapper = ttk.Frame(parent) + + back_button = ttk.Label(wrapper, image=self.images.get("left-chevron")) + back_button.bind("", lambda e: self.go_back()) + back_button.grid(row=0, column=1, sticky=ttk.W, padx=(0, 10)) + + page_name = ttk.Label(wrapper, text=self.title, font=("Host Grotesk", 16, "bold")) + page_name.grid(row=0, column=2, sticky=ttk.W) + + return wrapper + + def draw(self, parent): + # Draw shared navigation + navigation = self.draw_navigation(parent) + navigation.pack(side=ttk.TOP, fill=ttk.X, pady=(0, 10)) + + # Create shared content wrapper + if self.frame: + wrapper = RoundedFrame(parent, radius=10, style="dark.TFrame") + else: + wrapper = ttk.Frame(parent) + wrapper.pack(side=ttk.TOP, fill=ttk.BOTH, expand=True, pady=(10, 0)) + + # Call subclass-specific content render + self.draw_content(wrapper) + + @abc.abstractmethod + def draw_content(self, wrapper): + """Implemented by subclasses to draw page-specific content inside the wrapper.""" + pass diff --git a/gui/components/tools/__init__.py b/gui/components/tools/__init__.py new file mode 100644 index 0000000..90522ab --- /dev/null +++ b/gui/components/tools/__init__.py @@ -0,0 +1 @@ +from .message_log_entry import MessageLogEntry \ No newline at end of file diff --git a/gui/components/tools/message_log_entry.py b/gui/components/tools/message_log_entry.py new file mode 100644 index 0000000..efd1f22 --- /dev/null +++ b/gui/components/tools/message_log_entry.py @@ -0,0 +1,144 @@ +import sys +import time +import math +import ttkbootstrap as ttk +import discord + +from gui.components import RoundedFrame +import tkinter.font as tkFont + +class MessageLogEntry: + def __init__(self, parent, root, bot_controller, avatars, log_entry): + self.parent = parent + self.root = root + self.bot_controller = bot_controller + self.avatars = avatars + self.author, self.message, self.delete_time = log_entry + + self.frame = None + self.content_label = None + self.selected = False + + self._draw() + + def _autoresize_text(self, text_widget): + px_width = text_widget.winfo_width() + if px_width <= 1: + text_widget.after(50, lambda: self._autoresize_text(text_widget)) + return + + font = tkFont.Font(font=("Host Grotesk", 10 if sys.platform != "darwin" else 12)) + char_width = font.measure("0") + max_chars = max(1, px_width // char_width) + 15 + text = text_widget.get("1.0", "end-1c") + + lines = text.split("\n") + total_lines = 0 + for line in lines: + total_lines += math.ceil(len(line) / max_chars) or 1 + + text_widget.configure(height=total_lines) + + def _draw(self): + try: + # Outer frame + self.frame = RoundedFrame( + self.parent, + radius=(8, 8, 8, 8), + bootstyle="secondary.TFrame", + parent_background=self.root.style.colors.get("dark") + ) + self.frame.pack(fill=ttk.X, pady=(0, 8), padx=(0, 8), expand=True) + + # Inner content + content_frame = ttk.Frame(self.frame, style="secondary.TFrame") + content_frame.pack(fill=ttk.BOTH, expand=True, padx=(12, 20), pady=10) + + # Author line + author_frame = ttk.Frame(content_frame, style="secondary.TFrame") + author_frame.pack(fill=ttk.X, pady=(0, 8)) + + # Avatar + if self.author.avatar: + if self.author.id not in self.avatars: + try: + self.avatars[self.author.id] = self.bot_controller.get_avatar_from_url( + str(self.author.avatar.url), size=28, radius=14 + ) + except Exception: + self.avatars[self.author.id] = None + + if self.avatars[self.author.id]: + avatar_label = ttk.Label(author_frame, image=self.avatars[self.author.id]) + avatar_label.configure(background=self.root.style.colors.get("secondary")) + avatar_label.pack(side=ttk.LEFT, padx=(0, 5)) + + # Author name + author_label = ttk.Label( + author_frame, + text=self.author.display_name, + font=("Host Grotesk", 12 if sys.platform != "darwin" else 14, "bold") + ) + author_label.configure(background=self.root.style.colors.get("secondary"), foreground="white") + author_label.pack(side=ttk.LEFT) + + # Time + formatted_time = time.strftime("%H:%M:%S", time.localtime(self.delete_time)) + time_label = ttk.Label( + author_frame, + text=formatted_time, + font=("Host Grotesk", 8 if sys.platform != "darwin" else 10) + ) + time_label.configure(background=self.root.style.colors.get("secondary"), foreground="lightgrey") + time_label.pack(side=ttk.LEFT, padx=(5, 0), pady=(2, 0)) + + # Channel info + channel_label_text = ( + f"Deleted in DMs" + if isinstance(self.message.channel, discord.DMChannel) + else f"Deleted in {self.message.guild.name} > #{self.message.channel.name}" + ) + channel_label = ttk.Label( + content_frame, + text=channel_label_text, + font=("Host Grotesk", 8 if sys.platform != "darwin" else 10, "italic") + ) + channel_label.configure(background=self.root.style.colors.get("secondary"), foreground="lightgrey") + channel_label.pack(fill=ttk.X, pady=(0, 8)) + + self.content_label = ttk.Text(content_frame, font=("Host Grotesk", 10 if sys.platform != "darwin" else 12), wrap="word", state="normal") + self.content_label.insert("1.0", self.message.content or "[No text content]") + self.content_label.configure( + background=self.root.style.colors.get("secondary"), + foreground="white" if self.message.content else "grey", + border=False, + borderwidth=0, + highlightthickness=0 + ) + + self.content_label.pack(fill=ttk.X) + + self.root.after(100, lambda: self._autoresize_text(self.content_label)) + self.content_label.bind("", lambda e: self._autoresize_text(self.content_label)) + self.content_label.bind_all( + "" if sys.platform != "darwin" else "", + lambda _: self.content_label.event_generate("<>") + ) + + # Attachments + if self.message.attachments: + attachments_label = ttk.Label( + content_frame, + text=f"📎 {len(self.message.attachments)} attachment(s)", + font=("Host Grotesk", 9, "italic") + ) + attachments_label.configure(background=self.root.style.colors.get("secondary"), foreground="lightgrey") + attachments_label.pack(fill=ttk.X, pady=(4, 0)) + + self.frame.bind("", self.on_click) + + except Exception as e: + print(f"Error displaying log: {e}") + + def on_click(self, event): + pass \ No newline at end of file diff --git a/gui/helpers/images.py b/gui/helpers/images.py index a2b00b5..a2fdc3a 100644 --- a/gui/helpers/images.py +++ b/gui/helpers/images.py @@ -2,10 +2,31 @@ import os import threading from utils.files import resource_path +import requests +from io import BytesIO +from collections import Counter def resize_and_sharpen(image, size): - resized_image = image.resize(size, Image.LANCZOS) - sharpened_image = ImageEnhance.Sharpness(resized_image).enhance(2.0) + try: + # Convert palette images to RGB to avoid "cannot filter palette images" error + if image.mode == 'P': + image = image.convert('RGB') + elif image.mode == 'RGBA': + # Keep RGBA for transparency support + pass + elif image.mode not in ['RGB', 'RGBA']: + # Convert other modes to RGB + image = image.convert('RGB') + + resized_image = image.resize(size, Image.LANCZOS) + except Exception as e: + print("error resizing image:", e) + return image + try: + sharpened_image = ImageEnhance.Sharpness(resized_image).enhance(2.0) + except Exception as e: + print("error sharpening image:", e) + sharpened_image = resized_image return sharpened_image class Images: @@ -52,9 +73,9 @@ def _load_images(self): ICON_CONFIG = { "bigger": ["scripts"], - "small": ["trash", "github", "restart", "checkmark", "left-chevron", "file-signature", "trash-white"], + "small": ["trash", "github", "restart", "checkmark", "left-chevron", "file-signature", "trash-white", "right-chevron"], "tiny": ["submit", "max", "min", "search"], - "smaller": ["folder-open", "plus"], + "smaller": ["folder-open", "plus", "reset", "play", "stop"], "logo": ["ghost-logo"], } @@ -83,7 +104,11 @@ def _load_images(self): "folder-open": "data/icons/folder-open-solid.png", "file-signature": "data/icons/file-signature-solid.png", "left-chevron": "data/icons/chevron-left-solid.png", + "right-chevron": "data/icons/chevron-right-solid.png", "tools": "data/icons/screwdriver-wrench-solid.png", + "reset": "data/icons/rotate-left-solid.png", + "play": "data/icons/play-solid.png", + "stop": "data/icons/stop-solid.png", } for key, path in ICON_PATHS.items(): @@ -131,3 +156,35 @@ def get(self, key, hover_colour=None): return ImageTk.PhotoImage(image) return self.images.get(key) + + def get_majority_color_from_url(self, image_url: str) -> str: + response = requests.get(image_url) + image = Image.open(BytesIO(response.content)).convert("RGB") + + # Crop center to focus on subject + w, h = image.size + crop_margin = 0.2 + image = image.crop(( + int(w * crop_margin), + int(h * crop_margin), + int(w * (1 - crop_margin)), + int(h * (1 - crop_margin)) + )) + + # Slight blur to reduce texture noise + image = image.filter(ImageFilter.GaussianBlur(radius=1)) + image = image.resize((50, 50)) # reduce size for performance + + pixels = list(image.getdata()) + + # Filter out very dark pixels (likely shadows) + filtered_pixels = [rgb for rgb in pixels if sum(rgb) > 60] # brightness threshold + + if not filtered_pixels: + # fallback to original pixels if all were filtered + filtered_pixels = pixels + + counter = Counter(filtered_pixels) + most_common = counter.most_common(1)[0][0] + + return '#{:02x}{:02x}{:02x}'.format(*most_common) \ No newline at end of file diff --git a/gui/main.py b/gui/main.py index 8a2603d..df5f80f 100644 --- a/gui/main.py +++ b/gui/main.py @@ -12,7 +12,7 @@ from utils.files import resource_path from utils import uninstall_fonts -from gui.pages import HomePage, LoadingPage, SettingsPage, OnboardingPage, ScriptsPage +from gui.pages import HomePage, LoadingPage, SettingsPage, OnboardingPage, ScriptsPage, ToolsPage from gui.components import Sidebar, Console from gui.helpers import Images, Layout @@ -20,9 +20,6 @@ class GhostGUI: def __init__(self, bot_controller): self.size = (600, 530) self.bot_controller = bot_controller - - if bot_controller: - self.bot_controller.set_gui(self) enable_high_dpi_awareness() @@ -67,8 +64,12 @@ def __init__(self, bot_controller): self.home_page = HomePage(self.root, self.bot_controller, self._restart_bot) self.settings_page = SettingsPage(self.root, self.bot_controller) self.scripts_page = ScriptsPage(self, self.bot_controller, self.images) + self.tools_page = ToolsPage(self.root, self.bot_controller, self.images, self.layout) logging.set_gui(self) + + if bot_controller: + self.bot_controller.set_gui(self) def _show_window(self): self.root.deiconify() @@ -100,7 +101,8 @@ def draw_scripts(self): def draw_tools(self): self.sidebar.set_current_page("tools") self.layout.clear() - main = self.layout.main() + main = self.layout.main(scrollable=True) + self.tools_page.draw(main) # def draw_loading(self): # self.layout.hide_titlebar() @@ -142,8 +144,8 @@ def _on_bot_ready(self): self.layout.resize(600, 530) self.layout.center_window(600, 530) - self.draw_home() - self.notifier.send("Ghost", "Ghost has successfully started!") + self.root.after(50, lambda: self.notifier.send("Ghost", "Ghost has successfully started!")) + self.root.after(50, lambda: self.draw_home()) def _check_bot_started(self): if self.bot_controller.bot_running: @@ -176,14 +178,16 @@ def run(self): self.root.mainloop() def quit(self): - if str(Messagebox.yesno("Are you sure you want to quit?", title="Ghost")).lower() == "yes": - # uninstall_fonts() - # if os.name == "nt": - # os.kill(os.getpid(), 9) - # else: - # os._exit(0) - self.root.destroy() - sys.exit(0) + # if str(Messagebox.yesno("Are you sure you want to quit?", title="Ghost")).lower() == "yes": + # # uninstall_fonts() + # # if os.name == "nt": + # # os.kill(os.getpid(), 9) + # # else: + # # os._exit(0) + # self.root.destroy() + # sys.exit(0) + self.root.destroy() + sys.exit(0) def run_on_main_thread(self, func, *args, **kwargs): self.root.after(0, lambda: func(*args, **kwargs)) diff --git a/gui/pages/__init__.py b/gui/pages/__init__.py index 904232f..a6cfea0 100644 --- a/gui/pages/__init__.py +++ b/gui/pages/__init__.py @@ -3,4 +3,5 @@ from .settings import SettingsPage from .onboarding import OnboardingPage from .scripts import ScriptsPage -from .script import ScriptPage \ No newline at end of file +from .script import ScriptPage +from .tools import ToolsPage \ No newline at end of file diff --git a/gui/pages/home.py b/gui/pages/home.py index f2a38ee..4109f6d 100644 --- a/gui/pages/home.py +++ b/gui/pages/home.py @@ -19,15 +19,6 @@ def __init__(self, root, bot_controller, _restart_bot): self.images = Images() self.cfg = Config() - self.discord_logs_wrapper = None - self.discord_logs_footer = None - # self.discord_logs_textbox = None - self.discord_logs_inner_wrapper = None - self.discord_logs_max = False - self.discord_logs_max_min_btn = None - self.discord_logs_frame = None - self.discord_logs = [] - self.details_wrapper = None self.friends_label = None self.guilds_label = None @@ -188,189 +179,10 @@ def _draw_bot_details(self, parent): self.latency_label.grid(row=4, column=0, sticky=ttk.W, padx=(10, 0), pady=(0, 10)) def _update_wraplength(self, event=None): - if self.discord_logs_frame: - try: - new_width = self.discord_logs_frame.winfo_width() - 30 # Adjust for padding - for widget in self.discord_logs_frame.winfo_children(): - if isinstance(widget, ttk.Label): - widget.configure(wraplength=max(new_width, 100)) - except: - pass - - def _display_log(self, log_entry): - author, message, delete_time = log_entry - try: - - frame = RoundedFrame(self.discord_logs_frame, radius=(8, 8, 8, 8), bootstyle="secondary.TFrame", parent_background=self.root.style.colors.get("dark")) - frame.pack(fill=ttk.X, pady=(0, 5), padx=(0, 15)) - - try: - if author.id not in self.avatars: - self.avatars[author.id] = self.bot_controller.get_avatar_from_url(author.avatar.url, size=50, radius=5) - - avatar_image = self.avatars.get(author.id) - except: - url = "https://ia600305.us.archive.org/31/items/discordprofilepictures/discordblue.png" - avatar_image = self.bot_controller.get_avatar_from_url(url, size=50, radius=5) - - avatar = ttk.Label(frame, image=avatar_image) - avatar.configure(background=self.root.style.colors.get("secondary")) - avatar.grid(row=0, column=0, sticky=ttk.NW, padx=(5, 5), rowspan=3, pady=5) - - author_label = ttk.Label(frame, text=author.display_name, font=("Host Grotesk", 12 if sys.platform != "darwin" else 14, "bold")) - author_label.configure(background=self.root.style.colors.get("secondary")) - author_label.grid(row=0, column=1, sticky=ttk.NW, padx=(0, 10), pady=(5, 0)) - - message_content = message.content[:250] + "..." if len(message.content) > 250 else message.content - - message_label = ttk.Label(frame, text=message_content, font=("Host Grotesk", 10 if sys.platform != "darwin" else 12), wraplength=350) - message_label.configure(background=self.root.style.colors.get("secondary")) - message_label.grid(row=1, column=1, sticky=ttk.NW, padx=(0, 10)) - - channel_label_text = f"Deleted in DMs" if isinstance(message.channel, discord.DMChannel) else f"Deleted in {message.guild.name} > #{message.channel.name}" - channel_label = ttk.Label(frame, text=channel_label_text, font=("Host Grotesk", 8 if sys.platform != "darwin" else 10, "italic")) - channel_label.configure(background=self.root.style.colors.get("secondary"), foreground="lightgrey") - channel_label.grid(row=2, column=1, sticky=ttk.NW, padx=(0, 10), pady=(0, 5)) - - ttk.Label(frame, text="", background=self.root.style.colors.get("secondary")).grid(row=2, column=2, sticky=ttk.E) - frame.grid_columnconfigure(1, weight=1) - frame.grid_columnconfigure(2, weight=1) - - # # Function to update wraplength - # def update_wraplength(event): - # new_width = frame.winfo_width() - 60 # Adjusting for padding and avatar width - # message_label.configure(wraplength=new_width if new_width > 100 else 100) - - # # Bind window resize event - # self.root.bind("", update_wraplength) - - self.discord_logs_canvas.yview_moveto(1) - except: - pass - - def add_discord_log(self, author, message, delete_time): - log_entry = (author, message, delete_time) - self.discord_logs.append(log_entry) - self._display_log(log_entry) - - def _load_discord_logs(self): - for log_entry in self.discord_logs: - self._display_log(log_entry) + # This method can be used for future wraplength updates if needed + pass - try: - self._update_canvas_width(self.discord_logs_canvas, self.discord_logs_wrapper) - self.discord_logs_canvas.yview_moveto(1) - except: - pass - - def _clear_discord_logs(self): - for widget in self.discord_logs_frame.winfo_children(): - widget.destroy() - - self.discord_logs = [] - self.avatars = {} - - # Clear the canvas - self.discord_logs_canvas.delete("all") - - # Reset the scroll region - self.discord_logs_canvas.configure(scrollregion=self.discord_logs_canvas.bbox("all")) - - # Update the canvas width - self._update_canvas_width(self.discord_logs_canvas, self.discord_logs_wrapper) - - def _max_discord_logs(self): - self.root.update_idletasks() - self.discord_logs_max = True - self.discord_logs_max_min_btn.configure(image=self.images.get("min")) - self.discord_logs_max_min_btn.bind("", lambda e: self._min_discord_logs()) - # self.discord_logs_textbox.configure(height=18) - - # self.discord_logs_wrapper.pack_forget() - # self.discord_logs_footer.pack_forget() - self.details_wrapper.pack_forget() - - self.discord_logs_wrapper.pack(fill=ttk.BOTH, expand=True, pady=(10, 0)) - # self.discord_logs_footer.pack(fill=ttk.BOTH, expand=False) - self.root.update_idletasks() - - def _min_discord_logs(self): - self.root.update_idletasks() - self.discord_logs_max = False - self.discord_logs_max_min_btn.configure(image=self.images.get("max")) - self.discord_logs_max_min_btn.bind("", lambda e: self._max_discord_logs()) - # self.discord_logs_textbox.configure(height=10) - - self.discord_logs_wrapper.pack_forget() - # self.discord_logs_footer.pack_forget() - self.details_wrapper.pack(fill=ttk.BOTH, expand=False, pady=(10, 0)) - self.discord_logs_wrapper.pack(fill=ttk.BOTH, expand=True, pady=(5, 0)) - # self.discord_logs_footer.pack(fill=ttk.BOTH, expand=False) - self.root.update_idletasks() - - def _update_canvas_width(self, canvas, logs_wrapper): - canvas.itemconfig(self.canvas_window, width=logs_wrapper.winfo_width() - 24) - - def _draw_discord_logs(self, parent): - if self.restart: - wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame") - wrapper.pack(fill=ttk.BOTH, expand=True, pady=(5, 0)) - return wrapper - - wrapper = ttk.Frame(parent) - wrapper.pack(fill=ttk.BOTH, expand=True, pady=(5, 0)) - - header_wrapper = RoundedFrame(wrapper, radius=(15, 15, 0, 0), style="secondary.TFrame") - header_wrapper.pack(fill=ttk.BOTH, expand=False) - - title = ttk.Label(header_wrapper, text="Deleted Messages", font=("Host Grotesk", 14 if sys.platform != "darwin" else 16, "bold")) - title.configure(background=self.root.style.colors.get("secondary")) - title.grid(row=0, column=0, sticky=ttk.W, padx=10, pady=10) - header_wrapper.columnconfigure(1, weight=1) - - clear_btn = ttk.Label(header_wrapper, image=self.images.get("trash"), font=("Host Grotesk", 12 if sys.platform != "darwin" else 14, "bold")) - clear_btn.configure(background=self.root.style.colors.get("secondary"), foreground="white") - clear_btn.bind("", lambda e: self._clear_discord_logs()) - clear_btn.bind("", lambda e: clear_btn.configure(foreground="lightgrey")) - clear_btn.bind("", lambda e: clear_btn.configure(foreground="white")) - clear_btn.grid(row=0, column=2, sticky=ttk.E, pady=10) - - self.discord_logs_max_min_btn = ttk.Label(header_wrapper, image=self.images.get("max") if not self.discord_logs_max else self.images.get("min"), anchor="center") - self.discord_logs_max_min_btn.configure(background=self.root.style.colors.get("secondary")) - self.discord_logs_max_min_btn.bind("", lambda e: self._max_discord_logs() if not self.discord_logs_max else lambda e: self._min_discord_logs()) - self.discord_logs_max_min_btn.grid(row=0, column=3, sticky=ttk.E, padx=(10, 13), pady=8) - - logs_wrapper = RoundedFrame(wrapper, radius=(0, 0, 15, 15), style="dark.TFrame") - logs_wrapper.pack(fill=ttk.BOTH, expand=True) - - self.discord_logs_inner_wrapper = ttk.Frame(logs_wrapper) - self.discord_logs_inner_wrapper.pack(fill=ttk.BOTH, expand=True, padx=10, pady=10) - - canvas = ttk.Canvas(self.discord_logs_inner_wrapper) - scrollbar = ttk.Scrollbar(self.discord_logs_inner_wrapper, orient="vertical", command=canvas.yview) - scroll_frame = ttk.Frame(canvas, style="dark.TFrame") - - scroll_frame.bind( - "", - lambda e: canvas.configure(scrollregion=canvas.bbox("all")) - ) - - self.canvas_window = canvas.create_window((0, 0), window=scroll_frame, anchor="nw") - - canvas.configure(yscrollcommand=scrollbar.set, background=self.root.style.colors.get("dark")) - - canvas.pack(side="left", fill="both", expand=True) - scrollbar.pack(side="right", fill="y") - - self.discord_logs_frame = scroll_frame # Store this for adding logs later - self.discord_logs_canvas = canvas # Store this for updating the width later - - # Schedule the update of canvas width after the UI has been rendered - canvas.after(100, lambda: self._update_canvas_width(canvas, logs_wrapper)) - - return wrapper - def draw(self, parent, restart=False, start=False): self.restart = restart or start self.restart_title_text = "Ghost is starting" if start else "Ghost is restarting" @@ -380,18 +192,6 @@ def draw(self, parent, restart=False, start=False): self.details_wrapper = self._draw_details_wrapper(parent) self._draw_account_details(self.details_wrapper) self._draw_bot_details(self.details_wrapper) - self.discord_logs_wrapper = self._draw_discord_logs(parent) - - self._load_discord_logs() - - if self.discord_logs_max: - self._max_discord_logs() - - try: - self._update_canvas_width(self.discord_logs_canvas, self.discord_logs_wrapper) - self.discord_logs_canvas.yview_moveto(1) - except: - pass self._update_bot_details() self._update_account_details() \ No newline at end of file diff --git a/gui/pages/script.py b/gui/pages/script.py index 0f593f7..db90453 100644 --- a/gui/pages/script.py +++ b/gui/pages/script.py @@ -1,9 +1,10 @@ -import os +import sys import ttkbootstrap as ttk from utils.files import get_application_support from gui.helpers.images import Images from gui.components.rounded_frame import RoundedFrame +from cupcake import Editor, Languages class ScriptPage: def __init__(self, root, script): @@ -24,8 +25,9 @@ def _get_script_content(self): return file.read() def _save_script(self): - with open(get_application_support() + f"/scripts/{self.script}", "w") as file: - file.write(self.editor.get("1.0", ttk.END)) + # with open(get_application_support() + f"/scripts/{self.script}", "w") as file: + # file.write(self.editor.content.get("1.0", ttk.END)) + self.editor.save() def _draw_header(self, parent): wrapper = ttk.Frame(parent) @@ -39,79 +41,21 @@ def _draw_header(self, parent): return wrapper - def _update_line_numbers(self, event=None): - """Update line numbers in the left sidebar.""" - line_numbers = "" - for i in range(1, int(self.editor.index('end-1c').split('.')[0]) + 1): - line_numbers += f"{i}\n" - self.linenumbers.config(state="normal") - self.linenumbers.delete('1.0', 'end') - self.linenumbers.insert('1.0', line_numbers) - self.linenumbers.config(state="disabled") - - def move_cursor_to_line(self, line_number): - """Move the cursor to a specific line and set focus.""" - self.editor.mark_set("insert", f"{line_number}.0") # line_number.0 represents the start of the line - self.editor.see(f"{line_number}.0") # Scroll to the line if it's out of view - self.editor.focus_set() # Ensure focus is set after moving the cursor - - def _sync_scroll(self, *args): - """Sync the scrolling of line numbers and editor.""" - if args[0] == "moveto" or args[0] == "scroll": - # Sync both editor and line numbers - self.linenumbers.yview(*args) - self.editor.yview(*args) - def draw(self, parent): header = self._draw_header(parent) header.pack(fill=ttk.X, pady=(0, 20)) editor_wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame") editor_wrapper.pack(fill=ttk.BOTH, expand=True) - - # Create a frame to hold both the line numbers and editor - text_container = ttk.Frame(editor_wrapper) - text_container.pack(fill=ttk.BOTH, expand=True) - # Line number widget (disabled to prevent editing) - self.linenumbers = ttk.Text(text_container, width=4, font=("JetBrainsMono NF Regular", 12), state="disabled") - self.linenumbers.pack(side=ttk.LEFT, fill=ttk.Y) - - # Main text editor - self.editor = ttk.Text(text_container, wrap="word", font=("JetBrainsMono NF Regular", 12)) + self.editor = Editor( + editor_wrapper, + language=Languages.PYTHON, + darkmode=True, + font=("JetBrainsMono NF Regular", 10 if sys.platform != "darwin" else 12), + path=get_application_support() + f"/scripts/{self.script}", + showpath=False + ) self.editor.pack(fill=ttk.BOTH, expand=True) - - # Insert script content - self.editor.insert(ttk.END, self._get_script_content()) - - # Ensure the editor is editable - self.editor.configure(state="normal") - self.editor.config(tabs=("4c",)) - - # Create a scrollbar and link it to the editor and line numbers - self.text_scrollbar = ttk.Scrollbar(editor_wrapper, orient="vertical", command=self._sync_scroll) - self.text_scrollbar.pack(side=ttk.RIGHT, fill=ttk.Y) - - # Set the scrollbars for both widgets - self.editor.config(yscrollcommand=self.text_scrollbar.set) - self.linenumbers.config(yscrollcommand=self.text_scrollbar.set) - - # Focus handling - def set_focus(event): - # Set focus when clicking inside the editor - self.editor.focus_set() - - # Focus handling - self.editor.bind("", set_focus) # Set focus on click - self.editor.bind("", lambda e: self.editor.focus_set()) # Set focus when mouse enters - self.editor.bind("", lambda e: self.editor.focus_set()) # Set focus when it gains focus - - # Attach events to sync line numbers - self.editor.bind("", self._update_line_numbers) - self.editor.bind("", self._update_line_numbers) - - # Initialize line numbers - self._update_line_numbers() - - # Example usage: Move cursor to a specific line (for instance, line 5) - self.move_cursor_to_line(5) + content = self.editor.content + content.insert("end", self._get_script_content()) \ No newline at end of file diff --git a/gui/pages/scripts.py b/gui/pages/scripts.py index 3a7271b..b48ce5c 100644 --- a/gui/pages/scripts.py +++ b/gui/pages/scripts.py @@ -4,7 +4,12 @@ from ttkbootstrap.scrolled import ScrolledFrame from ttkbootstrap.dialogs import Messagebox from gui.components import RoundedFrame + +# Uncomment the below to enable the dedicated script page. +# Please be aware this is a work in progress and the current state of the page is laggy and sometimes unresponsive. + # from gui.pages.script import ScriptPage + from utils.config import Config from utils.files import open_path_in_explorer, get_application_support from utils.defaults import DEFAULT_SCRIPT @@ -19,12 +24,19 @@ def __init__(self, root, bot_controller, images): self.script_frames = [] def _open_editor(self, script): + # Uncomment the below to enable the dedicated script page. + # Please be aware this is a work in progress and the current state of the page is laggy and sometimes unresponsive. + # self.gui.sidebar.set_current_page("scripts") # self.gui.layout.clear() # main = self.gui.layout.main() # script_page = ScriptPage(self.gui, script) # script_page.draw(main) + + # Please comment out the below to enable the dedicated script page! + # Please be aware this is a work in progress and the current state of the page is laggy and sometimes unresponsive. + if sys.platform == "darwin": try: os.system(f"code '{get_application_support()}/scripts/{script}'") diff --git a/gui/pages/tools/__init__.py b/gui/pages/tools/__init__.py new file mode 100644 index 0000000..5496ec9 --- /dev/null +++ b/gui/pages/tools/__init__.py @@ -0,0 +1,4 @@ +from .spypet_page import SpyPetPage +from .tools import ToolsPage +from .message_logger_page import MessageLoggerPage +from .user_lookup_page import UserLookupPage \ No newline at end of file diff --git a/gui/pages/tools/message_logger_page.py b/gui/pages/tools/message_logger_page.py new file mode 100644 index 0000000..04e4aca --- /dev/null +++ b/gui/pages/tools/message_logger_page.py @@ -0,0 +1,178 @@ +import webbrowser, discord, sys, time +import ttkbootstrap as ttk +import tkinter.font as tkFont +from ttkbootstrap.scrolled import ScrolledFrame +from gui.components import RoundedFrame, ToolPage +from gui.components.tools.message_log_entry import MessageLogEntry +from gui.helpers import Images +from utils.config import VERSION, CHANGELOG, MOTD, Config + +class MessageLoggerPage(ToolPage): + def __init__(self, toolspage, root, bot_controller, images, layout): + super().__init__(toolspage, root, bot_controller, images, layout, title="Message Logger") + + self.discord_logs_wrapper = None + self.discord_logs_inner_wrapper = None + self.discord_logs_frame = None + self.discord_logs = [] + self.discord_logs_canvas = None + self.logs = [] + self.canvas_window = None + self.avatars = {} + + def draw_navigation(self, parent): + wrapper = ttk.Frame(parent) + + back_button = ttk.Label(wrapper, image=self.images.get("left-chevron")) + back_button.bind("", lambda e: self.go_back()) + back_button.grid(row=0, column=1, sticky=ttk.W, padx=(0, 10)) + + page_name = ttk.Label(wrapper, text=self.title, font=("Host Grotesk", 16, "bold")) + page_name.grid(row=0, column=2, sticky=ttk.W) + + clear_btn = ttk.Label(wrapper, image=self.images.get("trash")) + clear_btn.configure(foreground="white") + clear_btn.bind("", lambda e: self._clear_discord_logs()) + clear_btn.bind("", lambda e: clear_btn.configure(foreground="lightgrey")) + clear_btn.bind("", lambda e: clear_btn.configure(foreground="white")) + clear_btn.grid(row=0, column=3, sticky=ttk.E, padx=(20, 0)) + + wrapper.grid_columnconfigure(2, weight=1) + + return wrapper + + def add_discord_log(self, author, message, delete_time): + log_entry = (author, message, delete_time) + self.discord_logs.append(log_entry) + + if self.discord_logs_frame and self.discord_logs_canvas: + self._display_log(log_entry) + self.root.after_idle(lambda: self._update_canvas_after_add()) + + def _on_mousewheel(self, event): + if sys.platform == 'darwin': + self.discord_logs_canvas.yview_scroll(-1 * int(event.delta), "units") + else: + self.discord_logs_canvas.yview_scroll(-1 * int(event.delta / 120), "units") + + def _update_canvas_after_add(self): + try: + if self.discord_logs_canvas and self.discord_logs_frame: + self.discord_logs_canvas.configure(scrollregion=self.discord_logs_canvas.bbox("all")) + + if self.discord_logs_wrapper: + self._update_canvas_width(self.discord_logs_canvas, self.discord_logs_wrapper) + + self.discord_logs_canvas.yview_moveto(1) + except: + pass + + def _load_discord_logs(self): + for log_entry in self.discord_logs: + self._display_log(log_entry) + + try: + self._update_canvas_width(self.discord_logs_canvas, self.discord_logs_wrapper) + self.discord_logs_canvas.yview_moveto(1) + except: + pass + + def _clear_discord_logs(self): + if not self.discord_logs_frame: + return + + for widget in self.discord_logs_frame.winfo_children(): + widget.destroy() + + self.discord_logs = [] + self.avatars = {} + + if self.discord_logs_canvas: + self.discord_logs_canvas.configure(scrollregion="0 0 0 0") + + if self.discord_logs_wrapper: + self._update_canvas_width(self.discord_logs_canvas, self.discord_logs_wrapper) + + if hasattr(self.bot_controller, 'gui') and hasattr(self.bot_controller.gui, 'tools_page'): + try: + if hasattr(self.bot_controller.gui.tools_page, 'message_logger_page'): + pass + except: + pass + + def _update_canvas_width(self, canvas, logs_wrapper): + try: + available_width = logs_wrapper.winfo_width() - 35 + canvas.itemconfig(self.canvas_window, width=available_width) + except: + pass + + def _update_wraplength(self, event=None): + if self.discord_logs_frame: + try: + new_width = self.discord_logs_frame.winfo_width() - 60 + for widget in self.discord_logs_frame.winfo_children(): + if hasattr(widget, 'winfo_children'): + for child in widget.winfo_children(): + if isinstance(child, ttk.Label) and hasattr(child, 'configure'): + if hasattr(child, 'cget') and child.cget('wraplength') > 0: + child.configure(wraplength=max(300, new_width - 80)) + except: + pass + + def _display_log(self, log_entry): + if not self.discord_logs_frame: + return + + entry = MessageLogEntry( + parent=self.discord_logs_frame, + root=self.root, + bot_controller=self.bot_controller, + avatars=self.avatars, + log_entry=log_entry + ) + self.logs.append(entry) # keep track of all logs + + def draw_content(self, wrapper): + self.root.bind("", self._update_wraplength) + + self.discord_logs_inner_wrapper = ttk.Frame(wrapper) + self.discord_logs_inner_wrapper.pack(fill=ttk.BOTH, expand=True, padx=10, pady=(10, 10)) + + canvas = ttk.Canvas(self.discord_logs_inner_wrapper) + scrollbar = ttk.Scrollbar(self.discord_logs_inner_wrapper, orient="vertical", command=canvas.yview) + scroll_frame = ttk.Frame(canvas, style="dark.TFrame") + + scroll_frame.bind( + "", + lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + self.canvas_window = canvas.create_window((0, 0), window=scroll_frame, anchor="nw") + + def resize_canvas(event): + # Match the inner frame to the canvas width + canvas.itemconfig(self.canvas_window, width=event.width) + + canvas.bind("", resize_canvas) + + canvas.configure(yscrollcommand=scrollbar.set, background=self.root.style.colors.get("dark")) + + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + + self.discord_logs_frame = scroll_frame + self.discord_logs_canvas = canvas + self.discord_logs_wrapper = wrapper + + canvas.after(100, lambda: self._update_canvas_width(canvas, wrapper)) + + self.discord_logs_canvas.bind("", lambda e: self.discord_logs_canvas.bind_all("", self._on_mousewheel)) + self.discord_logs_canvas.bind("", lambda e: self.discord_logs_canvas.unbind_all("")) + self.discord_logs_canvas.bind("", self._on_mousewheel) + self.discord_logs_canvas.bind("", self._on_mousewheel) + self.discord_logs_canvas.bind("", self._on_mousewheel) + + + self._load_discord_logs() \ No newline at end of file diff --git a/gui/pages/tools/spypet_page.py b/gui/pages/tools/spypet_page.py new file mode 100644 index 0000000..1f0a035 --- /dev/null +++ b/gui/pages/tools/spypet_page.py @@ -0,0 +1,589 @@ +import sys, time +import ttkbootstrap as ttk +from ttkbootstrap.scrolled import ScrolledFrame +from ttkbootstrap.tableview import Tableview +from gui.components import ToolPage, RoundedFrame, RoundedButton +from utils.console import get_formatted_time + +class SpyPetPage(ToolPage): + def __init__(self, toolspage, root, bot_controller, images, layout): + super().__init__(toolspage, root, bot_controller, images, layout, title="ghetto spy.pet", frame=None) + self.wrapper = None + self.search_entry = None # Initialize search entry to None + self.user_id = None + self.user = None + self.user_avatar = None + self.user_wrapper = None + self.mutual_guilds = [] + self.spypet = self.bot_controller.spypet + self.log_wrapper = None + self.search_placeholder_text = "Search a Discord user ID..." + self.console = [] + self.textarea = None + self.avatar = None + self.darwin_font_size = 12 + self.non_darwin_font_size = 10 + self.reset_button_disabled = False + self.messages_textarea = None + self.messages_textarea_updating = False + self.messages_formatted = [] + self.current_search = "" + self.messages_all = [] + self.messages_displayed = [] + self.total_messages = 0 + self.user_total_messages = 0 + self.focused_textarea = None + + def _set_focused_textarea(self, widget): + self.focused_textarea = widget + + def _redraw_user_wrapper(self): + if self.user_wrapper: + self.user_wrapper.destroy() + self.user_wrapper = self._draw_user_wrapper(self.user_progress_wrapper) + self.user_wrapper.pack(side=ttk.BOTTOM, fill=ttk.BOTH, expand=True) + self.user_wrapper.set_width(200) + + def _check_mutual_guilds(self): + if not self.mutual_guilds: + self.root.after(100, lambda: self._check_mutual_guilds()) + else: + self.add_log("info", f"Found {len(self.mutual_guilds)} mutual guilds for user ID: {self.user_id}") + self._redraw_user_wrapper() + + def _get_user(self, user_id): + if user_id is None or not user_id.isdigit(): + self.add_log("error", "Invalid user ID. Please enter a valid Discord user ID.") + return + + if user_id == self.user_id or self.spypet.member_id == user_id: + self.add_log("info", "You are already viewing this user.") + return + else: + self.add_log("warning", f"Switching to user ID: {user_id}") + self._reset_spypet() + + self.user_id = user_id + self.spypet.set_member_id(user_id) + user = self.bot_controller.get_user_from_id(int(user_id)) + self.user = user + + avatar_url = self.user.avatar.url if self.user and self.user.avatar else "https://ia600305.us.archive.org/31/items/discordprofilepictures/discordblue.png" + print(avatar_url) + self.user_avatar = self.bot_controller.get_avatar_from_url(avatar_url, size=65, radius=65//2) + self.user_banner_colour = self.images.get_majority_color_from_url(avatar_url) + + self._check_mutual_guilds() + self._redraw_user_wrapper() + + def _search(self): + pass + + def _configure_start_stop_button(self, running): + try: + if running: + self.start_stop_button.configure(image=self.images.get("stop"), bootstyle="danger") + else: + self.start_stop_button.configure(image=self.images.get("play"), bootstyle="primary") + except Exception as e: + print(f"Error configuring start/stop button: {e}") + + def _check_spypet_running(self): + if self.spypet.running: + self.search_placeholder_text = "Search for a message..." + self.messages_textarea_updating = True + self.root.after(50, lambda: self._disable_reset_button()) + self.root.after(50, lambda: self._configure_start_stop_button(True)) + + if not self.search_button.winfo_ismapped(): + self.search_button.grid(row=0, column=2, sticky=ttk.E, padx=(0, 10), pady=10) + else: + self.search_placeholder_text = "Search a Discord user ID..." + self.messages_textarea_updating = False + self.root.after(50, lambda: self._enable_reset_button()) + self.root.after(50, lambda: self._configure_start_stop_button(False)) + + if self.search_button.winfo_ismapped(): + self.search_button.grid_forget() + + if not self.spypet.running and len(self.messages_all) > 0: + self.search_placeholder_text = "Search for a message..." + if not self.search_button.winfo_ismapped(): + self.search_button.grid(row=0, column=2, sticky=ttk.E, padx=(0, 10), pady=10) + # elif self.spypet.running and len(self.messages_all) > 0: + # self.search_placeholder_text = "Search for a message..." + + try: + self.search_entry.configure(foreground="grey") + self.search_var.set("") + self.search_entry.delete(0, ttk.END) + self.search_entry.insert(0, self.search_placeholder_text) + except Exception as e: + print(f"Error resetting search entry: {e}") + + def _disable_reset_button(self): + self.reset_button_disabled = True + try: + if hasattr(self, 'reset_button_wrapper') and self.reset_button_wrapper: + self.reset_button_wrapper.set_background(background=self.root.style.colors.get("dark")) + if hasattr(self, 'reset_button') and self.reset_button: + self.reset_button.configure(background=self.root.style.colors.get("dark")) + except Exception as e: + print(f"Error disabling reset button: {e}") + + def _enable_reset_button(self): + self.reset_button_disabled = False + try: + if hasattr(self, 'reset_button_wrapper') and self.reset_button_wrapper: + self.reset_button_wrapper.set_background(background=self.root.style.colors.get("danger")) + if hasattr(self, 'reset_button') and self.reset_button: + self.reset_button.configure(background=self.root.style.colors.get("danger")) + except Exception as e: + print(f"Error enabling reset button: {e}") + + def _toggle_spypet(self): + search_text = self.search_entry.get().strip() + + if not self.spypet.running: + if self.user and int(self.user.id) == int(self.spypet.member_id): + self.bot_controller.start_spypet() + elif not search_text or search_text == self.search_placeholder_text: + self.add_log("error", "Please enter a valid Discord user ID to start SpyPet.") + return + else: + self._get_user(search_text) + if not self.user: + self.add_log("error", "Failed to fetch user. Please check the user ID.") + return + + self.bot_controller.start_spypet() + self.add_log("info", f"SpyPet started for user ID: {self.user_id}") + + self.messages_textarea_updating = True + self.root.after(50, lambda: self.update_messages()) + else: + self.bot_controller.stop_spypet() + self.add_log("info", "SpyPet stopped.") + + self._check_spypet_running() + + def _reset_spypet(self): + if self.spypet.running: + self.add_log("warning", "You cannot reset SpyPet while it is running. Please stop it first.") + return + print("Resetting SpyPet...") + self.clear() + self.spypet.reset() + self.user_id = None + self.user = None + self.user_avatar = None + self.mutual_guilds = [] + self._redraw_user_wrapper() + self.clear_messages() + self._check_spypet_running() + self._update_progress_labels(0, 0) + + self.root.after(150, self._disable_reset_button) + + def _draw_start_stop_button(self, parent): + def _hover_enter(_): + wrapper.set_background(background="#322bef" if not self.spypet.running else "#de2d1b") + self.start_stop_button.configure(background="#322bef" if not self.spypet.running else "#de2d1b") + + def _hover_leave(_): + wrapper.set_background(background=self.root.style.colors.get("primary") if not self.spypet.running else self.root.style.colors.get("danger")) + self.start_stop_button.configure(background=self.root.style.colors.get("primary") if not self.spypet.running else self.root.style.colors.get("danger")) + + wrapper = RoundedFrame(parent, radius=(10, 10, 10, 10), bootstyle="primary" if not self.spypet.running else "danger") + wrapper.bind("", _hover_enter) + wrapper.bind("", _hover_leave) + wrapper.bind("", lambda e: self._toggle_spypet()) + + self.start_stop_button = ttk.Label(wrapper, image=self.images.get("play"), style="primary") + self.start_stop_button.configure(background=self.root.style.colors.get("primary")) + self.start_stop_button.pack(side=ttk.LEFT, padx=15, pady=14) + self.start_stop_button.bind("", lambda e: self._toggle_spypet()) + self.start_stop_button.bind("", _hover_enter) + self.start_stop_button.bind("", _hover_leave) + + return wrapper + + def _draw_reset_button(self, parent): + def _reset(_): + if not self.reset_button_disabled: + self._reset_spypet() + + def _hover_enter(_): + self.reset_button_wrapper.set_background(background="#de2d1b" if not self.reset_button_disabled else self.root.style.colors.get("dark")) + self.reset_button.configure(background="#de2d1b" if not self.reset_button_disabled else self.root.style.colors.get("dark")) + + def _hover_leave(_): + self.reset_button_wrapper.set_background(background=self.root.style.colors.get("danger") if not self.reset_button_disabled else self.root.style.colors.get("dark")) + self.reset_button.configure(background=self.root.style.colors.get("danger") if not self.reset_button_disabled else self.root.style.colors.get("dark")) + + self.reset_button_wrapper = RoundedFrame(parent, radius=(10, 10, 10, 10), bootstyle="danger" if not self.reset_button_disabled else "dark") + self.reset_button_wrapper.bind("", _reset) + self.reset_button_wrapper.bind("", _hover_enter) + self.reset_button_wrapper.bind("", _hover_leave) + + self.reset_button = ttk.Label(self.reset_button_wrapper, image=self.images.get("reset"), style="danger" if not self.reset_button_disabled else "dark") + self.reset_button.configure(background=self.root.style.colors.get("danger") if not self.reset_button_disabled else self.root.style.colors.get("dark")) + self.reset_button.pack(side=ttk.LEFT, padx=15, pady=14) + self.reset_button.bind("", _reset) + self.reset_button.bind("", _hover_enter) + self.reset_button.bind("", _hover_leave) + + return self.reset_button_wrapper + + def _draw_search_bar(self, parent): + entry_wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame") + + def on_focus_in(event): + if self.search_entry.get() == self.search_placeholder_text: + self.search_entry.delete(0, ttk.END) + self.search_entry.configure(foreground="white") + + def on_focus_out(event): + if self.search_entry.get() == "": + self.search_entry.insert(0, self.search_placeholder_text) + self.search_entry.configure(foreground="grey") + + self.search_var = ttk.StringVar() + self.search_var.trace_add("write", lambda *args: self._on_search_change()) + + self.search_entry = ttk.Entry( + entry_wrapper, + bootstyle="dark.TFrame", + textvariable=self.search_var, + font=("Host Grotesk",) + ) + self.search_entry.grid(row=0, column=0, sticky=ttk.EW, padx=(18, 0), pady=10, columnspan=2, ipady=10) + + self.search_entry.insert(0, self.search_placeholder_text) + self.search_entry.configure(foreground="grey") + self.search_entry.bind("", on_focus_in) + self.search_entry.bind("", on_focus_out) + + self.search_button = ttk.Label(entry_wrapper, image=self.images.get("search"), style="dark.TButton") + self.search_button.bind("", lambda e: self._on_search_change()) + + entry_wrapper.columnconfigure(1, weight=1) + return entry_wrapper + + def _draw_header(self, parent): + header = ttk.Frame(parent) + + search_bar = self._draw_search_bar(header) + search_bar.grid(row=0, column=0, sticky=ttk.EW, padx=(0, 5)) + + start_stop_button = self._draw_start_stop_button(header) + start_stop_button.grid(row=0, column=1, sticky=ttk.E) + + reset_button = self._draw_reset_button(header) + reset_button.grid(row=0, column=2, sticky=ttk.E, padx=(5, 0)) + + header.grid_columnconfigure(0, weight=1) + + return header + + def update(self): + try: + self.textarea.delete("1.0", "end") + + for time, prefix, text in self.console: + # self.textarea.insert("end", f"[{time}] ", "timestamp") + self.textarea.insert("end", f"[{prefix}] ", f"prefix_{prefix.lower()}") + self.textarea.insert("end", f"{text}\n", "log_text") + + self.textarea.yview_moveto(1) + except: + print("SpyPet console tried to update without being drawn.") + + def clear(self): + self.console = [] + try: + self.textarea.delete("1.0", "end") + except: + print("SpyPet console tried to clear without being drawn.") + + def add_log(self, prefix, text): + time = get_formatted_time() + self.console.append((time, str(prefix).upper(), text)) + self.update() + + def _load_tags(self): + self.textarea.tag_config("timestamp", foreground="gray") + self.textarea.tag_config("log_text", foreground="lightgrey") + + self.textarea.tag_config("prefix_sniper", foreground="red", font=("JetBrainsMono NF Bold", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size)) + self.textarea.tag_config("sniper_key", foreground="#eceb18", font=("JetBrainsMono NF Bold", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size)) + self.textarea.tag_config("prefix_command", foreground="#0b91ff", font=("JetBrainsMono NF Bold", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size)) + self.textarea.tag_config("prefix_info", foreground="#2aefef", font=("JetBrainsMono NF Bold", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size)) + self.textarea.tag_config("prefix_success", foreground="#4fee4c", font=("JetBrainsMono NF Bold", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size)) + self.textarea.tag_config("prefix_warning", foreground="#eceb18", font=("JetBrainsMono NF Bold", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size)) + self.textarea.tag_config("prefix_error", foreground="red", font=("JetBrainsMono NF Bold", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size)) + self.textarea.tag_config("prefix_cli", foreground="pink", font=("JetBrainsMono NF Bold", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size)) + self.textarea.tag_config("prefix_rpc", foreground="pink", font=("JetBrainsMono NF Bold", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size)) + + def _draw_log_wrapper(self, parent): + wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame", custom_size=True) + + self.textarea = ttk.Text(wrapper, wrap="word", height=20, + font=("JetBrainsMono NF Bold", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size) + ) + self.textarea.config( + border=0, + background=self.root.style.colors.get("dark"), + foreground="lightgrey", + highlightcolor=self.root.style.colors.get("dark"), + highlightbackground=self.root.style.colors.get("dark"), + state="normal" + ) + + self.textarea.pack(fill="both", expand=True, padx=5, pady=5) + self._load_tags() + + return wrapper + + def _draw_user_wrapper(self, parent): + wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame", custom_size=True) + wrapper.set_width(200) + + if self.user_avatar: + accent_colour_banner = RoundedFrame(wrapper, radius=(15, 15, 0, 0), background=self.user_banner_colour, parent_background=self.root.style.colors.get("bg")) + accent_colour_banner.set_height(85) + accent_colour_banner.pack(side=ttk.TOP, fill=ttk.X) + accent_colour_banner.columnconfigure(0, weight=1) + + avatar_label = ttk.Canvas(wrapper, width=100, height=200, background=self.user_banner_colour, highlightthickness=0) + + avatar_label.create_rectangle(0, 0, 100, 50, fill=self.user_banner_colour, outline="") + avatar_label.create_rectangle(0, 50, 100, 200, fill=self.root.style.colors.get("dark"), outline="") + + # create an ovel the same size of the avatar but an extra 5px on each side and use the dark background color, this is to create a border + avatar_label.create_oval(9, 8, 85, 85, fill=self.root.style.colors.get("dark"), outline="") + avatar_label.create_image(65//2 + 15, 65//2 + 15, image=self.user_avatar, anchor="center") + + avatar_label.place(x=0, y=85-50, width=100, height=200) + + if self.user: + user_info_wrapper = ttk.Frame(wrapper, style="dark.TFrame") + user_info_wrapper.pack(side=ttk.TOP, fill=ttk.X, pady=(35, 0), padx=(10, 10)) + user_info_wrapper.configure(height=50) + + display_name = ttk.Label(user_info_wrapper, text=self.user.display_name, font=("Host Grotesk", 16 if sys.platform != "darwin" else 18, "bold")) + display_name.configure(background=self.root.style.colors.get("dark")) + display_name.place(relx=0, rely=0) + + username = ttk.Label(user_info_wrapper, text=f"{self.user.name}", font=("Host Grotesk", 12 if sys.platform != "darwin" else 14)) + username.configure(background=self.root.style.colors.get("dark"), foreground="lightgrey") + username.place(relx=0, rely=0.42) + + if self.mutual_guilds: + mutual_guilds_subtitle = ttk.Label(wrapper, text="Mutual Guilds", font=("Host Grotesk", 12 if sys.platform != "darwin" else 14, "bold")) + mutual_guilds_subtitle.configure(background=self.root.style.colors.get("dark"), foreground="white") + mutual_guilds_subtitle.pack(side=ttk.TOP, fill=ttk.X, padx=10) + + guilds_wrapper = ScrolledFrame(wrapper, bootstyle="dark.TFrame", autohide=True) + guilds_wrapper.container.configure(style="dark.TFrame") + guilds_wrapper.pack(side=ttk.TOP, fill=ttk.BOTH, pady=(3, 10), padx=(10, 10), expand=True) + guilds_wrapper.columnconfigure(0, weight=1) + + row = 0 + for guild in self.mutual_guilds: + guild_frame = RoundedFrame(guilds_wrapper, radius=5, bootstyle="secondary.TFrame") + guild_frame.grid(row=row, column=0, sticky=ttk.EW, pady=(0, 5)) + + guild_label = ttk.Label(guild_frame, text=guild.name, font=("Host Grotesk", 10 if sys.platform != "darwin" else 12)) + guild_label.configure(background=self.root.style.colors.get("secondary"), foreground="white") + guild_label.grid(row=0, column=0, sticky=ttk.EW, padx=5, pady=5) + guild_frame.grid_columnconfigure(0, weight=1) + + row += 1 + else: + mutual_guilds_subtitle = ttk.Label(wrapper, text="No Mutual Guilds", font=("Host Grotesk", 12 if sys.platform != "darwin" else 14)) + mutual_guilds_subtitle.configure(background=self.root.style.colors.get("dark"), foreground="lightgrey") + mutual_guilds_subtitle.place(relx=.7, rely=0.65, relwidth=1, anchor="center") + + return wrapper + + def clear_messages(self): + if self.messages_textarea: + self.messages_textarea.delete("1.0", "end") + self.messages_formatted = [] + self.messages_all = [] + self.messages_displayed = [] + self.current_search = "" + self.messages_textarea_updating = False + + # Reset placeholder for search bar + # self.search_placeholder_text = "Search a Discord user ID..." if not self.spypet.running else "Search for a message..." + # if self.search_entry: + # self.search_entry.configure(foreground="grey") + # self.search_var.set("") # Resets the actual entry content + # self.search_entry.delete(0, ttk.END) + # self.search_entry.insert(0, self.search_placeholder_text) + + print("Messages fully cleared.") + else: + print("Messages textarea not found. Cannot clear messages.") + + def _on_search_change(self): + query = self.search_var.get().strip() + + if query == self.search_placeholder_text or query == "": + self.current_search = "" + else: + self.current_search = query.lower() + + self._apply_message_filter() + + def _apply_message_filter(self): + if not self.messages_textarea or len(self.messages_all) == 0: + return + + try: + self.messages_textarea.delete("1.0", "end") + self.messages_displayed = [] + + for msg in self.messages_all: + formatted = f"[{msg[0]}] [{msg[1]}] [{msg[2]}]: {msg[3]}" + if not self.current_search or self.current_search in formatted.lower(): + self.messages_displayed.append(msg) + self.messages_textarea.insert("end", f"{formatted}\n") + + self.messages_textarea.update_idletasks() + except: + pass + + def update_messages(self): + if self.messages_textarea_updating and self.messages_textarea: + try: + data = self.spypet.data + current_yview = self.messages_textarea.yview() + at_bottom = current_yview[1] >= 0.999 + + new_msgs = [] + + for guild, channels in data.items(): + for channel, channel_data in channels.items(): + for message in channel_data.get("messages", []): + timestamp = message.get("timestamp", None) + try: + ts = time.strftime("%d/%m/%Y %H:%M:%S", time.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f%z")) if timestamp else "Unknown" + except: + ts = timestamp + content = message.get("content", "") + msg = (ts, guild, channel, content) + + if msg not in self.messages_all: + self.messages_all.append(msg) + new_msgs.append(msg) + + for msg in new_msgs: + formatted = f"[{msg[0]}] [{msg[1]}] [{msg[2]}]: {msg[3]}" + if self.current_search.lower() in formatted.lower(): + self.messages_displayed.append(msg) + self.messages_textarea.insert("end", f"{formatted}\n") + + if at_bottom: + self.messages_textarea.see("end") + else: + self.messages_textarea.yview_moveto(current_yview[0]) + + self.messages_textarea.update_idletasks() + except Exception as e: + pass + + self.root.after(500, self.update_messages) + + def _draw_messages_wrapper(self, parent): + wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame", custom_size=True) + + self.messages_textarea = ttk.Text(wrapper, wrap="word", height=20, + font=("JetBrainsMono NF Bold", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size) + ) + self.messages_textarea.config( + border=0, + background=self.root.style.colors.get("dark"), + foreground="lightgrey", + highlightcolor=self.root.style.colors.get("dark"), + highlightbackground=self.root.style.colors.get("dark"), + state="normal" + ) + + self.messages_textarea.pack(fill="both", expand=True, padx=5, pady=5) + + return wrapper + + def _update_progress_labels(self, total, user_total): + if self.total_messages_label: + self.total_messages = total + self.total_messages_label.configure(text=f"Total: {total}") + if self.total_user_messages_label: + self.user_total_messages = user_total + self.total_user_messages_label.configure(text=f"Sent by user: {user_total}") + + def _draw_progress_wrapper(self, parent): + wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame", custom_size=True) + + self.total_messages_label = ttk.Label(wrapper, text="Total: 0", font=("Host Grotesk", 12 if sys.platform != "darwin" else 14)) + self.total_messages_label.configure(background=self.root.style.colors.get("dark"), foreground="lightgrey") + self.total_messages_label.place(relx=0.05, rely=0.13, relwidth=1, anchor="nw") + + self.total_user_messages_label = ttk.Label(wrapper, text="Sent by user: 0", font=("Host Grotesk", 12 if sys.platform != "darwin" else 14)) + self.total_user_messages_label.configure(background=self.root.style.colors.get("dark"), foreground="lightgrey") + self.total_user_messages_label.place(relx=0.05, rely=0.3 + 0.13, relwidth=1, anchor="nw") + + return wrapper + + def draw_content(self, wrapper): + self.wrapper = wrapper + + header = self._draw_header(wrapper) + header.pack(side=ttk.TOP, fill=ttk.X, pady=(0, 10)) + + self.msgs_logs_wrapper = ttk.Frame(wrapper) + self.msgs_logs_wrapper.pack(side=ttk.LEFT, fill=ttk.BOTH, expand=True, padx=(0, 5)) + self.msgs_logs_wrapper.columnconfigure(0, weight=1) + self.msgs_logs_wrapper.rowconfigure(0, weight=1) + self.msgs_logs_wrapper.rowconfigure(1, weight=0) + + self.messages_wrapper = self._draw_messages_wrapper(self.msgs_logs_wrapper) + self.messages_wrapper.grid(row=0, column=0, sticky="nsew", pady=(0, 10)) + + self.log_wrapper = self._draw_log_wrapper(self.msgs_logs_wrapper) + self.log_wrapper.set_height(150) + self.log_wrapper.grid(row=1, column=0, sticky="ew") + + self.user_progress_wrapper = ttk.Frame(wrapper) + self.user_progress_wrapper.pack(side=ttk.RIGHT, fill=ttk.Y, expand=False, padx=(5, 0)) + + self.progress_wrapper = self._draw_progress_wrapper(self.user_progress_wrapper) + self.progress_wrapper.set_height(55) + self.progress_wrapper.set_width(200) # fixed width + self.progress_wrapper.pack(side=ttk.TOP, fill=ttk.X, pady=(0, 10)) + + self.user_wrapper = self._draw_user_wrapper(self.user_progress_wrapper) + self.user_wrapper.set_width(200) # fixed width + self.user_wrapper.pack(side=ttk.BOTTOM, fill=ttk.BOTH, expand=True) + + self.update() + + if self.spypet.running or len(self.messages_all) > 0: + print("SpyPet is running, updating messages...") + self._check_spypet_running() + self._update_progress_labels(self.spypet.total_messages, self.spypet.user_total_messages) + + if self._disable_reset_button: + self._disable_reset_button() + else: + self._enable_reset_button() + + self.messages_textarea.bind("", lambda e: self._set_focused_textarea(self.messages_textarea)) + self.messages_textarea.bind("", lambda e: self.textarea.tag_remove("sel", "1.0", "end")) + + self.textarea.bind("", lambda e: self._set_focused_textarea(self.textarea)) + self.textarea.bind("", lambda e: self.messages_textarea.tag_remove("sel", "1.0", "end")) + + self.root.bind_all("", lambda e: self.focused_textarea.event_generate("<>") if self.focused_textarea else None) \ No newline at end of file diff --git a/gui/pages/tools/tools.py b/gui/pages/tools/tools.py new file mode 100644 index 0000000..cd166fc --- /dev/null +++ b/gui/pages/tools/tools.py @@ -0,0 +1,102 @@ +import os, sys +import ttkbootstrap as ttk + +from gui.components import RoundedFrame +from gui.pages.tools.spypet_page import SpyPetPage +from gui.pages.tools.message_logger_page import MessageLoggerPage +from gui.pages.tools.user_lookup_page import UserLookupPage + +class ToolsPage: + def __init__(self, root, bot_controller, images, layout): + self.root = root + self.bot_controller = bot_controller + self.images = images + self.layout = layout + self.hover_colour = "#282a2a" + + self.spypet_page = SpyPetPage(self, root, bot_controller, images, layout) + self.message_logger_page = MessageLoggerPage(self, root, bot_controller, images, layout) + self.user_lookup_page = UserLookupPage(self, root, bot_controller, images, layout) + + self.pages = [ + { + "name": "ghetto spy.pet", + "description": "A tool to look up every message sent by a user you share mutual servers with.", + "page": self.spypet_page, + "command": self.draw_spypet + }, + { + "name": "Message Logger", + "description": "A tool to log deleted messages from every server you're in.", + "page": self.message_logger_page, + "command": self.draw_message_logger + }, + { + "name": "User Lookup", + "description": "A tool to look up a user's information.", + "page": self.user_lookup_page, + "command": self.draw_user_lookup + } + ] + + def draw_spypet(self): + self.layout.sidebar.set_current_page("tools") + self.layout.clear() + main = self.layout.main() + self.spypet_page.draw(main) + self.layout.sidebar.set_button_command("tools", self.draw_spypet) + + def draw_message_logger(self): + self.layout.sidebar.set_current_page("tools") + self.layout.clear() + main = self.layout.main() + self.message_logger_page.draw(main) + self.layout.sidebar.set_button_command("tools", self.draw_message_logger) + + def draw_user_lookup(self): + self.layout.sidebar.set_current_page("tools") + self.layout.clear() + main = self.layout.main() + self.user_lookup_page.draw(main) + self.layout.sidebar.set_button_command("tools", self.draw_user_lookup) + + def _bind_hover_effects(self, widget, targets, hover_bg, normal_bg): + def on_enter(_): + for target in targets: + if isinstance(target, RoundedFrame): + target.set_background(background=hover_bg) + else: + target.configure(background=hover_bg) + + def on_leave(_): + for target in targets: + if isinstance(target, RoundedFrame): + target.set_background(background=normal_bg) + else: + target.configure(background=normal_bg) + + widget.bind("", on_enter) + widget.bind("", on_leave) + + def draw(self, parent): + for page in self.pages: + page_wrapper = RoundedFrame(parent, radius=15, bootstyle="secondary.TFrame") + page_wrapper.pack(fill="x", expand=True, pady=(0, 10)) + page_wrapper.bind("", lambda e, cmd=page["command"]: cmd()) + + page_title = ttk.Label(page_wrapper, text=page["name"], font=("Host Grotesk", 14 if sys.platform != "darwin" else 20, "bold")) + page_title.configure(background=self.root.style.colors.get("secondary")) + page_title.grid(row=0, column=0, sticky=ttk.NSEW, padx=15, pady=(15, 15)) + page_title.bind("", lambda e, cmd=page["command"]: cmd()) + + # page_description = ttk.Label(page_wrapper, text=page["description"], font=("Host Grotesk", 12 if sys.platform != "darwin" else 16), wraplength=450) + # page_description.configure(background=self.root.style.colors.get("secondary")) + # page_description.grid(row=1, column=0, sticky=ttk.NSEW, padx=15, pady=(0, 15)) + # page_description.bind("", lambda e, cmd=page["command"]: cmd()) + + page_icon = ttk.Label(page_wrapper, image=self.images.get("right-chevron")) + page_icon.configure(background=self.root.style.colors.get("secondary")) + page_icon.grid(row=0, column=1, sticky=ttk.E, padx=(0, 20), pady=15) + + page_wrapper.grid_columnconfigure(1, weight=1) + self._bind_hover_effects(page_wrapper, [page_title, page_wrapper, page_icon], self.hover_colour, self.root.style.colors.get("secondary")) \ No newline at end of file diff --git a/gui/pages/tools/user_lookup_page.py b/gui/pages/tools/user_lookup_page.py new file mode 100644 index 0000000..7a09239 --- /dev/null +++ b/gui/pages/tools/user_lookup_page.py @@ -0,0 +1,96 @@ +import sys +import ttkbootstrap as ttk +from gui.components import RoundedFrame, ToolPage + +class UserLookupPage(ToolPage): + def __init__(self, toolspage, root, bot_controller, images, layout): + super().__init__(toolspage, root, bot_controller, images, layout, title="User Lookup", frame=None) + self.search_entry = None # Initialize search entry to None + self.user = None + self.user_avatar = None + self.search_results_widget = None + self.wrapper = None # Initialize wrapper to None + + def _search_user(self, user_id): + user = self.bot_controller.get_user_from_id(user_id) + self.user = user + + avatar_url = self.user.avatar.url if self.user and self.user.avatar else "https://ia600305.us.archive.org/31/items/discordprofilepictures/discordblue.png" + self.user_avatar = self.bot_controller.get_avatar_from_url(avatar_url, size=100, radius=10) + + if self.search_results_widget: + self.search_results_widget.destroy() + self.search_results_widget = self._draw_search_results(self.wrapper) + self.search_results_widget.pack(side=ttk.TOP, fill=ttk.BOTH, expand=True) + + def _draw_search_bar(self, parent): + entry_wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame") + # entry_wrapper.pack(fill=ttk.BOTH) + + placeholder_text = "Search a Discord user ID..." + + def on_focus_in(event): + if self.search_entry.get() == placeholder_text: + self.search_entry.delete(0, ttk.END) + self.search_entry.configure(foreground="white") + + def on_focus_out(event): + if self.search_entry.get() == "": + self.search_entry.insert(0, placeholder_text) + self.search_entry.configure(foreground="grey") + + self.search_entry = ttk.Entry(entry_wrapper, bootstyle="dark.TFrame", font=("Host Grotesk",)) + self.search_entry.grid(row=0, column=0, sticky=ttk.EW, padx=(18, 0), pady=10, columnspan=2, ipady=10) + self.search_entry.configure(foreground="grey") + self.search_entry.insert(0, placeholder_text) + self.search_entry.bind("", on_focus_in) + self.search_entry.bind("", on_focus_out) + self.search_entry.bind("", lambda e: self._search_user(self.search_entry.get())) + + search_button = ttk.Label(entry_wrapper, image=self.images.get("search"), style="dark.TButton") + search_button.grid(row=0, column=2, sticky=ttk.E, padx=(0, 10), pady=10) + search_button.bind("", lambda e: self._search_user(self.search_entry.get())) + + entry_wrapper.columnconfigure(1, weight=1) + + return entry_wrapper + + def _draw_search_results(self, parent): + wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame") + + if self.user_avatar: + avatar_label = ttk.Label(wrapper, image=self.user_avatar) + avatar_label.configure(background=self.root.style.colors.get("dark")) + avatar_label.grid(row=0, column=1, sticky=ttk.E, padx=15, pady=15) + wrapper.grid_columnconfigure(1, weight=1) + + if self.user: + user_info_wrapper = ttk.Frame(wrapper, style="dark.TFrame") + user_info_wrapper.grid(row=0, column=0, sticky=ttk.NSEW, padx=(15, 0), pady=15) + + display_name = ttk.Label(user_info_wrapper, text=self.user.display_name, font=("Host Grotesk", 16 if sys.platform != "darwin" else 20, "bold")) + display_name.configure(background=self.root.style.colors.get("dark")) + display_name.grid(row=0, column=1, sticky=ttk.W) + + if self.user.bot: + bot_tag = ttk.Label(user_info_wrapper, text="Bot", font=("Host Grotesk", 12 if sys.platform != "darwin" else 14), foreground="red") + bot_tag.configure(background=self.root.style.colors.get("dark")) + bot_tag.grid(row=0, column=2, sticky=ttk.W) + + username = ttk.Label(user_info_wrapper, text=f"@{self.user.name}", font=("Host Grotesk", 12 if sys.platform != "darwin" else 14)) + username.configure(background=self.root.style.colors.get("dark"), foreground="lightgrey") + username.grid(row=1, column=1, sticky=ttk.W) + + user_id = ttk.Label(user_info_wrapper, text=f"ID: {self.user.id}", font=("Host Grotesk", 12 if sys.platform != "darwin" else 14)) + user_id.configure(background=self.root.style.colors.get("dark")) + user_id.grid(row=2, column=1, sticky=ttk.W, pady=(5, 0)) + + return wrapper + + def draw_content(self, wrapper): + self.wrapper = wrapper + search_bar = self._draw_search_bar(self.wrapper) + search_bar.pack(side=ttk.TOP, fill=ttk.X, pady=(0, 10)) + + self.search_results_widget = self._draw_search_results(self.wrapper) + self.search_results_widget.pack(side=ttk.TOP, fill=ttk.BOTH, expand=True) \ No newline at end of file diff --git a/utils/config/__init__.py b/utils/config/__init__.py index b745755..7d99cf3 100644 --- a/utils/config/__init__.py +++ b/utils/config/__init__.py @@ -4,7 +4,7 @@ from .config import Config from .token import Token -VERSION = "4.0.1" +VERSION = "4.1.0-dev" PRODUCTION = False MOTD = "gotta love tkinter ;)" CHANGELOG = """""" \ No newline at end of file diff --git a/utils/defaults.py b/utils/defaults.py index 24e2440..05fd6aa 100644 --- a/utils/defaults.py +++ b/utils/defaults.py @@ -49,7 +49,7 @@ DEFAULT_THEME = { "title": "ghost selfbot", "emoji": "\ud83d\udc7b", - "image": "https://ghost.benny.fun/assets/ghost.png", + "image": "https://ghostt.cc/assets/ghost.png", "colour": "#575757", "footer": "ghost aint dead" } diff --git a/utils/startup_check.py b/utils/startup_check.py index 8f01735..6859ec6 100644 --- a/utils/startup_check.py +++ b/utils/startup_check.py @@ -11,7 +11,8 @@ "data/cache", "data", "scripts", - "themes" + "themes", + "data/spypet" ] REQUIRED_FILES = {