Skip to content

Commit

Permalink
feat: Add Slack OAuth integration and messaging capabilities
Browse files Browse the repository at this point in the history
- Introduced Slack OAuth handlers for both bot and user authentication.
- Added new environment variables for Slack client IDs and secrets in `.env.example`.
- Implemented `SlackPostMessageBlock` for posting messages to Slack channels.
- Updated `pyproject.toml` to include Slack SDK dependencies.
- Enhanced OAuth integration by registering Slack handlers in the OAuth module.
- Updated frontend components to support Slack provider display and icons.

# Conflicts:
#	autogpt_platform/backend/backend/integrations/oauth/__init__.py
#	autogpt_platform/backend/poetry.lock
#	autogpt_platform/backend/pyproject.toml
  • Loading branch information
Abhi1992002 committed Dec 13, 2024
1 parent 5ceca51 commit 9b13e91
Show file tree
Hide file tree
Showing 13 changed files with 446 additions and 2 deletions.
7 changes: 7 additions & 0 deletions autogpt_platform/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ GOOGLE_CLIENT_SECRET=
TWITTER_CLIENT_ID=
TWITTER_CLIENT_SECRET=

# Slack OAuth Configuration
# Note: A local tunnel (e.g. ngrok) is required for local development environments
SLACK_BOT_CLIENT_ID=
SLACK_BOT_CLIENT_SECRET=

SLACK_USER_CLIENT_ID=
SLACK_USER_CLIENT_SECRET=

## ===== OPTIONAL API KEYS ===== ##

Expand Down
50 changes: 50 additions & 0 deletions autogpt_platform/backend/backend/blocks/slack/_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from enum import Enum
from typing import Literal
from backend.data.model import OAuth2Credentials
from pydantic import SecretStr

from backend.data.model import CredentialsField, CredentialsMetaInput
from backend.util.settings import Secrets

secrets = Secrets()

SlackProviderName = Literal["slack_bot", "slack_user"]
SlackCredentials = OAuth2Credentials
SlackCredentialsInput = CredentialsMetaInput[SlackProviderName, Literal["oauth2"]]


def SlackCredentialsField(scopes: list[str]) -> SlackCredentialsInput:
return CredentialsField(
provider=["slack_bot","slack_user"],
supported_credential_types={"oauth2"},
description="The Slack integration requires OAuth2 authentication.",
discriminator="type",
discriminator_mapping={
model.value: model.value for model in SlackModel
},
)

class SlackModel(str, Enum):
SLACK_BOT = "slack_bot"
SLACK_USER = "slack_user"


TEST_CREDENTIALS = OAuth2Credentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="slack_bot",
type="oauth2",
access_token=SecretStr("mock-slack-access-token"),
refresh_token=SecretStr("mock-slack-refresh-token"),
access_token_expires_at=1234567890,
scopes=["chat:write", "channels:read", "users:read", "channels:history"],
title="Mock Slack OAuth2 Credentials",
username="mock-slack-username",
refresh_token_expires_at=1234567890,
)

TEST_CREDENTIALS_INPUT = {
"provider": "slack_bot",
"id": TEST_CREDENTIALS.id,
"type": "oauth2",
"title": TEST_CREDENTIALS.title,
}
102 changes: 102 additions & 0 deletions autogpt_platform/backend/backend/blocks/slack/send_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from backend.blocks.slack._auth import TEST_CREDENTIALS, TEST_CREDENTIALS_INPUT, SlackCredentials, SlackCredentialsField, SlackCredentialsInput, SlackModel
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from slack_bolt import App
from slack_sdk.errors import SlackApiError

class SlackPostMessageBlock(Block):
"""
Posts a message to a Slack channel
"""

class Input(BlockSchema):
credentials: SlackCredentialsInput = SlackCredentialsField(
["chat:write"]
)

type: SlackModel = SchemaField(
title ="type",
default=SlackModel.SLACK_USER,
description=(
"Select how you want to interact with Slack\n\n"
"User: Interact with Slack using your personal Slack account\n\n"
"Bot: Interact with Slack as a bot [AutoGPT_Bot] (requires installation in your workspace)"
),
advanced=False,
)

channel: str = SchemaField(
description="Channel ID to post message to (ex: C0XXXXXX)",
placeholder="Enter channel ID"
)

text: str = SchemaField(
description="Message text to post",
placeholder="Enter message text"
)

class Output(BlockSchema):
success : bool = SchemaField(description="True if the message was successfully posted to Slack")
error: str = SchemaField(description="Error message if request failed")

