diff --git a/gateway/platforms/slack.py b/gateway/platforms/slack.py index 11a73461e79c..712061f3094f 100644 --- a/gateway/platforms/slack.py +++ b/gateway/platforms/slack.py @@ -9,6 +9,7 @@ """ import asyncio +import logging import os from typing import Dict, List, Optional, Any @@ -38,6 +39,9 @@ ) +logger = logging.getLogger(__name__) + + def check_slack_requirements() -> bool: """Check if Slack dependencies are available.""" return SLACK_AVAILABLE @@ -70,17 +74,19 @@ def __init__(self, config: PlatformConfig): async def connect(self) -> bool: """Connect to Slack via Socket Mode.""" if not SLACK_AVAILABLE: - print("[Slack] slack-bolt not installed. Run: pip install slack-bolt") + logger.error( + "[Slack] slack-bolt not installed. Run: pip install slack-bolt", + ) return False bot_token = self.config.token app_token = os.getenv("SLACK_APP_TOKEN") if not bot_token: - print("[Slack] SLACK_BOT_TOKEN not set") + logger.error("[Slack] SLACK_BOT_TOKEN not set") return False if not app_token: - print("[Slack] SLACK_APP_TOKEN not set") + logger.error("[Slack] SLACK_APP_TOKEN not set") return False try: @@ -107,19 +113,22 @@ async def handle_hermes_command(ack, command): asyncio.create_task(self._handler.start_async()) self._running = True - print(f"[Slack] Connected as @{bot_name} (Socket Mode)") + logger.info("[Slack] Connected as @%s (Socket Mode)", bot_name) return True - except Exception as e: - print(f"[Slack] Connection failed: {e}") + except Exception as e: # pragma: no cover - defensive logging + logger.error("[Slack] Connection failed: %s", e, exc_info=True) return False async def disconnect(self) -> None: """Disconnect from Slack.""" if self._handler: - await self._handler.close_async() + try: + await self._handler.close_async() + except Exception as e: # pragma: no cover - defensive logging + logger.warning("[Slack] Error while closing Socket Mode handler: %s", e, exc_info=True) self._running = False - print("[Slack] Disconnected") + logger.info("[Slack] Disconnected") async def send( self, @@ -152,8 +161,8 @@ async def send( raw_response=result, ) - except Exception as e: - print(f"[Slack] Send error: {e}") + except Exception as e: # pragma: no cover - defensive logging + logger.error("[Slack] Send error: %s", e, exc_info=True) return SendResult(success=False, error=str(e)) async def edit_message( @@ -172,7 +181,14 @@ async def edit_message( text=content, ) return SendResult(success=True, message_id=message_id) - except Exception as e: + except Exception as e: # pragma: no cover - defensive logging + logger.error( + "[Slack] Failed to edit message %s in channel %s: %s", + message_id, + chat_id, + e, + exc_info=True, + ) return SendResult(success=False, error=str(e)) async def send_typing(self, chat_id: str) -> None: @@ -204,8 +220,14 @@ async def send_image_file( ) return SendResult(success=True, raw_response=result) - except Exception as e: - print(f"[{self.name}] Failed to send local image: {e}") + except Exception as e: # pragma: no cover - defensive logging + logger.error( + "[%s] Failed to send local Slack image %s: %s", + self.name, + image_path, + e, + exc_info=True, + ) return await super().send_image_file(chat_id, image_path, caption, reply_to) async def send_image( @@ -237,7 +259,13 @@ async def send_image( return SendResult(success=True, raw_response=result) - except Exception as e: + except Exception as e: # pragma: no cover - defensive logging + logger.warning( + "[Slack] Failed to upload image from URL %s, falling back to text: %s", + image_url, + e, + exc_info=True, + ) # Fall back to sending the URL as text text = f"{caption}\n{image_url}" if caption else image_url return await self.send(chat_id=chat_id, content=text, reply_to=reply_to) @@ -263,7 +291,13 @@ async def send_voice( ) return SendResult(success=True, raw_response=result) - except Exception as e: + except Exception as e: # pragma: no cover - defensive logging + logger.error( + "[Slack] Failed to send audio file %s: %s", + audio_path, + e, + exc_info=True, + ) return SendResult(success=False, error=str(e)) async def get_chat_info(self, chat_id: str) -> Dict[str, Any]: @@ -279,7 +313,13 @@ async def get_chat_info(self, chat_id: str) -> Dict[str, Any]: "name": channel.get("name", chat_id), "type": "dm" if is_dm else "group", } - except Exception: + except Exception as e: # pragma: no cover - defensive logging + logger.error( + "[Slack] Failed to fetch chat info for %s: %s", + chat_id, + e, + exc_info=True, + ) return {"name": chat_id, "type": "unknown"} # ----- Internal handlers ----- @@ -334,8 +374,8 @@ async def _handle_slack_message(self, event: dict) -> None: media_urls.append(cached) media_types.append(mimetype) msg_type = MessageType.PHOTO - except Exception as e: - print(f"[Slack] Failed to cache image: {e}", flush=True) + except Exception as e: # pragma: no cover - defensive logging + logger.warning("[Slack] Failed to cache image from %s: %s", url, e, exc_info=True) elif mimetype.startswith("audio/") and url: try: ext = "." + mimetype.split("/")[-1].split(";")[0] @@ -345,8 +385,8 @@ async def _handle_slack_message(self, event: dict) -> None: media_urls.append(cached) media_types.append(mimetype) msg_type = MessageType.VOICE - except Exception as e: - print(f"[Slack] Failed to cache audio: {e}", flush=True) + except Exception as e: # pragma: no cover - defensive logging + logger.warning("[Slack] Failed to cache audio from %s: %s", url, e, exc_info=True) # Build source source = self.build_source(