# -*- coding: utf-8 -*- """ Module to perform session actions such as addition, removal or display of the global settings dialogue. """ import time import os import logging import shutil import widgetUtils import sessions import output import paths import config_utils import config import application import asyncio # For async event handling import wx from pubsub import pub from controller import settings from sessions.mastodon import session as MastodonSession from sessions.gotosocial import session as GotosocialSession from sessions.blueski import session as BlueskiSession # Import Blueski session from . import manager from . import wxUI as view log = logging.getLogger("sessionmanager.sessionManager") class sessionManagerController(object): def __init__(self, started: bool = False): """ Class constructor. Creates the SessionManager class controller, responsible for the accounts within TWBlue. From this dialog, users can add/Remove accounts, or load the global settings dialog. :param started: Indicates whether this object is being created during application startup (when no other controller has been instantiated) or not. It is important for us to know this, as we won't show the button to open global settings dialog if the application has been started. Users must choose the corresponding option in the menu bar. :type started: bool """ super(sessionManagerController, self).__init__() log.debug("Setting up the session manager.") self.started = started # Initialize the manager, responsible for storing session objects. manager.setup() self.view = view.sessionManagerWindow() # Handle new account synchronously on the UI thread pub.subscribe(self.manage_new_account, "sessionmanager.new_account") pub.subscribe(self.remove_account, "sessionmanager.remove_account") if self.started == False: pub.subscribe(self.configuration, "sessionmanager.configuration") else: self.view.hide_configuration() # Store a temporary copy of new and removed sessions, so we will perform actions on them during call to on_ok. self.new_sessions = {} self.removed_sessions = [] def fill_list(self): """ Fills the session list with all valid sessions that could be found in config path. """ sessionsList = [] reserved_dirs = ["dicts"] log.debug("Filling the sessions list.") self.sessions = [] for i in os.listdir(paths.config_path()): if os.path.isdir(os.path.join(paths.config_path(), i)) and i not in reserved_dirs: log.debug("Adding session %s" % (i,)) strconfig = "%s/session.conf" % (os.path.join(paths.config_path(), i)) config_test = config_utils.load_config(strconfig) if len(config_test) == 0: try: log.debug("Deleting session %s" % (i,)) shutil.rmtree(os.path.join(paths.config_path(), i)) continue except: output.speak("An exception was raised while attempting to clean malformed session data. See the error log for details. If this message persists, contact the developers.",True) os.exception("Exception thrown while removing malformed session") continue if config_test.get("mastodon") != None: name = _("{account_name}@{instance} (Mastodon)").format(account_name=config_test["mastodon"]["user_name"], instance=config_test["mastodon"]["instance"].replace("https://", "")) if config_test["mastodon"]["instance"] != "" and config_test["mastodon"]["access_token"] != "": # Basic validation sessionsList.append(name) self.sessions.append(dict(type=config_test["mastodon"].get("type", "mastodon"), id=i)) elif config_test.get("blueski") != None: # Check for Blueski config handle = config_test["blueski"].get("handle") did = config_test["blueski"].get("did") # DID confirms it was authorized if handle and did: name = _("{handle} (Bluesky)").format(handle=handle) sessionsList.append(name) self.sessions.append(dict(type="blueski", id=i)) else: # Incomplete config, might be an old attempt or error log.warning(f"Incomplete Blueski session config found for {i}, skipping.") # Optionally delete malformed config here too try: log.debug("Deleting incomplete Blueski session %s" % (i,)) shutil.rmtree(os.path.join(paths.config_path(), i)) except Exception as e: log.exception(f"Error deleting incomplete Blueski session {i}: {e}") continue elif config_test.get("atprotosocial") != None: # Legacy config namespace handle = config_test["atprotosocial"].get("handle") did = config_test["atprotosocial"].get("did") if handle and did: name = _("{handle} (Bluesky)").format(handle=handle) sessionsList.append(name) self.sessions.append(dict(type="blueski", id=i)) else: # Incomplete config, might be an old attempt or error log.warning(f"Incomplete Blueski session config found for {i}, skipping.") # Optionally delete malformed config here too try: log.debug("Deleting incomplete Blueski session %s" % (i,)) shutil.rmtree(os.path.join(paths.config_path(), i)) except Exception as e: log.exception(f"Error deleting incomplete Blueski session {i}: {e}") continue else: # Unknown or other session type not explicitly handled here for display try: log.debug("Deleting session %s with unknown type" % (i,)) shutil.rmtree(os.path.join(paths.config_path(), i)) except: output.speak("An exception was raised while attempting to clean malformed session data. See the error log for details. If this message persists, contact the developers.",True) os.exception("Exception thrown while removing malformed session") self.view.fill_list(sessionsList) def show(self): """ Displays the session manager dialog. """ if self.view.get_response() == widgetUtils.OK: self.do_ok() # else: self.view.destroy() def do_ok(self): log.debug("Starting sessions...") for i in self.sessions: # Skip already created sessions. Useful when session manager controller is not created during startup. if sessions.sessions.get(i.get("id")) != None: continue # Create the session object based in session type. if i.get("type") == "mastodon": s = MastodonSession.Session(i.get("id")) elif i.get("type") == "gotosocial": s = GotosocialSession.Session(i.get("id")) elif i.get("type") == "blueski": # Handle Blueski session type s = BlueskiSession.Session(i.get("id")) else: log.warning(f"Unknown session type '{i.get('type')}' for ID {i.get('id')}. Skipping.") continue s.get_configuration() # Load per-session configuration # For Blueski, this loads from its specific config file. # Login is now primarily handled by session.start() via mainController, # which calls _ensure_dependencies_ready(). # Explicit s.login() here might be redundant or premature if full app context isn't ready. # We'll rely on the mainController to call session.start() which handles login. # if i.get("id") not in config.app["sessions"]["ignored_sessions"]: # try: # # For Blueski, login is async and handled by session.start() # # if not s.is_ready(): # Only attempt login if not already ready # # log.info(f"Session {s.uid} ({s.kind}) not ready, login will be attempted by start().") # pass # except Exception as e: # log.exception(f"Exception during pre-emptive login check for session {s.uid} ({s.kind}).") # continue # Try to auto-login for Blueski so the app starts with buffers ready try: if i.get("type") == "blueski": s.login() except Exception: log.exception("Auto-login failed for Blueski session %s", i.get("id")) sessions.sessions[i.get("id")] = s # Add to global session store self.new_sessions[i.get("id")] = s # Track as a new session for this manager instance # self.view.destroy() def show_auth_error(self): error = view.auth_error() # This seems to be a generic auth error display def manage_new_account(self, type): # Generic settings for all account types. location = (str(time.time())[-6:]) # Unique ID for the session config directory log.debug("Creating %s session in the %s path" % (type, location)) s: sessions.base.baseSession | None = None # Type hint for session object if type == "mastodon": s = MastodonSession.Session(location) elif type == "blueski": s = BlueskiSession.Session(location) # Add other session types here if needed (e.g., gotosocial) # elif type == "gotosocial": # s = GotosocialSession.Session(location) if not s: log.error(f"Unsupported session type for creation: {type}") self.view.show_unauthorised_error() # Or a more generic "cannot create" error return try: result = s.authorise() if result == True: # Session config (handle, did for atproto) should be saved by authorise/login. # Here we just update the session manager's internal list and UI. session_type_for_dict = type # Store the actual type string if hasattr(s, 'settings') and s.settings and s.settings.get(type) and s.settings[type].get("type"): # Mastodon might have a more specific type in its settings (e.g. gotosocial) session_type_for_dict = s.settings[type].get("type") self.sessions.append(dict(id=location, type=session_type_for_dict)) self.view.add_new_session_to_list() # This should update the UI list # The session object 's' itself isn't stored in self.new_sessions until do_ok if app is restarting # But for immediate use if not restarting, it might need to be added to sessions.sessions sessions.sessions[location] = s # Make it globally available immediately self.new_sessions[location] = s # Sync with global config if location not in config.app["sessions"]["sessions"]: config.app["sessions"]["sessions"].append(location) config.app.write() else: # Authorise returned False or None self.view.show_unauthorised_error() # Clean up the directory if authorization failed and nothing was saved if os.path.exists(os.path.join(paths.config_path(), location)): try: shutil.rmtree(os.path.join(paths.config_path(), location)) log.info(f"Cleaned up directory for failed auth: {location}") except Exception as e_rm: log.error(f"Error cleaning up directory {location} after failed auth: {e_rm}") except Exception as e: log.error(f"Error during new account authorization for type {type}: {e}", exc_info=True) self.view.show_unauthorised_error() # Show generic error # Clean up if os.path.exists(os.path.join(paths.config_path(), location)): try: shutil.rmtree(os.path.join(paths.config_path(), location)) except Exception as e_rm: log.error(f"Error cleaning up directory {location} after exception: {e_rm}") def remove_account(self, index): selected_account = self.sessions[index] self.view.remove_session(index) self.removed_sessions.append(selected_account.get("id")) self.sessions.remove(selected_account) if selected_account.get("id") in config.app["sessions"]["sessions"]: config.app["sessions"]["sessions"].remove(selected_account.get("id")) config.app.write() shutil.rmtree(path=os.path.join(paths.config_path(), selected_account.get("id")), ignore_errors=True) def configuration(self): """ Opens the global settings dialogue.""" d = settings.globalSettingsController() if d.response == widgetUtils.OK: d.save_configuration()