From ae2febb9e5be7f139cad8967d06e87f6b35b5bd4 Mon Sep 17 00:00:00 2001 From: Devin Collins <3997333+ImDevinC@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:12:31 -0800 Subject: [PATCH] feat(events): migrate to StreamController event system - Replace register_backend_callback with plugin_base.connect_to_event - Update callback signatures to use *args, **kwargs pattern - Remove custom callback management from backend.py - Add EventHolder registration for voice and channel events - Implement trigger_event method in main.py for event dispatching - Simplify backend by removing callbacks dict - Update version from 1.10.0 to 1.11.0 --- actions/ChangeVoiceChannel.py | 8 +++++-- actions/Deafen.py | 10 +++++--- actions/Mute.py | 10 +++++--- actions/TogglePTT.py | 11 ++++++--- actions/UserVolume.py | 19 +++++++++------ backend.py | 44 +++++++---------------------------- main.py | 35 ++++++++++++++++++++++++++++ manifest.json | 2 +- 8 files changed, 84 insertions(+), 55 deletions(-) diff --git a/actions/ChangeVoiceChannel.py b/actions/ChangeVoiceChannel.py index e1d0ca6..74024cf 100644 --- a/actions/ChangeVoiceChannel.py +++ b/actions/ChangeVoiceChannel.py @@ -27,13 +27,17 @@ def __init__(self, *args, **kwargs): def on_ready(self): super().on_ready() - self.register_backend_callback(VOICE_CHANNEL_SELECT, self._update_display) + self.plugin_base.connect_to_event( + event_id=f"{self.plugin_base.get_plugin_id()}::{VOICE_CHANNEL_SELECT}", + callback=self._update_display, + ) - def _update_display(self, value: dict): + def _update_display(self, *args, **kwargs): if not self.backend: self.show_error() return self.hide_error() + value = args[1] self._current_channel = value.get("channel_id", None) if value else None self.icon_name = ( Icons.VOICE_CHANNEL_INACTIVE diff --git a/actions/Deafen.py b/actions/Deafen.py index 61ee9ec..6e71638 100644 --- a/actions/Deafen.py +++ b/actions/Deafen.py @@ -27,7 +27,10 @@ def __init__(self, *args, **kwargs): def on_ready(self): super().on_ready() - self.register_backend_callback(VOICE_SETTINGS_UPDATE, self._update_display) + self.plugin_base.connect_to_event( + event_id=f"{self.plugin_base.get_plugin_id()}::{VOICE_SETTINGS_UPDATE}", + callback=self._update_display, + ) def create_event_assigners(self): self.event_manager.add_event_assigner( @@ -46,13 +49,14 @@ def _on_toggle(self, _): log.error(ex) self.show_error(3) - def _update_display(self, value: dict): + def _update_display(self, *args, **kwargs): if not self.backend: self.show_error() return else: self.hide_error() - self._deafened = value["deaf"] + data = args[1] + self._deafened = data.get("deaf", False) icon = Icons.DEAFEN if self._deafened else Icons.UNDEAFEN self.icon_name = Icons(icon) self.current_icon = self.get_icon(self.icon_name) diff --git a/actions/Mute.py b/actions/Mute.py index d8ea192..32535f4 100644 --- a/actions/Mute.py +++ b/actions/Mute.py @@ -27,7 +27,10 @@ def __init__(self, *args, **kwargs): def on_ready(self): super().on_ready() - self.register_backend_callback(VOICE_SETTINGS_UPDATE, self._update_display) + self.plugin_base.connect_to_event( + event_id=f"{self.plugin_base.get_plugin_id()}::{VOICE_SETTINGS_UPDATE}", + callback=self._update_display, + ) def create_event_assigners(self): self.event_manager.add_event_assigner( @@ -78,13 +81,14 @@ def _off_mute(self, _): log.error(ex) self.show_error(3) - def _update_display(self, value: dict): + def _update_display(self, *args, **kwargs): if not self.backend: self.show_error() return else: self.hide_error() - self._muted = value["mute"] + data = args[1] + self._muted = data.get("mute", False) icon = Icons.MUTE if self._muted else Icons.UNMUTE self.icon_name = Icons(icon) self.current_icon = self.get_icon(self.icon_name) diff --git a/actions/TogglePTT.py b/actions/TogglePTT.py index de48a30..7e481c8 100644 --- a/actions/TogglePTT.py +++ b/actions/TogglePTT.py @@ -30,7 +30,11 @@ def __init__(self, *args, **kwargs): def on_ready(self): super().on_ready() - self.register_backend_callback(VOICE_SETTINGS_UPDATE, self._update_display) + #self.register_backend_callback(VOICE_SETTINGS_UPDATE, self._update_display) + self.plugin_base.connect_to_event( + event_id=f"{self.plugin_base.get_plugin_id()}::{VOICE_SETTINGS_UPDATE}", + callback=self._update_display, + ) def create_event_assigners(self): self.event_manager.add_event_assigner( @@ -52,13 +56,14 @@ def _on_toggle(self, _): log.error(ex) self.show_error(3) - def _update_display(self, value: dict): + def _update_display(self, *args, **kwargs): if not self.backend: self.show_error() return else: self.hide_error() - self._mode = value["mode"]["type"] + data = args[1] + self._mode = data["mode"]["type"] icon = Icons.PTT if self._mode == ActivityMethod.PTT else Icons.VOICE self.icon_name = Icons(icon) self.current_icon = self.get_icon(self.icon_name) diff --git a/actions/UserVolume.py b/actions/UserVolume.py index c0ac506..ba60db7 100644 --- a/actions/UserVolume.py +++ b/actions/UserVolume.py @@ -43,11 +43,14 @@ def __init__(self, *args, **kwargs): def on_ready(self): super().on_ready() - # Subscribe to voice channel changes (doesn't need channel_id) - self.register_backend_callback(VOICE_CHANNEL_SELECT, self._on_voice_channel_select) - - # Subscribe to GET_CHANNEL responses - self.register_backend_callback(GET_CHANNEL, self._on_get_channel) + self.plugin_base.connect_to_event( + event_id=f"{self.plugin_base.get_plugin_id()}::{VOICE_CHANNEL_SELECT}", + callback=self._on_voice_channel_select, + ) + self.plugin_base.connect_to_event( + event_id=f"{self.plugin_base.get_plugin_id()}::{GET_CHANNEL}", + callback=self._on_get_channel, + ) # Initialize display self._update_display() @@ -130,8 +133,9 @@ def _adjust_volume(self, delta: int): # === Discord Event Callbacks === - def _on_voice_channel_select(self, data: dict): + def _on_voice_channel_select(self, *args, **kwargs): """Handle user joining/leaving voice channel.""" + data = args[1] try: if data is None or data.get("channel_id") is None: # Left voice channel - unsubscribe from previous channel @@ -172,8 +176,9 @@ def _on_voice_channel_select(self, data: dict): except Exception as ex: log.error(f"UserVolume[{id(self)}]: Error in _on_voice_channel_select: {ex}") - def _on_get_channel(self, data: dict): + def _on_get_channel(self, *args, **kwargs): """Handle GET_CHANNEL response with initial user list.""" + data = args[1] if not data: return diff --git a/backend.py b/backend.py index ff7a4da..5bf6241 100644 --- a/backend.py +++ b/backend.py @@ -15,7 +15,6 @@ def __init__(self): self.access_token: str = None self.refresh_token: str = None self.discord_client: AsyncDiscord = None - self.callbacks: dict = {} self._is_authed: bool = False self._current_voice_channel: str = None self._is_reconnecting: bool = False @@ -64,25 +63,19 @@ def discord_callback(self, code, event): # Capture current user ID for filtering in UserVolume data = event.get("data", {}) user = data.get("user", {}) + self._register_callbacks() self._current_user_id = user.get("id") - for k in self.callbacks: - self.discord_client.subscribe(k) self._get_current_voice_channel() case commands.DISPATCH: evt = event.get("evt") - self.frontend.handle_callback(evt, event.get("data")) + self.frontend.trigger_event(evt, event.get("data")) case commands.GET_SELECTED_VOICE_CHANNEL: self._current_voice_channel = ( event.get("data").get("channel_id") if event.get("data") else None ) - self.frontend.handle_callback( - commands.VOICE_CHANNEL_SELECT, event.get("data") - ) + self.frontend.trigger_event(commands.VOICE_CHANNEL_SELECT, event.get("data")) case commands.GET_CHANNEL: - # Dispatch channel info (including voice_states) to frontend - self.frontend.handle_callback( - commands.GET_CHANNEL, event.get("data") - ) + self.frontend.trigger_event(commands.GET_CHANNEL, event.get("data")) def _update_tokens(self, access_token: str = "", refresh_token: str = ""): self.access_token = access_token @@ -96,15 +89,11 @@ def setup_client(self): return try: self._is_reconnecting = True - log.debug("new client") self.discord_client = AsyncDiscord(self.client_id, self.client_secret) - log.debug("connect") self.discord_client.connect(self.discord_callback) if not self.access_token: - log.debug("authorize") self.discord_client.authorize() else: - log.debug("authenticate") self.discord_client.authenticate(self.access_token) except Exception as ex: self.frontend.on_auth_callback(False, str(ex)) @@ -136,25 +125,10 @@ def update_client_credentials( def is_authed(self) -> bool: return self._is_authed - def register_callback(self, key: str, callback: callable): - callbacks = self.callbacks.get(key, []) - # Deduplicate callbacks to prevent multiple executions - if callback not in callbacks: - callbacks.append(callback) - self.callbacks[key] = callbacks - if self._is_authed: - self.discord_client.subscribe(key) - - def unregister_callback(self, key: str, callback: callable): - """Remove a callback from the callback list.""" - callbacks = self.callbacks.get(key, []) - if callback in callbacks: - callbacks.remove(callback) - if callbacks: - self.callbacks[key] = callbacks - else: - # Remove key entirely if no callbacks remain - del self.callbacks[key] + def _register_callbacks(self): + self.discord_client.subscribe(commands.VOICE_SETTINGS_UPDATE) + self.discord_client.subscribe(commands.VOICE_CHANNEL_SELECT) + self.discord_client.subscribe(commands.GET_CHANNEL) def _ensure_connected(self) -> bool: """Ensure client is connected, trigger reconnection if needed.""" @@ -274,7 +248,6 @@ def subscribe_voice_states(self, channel_id: str) -> bool: if not self._ensure_connected(): log.warning("Discord client not connected, cannot subscribe to voice states") return False - log.debug(f"Subscribing to voice state events for channel {channel_id}") args = {"channel_id": channel_id} self.discord_client.subscribe(commands.VOICE_STATE_CREATE, args) self.discord_client.subscribe(commands.VOICE_STATE_DELETE, args) @@ -285,7 +258,6 @@ def unsubscribe_voice_states(self, channel_id: str) -> bool: """Unsubscribe from voice state events for a specific channel.""" if not self._ensure_connected(): return False - log.debug(f"Unsubscribing from voice state events for channel {channel_id}") args = {"channel_id": channel_id} self.discord_client.unsubscribe(commands.VOICE_STATE_CREATE, args) self.discord_client.unsubscribe(commands.VOICE_STATE_DELETE, args) diff --git a/main.py b/main.py index 1448142..7c3d5aa 100644 --- a/main.py +++ b/main.py @@ -11,6 +11,7 @@ from src.backend.DeckManagement.InputIdentifier import Input from src.backend.PluginManager.ActionInputSupport import ActionInputSupport from src.backend.DeckManagement.ImageHelpers import image2pixbuf +from src.backend.PluginManager.EventHolder import EventHolder # Import actions from .settings import PluginSettings @@ -21,6 +22,9 @@ from .actions.TogglePTT import TogglePTT from .actions.UserVolume import UserVolume +# Import event IDs +from .discordrpc.commands import VOICE_CHANNEL_SELECT, VOICE_SETTINGS_UPDATE, GET_CHANNEL + class PluginTemplate(PluginBase): def get_selector_icon(self) -> Gtk.Widget: @@ -40,6 +44,7 @@ def __init__(self): ) self._add_icons() self._register_actions() + self._create_event_holders() backend_path = os.path.join(self.PATH, "backend.py") self.launch_backend( backend_path=backend_path, @@ -70,6 +75,29 @@ def __init__(self): self.add_css_stylesheet(os.path.join(self.PATH, "style.css")) self.setup_backend() + def _create_event_holders(self): + voice_channel_select = EventHolder( + plugin_base=self, + event_id_suffix=VOICE_CHANNEL_SELECT, + ) + + voice_settings_update = EventHolder( + plugin_base=self, + event_id_suffix=VOICE_SETTINGS_UPDATE, + ) + + get_channel = EventHolder( + plugin_base=self, + event_id_suffix=GET_CHANNEL, + ) + + self.add_event_holders([ + voice_channel_select, + voice_settings_update, + get_channel, + ]) + + def _add_icons(self): self.add_icon("main", self.get_asset_path("Discord-Symbol-Blurple.png")) self.add_icon("deafen", self.get_asset_path("deafen.png")) @@ -213,3 +241,10 @@ def clear_callbacks(self, key: str, callback: callable): self.callbacks[key] = callbacks else: del self.callbacks[key] + + def trigger_event(self, event_id_suffix: str, data: any): + event_id = f"{self.get_plugin_id()}::{event_id_suffix}" + if not event_id in self.event_holders: + log.warning(f"Event ID {event_id} not registered.") + return + self.event_holders[event_id].trigger_event(data) diff --git a/manifest.json b/manifest.json index db6981c..4205665 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "version": "1.10.0", + "version": "1.11.0", "thumbnail": "store/thumbnail.png", "id": "com_imdevinc_StreamControllerDiscordPlugin", "name": "Discord - Debug",