mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-06 01:17:32 +01:00
249 lines
13 KiB
Python
249 lines
13 KiB
Python
# -*- 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()
|