diff --git a/bot/bot.py b/bot/bot.py index e239117..5c452f6 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -145,6 +145,14 @@ async def on_message(self, message): async def on_ready(self): try: self.start_time = time.time() + + is_secondary = getattr(self.controller, '_is_secondary', False) + + if is_secondary: + self.controller.bot_running = True + await self.load_cogs() + return + self.cfg.add_token(self.cfg.get("token"), self.user.name, self.user.id) await self.load_cogs() @@ -168,7 +176,6 @@ async def on_ready(self): if self.session_spoofing: console.success(f"Spoofing session as {self.session_spoofing_device}") - # console.print_warning("Your account is at higher risk of termination by using session spoofer.") await self._setup_scripts() await self.controller.setup_webhooks() @@ -177,6 +184,9 @@ async def on_ready(self): except Exception as e: console.print_error(str(e)) + if is_secondary: + return + cfg_rpc = self.cfg.get_rich_presence() if cfg_rpc.enabled: external_assets = { diff --git a/gui/components/console.py b/gui/components/console.py index 27d18ff..6ae223d 100644 --- a/gui/components/console.py +++ b/gui/components/console.py @@ -6,6 +6,8 @@ from utils.console import get_formatted_time class Console: + MAX_LOG_ENTRIES = 500 + def __init__(self, root, bot_controller): self.root = root self.bot = bot_controller @@ -18,25 +20,34 @@ def __init__(self, root, bot_controller): self.non_darwin_font_size = 12 def update(self): + if self.textarea is None: + return try: self.textarea.delete("1.0", "end") - for time, prefix, text in self.console: + visible = self.console[-self.MAX_LOG_ENTRIES:] if len(self.console) > self.MAX_LOG_ENTRIES else self.console + + for time, prefix, text in visible: if prefix.lower() == "sniper": content = text - type = content.get("type") + type_name = content.get("type") title = content.get("title") description = content.get("description") + success = content.get("success", True) + + self.textarea.insert("end", "\n", "log_text") + self.textarea.insert("end", f" {time} ", "timestamp") + + if success: + self.textarea.insert("end", f" {type_name.upper()} ", "prefix_sniper_success") + else: + self.textarea.insert("end", f" {type_name.upper()} ", "prefix_sniper_fail") - self.textarea.insert("end", f"\n[{time}] ", "timestamp") - self.textarea.insert("end", f"[{type.upper()}] ", f"prefix_{prefix.lower()}") self.textarea.insert("end", f"{title}\n", "log_text") for key, value in description.items(): - self.textarea.insert("end", f"{' ' * len(f'[{time}] ')}{key}: ", "sniper_key") + self.textarea.insert("end", f" {key}: ", "sniper_key") self.textarea.insert("end", f"{value}\n", "log_text") - - self.textarea.insert("end", "\n", "log_text") else: self.textarea.insert("end", f"[{time}] ", "timestamp") self.textarea.insert("end", f"[{prefix}] ", f"prefix_{prefix.lower()}") @@ -44,17 +55,37 @@ def update(self): self.textarea.yview_moveto(1) except: - print("Console tried to update without being drawn.") + pass def clear(self): self.console = [] try: self.textarea.delete("1.0", "end") except: - print("Console tried to clear without being drawn.") + pass def add_sniper(self, sniper_obj): self.add_log("sniper", sniper_obj) + self._notify_home_feed(sniper_obj) + + def _notify_home_feed(self, sniper_obj): + try: + from gui.pages.home import HomePage + gui = self.bot.gui if hasattr(self.bot, 'gui') else None + if gui and hasattr(gui, 'home_page'): + event_type = sniper_obj.get("type", "sniper") + title = sniper_obj.get("title", "Sniper Event") + desc_parts = sniper_obj.get("description", {}) + description = "\n".join(f"{k}: {v}" for k, v in desc_parts.items()) + success = "success" not in title.lower() or "failed" not in title.lower() + gui.home_page._add_notification( + f"{event_type.lower()}_snipe", + title, + description, + color="#4fee4c" if success else "#ff6464" + ) + except Exception: + pass def add_log(self, prefix, text): time = get_formatted_time() @@ -62,20 +93,25 @@ def add_log(self, prefix, text): self.update() def _load_tags(self): - self.textarea.tag_config("timestamp", foreground=Style.DARK_GREY.value, font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) - self.textarea.tag_config("log_text", foreground=Style.LIGHT_GREY.value, font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) + font_size = self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size + mono_font = ("JetBrainsMono NF", font_size, "bold") + + self.textarea.tag_config("timestamp", foreground=Style.DARK_GREY.value, font=mono_font) + self.textarea.tag_config("log_text", foreground=Style.LIGHT_GREY.value, font=mono_font) - self.textarea.tag_config("prefix_sniper", foreground="red", font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) - self.textarea.tag_config("sniper_key", foreground="#eceb18", font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) - self.textarea.tag_config("prefix_command", foreground="#0b91ff", font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) - self.textarea.tag_config("prefix_info", foreground="#2aefef", font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) - self.textarea.tag_config("prefix_success", foreground="#4fee4c", font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) - self.textarea.tag_config("prefix_warning", foreground="#eceb18", font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) - self.textarea.tag_config("prefix_error", foreground="red", font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) - self.textarea.tag_config("prefix_cli", foreground="pink", font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) - self.textarea.tag_config("prefix_rpc", foreground="pink", font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) - self.textarea.tag_config("prefix_captcha", foreground="#eceb18", font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) - self.textarea.tag_config("prefix_nitro", foreground="#0b91ff", font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold")) + self.textarea.tag_config("prefix_sniper", foreground="red", font=mono_font) + self.textarea.tag_config("prefix_sniper_success", foreground="#4fee4c", font=mono_font) + self.textarea.tag_config("prefix_sniper_fail", foreground="#ff6464", font=mono_font) + self.textarea.tag_config("sniper_key", foreground="#eceb18", font=mono_font) + self.textarea.tag_config("prefix_command", foreground="#0b91ff", font=mono_font) + self.textarea.tag_config("prefix_info", foreground="#2aefef", font=mono_font) + self.textarea.tag_config("prefix_success", foreground="#4fee4c", font=mono_font) + self.textarea.tag_config("prefix_warning", foreground="#eceb18", font=mono_font) + self.textarea.tag_config("prefix_error", foreground="red", font=mono_font) + self.textarea.tag_config("prefix_cli", foreground="pink", font=mono_font) + self.textarea.tag_config("prefix_rpc", foreground="pink", font=mono_font) + self.textarea.tag_config("prefix_captcha", foreground="#eceb18", font=mono_font) + self.textarea.tag_config("prefix_nitro", foreground="#0b91ff", font=mono_font) def _draw_footer(self, parent): """ Draw the footer with user info and clear button. """ @@ -102,7 +138,7 @@ def _draw_footer(self, parent): def _draw_main(self, parent): wrapper = RoundedFrame(parent, radius=15, bootstyle="dark.TFrame") - wrapper.pack(side="top", fill="both", expand=True) + wrapper.pack(fill="both", expand=True) self.textarea = ttk.Text(wrapper, wrap="word", height=20, font=("JetBrainsMono NF", self.non_darwin_font_size if sys.platform != "darwin" else self.darwin_font_size, "bold") @@ -126,10 +162,12 @@ def _draw_main(self, parent): self.textarea.pack(fill="both", expand=True, padx=5, pady=5) self._load_tags() + self._wrapper = wrapper def draw(self, parent): self.avatar = self.bot.get_avatar(size=15, radius=2) self._draw_main(parent) + return self._wrapper # if self.bot.running: # self._draw_footer(parent) self.update() \ No newline at end of file diff --git a/gui/components/notification_feed.py b/gui/components/notification_feed.py new file mode 100644 index 0000000..8720655 --- /dev/null +++ b/gui/components/notification_feed.py @@ -0,0 +1,154 @@ +import sys +import time +import ttkbootstrap as ttk + +from gui.helpers.style import Style + + +class Toast: + def __init__(self, parent, root, event_type, title, description="", timestamp=None, color=None, on_dismiss=None): + from utils.config import Config + self.parent = parent + self.root = root + self.on_dismiss = on_dismiss + self.color = color or self._get_color(event_type) + self.timestamp = timestamp or time.strftime("%H:%M:%S") + + timeout_sec = Config().get("notification_timeout") + self.AUTO_DISMISS_MS = (timeout_sec if isinstance(timeout_sec, (int, float)) and timeout_sec > 0 else 5) * 1000 + + self.frame = ttk.Frame(parent, style="dark.TFrame") + self.frame.pack(fill=ttk.X, padx=4, pady=(0, 4)) + + card = ttk.Frame(self.frame, style="dark.TFrame") + card.pack(fill=ttk.X) + + accent = ttk.Canvas(card, width=3, height=3, highlightthickness=0, + background=self.root.style.colors.get("dark")) + accent.pack(side=ttk.LEFT, fill="y", padx=(0, 8)) + accent.create_rectangle(0, 0, 3, 500, fill=self.color, outline=self.color) + + content = ttk.Frame(card, style="dark.TFrame") + content.pack(side=ttk.LEFT, fill=ttk.X, expand=True, pady=6) + + top = ttk.Frame(content, style="dark.TFrame") + top.pack(fill=ttk.X) + + type_lbl = ttk.Label(top, text=event_type.replace("_", " ").title(), + font=("Host Grotesk", 9, "bold"), foreground=self.color) + type_lbl.configure(background=self.root.style.colors.get("dark")) + type_lbl.pack(side=ttk.LEFT) + + time_lbl = ttk.Label(top, text=self.timestamp, font=("Host Grotesk", 8), + foreground=Style.DARK_GREY.value) + time_lbl.configure(background=self.root.style.colors.get("dark")) + time_lbl.pack(side=ttk.RIGHT) + + title_lbl = ttk.Label(content, text=title, font=("Host Grotesk", 10, "bold")) + title_lbl.configure(background=self.root.style.colors.get("dark")) + title_lbl.pack(anchor=ttk.W) + + if description: + desc_lbl = ttk.Label(content, text=description, font=("Host Grotesk", 9), + foreground=Style.LIGHT_GREY.value, wraplength=400) + desc_lbl.configure(background=self.root.style.colors.get("dark")) + desc_lbl.pack(anchor=ttk.W) + + dismiss = ttk.Label(card, text="\u00d7", font=("Host Grotesk", 14, "bold"), + cursor="hand2", foreground=Style.DARK_GREY.value) + dismiss.configure(background=self.root.style.colors.get("dark")) + dismiss.pack(side=ttk.RIGHT, padx=8, anchor=ttk.N) + dismiss.bind("", lambda e: self.dismiss()) + dismiss.bind("", lambda e: dismiss.configure(foreground="white")) + dismiss.bind("", lambda e: dismiss.configure(foreground=Style.DARK_GREY.value)) + + self._timer = self.root.after(self.AUTO_DISMISS_MS, self.dismiss) + + def _get_color(self, event_type): + colors = { + "friend_add": "#4fee4c", "friend_remove": "#ff6464", + "relationship_add": "#4fee4c", "relationship_remove": "#ff6464", + "relationship_update": "#eceb18", "guild_remove": "#ff6464", + "guild_join": "#4fee4c", "nitro_snipe": "#0b91ff", + "privnote_snipe": "#a07cff", "info": "#2aefef", + "warning": "#eceb18", "error": "#ff6464", "success": "#4fee4c", + } + return colors.get(event_type, Style.LIGHT_GREY.value) + + def dismiss(self): + try: + if self._timer: + self.root.after_cancel(self._timer) + except Exception: + pass + try: + if self.frame and self.frame.winfo_exists(): + self.frame.destroy() + except Exception: + pass + if self.on_dismiss: + self.on_dismiss(self) + + +class NotificationFeed: + MAX_VISIBLE = 5 + + def __init__(self, root): + self.root = root + self.wrapper = None + self.toasts = [] + self.history = [] + self._logger_page = None + + def set_logger(self, logger_page): + self._logger_page = logger_page + + def draw(self, parent): + self.wrapper = ttk.Frame(parent, style="dark.TFrame") + return self.wrapper + + def add_notification(self, event_type, title, description="", timestamp=None, color=None): + ts = timestamp or time.strftime("%H:%M:%S") + clr = color or self._get_color(event_type) + + self.history.append({ + "event_type": event_type, + "title": title, + "description": description, + "timestamp": ts, + "color": clr, + }) + + if self._logger_page: + self._logger_page.add_entry(event_type, title, description, timestamp=ts, color=clr) + + if not self.wrapper or not self.wrapper.winfo_exists(): + return + + toast = Toast(self.wrapper, self.root, event_type, title, description, + timestamp=ts, color=clr, on_dismiss=self._on_dismiss) + self.toasts.append(toast) + + while len(self.toasts) > self.MAX_VISIBLE: + old = self.toasts.pop(0) + old.dismiss() + + def _get_color(self, event_type): + colors = { + "friend_add": "#4fee4c", "friend_remove": "#ff6464", + "relationship_add": "#4fee4c", "relationship_remove": "#ff6464", + "relationship_update": "#eceb18", "guild_remove": "#ff6464", + "guild_join": "#4fee4c", "nitro_snipe": "#0b91ff", + "privnote_snipe": "#a07cff", "info": "#2aefef", + "warning": "#eceb18", "error": "#ff6464", "success": "#4fee4c", + } + return colors.get(event_type, Style.LIGHT_GREY.value) + + def _on_dismiss(self, toast): + if toast in self.toasts: + self.toasts.remove(toast) + + def clear(self): + for t in self.toasts[:]: + t.dismiss() + self.toasts.clear() diff --git a/gui/components/settings/general.py b/gui/components/settings/general.py index 2645207..c140bc2 100644 --- a/gui/components/settings/general.py +++ b/gui/components/settings/general.py @@ -3,6 +3,7 @@ import utils.console as console from gui.components import SettingsPanel, DropdownMenu, RoundedButton, RoundedSwitch from gui.helpers import apply_theme, get_themes +from utils.files import set_launch_on_startup class GeneralPanel(SettingsPanel): def __init__(self, root, parent, bot_controller, images, config, draw_settings, width=None): @@ -49,6 +50,21 @@ def _save_cfg(self): self.cfg.set("telemetry", self.telemetry_entry.instate(["selected"]), save=False) except Exception as e: console.error(f"Failed to set telemetry setting: {e}") + + try: + startup_enabled = self.startup_entry.instate(["selected"]) + self.cfg.set("launch_on_startup", startup_enabled, save=False) + success, err = set_launch_on_startup(startup_enabled) + if not success: + console.warning(f"Could not update startup setting: {err}") + except Exception as e: + console.error(f"Failed to set startup setting: {e}") + + try: + timeout_val = int(self.notif_timeout_entry.value()) + self.cfg.set("notification_timeout", timeout_val, save=False) + except Exception as e: + console.error(f"Failed to set notification timeout: {e}") self.cfg.save(notify=False) @@ -131,6 +147,22 @@ def draw(self): self.telemetry_entry.configure(variable=ttk.BooleanVar(value=self.cfg.get("telemetry"))) self.telemetry_entry.grid(row=1, column=1, sticky=ttk.E, padx=(10, 10), pady=(2, 10)) + startup_label = ttk.Label(checkboxes_frame, text="Launch Ghost on startup") + startup_label.configure(background=self.root.style.colors.get("dark")) + startup_label.grid(row=2, column=0, sticky=ttk.NW, padx=(10, 0), pady=(2, 10)) + + self.startup_entry = RoundedSwitch(checkboxes_frame, command=self._save_cfg, parent_background=self.root.style.colors.get("dark")) + self.startup_entry.configure(variable=ttk.BooleanVar(value=self.cfg.get("launch_on_startup"))) + self.startup_entry.grid(row=2, column=1, sticky=ttk.E, padx=(10, 10), pady=(2, 10)) + + notif_timeout_label = ttk.Label(self.body, text="Notification timeout (seconds)") + notif_timeout_label.configure(background=self.root.style.colors.get("dark")) + notif_timeout_label.grid(row=len(self.config_entries) + 4, column=0, sticky=ttk.NW, padx=(10, 0), pady=(5, 10)) + + self.notif_timeout_entry = DropdownMenu(self.body, options=["3", "5", "10", "15", "30", "60"], command=lambda _: self._save_cfg()) + self.notif_timeout_entry.set_selected(str(self.cfg.get("notification_timeout"))) + self.notif_timeout_entry.draw().grid(row=len(self.config_entries) + 4, column=1, sticky="we", padx=(10, 10), pady=(2, 10), columnspan=3) + return self.wrapper def save(self): diff --git a/gui/components/titlebar.py b/gui/components/titlebar.py index 5b911d4..2d7f20b 100644 --- a/gui/components/titlebar.py +++ b/gui/components/titlebar.py @@ -15,6 +15,7 @@ def __init__(self, root, images): self._offset_x = 0 self._offset_y = 0 self._dragging = False + self._move_in_progress = False def _on_press(self, event): self._dragging = True @@ -22,15 +23,24 @@ def _on_press(self, event): self._offset_y = event.y_root - self.root.winfo_y() def _on_motion(self, event): - if not self._dragging: - return + if not self._dragging or self._move_in_progress: + return "break" - if sys.platform == "darwin": - x = event.x_root - self._offset_x - y = event.y_root - self._offset_y - self.root.geometry(f"+{x}+{y}") - elif sys.platform == "win32": - hPyT.window_frame.move(self.root, event.x_root - self._offset_x, event.y_root - self._offset_y) + target_x = event.x_root - self._offset_x + target_y = event.y_root - self._offset_y + + self.root.after(1, lambda x=target_x, y=target_y: self._do_move(x, y)) + return "break" + + def _do_move(self, x, y): + self._move_in_progress = True + try: + if sys.platform == "darwin": + self.root.geometry(f"+{x}+{y}") + elif sys.platform == "win32": + hPyT.window_frame.move(self.root, x, y) + finally: + self._move_in_progress = False def _on_release(self, event): self._dragging = False @@ -91,6 +101,7 @@ def draw(self): ) inner_wrapper = RoundedFrame(titlebar, background=Style.WINDOW_BORDER.value, radius=0) + padx = 8 pady = 8 diff --git a/gui/main.py b/gui/main.py index 1892913..495b2bb 100644 --- a/gui/main.py +++ b/gui/main.py @@ -86,6 +86,7 @@ def __init__(self, bot_controller, update_info=None): self.settings_page = SettingsPage(self.root, self.bot_controller, self.draw_settings) self.scripts_page = ScriptsPage(self, self.bot_controller, self.images) self.tools_page = ToolsPage(self.root, self.bot_controller, self.images, self.layout, self._position_resize_grips) + self.home_page.notification_feed.set_logger(self.tools_page.notification_logger_page) self.update_page = UpdatePage(self.root, self) logging.set_gui(self) @@ -347,6 +348,7 @@ def _on_bot_ready(self): self.root.after(50, lambda: self.notifier.send("Ghost", "Ghost has successfully started!")) self.root.after(75, lambda: self.draw_home()) + self.root.after(200, lambda: self.tools_page.multi_token_page.auto_start_tokens()) def _check_bot_started(self): if self.bot_controller.bot_running: diff --git a/gui/pages/home.py b/gui/pages/home.py index 6a9be22..48d9715 100644 --- a/gui/pages/home.py +++ b/gui/pages/home.py @@ -1,11 +1,13 @@ -import webbrowser, discord, sys +import sys import ttkbootstrap as ttk import tkinter.font as tkFont -from ttkbootstrap.scrolled import ScrolledFrame + from gui.components import RoundedFrame, RoundedButton +from gui.components.notification_feed import NotificationFeed from gui.helpers import Images from gui.helpers.style import Style, get_current_theme_str -from utils.config import VERSION, CHANGELOG, MOTD, Config +from utils.config import VERSION, Config + class HomePage: def __init__(self, root, bot_controller, _restart_bot, console): @@ -13,216 +15,276 @@ def __init__(self, root, bot_controller, _restart_bot, console): self.bot_controller = bot_controller self._restart_bot = _restart_bot self.console = console - self.width = root.winfo_width() - self.height = root.winfo_height() self.restart = False self.avatar = None - self.avatars = {} self.images = Images() self.cfg = Config() - - self.details_wrapper = None + self.friends_label = None self.guilds_label = None self.uptime_label = None self.latency_label = None - + self.restart_title = None self.restart_title_elipsis = "..." self.restart_title_text = "Ghost is restarting" - - self.root.bind("", self._update_wraplength) - - def _clear_everything(self): - for widget in self.root.winfo_children(): - widget.destroy() - + + self.notification_feed = NotificationFeed(root) + self._notification_feed_widget = None + self._hooks_installed = False + def _update_restart_title(self): - if self.restart: - if len(self.restart_title_elipsis) == 3: - self.restart_title_elipsis = "." - else: - self.restart_title_elipsis += "." - self.restart_title.config(text=f"{self.restart_title_text}{self.restart_title_elipsis}") - self.root.after(750, self._update_restart_title) - - def _update_account_details(self): try: - if not self.restart: - self.friends_label.config(text=f"Friends: {len(self.bot_controller.get_friends())}") - self.guilds_label.config(text=f"Guilds: {len(self.bot_controller.get_guilds())}") - except: + if self.restart: + if len(self.restart_title_elipsis) == 3: + self.restart_title_elipsis = "." + else: + self.restart_title_elipsis += "." + self.restart_title.config(text=f"{self.restart_title_text}{self.restart_title_elipsis}") + self.restart_title.after(750, self._update_restart_title) + except Exception: pass - self.root.after(1000, self._update_account_details) - + def _update_bot_details(self): try: - if not self.restart: + if not self.restart and self.uptime_label and self.latency_label: self.uptime_label.config(text=f"Uptime: {self.bot_controller.get_uptime()}") self.latency_label.config(text=f"Latency: {self.bot_controller.get_latency()}") - except: + if self.uptime_label: + self.uptime_label.after(1000, self._update_bot_details) + except Exception: pass - self.root.after(1000, self._update_bot_details) - - def _draw_restart_button(self, parent, disabled=False): - def _hover_enter(_): - frame.set_background(background=Style.PRIMARY_BTN_HOVER.value) - restart_label.configure(background=Style.PRIMARY_BTN_HOVER.value) - - def _hover_leave(_): - frame.set_background(background=self.root.style.colors.get("primary")) - restart_label.configure(background=self.root.style.colors.get("primary")) - - frame = RoundedFrame(parent, radius=(8, 8, 8, 8), bootstyle="primary.TButton" if not disabled else "disabled.TButton") - - restart_label = ttk.Label(frame, image=self.images.get("restart"), anchor="center") - restart_label.configure(background=self.root.style.colors.get("primary") if not disabled else self.root.style.colors.get("disabled")) - # restart_label.configure(background=self.root.style.colors.get("secondary")) - restart_label.pack(anchor="center", fill=ttk.BOTH, expand=False, padx=25, pady=10) - - if not disabled: - restart_label.bind("", lambda e: self._restart_bot()) - restart_label.bind("", _hover_enter) - restart_label.bind("", _hover_leave) - frame.bind("", lambda e: self._restart_bot()) - frame.bind("", _hover_enter) - frame.bind("", _hover_leave) - - return frame - + + def _update_account_details(self): + try: + if not self.restart and self.friends_label and self.guilds_label: + friends = self.bot_controller.get_friends() + guilds = self.bot_controller.get_guilds() + self.friends_label.config(text=str(len(friends)) if friends else "0") + self.guilds_label.config(text=str(len(guilds)) if guilds else "0") + if self.friends_label: + self.friends_label.after(1000, self._update_account_details) + except Exception: + pass + def _draw_header(self, parent): wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="secondary.TFrame") - wrapper.pack(fill=ttk.BOTH, expand=False, pady=(0, 10)) - - if self.avatar and not self.restart: - avatar = ttk.Label(wrapper, image=self.avatar) - avatar.configure(background=self.root.style.colors.get("secondary")) - avatar.grid(row=0, column=0, sticky=ttk.W, padx=(15, 10), pady=15, rowspan=2) - - if not self.restart: - display_name = ttk.Label(wrapper, text=self.bot_controller.get_user().display_name, font=("Host Grotesk", 24, "bold")) - display_name.configure(background=self.root.style.colors.get("secondary")) - display_name.grid(row=0, column=1, sticky=ttk.W, pady=(15, 0)) - - username = ttk.Label(wrapper, text=self.bot_controller.get_user().name, font=("Host Grotesk", 14 if sys.platform != "darwin" else 16, "italic")) - username.configure(background=self.root.style.colors.get("secondary"), foreground=Style.LIGHT_GREY.value) - username.grid(row=1, column=1, sticky=ttk.W, pady=(0, 15)) - - # restart_btn = self._draw_restart_button(wrapper) - # restart_btn.grid(row=0, column=3, rowspan=2, sticky=ttk.EW, padx=(10, 16), pady=(10, 10)) - - self.restart_image = self.images.get("restart") - - if get_current_theme_str() == "light": - self.restart_image = self.images.change_image_colour("restart", "#ffffff", tk_image=True) - - restart_btn = RoundedButton(wrapper, radius=20, bootstyle="primary.TButton", command=lambda _: self._restart_bot(), image=self.restart_image, padx=15, pady=6) - restart_btn.grid(row=0, column=3, rowspan=2, sticky=ttk.EW, padx=(10, 16), pady=(10, 10)) - - wrapper.grid_columnconfigure(2, weight=1) - else: - self.restart_title = ttk.Label(wrapper, text=f"{self.restart_title_text}...", font=("Host Grotesk", 24, "bold"), anchor="center") + wrapper.pack(fill=ttk.X, pady=(0, 8)) + + if self.restart: + self.restart_title = ttk.Label( + wrapper, text=f"{self.restart_title_text}...", + font=("Host Grotesk", 20, "bold"), anchor="center" + ) self.restart_title.configure(background=self.root.style.colors.get("secondary")) - self.restart_title.grid(row=0, column=0, sticky=ttk.NSEW, pady=26, padx=15, columnspan=2) - wrapper.grid_columnconfigure(0, weight=1) + self.restart_title.pack(pady=16, padx=15) self.root.after(750, self._update_restart_title) - - def _draw_details_wrapper(self, parent): - wrapper = ttk.Frame(parent, width=self.width) - wrapper.configure(style="default.TLabel") - wrapper.pack(fill=ttk.BOTH, expand=False, pady=(10, 0)) - - wrapper.grid_rowconfigure(0, weight=1) - wrapper.grid_columnconfigure(0, weight=1) - wrapper.grid_columnconfigure(1, weight=1) - - return wrapper - - def _draw_account_details(self, parent): - wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame") - wrapper.grid(row=0, column=0, sticky=ttk.NSEW, padx=(0, 5), pady=(0, 5)) - - if self.restart: return - - title = ttk.Label(wrapper, text="Discord", font=("Host Grotesk", 14 if sys.platform != "darwin" else 18, "bold")) - title.configure(background=self.root.style.colors.get("dark")) - title.grid(row=0, column=0, sticky=ttk.W, padx=10, pady=(10, 0)) - - ttk.Separator(wrapper, orient="horizontal").grid(row=1, column=0, columnspan=2, sticky="we", padx=(10, 10), pady=5) - wrapper.grid_columnconfigure(1, weight=1) - - self.friends_label = ttk.Label(wrapper, text=f"Friends: {len(self.bot_controller.get_friends())}", font=("Host Grotesk", 12 if sys.platform != "darwin" else 13)) - self.friends_label.configure(background=self.root.style.colors.get("dark"), foreground="white" if not self.restart else Style.LIGHT_GREY.value) - self.friends_label.grid(row=2, column=0, sticky=ttk.W, padx=10, pady=(5, 0)) - - self.guilds_label = ttk.Label(wrapper, text=f"Guilds: {len(self.bot_controller.get_guilds())}", font=("Host Grotesk", 12 if sys.platform != "darwin" else 13)) - self.guilds_label.configure(background=self.root.style.colors.get("dark"), foreground="white" if not self.restart else Style.LIGHT_GREY.value) - self.guilds_label.grid(row=3, column=0, sticky=ttk.W, padx=10, pady=(0, 10)) - - def _draw_bot_details(self, parent): - wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame") - wrapper.grid(row=0, column=1, sticky=ttk.NSEW, padx=(5, 0), pady=(0, 5)) - - if self.restart: - return - - title = ttk.Label(wrapper, text="Ghost", font=("Host Grotesk", 14 if sys.platform != "darwin" else 18, "bold")) - title.configure(background=self.root.style.colors.get("dark")) - title.grid(row=0, column=0, sticky=ttk.W, padx=10, pady=(10, 0)) - - ttk.Separator(wrapper, orient="horizontal").grid(row=1, column=0, columnspan=2, sticky="we", padx=(10, 10), pady=5) - wrapper.grid_columnconfigure(1, weight=1) - - version = ttk.Label(wrapper, text=f"Version: {VERSION}", font=("Host Grotesk", 12 if sys.platform != "darwin" else 13)) - version.configure(background=self.root.style.colors.get("dark")) - version.grid(row=2, column=0, sticky=ttk.W, padx=(10, 0), pady=(5, 0)) - - self.uptime_label = ttk.Label(wrapper, text=f"Uptime: {self.bot_controller.get_uptime()}", font=("Host Grotesk", 12 if sys.platform != "darwin" else 13)) - self.uptime_label.configure(background=self.root.style.colors.get("dark"), foreground="white" if not self.restart else Style.LIGHT_GREY.value) - self.uptime_label.grid(row=3, column=0, sticky=ttk.W, padx=(10, 0)) - - self.latency_label = ttk.Label(wrapper, text=f"Latency: {self.bot_controller.get_latency()}", font=("Host Grotesk", 12 if sys.platform != "darwin" else 13)) - self.latency_label.configure(background=self.root.style.colors.get("dark"), foreground="white" if not self.restart else Style.LIGHT_GREY.value) - self.latency_label.grid(row=4, column=0, sticky=ttk.W, padx=(10, 0), pady=(0, 10)) - + + content = ttk.Frame(wrapper, style="secondary.TFrame") + content.pack(fill=ttk.BOTH, padx=12, pady=8) + + if self.avatar: + avatar_label = ttk.Label(content, image=self.avatar) + avatar_label.configure(background=self.root.style.colors.get("secondary")) + avatar_label.grid(row=0, column=0, rowspan=2, sticky=ttk.W, padx=(0, 10)) + + name_frame = ttk.Frame(content, style="secondary.TFrame") + name_frame.grid(row=0, column=1, sticky=ttk.W) + + display_name = ttk.Label( + name_frame, text=self.bot_controller.get_user().display_name, + font=("Host Grotesk", 18, "bold") + ) + display_name.configure(background=self.root.style.colors.get("secondary")) + display_name.pack(anchor=ttk.W) + + username = ttk.Label( + name_frame, text=self.bot_controller.get_user().name, + font=("Host Grotesk", 11 if sys.platform != "darwin" else 13, "italic") + ) + username.configure(background=self.root.style.colors.get("secondary"), foreground=Style.LIGHT_GREY.value) + username.pack(anchor=ttk.W) + + content.grid_columnconfigure(1, weight=1) + + restart_img = self.images.get("restart") + if get_current_theme_str() == "light": + restart_img = self.images.change_image_colour("restart", "#ffffff", tk_image=True) + restart_btn = RoundedButton( + content, radius=18, bootstyle="primary.TButton", + command=lambda _: self._restart_bot(), image=restart_img, + padx=12, pady=4 + ) + restart_btn.grid(row=0, column=2, rowspan=2, sticky=ttk.E, padx=(8, 0)) + + def _draw_stat_card(self, parent, title, row, col): + card = RoundedFrame(parent, radius=(10, 10, 10, 10), bootstyle="dark.TFrame") + card.grid(row=row, column=col, sticky=ttk.NSEW, padx=(0, 4), pady=(0, 4)) + + inner = ttk.Frame(card, style="dark.TFrame") + inner.pack(fill=ttk.BOTH, padx=10, pady=6) + + header = ttk.Frame(inner, style="dark.TFrame") + header.pack(fill=ttk.X) + + title_lbl = ttk.Label(header, text=title, font=("Host Grotesk", 11, "bold")) + title_lbl.configure(background=self.root.style.colors.get("dark")) + title_lbl.pack(side=ttk.LEFT) + + ttk.Separator(inner, orient="horizontal").pack(fill=ttk.X, pady=(4, 6)) + + return inner + def _draw_details(self, parent): - wrapper = RoundedFrame(parent, radius=(15, 15, 15, 15), bootstyle="dark.TFrame") - wrapper.pack(fill=ttk.BOTH, expand=False, pady=(0, 10)) - - font = ("Host Grotesk", 14) - - version = ttk.Label(wrapper, text=f"Ghost v{VERSION}", font=font) - version.configure(background=self.root.style.colors.get("dark")) - version.grid(row=0, column=0, sticky=ttk.W, padx=(10, 0), pady=10) - - self.uptime_label = ttk.Label(wrapper, text=f"Uptime: {self.bot_controller.get_uptime()}", font=font) + container = ttk.Frame(parent, style="default.TFrame") + container.pack(fill=ttk.X, pady=(0, 8)) + container.grid_columnconfigure(0, weight=1) + container.grid_columnconfigure(1, weight=1) + + discord_card = self._draw_stat_card(container, "Discord", 0, 0) + self.friends_label = ttk.Label( + discord_card, text="0", + font=("Host Grotesk", 18, "bold") + ) + self.friends_label.configure(background=self.root.style.colors.get("dark"), foreground="#4fee4c") + self.friends_label.pack(anchor=ttk.W) + friends_sub = ttk.Label( + discord_card, text="Friends", + font=("Host Grotesk", 9) + ) + friends_sub.configure(background=self.root.style.colors.get("dark"), foreground=Style.LIGHT_GREY.value) + friends_sub.pack(anchor=ttk.W) + + ttk.Frame(discord_card, height=4, style="dark.TFrame").pack() + + self.guilds_label = ttk.Label( + discord_card, text="0", + font=("Host Grotesk", 18, "bold") + ) + self.guilds_label.configure(background=self.root.style.colors.get("dark"), foreground="#5865f2") + self.guilds_label.pack(anchor=ttk.W) + guilds_sub = ttk.Label( + discord_card, text="Guilds", + font=("Host Grotesk", 9) + ) + guilds_sub.configure(background=self.root.style.colors.get("dark"), foreground=Style.LIGHT_GREY.value) + guilds_sub.pack(anchor=ttk.W) + + ghost_card = self._draw_stat_card(container, "Ghost", 0, 1) + + version_val = ttk.Label(ghost_card, text=f"v{VERSION}", font=("Host Grotesk", 18, "bold")) + version_val.configure(background=self.root.style.colors.get("dark"), foreground="#fee75c") + version_val.pack(anchor=ttk.W) + version_sub = ttk.Label(ghost_card, text="Version", font=("Host Grotesk", 9)) + version_sub.configure(background=self.root.style.colors.get("dark"), foreground=Style.LIGHT_GREY.value) + version_sub.pack(anchor=ttk.W) + + ttk.Frame(ghost_card, height=4, style="dark.TFrame").pack() + + self.uptime_label = ttk.Label(ghost_card, text="Uptime: 0s", font=("Host Grotesk", 11)) self.uptime_label.configure(background=self.root.style.colors.get("dark")) - self.uptime_label.grid(row=0, column=1, sticky=ttk.E, padx=(10, 0), pady=10) - - self.latency_label = ttk.Label(wrapper, text=f"Latency: {self.bot_controller.get_latency()}", font=font) + self.uptime_label.pack(anchor=ttk.W) + + self.latency_label = ttk.Label(ghost_card, text="Latency: 0ms", font=("Host Grotesk", 11)) self.latency_label.configure(background=self.root.style.colors.get("dark")) - self.latency_label.grid(row=0, column=2, sticky=ttk.E, padx=10, pady=10) - - wrapper.grid_columnconfigure(1, weight=1) - - def _update_wraplength(self, event=None): - # This method can be used for future wraplength updates if needed - pass - + self.latency_label.pack(anchor=ttk.W) + 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" - self.avatar = self.bot_controller.get_avatar(size=55) + self.avatar = self.bot_controller.get_avatar(size=50) if not self.restart else None + self._draw_header(parent) - self._draw_details(parent) - - # self.details_wrapper = self._draw_details_wrapper(parent) - # self._draw_account_details(self.details_wrapper) - # self._draw_bot_details(self.details_wrapper) - self._update_bot_details() - # self._update_account_details() - - self.console.draw(parent) \ No newline at end of file + + if not self.restart: + self._update_account_details() + + self._notification_feed_widget = self.notification_feed.draw(parent) + self._notification_feed_widget.pack(fill=ttk.X) + + self.console.draw(parent) + self._setup_event_hooks() + else: + self.console.draw(parent) + + def _setup_event_hooks(self): + if self._hooks_installed: + return + if not self.bot_controller.bot: + return + try: + self.bot_controller.bot.add_listener(self._on_relationship_add, 'on_relationship_add') + self.bot_controller.bot.add_listener(self._on_relationship_remove, 'on_relationship_remove') + self.bot_controller.bot.add_listener(self._on_relationship_update, 'on_relationship_update') + self.bot_controller.bot.add_listener(self._on_guild_remove, 'on_guild_remove') + self.bot_controller.bot.add_listener(self._on_guild_join, 'on_guild_join') + self._hooks_installed = True + except Exception: + pass + + async def _on_relationship_add(self, relationship): + try: + user_name = getattr(relationship, 'user', relationship).name if hasattr(relationship, 'user') else str(relationship) + self._add_notification( + "friend_add", + f"New {getattr(relationship, 'type', 'friend').name.replace('_', ' ').title() if hasattr(relationship, 'type') else 'friend'}", + f"{user_name} has been added to your relationships." + ) + except Exception: + pass + + async def _on_relationship_remove(self, relationship): + try: + user_name = getattr(relationship, 'user', relationship).name if hasattr(relationship, 'user') else str(relationship) + self._add_notification( + "friend_remove", + f"Removed {getattr(relationship, 'type', 'friend').name.replace('_', ' ').title() if hasattr(relationship, 'type') else 'friend'}", + f"{user_name} has been removed from your relationships." + ) + except Exception: + pass + + async def _on_relationship_update(self, before, after): + try: + user_name = getattr(after, 'user', after).name if hasattr(after, 'user') else str(after) + self._add_notification( + "relationship_update", + "Relationship Updated", + f"Relationship with {user_name} has been updated." + ) + except Exception: + pass + + async def _on_guild_remove(self, guild): + try: + self._add_notification( + "guild_remove", + "Removed from Server", + f"You were removed from {guild.name}." + ) + except Exception: + pass + + async def _on_guild_join(self, guild): + try: + self._add_notification( + "guild_join", + "Joined Server", + f"You joined {guild.name}." + ) + except Exception: + pass + + def _add_notification(self, event_type, title, description="", color=None): + import time as _time + timestamp = _time.strftime("%H:%M:%S") + try: + if self.notification_feed.wrapper and self.notification_feed.wrapper.winfo_exists(): + self.root.after(0, lambda: self.notification_feed.add_notification( + event_type, title, description, timestamp=timestamp, color=color + )) + except Exception: + pass diff --git a/gui/pages/script.py b/gui/pages/script.py index db90453..4516dca 100644 --- a/gui/pages/script.py +++ b/gui/pages/script.py @@ -1,10 +1,13 @@ import sys +import os +import tkinter import ttkbootstrap as ttk +from ttkbootstrap.dialogs import Messagebox 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 +from gui.helpers.style import Style + class ScriptPage: def __init__(self, root, script): @@ -12,50 +15,192 @@ def __init__(self, root, script): self.root = root.root self.script = script self.images = Images() - self.editor = None - self.linenumbers = None - self.text_scrollbar = None - + self.text_widget = None + self.line_numbers = None + self._dirty = False + self._script_path = os.path.join(get_application_support(), "scripts", self.script) + self._original_content = "" + self._title_label = None + self._status_label = None + def _go_back(self): - self._save_script() + if self._dirty: + result = str(Messagebox.yesnocancel( + "You have unsaved changes. Do you want to save before leaving?", + title="Unsaved Changes" + )).lower() + if result == "yes": + self._save_script() + elif result == "cancel": + return self.gui.draw_scripts() - + def _get_script_content(self): - with open(get_application_support() + f"/scripts/{self.script}", "r") as file: - return file.read() - + try: + with open(self._script_path, "r", encoding="utf-8") as f: + return f.read() + except Exception as e: + return f"# Error reading script: {e}\n" + def _save_script(self): - # 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() + try: + content = self.text_widget.get("1.0", "end-1c") if self.text_widget else "" + with open(self._script_path, "w", encoding="utf-8") as f: + f.write(content) + self._original_content = content + self._dirty = False + self._update_title() + self._show_save_status("Saved") + except Exception as e: + Messagebox.show_error(f"Failed to save: {e}", title="Save Error") + + def _reload_script(self): + if self._dirty: + result = str(Messagebox.yesno( + "Discard unsaved changes and reload?", + title="Reload Script" + )).lower() + if result != "yes": + return + content = self._get_script_content() + self._set_content(content) + self._original_content = content + self._dirty = False + self._update_title() + + def _new_script(self): + if self._dirty: + result = str(Messagebox.yesno( + "Discard unsaved changes to create a new script?", + title="New Script" + )).lower() + if result != "yes": + return + self.gui.draw_scripts() + self.gui.scripts_page._create_script() + + def _set_content(self, content): + if self.text_widget: + self.text_widget.delete("1.0", "end") + self.text_widget.insert("1.0", content) + self._refresh_line_numbers() + + def _on_change(self, event=None): + if not self.text_widget: + return + try: + self.text_widget.edit_modified(False) + except Exception: + pass + content = self.text_widget.get("1.0", "end-1c") + self._dirty = content != self._original_content + self._update_title() + self.root.after(1, self._refresh_line_numbers) + + def _refresh_line_numbers(self): + if not self.line_numbers or not self.text_widget: + return + try: + self.line_numbers.config(state="normal") + self.line_numbers.delete("1.0", "end") + count = self.text_widget.index("end-1c").split(".")[0] + self.line_numbers.insert("1.0", "\n".join(str(i) for i in range(1, int(count) + 1))) + self.line_numbers.config(state="disabled") + except Exception: + pass + + def _sync_scroll(self, *args): + if self.line_numbers: + self.line_numbers.yview_moveto(args[0]) + + def _update_title(self): + if self._title_label: + self._title_label.configure(text=f"{self.script} {'*' if self._dirty else ''}") + + def _show_save_status(self, text): + if self._status_label: + self._status_label.configure(text=text) + self.root.after(2000, lambda: self._status_label.configure(text="") if self._status_label.winfo_exists() else None) def _draw_header(self, parent): wrapper = ttk.Frame(parent) - - back_button = ttk.Label(wrapper, image=self.images.get("left-chevron")) - back_button.grid(row=0, column=0, sticky=ttk.W, padx=(0, 10)) - back_button.bind("", lambda e: self._go_back()) - - script_name = ttk.Label(wrapper, text=self.script, font=("Host Grotesk", 16, "bold")) - script_name.grid(row=0, column=1, sticky=ttk.W) - + + back = ttk.Label(wrapper, image=self.images.get("left-chevron"), cursor="hand2") + back.grid(row=0, column=0, sticky=ttk.W, padx=(0, 10)) + back.bind("", lambda e: self._go_back()) + + self._title_label = ttk.Label(wrapper, text=self.script, font=("Host Grotesk", 16, "bold")) + self._title_label.grid(row=0, column=1, sticky=ttk.W) + + self._status_label = ttk.Label(wrapper, text="", font=("Host Grotesk", 11), foreground="#4fee4c") + self._status_label.grid(row=0, column=2, sticky=ttk.W, padx=(10, 0)) + + toolbar = ttk.Frame(wrapper) + for i, (label, cmd) in enumerate([("Save", self._save_script), ("Reload", self._reload_script), ("New", self._new_script)]): + btn = ttk.Label(toolbar, text=label, font=("Host Grotesk", 11), style="secondary.TLabel", cursor="hand2") + btn.configure(foreground=Style.LIGHT_GREY.value) + btn.bind("", lambda e, c=cmd: c()) + btn.bind("", lambda e, b=btn: b.configure(foreground="white")) + btn.bind("", lambda e, b=btn: b.configure(foreground=Style.LIGHT_GREY.value)) + btn.grid(row=0, column=i, padx=(0, 8) if i < 2 else 0) + toolbar.grid(row=0, column=3, sticky=ttk.E, padx=(15, 0)) + wrapper.columnconfigure(2, weight=1) return wrapper 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) - - 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 + header.pack(fill=ttk.X, pady=(0, 10)) + + content = self._get_script_content() + bg = self.root.style.colors.get("dark") + fg = Style.LIGHT_GREY.value + font_size = 10 if sys.platform != "darwin" else 12 + mono = ("JetBrainsMono NF", font_size) + + gutter_bg = "#181820" + shared_opts = dict( + font=mono, bd=0, highlightthickness=0, wrap="none", + spacing1=0, spacing2=0, spacing3=0, padx=0, pady=0, + ) + + self.line_numbers = tkinter.Text( + parent, width=4, state="disabled", cursor="arrow", + bg=gutter_bg, fg="#4a4a5e", + selectbackground=gutter_bg, takefocus=False, + **shared_opts, ) - self.editor.pack(fill=ttk.BOTH, expand=True) - content = self.editor.content - content.insert("end", self._get_script_content()) \ No newline at end of file + self.line_numbers.pack(side="left", fill="y", pady=5) + self.line_numbers.config(padx=8) + + sep = ttk.Frame(parent, width=1) + sep.pack(side="left", fill="y", pady=5) + + self.text_widget = tkinter.Text( + parent, undo=True, autoseparators=True, maxundo=-1, + bg=bg, fg=fg, insertbackground="white", + selectbackground="#3d3d5c", + **shared_opts, + ) + self.text_widget.pack(side="left", fill="both", expand=True, padx=(0, 5), pady=5) + self.text_widget.config(padx=8) + + v_scroll = ttk.Scrollbar(parent, orient="vertical", + command=lambda *a: (self.text_widget.yview(*a), self.line_numbers.yview(*a))) + v_scroll.pack(side="right", fill="y", pady=5) + self.text_widget.config(yscrollcommand=lambda *a: (v_scroll.set(*a), self.line_numbers.yview_moveto(a[0]))) + + self.text_widget.insert("1.0", content) + self.text_widget.mark_set("insert", "1.0") + self.text_widget.edit_reset() + self.text_widget.edit_modified(False) + self._original_content = content + self._refresh_line_numbers() + + self.text_widget.bind("<>", self._on_change) + self.text_widget.bind("", lambda e: self.root.after(50, self._on_change)) + self.text_widget.bind("", lambda e: self.root.after(1, self._refresh_line_numbers)) + self.text_widget.bind("", lambda e: self._save_script()) + self.text_widget.bind("", lambda e: self._reload_script()) + + parent.bind("", lambda e: self.text_widget.focus_set()) + self.text_widget.focus_set() diff --git a/gui/pages/scripts.py b/gui/pages/scripts.py index dbc59a6..5109338 100644 --- a/gui/pages/scripts.py +++ b/gui/pages/scripts.py @@ -6,11 +6,7 @@ from ttkbootstrap.dialogs import Messagebox from gui.components import RoundedFrame from gui.helpers.style import Style, get_current_theme_str - -# 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 gui.pages.script import ScriptPage from utils.config import Config from utils.files import open_path_in_explorer, get_application_support @@ -30,34 +26,11 @@ def _is_current_scripts_page(self, token): return token == self._scripts_page_token and hasattr(self, "scripts_wrapper") and self.scripts_wrapper.winfo_exists() 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: - subprocess.run(["code", get_application_support() + f"/scripts/{script}"], creationflags=subprocess.CREATE_NO_WINDOW) - except: - subprocess.run(["open", "-a", "TextEdit", get_application_support() + f"/scripts/{script}"], creationflags=subprocess.CREATE_NO_WINDOW) - elif sys.platform == "win32": - try: - subprocess.run(["code", get_application_support() + f"\\scripts\\{script}"], creationflags=subprocess.CREATE_NO_WINDOW) - except: - subprocess.run(["notepad", get_application_support() + f"\\scripts\\{script}"], creationflags=subprocess.CREATE_NO_WINDOW) - else: - try: - subprocess.run(["subl", get_application_support() + f"/scripts/{script}"], creationflags=subprocess.CREATE_NO_WINDOW) - except: - subprocess.run(["gedit", get_application_support() + f"/scripts/{script}"], creationflags=subprocess.CREATE_NO_WINDOW) + 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) def _new_scripts_listener(self, token): if not self._is_current_scripts_page(token): diff --git a/gui/pages/settings.py b/gui/pages/settings.py index 056ef56..b6b0384 100644 --- a/gui/pages/settings.py +++ b/gui/pages/settings.py @@ -31,7 +31,12 @@ def __init__(self, root, bot_controller, draw_settings): def refresh_config(self): if not self.parent: return - + try: + if not self.parent.winfo_exists(): + return + except Exception: + return + try: for widget in self.parent.winfo_children(): widget.destroy() @@ -40,8 +45,8 @@ def refresh_config(self): self.draw(self.parent) if self.parent: self.root.after_idle(lambda: self.root.focus_force()) - except Exception as e: - print(f"Error refreshing config: {e}") + except Exception: + pass def _create_sections(self, wrapper): general_wrapper = ttk.Frame(wrapper) diff --git a/gui/pages/tools/__init__.py b/gui/pages/tools/__init__.py index 94c44f9..ec10e20 100644 --- a/gui/pages/tools/__init__.py +++ b/gui/pages/tools/__init__.py @@ -3,4 +3,6 @@ from .message_logger_page import MessageLoggerPage from .user_lookup_page import UserLookupPage from .password_gen import PasswordGenPage -from .auto_afk_reply_page import AutoAFKReplyPage \ No newline at end of file +from .backups_page import BackupsPage +from .multi_token_page import MultiTokenPage +from .auto_afk_reply_page import AutoAFKReplyPage diff --git a/gui/pages/tools/backups_page.py b/gui/pages/tools/backups_page.py new file mode 100644 index 0000000..014bfd0 --- /dev/null +++ b/gui/pages/tools/backups_page.py @@ -0,0 +1,438 @@ +import os +import json +import time +import asyncio +import threading +import discord +import ttkbootstrap as ttk +from ttkbootstrap.dialogs import Messagebox +from ttkbootstrap.scrolled import ScrolledFrame + +from gui.components import ToolPage, RoundedFrame +from gui.helpers import Style +from utils.files import get_application_support + + +class BackupsPage(ToolPage): + def __init__(self, toolspage, root, bot_controller, images, layout): + super().__init__(toolspage, root, bot_controller, images, layout, title="Backups", frame=None) + self.images = images + self.cfg = bot_controller.cfg + self.backups_dir = os.path.join(get_application_support(), "backups") + self.backup_list_frame = None + self.status_label = None + + def _ensure_backups_dir(self): + os.makedirs(self.backups_dir, exist_ok=True) + + def _get_backups(self): + self._ensure_backups_dir() + backups = [] + for f in os.listdir(self.backups_dir): + if f.endswith(".json"): + path = os.path.join(self.backups_dir, f) + try: + with open(path, "r", encoding="utf-8") as fh: + data = json.load(fh) + stat = os.stat(path) + backups.append({ + "name": f, + "path": path, + "type": data.get("type", f.replace(".json", "")), + "created_at": data.get("created_at"), + "size": stat.st_size, + "time": stat.st_mtime, + "data": data, + }) + except Exception: + pass + backups.sort(key=lambda b: b["time"], reverse=True) + return backups + + def _format_time(self, timestamp): + if timestamp: + return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + return "Unknown" + + def _set_status(self, text): + if self.status_label: + self.status_label.configure(text=text) + + def _run_async(self, coro): + def _worker(): + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(coro) + except Exception as e: + self.root.after(0, lambda: self._set_status(f"Error: {e}")) + finally: + loop.close() + threading.Thread(target=_worker, daemon=True).start() + + def _get_available_bots(self): + bots = [] + main_bot = getattr(self.bot_controller, "bot", None) + if main_bot and main_bot.is_ready(): + bots.append({ + "label": f"Main — {main_bot.user.name}", + "bot": main_bot, + "token": self.cfg.get("token"), + }) + if hasattr(self, 'toolspage') and hasattr(self.toolspage, 'multi_token_page'): + for session in self.toolspage.multi_token_page.sessions: + if session.running and session.bot and session.bot.is_ready(): + bots.append({ + "label": f"Multi — {session.bot.user.name}", + "bot": session.bot, + "token": session.token, + }) + return bots + + def _pick_account(self, backup_type, callback): + bots = self._get_available_bots() + if not bots: + self._set_status("No bots connected. Start the bot first.") + return + if len(bots) == 1: + callback(bots[0]["bot"], bots[0]["token"]) + return + + dialog = ttk.Toplevel(self.root) + dialog.title(f"Backup {backup_type.title()}") + dialog.geometry("350x250") + dialog.resizable(False, False) + dialog.configure(background=self.root.style.colors.get("dark")) + dialog.transient(self.root) + + wrapper = ttk.Frame(dialog, style="dark.TFrame") + wrapper.pack(fill=ttk.BOTH, expand=True, padx=15, pady=15) + + title = ttk.Label(wrapper, text=f"Select account to backup {backup_type}:", + font=("Host Grotesk", 11, "bold")) + title.configure(background=self.root.style.colors.get("dark"), foreground="white") + title.pack(anchor=ttk.W, pady=(0, 10)) + + for bot_info in bots: + card = RoundedFrame(wrapper, radius=(8, 8, 8, 8), bootstyle="dark.TFrame") + card.pack(fill=ttk.X, pady=(0, 5)) + + lbl = ttk.Label(card, text=bot_info["label"], + font=("Host Grotesk", 11), cursor="hand2", + foreground="#5865f2") + lbl.configure(background=self.root.style.colors.get("dark")) + lbl.pack(padx=12, pady=8, anchor=ttk.W) + lbl.bind("", lambda e, b=bot_info["bot"], t=bot_info["token"], d=dialog: ( + d.destroy(), callback(b, t))) + lbl.bind("", lambda e, l=lbl: l.configure(foreground="white")) + lbl.bind("", lambda e, l=lbl: l.configure(foreground="#5865f2")) + + cancel = ttk.Label(wrapper, text="Cancel", font=("Host Grotesk", 10), + cursor="hand2", foreground=Style.LIGHT_GREY.value) + cancel.configure(background=self.root.style.colors.get("dark")) + cancel.pack(anchor=ttk.W, pady=(5, 0)) + cancel.bind("", lambda e: dialog.destroy()) + cancel.bind("", lambda e: cancel.configure(foreground="white")) + cancel.bind("", lambda e: cancel.configure(foreground=Style.LIGHT_GREY.value)) + + def _create_backup_account(self): + self._pick_account("account", self._do_backup_account) + + def _do_backup_account(self, bot, token): + self._set_status("Backing up account...") + async def _do(): + try: + user = bot.user + data = { + "created_at": time.time(), + "type": "account", + "info": { + "id": user.id, + "name": user.name, + "display_name": user.display_name, + "accent_colour": str(user.accent_colour) if user.accent_colour else None, + "avatar": str(user.avatar.url) if user.avatar else None, + "banner": str(user.banner.url) if user.banner else None, + "bio": user.bio or None, + }, + } + self._ensure_backups_dir() + name = user.name.lower().replace(" ", "_") + path = os.path.join(self.backups_dir, f"account_{name}.json") + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + self.root.after(0, lambda: self._set_status(f"Account backed up ({user.name})")) + self.root.after(0, self._refresh_backup_list) + except Exception as e: + self.root.after(0, lambda: self._set_status(f"Failed: {e}")) + self._run_async(_do()) + + def _create_backup_friends(self): + self._pick_account("friends", self._do_backup_friends) + + def _do_backup_friends(self, bot, token): + self._set_status("Backing up friends...") + async def _do(): + try: + friends = [] + for relationship in bot.friends: + if relationship.type == 1: + friends.append({"username": relationship.user.name, "id": relationship.user.id}) + data = { + "created_at": time.time(), + "type": "friends", + "list": friends, + } + self._ensure_backups_dir() + name = bot.user.name.lower().replace(" ", "_") + path = os.path.join(self.backups_dir, f"friends_{name}.json") + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + self.root.after(0, lambda: self._set_status(f"Friends backed up ({len(friends)} friends)")) + self.root.after(0, self._refresh_backup_list) + except Exception as e: + self.root.after(0, lambda: self._set_status(f"Failed: {e}")) + self._run_async(_do()) + + def _create_backup_guilds(self): + self._pick_account("guilds", self._do_backup_guilds) + + def _do_backup_guilds(self, bot, token): + self._set_status("Backing up guilds...") + async def _do(): + try: + guilds = [] + for guild in bot.guilds: + invite = None + try: + invite = guild.vanity_url + except Exception: + pass + if not invite: + for channel in guild.channels: + if isinstance(channel, discord.TextChannel) and channel.permissions_for(guild.me).create_instant_invite: + try: + invite = await channel.create_invite(max_age=0, max_uses=1, unique=True) + except Exception: + break + if invite: + break + guilds.append({"name": guild.name, "id": guild.id, "invite": str(invite) if invite else "None"}) + await asyncio.sleep(0.75) + data = { + "created_at": time.time(), + "type": "guilds", + "list": guilds, + } + self._ensure_backups_dir() + name = bot.user.name.lower().replace(" ", "_") + path = os.path.join(self.backups_dir, f"guilds_{name}.json") + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + self.root.after(0, lambda: self._set_status(f"Guilds backed up ({len(guilds)} servers)")) + self.root.after(0, self._refresh_backup_list) + except Exception as e: + self.root.after(0, lambda: self._set_status(f"Failed: {e}")) + self._run_async(_do()) + + def _view_backup(self, backup_info): + data = backup_info["data"] + display = json.dumps(data, indent=2) + win = ttk.Toplevel(self.root) + win.title(f"Backup - {backup_info['name']}") + win.geometry("500x400") + win.configure(bg=self.root.style.colors.get("bg")) + text = ttk.ScrolledText(win, wrap=ttk.WORD, font=("Consolas", 10)) + text.pack(fill=ttk.BOTH, expand=True, padx=10, pady=10) + text.insert("1.0", display) + text.configure(state="disabled") + + def _restore_backup(self, backup_info): + btype = backup_info["type"] + if btype == "account": + Messagebox.show_info("Account backups are for reference only.\nNo restore action needed.", title="Restore") + return + if btype == "friends": + result = str(Messagebox.yesno( + "This will send friend requests to all users in the backup.\n\n" + "This could result in a ban. Continue?", + title="Restore Friends" + )).lower() + if result != "yes": + return + self._pick_account("friends", lambda bot, token: self._do_restore_friends(backup_info, bot, token)) + return + elif btype == "guilds": + invites = [g["invite"] for g in backup_info["data"].get("list", []) if g.get("invite") and g["invite"] != "None"] + if not invites: + Messagebox.show_info("No invite links found in this backup.", title="Restore Guilds") + return + msg = "Join these servers manually:\n\n" + "\n".join(invites) + win = ttk.Toplevel(self.root) + win.title("Guild Invites") + win.geometry("500x350") + win.configure(bg=self.root.style.colors.get("bg")) + text = ttk.ScrolledText(win, wrap=ttk.WORD, font=("Consolas", 10)) + text.pack(fill=ttk.BOTH, expand=True, padx=10, pady=10) + text.insert("1.0", msg) + text.configure(state="disabled") + + def _do_restore_friends(self, backup_info, bot, token): + self._set_status("Restoring friends...") + async def _do(): + try: + import requests as _req + headers = { + "Authorization": token, + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + } + restored = 0 + for entry in backup_info["data"].get("list", []): + try: + user_resp = _req.get(f"https://discord.com/api/v9/users/{entry['id']}", headers=headers) + if user_resp.status_code == 200: + user = discord.User(state=bot._connection, data=user_resp.json()) + user.send_friend_request() + restored += 1 + await asyncio.sleep(1) + except Exception: + pass + self.root.after(0, lambda: self._set_status(f"Restored {restored} friend requests")) + except Exception as e: + self.root.after(0, lambda: self._set_status(f"Failed: {e}")) + self._run_async(_do()) + + def _delete_backup(self, backup_info): + result = str(Messagebox.yesno( + f"Delete backup '{backup_info['name']}'?", + title="Delete Backup" + )).lower() + if result != "yes": + return + try: + os.remove(backup_info["path"]) + self._set_status(f"Deleted: {backup_info['name']}") + self._refresh_backup_list() + except Exception as e: + self._set_status(f"Delete failed: {e}") + + def _refresh_backup_list(self): + if not self.backup_list_frame: + return + for child in self.backup_list_frame.winfo_children(): + child.destroy() + self._draw_backup_entries() + + def _draw_backup_entries(self): + backups = self._get_backups() + if not backups: + empty = ttk.Label(self.backup_list_frame, + text="No backups yet. Create one above.", + font=("Host Grotesk", 11)) + empty.configure(background=self.root.style.colors.get("dark"), + foreground=Style.DARK_GREY.value) + empty.pack(pady=20) + return + + type_colors = {"account": "#5865f2", "friends": "#4fee4c", "guilds": "#fee75c"} + for backup in backups: + card = RoundedFrame(self.backup_list_frame, radius=(8, 8, 8, 8), + bootstyle="dark.TFrame") + card.pack(fill=ttk.X, pady=(0, 5)) + + inner = ttk.Frame(card, style="dark.TFrame") + inner.pack(fill=ttk.BOTH, padx=12, pady=8) + + name_frame = ttk.Frame(inner, style="dark.TFrame") + name_frame.grid(row=0, column=0, sticky=ttk.W) + + btype = backup.get("type", "unknown") + color = type_colors.get(btype, Style.LIGHT_GREY.value) + type_label = ttk.Label(name_frame, text=btype.upper(), + font=("Host Grotesk", 9, "bold"), + foreground=color) + type_label.configure(background=self.root.style.colors.get("dark")) + type_label.pack(side=ttk.LEFT, padx=(0, 8)) + + name_label = ttk.Label(name_frame, text=backup["name"], + font=("Host Grotesk", 11, "bold")) + name_label.configure(background=self.root.style.colors.get("dark")) + name_label.pack(side=ttk.LEFT) + + info_text = self._format_time(backup["created_at"]) + info_label = ttk.Label(inner, text=info_text, + font=("Host Grotesk", 9)) + info_label.configure(background=self.root.style.colors.get("dark"), + foreground=Style.LIGHT_GREY.value) + info_label.grid(row=1, column=0, sticky=ttk.W) + + btn_frame = ttk.Frame(inner, style="dark.TFrame") + btn_frame.grid(row=0, column=1, rowspan=2, sticky=ttk.E) + + view_btn = ttk.Label(btn_frame, text="View", + font=("Host Grotesk", 10), + cursor="hand2", foreground="#5865f2") + view_btn.pack(side=ttk.LEFT, padx=(0, 10)) + view_btn.bind("", lambda e, b=backup: self._view_backup(b)) + view_btn.bind("", lambda e, b=view_btn: b.configure(foreground="white")) + view_btn.bind("", lambda e, b=view_btn: b.configure(foreground="#5865f2")) + + restore_btn = ttk.Label(btn_frame, text="Restore", + font=("Host Grotesk", 10, "bold"), + cursor="hand2", foreground="#4fee4c") + restore_btn.pack(side=ttk.LEFT, padx=(0, 10)) + restore_btn.bind("", lambda e, b=backup: self._restore_backup(b)) + restore_btn.bind("", lambda e, b=restore_btn: b.configure(foreground="white")) + restore_btn.bind("", lambda e, b=restore_btn: b.configure(foreground="#4fee4c")) + + delete_btn = ttk.Label(btn_frame, text="Delete", + font=("Host Grotesk", 10), + cursor="hand2", foreground="#ff6464") + delete_btn.pack(side=ttk.LEFT) + delete_btn.bind("", lambda e, b=backup: self._delete_backup(b)) + delete_btn.bind("", lambda e, b=delete_btn: b.configure(foreground="white")) + delete_btn.bind("", lambda e, b=delete_btn: b.configure(foreground="#ff6464")) + + inner.grid_columnconfigure(0, weight=1) + + def draw_content(self, wrapper): + create_label = ttk.Label(wrapper, text="Create Backup", + font=("Host Grotesk", 14, "bold")) + create_label.configure(background=self.root.style.colors.get("dark")) + create_label.pack(anchor=ttk.W, pady=(0, 5)) + + btn_row = ttk.Frame(wrapper, style="dark.TFrame") + btn_row.pack(fill=ttk.X, pady=(0, 10)) + + for text, cmd in [("Account", self._create_backup_account), + ("Friends", self._create_backup_friends), + ("Guilds", self._create_backup_guilds)]: + btn_frame = RoundedFrame(btn_row, radius=(8, 8, 8, 8), + bootstyle="dark.TFrame") + btn_frame.pack(side=ttk.LEFT, padx=(0, 8)) + + btn = ttk.Label(btn_frame, text=text, + font=("Host Grotesk", 10, "bold"), + cursor="hand2", foreground="#5865f2") + btn.configure(background=self.root.style.colors.get("dark")) + btn.pack(padx=12, pady=6) + btn.bind("", lambda e, c=cmd: c()) + btn.bind("", lambda e, b=btn: b.configure(foreground="white")) + btn.bind("", lambda e, b=btn: b.configure(foreground="#5865f2")) + + self.status_label = ttk.Label(btn_row, text="", + font=("Host Grotesk", 10)) + self.status_label.configure(background=self.root.style.colors.get("dark"), + foreground=Style.LIGHT_GREY.value) + self.status_label.pack(side=ttk.RIGHT) + + backups_label = ttk.Label(wrapper, text="Available Backups", + font=("Host Grotesk", 14, "bold")) + backups_label.configure(background=self.root.style.colors.get("dark")) + backups_label.pack(anchor=ttk.W, pady=(5, 5)) + + self.backup_list_frame = ScrolledFrame(wrapper, bootstyle="dark.TFrame", autohide=True) + self.backup_list_frame.container.configure(style="dark.TFrame") + self.backup_list_frame.pack(fill=ttk.BOTH, expand=True) + + self._draw_backup_entries() diff --git a/gui/pages/tools/multi_token_page.py b/gui/pages/tools/multi_token_page.py new file mode 100644 index 0000000..013a64a --- /dev/null +++ b/gui/pages/tools/multi_token_page.py @@ -0,0 +1,393 @@ +import os +import sys +import time +import asyncio +import threading +import requests +import ttkbootstrap as ttk +from ttkbootstrap.scrolled import ScrolledFrame +from ttkbootstrap.dialogs import Messagebox + +from gui.components import ToolPage, RoundedFrame +from gui.helpers import Style +from gui.helpers.images import Images +from utils.config import Config + + +class TokenInstance: + def __init__(self, token_data, on_status_change=None): + self.token = token_data.get("token", "") + self.name = token_data.get("name", "Unnamed") + self.bot = None + self.loop = None + self.thread = None + self.running = False + self.start_time = None + self.on_status_change = on_status_change + self._username = self.name + + def start(self): + if self.running or not self.token: + return + + try: + resp = requests.get( + "https://discord.com/api/v9/users/@me", + headers={"Authorization": self.token}, + timeout=10, + ) + if resp.status_code != 200: + return + self._username = resp.json().get("username", "Unknown") + except Exception: + return + + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + from bot.bot import Ghost + from bot.controller import BotController + + controller = BotController() + controller._is_secondary = True + controller.bot_running = False + controller.loop = self.loop + controller.running = True + self._controller = controller + + self.bot = Ghost(controller) + controller.bot = self.bot + + async def _on_ready(): + self.start_time = time.time() + self.running = True + self._username = self.bot.user.name if self.bot.user else self.name + if self.on_status_change: + self.on_status_change(self) + + async def _on_disconnect(): + self.running = False + if self.on_status_change: + self.on_status_change(self) + + self.bot.add_listener(_on_ready, 'on_ready') + self.bot.add_listener(_on_disconnect, 'on_disconnect') + + self.loop.create_task(self.bot.start(token=self.token, reconnect=True)) + self.thread = threading.Thread(target=self.loop.run_forever, daemon=True) + self.thread.start() + + def stop(self): + if not self.running or not self.bot or not self.loop: + return + + self.running = False + + async def shutdown(): + try: + if hasattr(self.bot, "telemetry_loop") and self.bot.telemetry_loop.is_running(): + self.bot.telemetry_loop.cancel() + self.bot._stop_rich_presence() + await self.bot.close() + except Exception: + pass + + asyncio.run_coroutine_threadsafe(shutdown(), self.loop) + if self.on_status_change: + self.on_status_change(self) + + def get_status(self): + if not self.running: + return "stopped" + try: + if self.bot and self.bot.is_ready(): + return "online" + except Exception: + pass + return "connecting" + + def get_username(self): + if self.bot and hasattr(self, "_username"): + return self._username + return self.name + + def get_uptime(self): + if not self.start_time or not self.running: + return "--" + elapsed = time.time() - self.start_time + hours = int(elapsed // 3600) + minutes = int((elapsed % 3600) // 60) + seconds = int(elapsed % 60) + if hours > 0: + return f"{hours}h {minutes}m" + return f"{minutes}m {seconds}s" + + +class MultiTokenPage(ToolPage): + def __init__(self, toolspage, root, bot_controller, images, layout): + super().__init__(toolspage, root, bot_controller, images, layout, title="Multi-Token", frame=None) + self.images = images + self.cfg = Config() + self.sessions = [] + self.token_frames = [] + self.list_frame = None + + def _get_tokens(self): + tokens = self.cfg.get("multi_tokens") + return tokens if tokens else [] + + def _save_tokens(self, tokens): + self.cfg.set("multi_tokens", tokens) + self.cfg.save(notify=False) + + def _add_token(self): + dialog = ttk.Toplevel(self.root) + dialog.title("Add Token") + dialog.geometry("450x200") + dialog.resizable(False, False) + dialog.configure(background=self.root.style.colors.get("dark")) + dialog.transient(self.root) + + wrapper = ttk.Frame(dialog, style="dark.TFrame") + wrapper.pack(fill=ttk.BOTH, expand=True, padx=20, pady=20) + + name_label = ttk.Label(wrapper, text="Name", font=("Host Grotesk", 11)) + name_label.configure(background=self.root.style.colors.get("dark"), + foreground=Style.LIGHT_GREY.value) + name_label.pack(anchor=ttk.W) + + name_var = ttk.StringVar(value="Account 1") + name_entry = ttk.Entry(wrapper, textvariable=name_var, font=("Host Grotesk", 11)) + name_entry.pack(fill=ttk.X, pady=(0, 10)) + + token_label = ttk.Label(wrapper, text="Token", font=("Host Grotesk", 11)) + token_label.configure(background=self.root.style.colors.get("dark"), + foreground=Style.LIGHT_GREY.value) + token_label.pack(anchor=ttk.W) + + token_var = ttk.StringVar() + token_entry = ttk.Entry(wrapper, textvariable=token_var, font=("Host Grotesk", 11), show="*") + token_entry.pack(fill=ttk.X, pady=(0, 10)) + + btn_frame = ttk.Frame(wrapper, style="dark.TFrame") + btn_frame.pack(fill=ttk.X) + + def confirm(): + name = name_var.get().strip() + token = token_var.get().strip() + if not token: + Messagebox.show_error("Token cannot be empty.", title="Error") + return + if not name: + name = "Account" + + tokens = self._get_tokens() + tokens.append({"name": name, "token": token, "enabled": False}) + self._save_tokens(tokens) + dialog.destroy() + self._refresh_list() + + cancel_btn = ttk.Label(btn_frame, text="Cancel", font=("Host Grotesk", 11), + cursor="hand2") + cancel_btn.configure(foreground=Style.LIGHT_GREY.value, + background=self.root.style.colors.get("dark")) + cancel_btn.pack(side=ttk.LEFT) + cancel_btn.bind("", lambda e: dialog.destroy()) + cancel_btn.bind("", lambda e: cancel_btn.configure(foreground="white")) + cancel_btn.bind("", lambda e: cancel_btn.configure(foreground=Style.LIGHT_GREY.value)) + + add_btn = ttk.Label(btn_frame, text="Add", font=("Host Grotesk", 11, "bold"), + cursor="hand2") + add_btn.configure(foreground="#4fee4c", + background=self.root.style.colors.get("dark")) + add_btn.pack(side=ttk.RIGHT) + add_btn.bind("", lambda e: confirm()) + add_btn.bind("", lambda e: add_btn.configure(foreground="white")) + add_btn.bind("", lambda e: add_btn.configure(foreground="#4fee4c")) + token_entry.bind("", lambda e: confirm()) + + dialog.grab_set() + name_entry.focus_set() + + def _toggle_token(self, index): + tokens = self._get_tokens() + if index >= len(tokens): + return + + token_data = tokens[index] + session = self._find_session(token_data["token"]) + + if session and session.running: + session.stop() + token_data["enabled"] = False + else: + if not session: + session = TokenInstance(token_data, on_status_change=lambda s: self.root.after(0, self._refresh_list)) + self.sessions.append(session) + session.start() + token_data["enabled"] = True + + self._save_tokens(tokens) + self.root.after(500, self._refresh_list) + + def _toggle_start_on_launch(self, index): + tokens = self._get_tokens() + if index >= len(tokens): + return + tokens[index]["start_on_launch"] = not tokens[index].get("start_on_launch", False) + self._save_tokens(tokens) + self._refresh_list() + + def auto_start_tokens(self): + tokens = self._get_tokens() + for token_data in tokens: + token_data["enabled"] = False + self._save_tokens(tokens) + for token_data in tokens: + if token_data.get("start_on_launch", False): + session = TokenInstance(token_data, on_status_change=lambda s: self.root.after(0, self._refresh_list)) + self.sessions.append(session) + session.start() + token_data["enabled"] = True + self._save_tokens(tokens) + + def _remove_token(self, index): + tokens = self._get_tokens() + if index >= len(tokens): + return + + token_data = tokens[index] + session = self._find_session(token_data["token"]) + if session: + session.stop() + self.sessions = [s for s in self.sessions if s.token != token_data["token"]] + + tokens.pop(index) + self._save_tokens(tokens) + self._refresh_list() + + def _find_session(self, token): + for s in self.sessions: + if s.token == token: + return s + return None + + def _refresh_list(self): + if not self.list_frame or not self.list_frame.winfo_exists(): + return + + for child in self.list_frame.winfo_children(): + child.destroy() + + tokens = self._get_tokens() + + if not tokens: + empty = ttk.Label(self.list_frame, text="No tokens added yet.\nClick + Add Token to get started.", + font=("Host Grotesk", 11), justify=ttk.CENTER) + empty.configure(background=self.root.style.colors.get("dark"), + foreground=Style.DARK_GREY.value) + empty.pack(pady=40) + return + + for i, token_data in enumerate(tokens): + self._draw_token_card(self.list_frame, token_data, i) + + def _draw_token_card(self, parent, token_data, index): + session = self._find_session(token_data["token"]) + status = session.get_status() if session else "stopped" + username = session.get_username() if session else token_data.get("name", "Unknown") + uptime = session.get_uptime() if session else "--" + start_on_launch = token_data.get("start_on_launch", False) + + card = RoundedFrame(parent, radius=(10, 10, 10, 10), bootstyle="dark.TFrame") + card.pack(fill=ttk.X, pady=(0, 5)) + + inner = ttk.Frame(card, style="dark.TFrame") + inner.pack(fill=ttk.BOTH, padx=12, pady=10) + + status_colors = {"online": "#4fee4c", "connecting": "#eceb18", "stopped": "#ff6464"} + status_color = status_colors.get(status, Style.DARK_GREY.value) + + dot = ttk.Canvas(inner, width=8, height=8, highlightthickness=0, + background=self.root.style.colors.get("dark")) + dot.grid(row=0, column=0, rowspan=2, sticky=ttk.N, pady=(4, 0)) + dot.create_oval(0, 0, 8, 8, fill=status_color, outline=status_color) + + name_label = ttk.Label(inner, text=username, font=("Host Grotesk", 12, "bold")) + name_label.configure(background=self.root.style.colors.get("dark")) + name_label.grid(row=0, column=1, sticky=ttk.W, padx=(8, 0)) + + detail_text = f"{status.title()}" + if status == "online": + detail_text += f" | Uptime: {uptime}" + detail_label = ttk.Label(inner, text=detail_text, font=("Host Grotesk", 9)) + detail_label.configure(background=self.root.style.colors.get("dark"), + foreground=Style.LIGHT_GREY.value) + detail_label.grid(row=1, column=1, sticky=ttk.W, padx=(8, 0)) + + btn_frame = ttk.Frame(inner, style="dark.TFrame") + btn_frame.grid(row=0, column=2, rowspan=2, sticky=ttk.E) + + launch_text = "Auto" if start_on_launch else "Manual" + launch_color = "#5865f2" if start_on_launch else Style.DARK_GREY.value + launch_btn = ttk.Label(btn_frame, text=launch_text, font=("Host Grotesk", 9), + cursor="hand2") + launch_btn.configure(foreground=launch_color, background=self.root.style.colors.get("dark")) + launch_btn.pack(side=ttk.LEFT, padx=(0, 10)) + launch_btn.bind("", lambda e, i=index: self._toggle_start_on_launch(i)) + launch_btn.bind("", lambda e: launch_btn.configure(foreground="white")) + launch_btn.bind("", lambda e, c=launch_color: launch_btn.configure(foreground=c)) + + toggle_text = "Stop" if status in ("online", "connecting") else "Start" + toggle_color = "#ff6464" if status in ("online", "connecting") else "#4fee4c" + + toggle_btn = ttk.Label(btn_frame, text=toggle_text, font=("Host Grotesk", 10, "bold"), + cursor="hand2") + toggle_btn.configure(foreground=toggle_color, background=self.root.style.colors.get("dark")) + toggle_btn.pack(side=ttk.LEFT, padx=(0, 10)) + toggle_btn.bind("", lambda e, i=index: self._toggle_token(i)) + toggle_btn.bind("", lambda e: toggle_btn.configure(foreground="white")) + toggle_btn.bind("", lambda e: toggle_btn.configure(foreground=toggle_color)) + + remove_btn = ttk.Label(btn_frame, text="Remove", font=("Host Grotesk", 10), + cursor="hand2") + remove_btn.configure(foreground="#ff6464", background=self.root.style.colors.get("dark")) + remove_btn.pack(side=ttk.LEFT) + remove_btn.bind("", lambda e, i=index: self._remove_token(i)) + remove_btn.bind("", lambda e: remove_btn.configure(foreground="white")) + remove_btn.bind("", lambda e: remove_btn.configure(foreground="#ff6464")) + + inner.grid_columnconfigure(1, weight=1) + + def draw_content(self, wrapper): + add_btn_frame = RoundedFrame(wrapper, radius=(10, 10, 10, 10), + bootstyle="primary.TFrame") + add_btn_frame.pack(fill=ttk.X, pady=(0, 10)) + + add_inner = ttk.Frame(add_btn_frame, style="primary.TFrame") + add_inner.pack(fill=ttk.BOTH, padx=12, pady=8) + + add_label = ttk.Label(add_inner, text="+ Add Token", + font=("Host Grotesk", 12, "bold"), + cursor="hand2") + add_label.configure(background=self.root.style.colors.get("primary")) + add_label.pack(side=ttk.LEFT) + add_label.bind("", lambda e: self._add_token()) + add_btn_frame.bind("", lambda e: self._add_token()) + + self.status_label = ttk.Label(add_inner, text="", + font=("Host Grotesk", 10)) + self.status_label.configure(background=self.root.style.colors.get("primary"), + foreground="#ffffff") + self.status_label.pack(side=ttk.RIGHT) + + tokens_label = ttk.Label(wrapper, text="Active Tokens", + font=("Host Grotesk", 14, "bold")) + tokens_label.configure(background=self.root.style.colors.get("dark")) + tokens_label.pack(anchor=ttk.W, pady=(5, 5)) + + self.list_frame = ScrolledFrame(wrapper, bootstyle="dark.TFrame", autohide=True) + self.list_frame.container.configure(style="dark.TFrame") + self.list_frame.pack(fill=ttk.BOTH, expand=True) + + self._refresh_list() diff --git a/gui/pages/tools/notification_logger_page.py b/gui/pages/tools/notification_logger_page.py new file mode 100644 index 0000000..283e8f4 --- /dev/null +++ b/gui/pages/tools/notification_logger_page.py @@ -0,0 +1,120 @@ +import time +import ttkbootstrap as ttk +from ttkbootstrap.scrolled import ScrolledFrame + +from gui.components import ToolPage, RoundedFrame +from gui.helpers import Style + + +class NotificationLoggerPage(ToolPage): + def __init__(self, toolspage, root, bot_controller, images, layout): + super().__init__(toolspage, root, bot_controller, images, layout, title="Notification Logger", frame=None) + self.history = [] + self.log_frame = None + + def add_entry(self, event_type, title, description="", timestamp=None, color=None): + ts = timestamp or time.strftime("%H:%M:%S") + self.history.append({ + "event_type": event_type, + "title": title, + "description": description, + "timestamp": ts, + "color": color or self._get_color(event_type), + }) + if self.log_frame and self.log_frame.winfo_exists(): + self.root.after(0, self._draw_last_entry) + + def _get_color(self, event_type): + colors = { + "friend_add": "#4fee4c", "friend_remove": "#ff6464", + "relationship_add": "#4fee4c", "relationship_remove": "#ff6464", + "relationship_update": "#eceb18", "guild_remove": "#ff6464", + "guild_join": "#4fee4c", "nitro_snipe": "#0b91ff", + "privnote_snipe": "#a07cff", "info": "#2aefef", + "warning": "#eceb18", "error": "#ff6464", "success": "#4fee4c", + } + return colors.get(event_type, Style.LIGHT_GREY.value) + + def _draw_last_entry(self): + if not self.log_frame or not self.log_frame.winfo_exists(): + return + if not self.history: + return + entry = self.history[-1] + self._draw_entry(self.log_frame, entry) + + def _draw_entry(self, parent, entry): + row = ttk.Frame(parent, style="dark.TFrame") + row.pack(fill=ttk.X, padx=4, pady=(0, 2)) + + accent = ttk.Canvas(row, width=3, height=3, highlightthickness=0, + background=self.root.style.colors.get("dark")) + accent.pack(side=ttk.LEFT, fill="y", padx=(0, 6)) + accent.create_rectangle(0, 0, 3, 500, fill=entry["color"], outline=entry["color"]) + + content = ttk.Frame(row, style="dark.TFrame") + content.pack(side=ttk.LEFT, fill=ttk.X, expand=True, pady=4) + + top = ttk.Frame(content, style="dark.TFrame") + top.pack(fill=ttk.X) + + type_lbl = ttk.Label(top, text=entry["event_type"].replace("_", " ").title(), + font=("Host Grotesk", 9, "bold"), foreground=entry["color"]) + type_lbl.configure(background=self.root.style.colors.get("dark")) + type_lbl.pack(side=ttk.LEFT) + + time_lbl = ttk.Label(top, text=entry["timestamp"], font=("Host Grotesk", 8), + foreground=Style.DARK_GREY.value) + time_lbl.configure(background=self.root.style.colors.get("dark")) + time_lbl.pack(side=ttk.RIGHT) + + title_lbl = ttk.Label(content, text=entry["title"], font=("Host Grotesk", 10, "bold")) + title_lbl.configure(background=self.root.style.colors.get("dark")) + title_lbl.pack(anchor=ttk.W) + + if entry["description"]: + desc_lbl = ttk.Label(content, text=entry["description"], font=("Host Grotesk", 9), + foreground=Style.LIGHT_GREY.value, wraplength=450) + desc_lbl.configure(background=self.root.style.colors.get("dark")) + desc_lbl.pack(anchor=ttk.W) + + def _clear_log(self): + self.history.clear() + if self.log_frame and self.log_frame.winfo_exists(): + for child in self.log_frame.winfo_children(): + child.destroy() + + def draw_content(self, wrapper): + header = ttk.Frame(wrapper, style="dark.TFrame") + header.pack(fill=ttk.X, pady=(0, 8)) + + title = ttk.Label(header, text="All Notifications", + font=("Host Grotesk", 14, "bold")) + title.configure(background=self.root.style.colors.get("dark")) + title.pack(side=ttk.LEFT) + + clear_btn = ttk.Label(header, text="Clear", font=("Host Grotesk", 10, "bold"), + cursor="hand2", foreground="#ff6464") + clear_btn.configure(background=self.root.style.colors.get("dark")) + clear_btn.pack(side=ttk.RIGHT) + clear_btn.bind("", lambda e: self._clear_log()) + clear_btn.bind("", lambda e: clear_btn.configure(foreground="white")) + clear_btn.bind("", lambda e: clear_btn.configure(foreground="#ff6464")) + + count_lbl = ttk.Label(header, text=f"{len(self.history)} entries", + font=("Host Grotesk", 9), foreground=Style.DARK_GREY.value) + count_lbl.configure(background=self.root.style.colors.get("dark")) + count_lbl.pack(side=ttk.RIGHT, padx=(0, 15)) + + self.log_frame = ScrolledFrame(wrapper, bootstyle="dark.TFrame", autohide=True) + self.log_frame.container.configure(style="dark.TFrame") + self.log_frame.pack(fill=ttk.BOTH, expand=True) + + for entry in self.history: + self._draw_entry(self.log_frame, entry) + + if not self.history: + empty = ttk.Label(self.log_frame, text="No notifications yet.", + font=("Host Grotesk", 11), foreground=Style.DARK_GREY.value) + empty.configure(background=self.root.style.colors.get("dark")) + empty.pack(pady=30) diff --git a/gui/pages/tools/surveillance_page.py b/gui/pages/tools/surveillance_page.py index 4151945..d4fa1e1 100644 --- a/gui/pages/tools/surveillance_page.py +++ b/gui/pages/tools/surveillance_page.py @@ -357,6 +357,8 @@ def _draw_header(self, parent): return header def update(self): + if self.textarea is None: + return try: self.textarea.delete("1.0", "end") @@ -367,14 +369,14 @@ def update(self): self.textarea.yview_moveto(1) except: - print("Surveillance console tried to update without being drawn.") - + pass + def clear(self): self.console = [] try: self.textarea.delete("1.0", "end") except: - print("Surveillance console tried to clear without being drawn.") + pass def add_log(self, prefix, text): time = get_formatted_time() diff --git a/gui/pages/tools/tools.py b/gui/pages/tools/tools.py index 3c8cb1e..782f4e9 100644 --- a/gui/pages/tools/tools.py +++ b/gui/pages/tools/tools.py @@ -6,6 +6,9 @@ from gui.pages.tools.message_logger_page import MessageLoggerPage from gui.pages.tools.user_lookup_page import UserLookupPage from gui.pages.tools.password_gen import PasswordGenPage +from gui.pages.tools.backups_page import BackupsPage +from gui.pages.tools.multi_token_page import MultiTokenPage +from gui.pages.tools.notification_logger_page import NotificationLoggerPage from gui.pages.tools.auto_afk_reply_page import AutoAFKReplyPage from gui.helpers.style import Style @@ -22,12 +25,15 @@ def __init__(self, root, bot_controller, images, layout, position_resize_grips): self.message_logger_page = MessageLoggerPage(self, root, bot_controller, images, layout) self.user_lookup_page = UserLookupPage(self, root, bot_controller, images, layout) self.password_gen_page = PasswordGenPage(self, root, bot_controller, images, layout) + self.backups_page = BackupsPage(self, root, bot_controller, images, layout) + self.multi_token_page = MultiTokenPage(self, root, bot_controller, images, layout) + self.notification_logger_page = NotificationLoggerPage(self, root, bot_controller, images, layout) self.auto_afk_reply_page = AutoAFKReplyPage(self, root, bot_controller, images, layout) self.pages = [ { "name": "Surveillance", - "description": "Search a user’s message history across mutual servers", + "description": "Search a user's message history across mutual servers", "page": self.surveillance_page, "command": self.draw_surveillance }, @@ -44,6 +50,23 @@ def __init__(self, root, bot_controller, images, layout, position_resize_grips): "command": self.draw_password_gen }, { + "name": "Backups", + "description": "Create and restore backups of your Discord account, friends, and servers", + "page": self.backups_page, + "command": self.draw_backups + }, + { + "name": "Multi-Token", + "description": "Run multiple selfbot accounts simultaneously", + "page": self.multi_token_page, + "command": self.draw_multi_token + }, + { + "name": "Notification Log", + "description": "View all past notifications and events", + "page": self.notification_logger_page, + "command": self.draw_notification_logger + }, "name": "Auto AFK Reply", "description": "Automatically reply to DMs when you're away from the keyboard", "page": self.auto_afk_reply_page, @@ -88,6 +111,30 @@ def draw_user_lookup(self): self.user_lookup_page.draw(main) self.layout.sidebar.set_button_command("tools", self.draw_user_lookup) self.position_resize_grips() + + def draw_backups(self): + self.layout.sidebar.set_current_page("tools") + self.layout.clear() + main = self.layout.main() + self.backups_page.draw(main) + self.layout.sidebar.set_button_command("tools", self.draw_backups) + self.position_resize_grips() + + def draw_multi_token(self): + self.layout.sidebar.set_current_page("tools") + self.layout.clear() + main = self.layout.main() + self.multi_token_page.draw(main) + self.layout.sidebar.set_button_command("tools", self.draw_multi_token) + self.position_resize_grips() + + def draw_notification_logger(self): + self.layout.sidebar.set_current_page("tools") + self.layout.clear() + main = self.layout.main() + self.notification_logger_page.draw(main) + self.layout.sidebar.set_button_command("tools", self.draw_notification_logger) + self.position_resize_grips() def draw_auto_afk_reply(self): self.layout.sidebar.set_current_page("tools") diff --git a/utils/defaults.py b/utils/defaults.py index b455170..335b5ca 100644 --- a/utils/defaults.py +++ b/utils/defaults.py @@ -54,6 +54,9 @@ "gui_theme": "dark", "telemetry": True, "install_id": "", + "launch_on_startup": False, + "notification_timeout": 5, + "multi_tokens": [], "afk": DEFAULT_AFK } diff --git a/utils/files.py b/utils/files.py index 6704c27..a09e737 100644 --- a/utils/files.py +++ b/utils/files.py @@ -57,7 +57,7 @@ def open_path_in_explorer(path): elif sys.platform == "win32": os.startfile(path) else: - subprocess.run(["xdg-open", path], creationflags=subprocess.CREATE_NO_WINDOW) + subprocess.run(["xdg-open", path]) def open_file_in_editor(file_path): if sys.platform == "darwin": @@ -65,4 +65,115 @@ def open_file_in_editor(file_path): elif sys.platform == "win32": subprocess.run(["notepad", file_path], creationflags=subprocess.CREATE_NO_WINDOW) else: - subprocess.run(["gedit", file_path]) \ No newline at end of file + subprocess.run(["gedit", file_path]) + +def set_launch_on_startup(enabled): + """Register or remove Ghost from OS startup. Returns (success, error_message).""" + import traceback + try: + if sys.platform == "win32": + return _set_startup_windows(enabled) + elif sys.platform == "darwin": + return _set_startup_macos(enabled) + else: + return _set_startup_linux(enabled) + except Exception as e: + return False, str(e) + +def _set_startup_windows(enabled): + try: + import winreg + key_path = r"Software\Microsoft\Windows\CurrentVersion\Run" + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_SET_VALUE) + + if getattr(sys, 'frozen', False): + exe_path = sys.executable + else: + script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ghost_path = os.path.join(script_dir, "ghost.py") + exe_path = f'cmd.exe /c "cd /d "{script_dir}" && python "{ghost_path}"' + + app_name = "GhostSelfbot" + + if enabled: + winreg.SetValueEx(key, app_name, 0, winreg.REG_SZ, exe_path) + else: + try: + winreg.DeleteValue(key, app_name) + except FileNotFoundError: + pass + + winreg.CloseKey(key) + return True, None + except Exception as e: + return False, str(e) + +def _set_startup_macos(enabled): + try: + plist_dir = os.path.expanduser("~/Library/LaunchAgents") + plist_path = os.path.join(plist_dir, "com.ghost.selfbot.plist") + + if not enabled: + if os.path.exists(plist_path): + os.remove(plist_path) + return True, None + + if getattr(sys, 'frozen', False): + exe_path = sys.executable + else: + exe_path = sys.executable + + plist_content = f""" + + + + Label + com.ghost.selfbot + ProgramArguments + + {exe_path} + + RunAtLoad + + WorkingDirectory + {os.path.dirname(exe_path)} + +""" + + os.makedirs(plist_dir, exist_ok=True) + with open(plist_path, "w") as f: + f.write(plist_content) + return True, None + except Exception as e: + return False, str(e) + +def _set_startup_linux(enabled): + try: + autostart_dir = os.path.expanduser("~/.config/autostart") + desktop_path = os.path.join(autostart_dir, "ghost-selfbot.desktop") + + if not enabled: + if os.path.exists(desktop_path): + os.remove(desktop_path) + return True, None + + if getattr(sys, 'frozen', False): + exec_path = sys.executable + else: + exec_path = f'{sys.executable} {os.path.abspath("ghost.py")}' + + desktop_content = f"""[Desktop Entry] +Type=Application +Name=Ghost Selfbot +Exec={exec_path} +Hidden=false +NoDisplay=false +X-GNOME-Autostart-enabled=true +""" + + os.makedirs(autostart_dir, exist_ok=True) + with open(desktop_path, "w") as f: + f.write(desktop_content) + return True, None + except Exception as e: + return False, str(e) \ No newline at end of file