def __init__(self):
super().__init__(
id="f8822c4b-b640-11ef-9cc1-a309988b4d92",
description="Posts a message to a Slack channel",
categories={BlockCategory.SOCIAL},
input_schema=SlackPostMessageBlock.Input,
output_schema=SlackPostMessageBlock.Output,
test_input={
"channel": "C0XXXXXX",
"text": "Test message",
"type": "user",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
( "success", True)
],
test_mock={
"post_message": lambda *args, **kwargs: True
},
)

@staticmethod
def post_message(
credentials: SlackCredentials,
channel: str,
text: str,
):
try:
app = App(token=credentials.access_token.get_secret_value())
response = app.client.chat_postMessage(
channel=channel,
text=text
)

if response["ok"]:
return True

raise Exception(response["error"])

except SlackApiError:
raise

def run(
self,
input_data: Input,
*,
credentials: SlackCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.post_message(
credentials,
input_data.channel,
input_data.text
)

yield "success" , success

except Exception as e:
yield "error", str(e)
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
GoogleOAuthHandler,
NotionOAuthHandler,
TwitterOAuthHandler,
SlackBotOAuthHandler,
SlackUserOAuthHandler
]
}
# --8<-- [end:HANDLERS_BY_NAMEExample]
Expand Down
118 changes: 118 additions & 0 deletions autogpt_platform/backend/backend/integrations/oauth/slack_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import urllib.parse
from typing import ClassVar, Optional

import requests
from backend.data.model import OAuth2Credentials

from backend.integrations.oauth.base import BaseOAuthHandler


class SlackBotOAuthHandler(BaseOAuthHandler):
PROVIDER_NAME: ClassVar[str] = "slack_bot"
DEFAULT_SCOPES: ClassVar[list[str]] = [
"chat:write",
"users:read",
]

AUTHORIZE_URL = "https://slack.com/oauth/v2/authorize"
TOKEN_URL = "https://slack.com/api/oauth.v2.access"
USERNAME_URL = "https://slack.com/api/auth.test"
REVOKE_URL = "https://slack.com/api/auth.revoke"

def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri

def get_login_url(
self, scopes: list[str], state: str, code_challenge: Optional[str]
) -> str:
"""Generate Slack OAuth 2.0 authorization URL"""
params = {
"client_id": self.client_id,
"redirect_uri": self.redirect_uri,
"scope": ",".join(self.DEFAULT_SCOPES),
"user_scope": ",".join(self.DEFAULT_SCOPES),
"state": state,
}

return f"{self.AUTHORIZE_URL}?{urllib.parse.urlencode(params)}"

def exchange_code_for_tokens(
self, code: str, scopes: list[str], code_verifier: Optional[str]
) -> OAuth2Credentials:
"""Exchange authorization code for access tokens"""

headers = {
"Content-Type": "application/x-www-form-urlencoded"
}

data = {
"code": code,
"client_id": self.client_id,
"client_secret": self.client_secret,
"redirect_uri": self.redirect_uri,
}

response = requests.post(self.TOKEN_URL, headers=headers, data=data)
response.raise_for_status()

tokens = response.json()

if not tokens["ok"]:
raise Exception(f"Failed to get tokens: {tokens['error']}")

username = self._get_username(tokens["access_token"])

bot_token = tokens["access_token"]

return OAuth2Credentials(
provider=self.PROVIDER_NAME,
title=None,
username=username,
access_token= bot_token,
refresh_token=None, # Slack doesn't use refresh tokens
access_token_expires_at=None, # Slack tokens don't expire
refresh_token_expires_at=None,
scopes=scopes
)

def _get_username(self, access_token: str) -> str:
"""Get the username from the access token"""
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}

response = requests.post(self.USERNAME_URL, headers=headers)
response.raise_for_status()

data = response.json()
if not data["ok"]:
raise Exception(f"Failed to get username: {data['error']}")

return data["user_id"]

def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials:
"""Refresh tokens not supported by Slack"""
return credentials

def revoke_tokens(self, credentials: OAuth2Credentials) -> bool:
"""Revoke the access token"""
headers = {
"Authorization": f"Bearer {credentials.access_token.get_secret_value()}",
"Content-Type": "application/json"
}

response = requests.post(self.REVOKE_URL, headers=headers)

try:
response.raise_for_status()
data = response.json()
return data["ok"]
except requests.exceptions.HTTPError as e:
print("HTTP Error:", e)
print("Response Content:", response.text)
raise

return False
Loading

0 comments on commit 9b13e91

Please sign in to comment.