suna/backend/triggers/providers/slack_oauth.py

226 lines
8.9 KiB
Python

import httpx
import secrets
import json
from typing import Dict, Any, Optional
from urllib.parse import urlencode
import os
from utils.logger import logger
from services.supabase import DBConnection
class SlackOAuthManager:
"""Manages Slack OAuth flow for agent installation."""
def __init__(self, db_connection: DBConnection):
self.db = db_connection
self.client_id = os.getenv("SLACK_CLIENT_ID")
self.client_secret = os.getenv("SLACK_CLIENT_SECRET")
self.redirect_uri = os.getenv("SLACK_REDIRECT_URI", "http://localhost:3000/api/integrations/slack/callback")
def generate_install_url(self, agent_id: str, user_id: str) -> str:
"""Generate Slack app installation URL with state parameter."""
state = self._create_state_token(agent_id, user_id)
params = {
"client_id": self.client_id,
"scope": "app_mentions:read,channels:read,chat:write,im:read,im:write,users:read",
"redirect_uri": self.redirect_uri,
"state": state
}
return f"https://slack.com/oauth/v2/authorize?{urlencode(params)}"
async def handle_oauth_callback(self, code: str, state: str) -> Dict[str, Any]:
"""Handle OAuth callback and set up agent trigger."""
try:
# Verify state and extract agent/user info
state_data = await self._verify_state_token(state)
if not state_data:
return {"success": False, "error": "Invalid state parameter"}
agent_id = state_data["agent_id"]
user_id = state_data["user_id"]
# Exchange code for access token
token_data = await self._exchange_code_for_token(code)
if not token_data:
return {"success": False, "error": "Failed to exchange code for token"}
# Get workspace and bot info
workspace_info = await self._get_workspace_info(token_data["access_token"])
bot_info = await self._get_bot_info(token_data["bot_user_id"], token_data["access_token"])
# Create trigger configuration
trigger_config = await self._create_agent_trigger(
agent_id=agent_id,
user_id=user_id,
workspace_info=workspace_info,
bot_info=bot_info,
oauth_data=token_data
)
return {
"success": True,
"trigger_id": trigger_config["trigger_id"],
"workspace_name": workspace_info.get("team", {}).get("name"),
"bot_name": bot_info.get("name"),
"webhook_url": trigger_config["webhook_url"]
}
except Exception as e:
logger.error(f"Error handling Slack OAuth callback: {e}")
return {"success": False, "error": str(e)}
async def _exchange_code_for_token(self, code: str) -> Optional[Dict[str, Any]]:
"""Exchange OAuth code for access token."""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
"https://slack.com/api/oauth.v2.access",
data={
"client_id": self.client_id,
"client_secret": self.client_secret,
"code": code,
"redirect_uri": self.redirect_uri
}
)
data = response.json()
if data.get("ok"):
return {
"access_token": data["access_token"],
"bot_user_id": data["bot_user_id"],
"team_id": data["team"]["id"],
"team_name": data["team"]["name"],
"scope": data["scope"],
"app_id": data["app_id"]
}
else:
logger.error(f"Slack OAuth error: {data.get('error')}")
return None
except Exception as e:
logger.error(f"Error exchanging code for token: {e}")
return None
async def _get_workspace_info(self, access_token: str) -> Dict[str, Any]:
"""Get workspace information."""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://slack.com/api/team.info",
headers={"Authorization": f"Bearer {access_token}"}
)
data = response.json()
return data if data.get("ok") else {}
except Exception as e:
logger.error(f"Error getting workspace info: {e}")
return {}
async def _get_bot_info(self, bot_user_id: str, access_token: str) -> Dict[str, Any]:
"""Get bot user information."""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://slack.com/api/users.info?user={bot_user_id}",
headers={"Authorization": f"Bearer {access_token}"}
)
data = response.json()
return data.get("user", {}) if data.get("ok") else {}
except Exception as e:
logger.error(f"Error getting bot info: {e}")
return {}
async def _create_agent_trigger(
self,
agent_id: str,
user_id: str,
workspace_info: Dict[str, Any],
bot_info: Dict[str, Any],
oauth_data: Dict[str, Any]
) -> Dict[str, Any]:
"""Create agent trigger with OAuth data."""
from ..core import TriggerManager
trigger_manager = TriggerManager(self.db)
await trigger_manager.load_provider_definitions()
config = {
"access_token": oauth_data["access_token"],
"bot_user_id": oauth_data["bot_user_id"],
"team_id": oauth_data["team_id"],
"team_name": oauth_data["team_name"],
"bot_name": bot_info.get("name", "Agent Bot"),
"respond_to_mentions": True,
"respond_to_direct_messages": True,
"allowed_channels": [],
"trigger_keywords": [],
"oauth_installed": True
}
trigger_config = await trigger_manager.create_trigger(
agent_id=agent_id,
provider_id="slack_oauth",
name=f"Slack - {oauth_data['team_name']}",
description=f"Auto-configured Slack integration for {oauth_data['team_name']} workspace",
config=config
)
# COMMENTED OUT: OAuth installations functionality deprecated
# await self._store_oauth_data(trigger_config.trigger_id, oauth_data, workspace_info, bot_info)
base_url = os.getenv("WEBHOOK_BASE_URL", "http://localhost:8000")
# Slack requires a single Event Request URL per app
webhook_url = f"{base_url}/api/triggers/slack/webhook"
return {
"trigger_id": trigger_config.trigger_id,
"webhook_url": webhook_url
}
def _create_state_token(self, agent_id: str, user_id: str) -> str:
state_data = {
"agent_id": agent_id,
"user_id": user_id,
"nonce": secrets.token_urlsafe(16)
}
import base64
state_json = json.dumps(state_data)
return base64.b64encode(state_json.encode()).decode()
async def _verify_state_token(self, state: str) -> Optional[Dict[str, Any]]:
"""Verify and decode state token."""
try:
import base64
state_json = base64.b64decode(state.encode()).decode()
return json.loads(state_json)
except Exception as e:
logger.error(f"Error verifying state token: {e}")
return None
# COMMENTED OUT: OAuth installations functionality deprecated
# async def _store_oauth_data(
# self,
# trigger_id: str,
# oauth_data: Dict[str, Any],
# workspace_info: Dict[str, Any],
# bot_info: Dict[str, Any]
# ):
# """Store OAuth data for the trigger."""
# client = await self.db.client
# await client.table('slack_oauth_installations').insert({
# 'trigger_id': trigger_id,
# 'team_id': oauth_data['team_id'],
# 'team_name': oauth_data['team_name'],
# 'access_token': oauth_data['access_token'],
# 'bot_user_id': oauth_data['bot_user_id'],
# 'bot_name': bot_info.get('name'),
# 'app_id': oauth_data['app_id'],
# 'scope': oauth_data['scope'],
# 'workspace_info': workspace_info,
# 'bot_info': bot_info,
# 'installed_at': 'now()'
# }).execute()