Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 60 additions & 20 deletions gateway/platforms/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

import asyncio
import logging
import os
from typing import Dict, List, Optional, Any

Expand Down Expand Up @@ -38,6 +39,9 @@
)


logger = logging.getLogger(__name__)


def check_slack_requirements() -> bool:
"""Check if Slack dependencies are available."""
return SLACK_AVAILABLE
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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]:
Expand All @@ -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 -----
Expand Down Expand Up @@ -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]
Expand All @@ -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(
Expand Down