-
Notifications
You must be signed in to change notification settings - Fork 44.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add Slack OAuth integration and messaging capabilities
- 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
1 parent
5ceca51
commit 9b13e91
Showing
13 changed files
with
446 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
102
autogpt_platform/backend/backend/blocks/slack/send_message.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
autogpt_platform/backend/backend/integrations/oauth/slack_bot.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.