# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
import logging
import os
from typing import TYPE_CHECKING, Any, Dict, Optional
from slack_sdk.oauth.installation_store.async_installation_store import (
AsyncInstallationStore,
)
from starlette import requests, responses
from camel.bots.slack.models import (
SlackAppMentionEventBody,
SlackAppMentionEventProfile,
SlackEventBody,
SlackEventProfile,
)
from camel.utils import dependencies_required
if TYPE_CHECKING:
from slack_bolt.context.async_context import AsyncBoltContext
from slack_bolt.context.say.async_say import AsyncSay
from slack_sdk.web.async_client import AsyncWebClient
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
[docs]
class SlackApp:
r"""Represents a Slack app that is powered by a Slack Bolt `AsyncApp`.
This class is responsible for initializing and managing the Slack
application by setting up event handlers, running the app server, and
handling events such as messages and mentions from Slack.
Args:
token (Optional[str]): Slack API token for authentication.
scopes (Optional[str]): Slack app scopes for permissions.
signing_secret (Optional[str]): Signing secret for verifying Slack
requests.
client_id (Optional[str]): Slack app client ID.
client_secret (Optional[str]): Slack app client secret.
redirect_uri_path (str): The URI path for OAuth redirect, defaults to
"/slack/oauth_redirect".
installation_store (Optional[AsyncInstallationStore]): The installation
store for handling OAuth installations.
"""
@dependencies_required('slack_bolt')
def __init__(
self,
token: Optional[str] = None,
scopes: Optional[str] = None,
signing_secret: Optional[str] = None,
client_id: Optional[str] = None,
client_secret: Optional[str] = None,
redirect_uri_path: str = "/slack/oauth_redirect",
installation_store: Optional[AsyncInstallationStore] = None,
) -> None:
r"""Initializes the SlackApp instance by setting up the Slack Bolt app
and configuring event handlers and OAuth settings.
Args:
token (Optional[str]): The Slack API token.
scopes (Optional[str]): The scopes for Slack app permissions.
signing_secret (Optional[str]): The signing secret for verifying
requests.
client_id (Optional[str]): The Slack app client ID.
client_secret (Optional[str]): The Slack app client secret.
redirect_uri_path (str): The URI path for handling OAuth redirects
(default is "/slack/oauth_redirect").
installation_store (Optional[AsyncInstallationStore]): An optional
installation store for OAuth installations.
"""
from slack_bolt.adapter.starlette.async_handler import (
AsyncSlackRequestHandler,
)
from slack_bolt.app.async_app import AsyncApp
from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings
self.token: Optional[str] = token or os.getenv("SLACK_TOKEN")
self.scopes: Optional[str] = scopes or os.getenv("SLACK_SCOPES")
self.signing_secret: Optional[str] = signing_secret or os.getenv(
"SLACK_SIGNING_SECRET"
)
self.client_id: Optional[str] = client_id or os.getenv(
"SLACK_CLIENT_ID"
)
self.client_secret: Optional[str] = client_secret or os.getenv(
"SLACK_CLIENT_SECRET"
)
if not all([self.token, self.scopes, self.signing_secret]):
raise ValueError(
"`SLACK_TOKEN`, `SLACK_SCOPES`, and `SLACK_SIGNING_SECRET` "
"environment variables must be set. Get it here: "
"`https://api.slack.com/apps`."
)
# Setup OAuth settings if client ID and secret are provided
if self.client_id and self.client_secret:
self._app = AsyncApp(
oauth_settings=AsyncOAuthSettings(
client_id=self.client_id,
client_secret=self.client_secret,
scopes=self.scopes,
redirect_uri_path=redirect_uri_path,
),
logger=logger,
signing_secret=self.signing_secret,
installation_store=installation_store,
token=self.token,
)
else:
# Initialize Slack Bolt AsyncApp with settings
self._app = AsyncApp(
logger=logger,
signing_secret=self.signing_secret,
installation_store=installation_store,
token=self.token,
)
self._handler = AsyncSlackRequestHandler(self._app)
self.setup_handlers()
[docs]
def setup_handlers(self) -> None:
r"""Sets up the event handlers for Slack events, such as `app_mention`
and `message`.
This method registers the `app_mention` and `on_message` event handlers
with the Slack Bolt app to respond to Slack events.
"""
self._app.event("app_mention")(self.app_mention)
self._app.event("message")(self.on_message)
[docs]
def run(
self,
port: int = 3000,
path: str = "/slack/events",
host: Optional[str] = None,
) -> None:
r"""Starts the Slack Bolt app server to listen for incoming Slack
events.
Args:
port (int): The port on which the server should run (default is
3000).
path (str): The endpoint path for receiving Slack events (default
is "/slack/events").
host (Optional[str]): The hostname to bind the server (default is
None).
"""
self._app.start(port=port, path=path, host=host)
[docs]
async def handle_request(
self, request: requests.Request
) -> responses.Response:
r"""Handles incoming requests from Slack through the request handler.
Args:
request (Request): A Starlette request object representing the
incoming request.
Returns:
The response generated by the Slack Bolt handler.
"""
return await self._handler.handle(request)
[docs]
async def app_mention(
self,
context: "AsyncBoltContext",
client: "AsyncWebClient",
event: Dict[str, Any],
body: Dict[str, Any],
say: "AsyncSay",
) -> None:
r"""Event handler for `app_mention` events.
This method is triggered when someone mentions the app in Slack.
Args:
context (AsyncBoltContext): The Slack Bolt context for the event.
client (AsyncWebClient): The Slack Web API client.
event (Dict[str, Any]): The event data for the app mention.
body (Dict[str, Any]): The full request body from Slack.
say (AsyncSay): A function to send a response back to the channel.
"""
event_profile = SlackAppMentionEventProfile(**event)
event_body = SlackAppMentionEventBody(**body)
logger.info(f"app_mention, context: {context}")
logger.info(f"app_mention, client: {client}")
logger.info(f"app_mention, event_profile: {event_profile}")
logger.info(f"app_mention, event_body: {event_body}")
logger.info(f"app_mention, say: {say}")
[docs]
async def on_message(
self,
context: "AsyncBoltContext",
client: "AsyncWebClient",
event: Dict[str, Any],
body: Dict[str, Any],
say: "AsyncSay",
) -> None:
r"""Event handler for `message` events.
This method is triggered when the app receives a message in Slack.
Args:
context (AsyncBoltContext): The Slack Bolt context for the event.
client (AsyncWebClient): The Slack Web API client.
event (Dict[str, Any]): The event data for the message.
body (Dict[str, Any]): The full request body from Slack.
say (AsyncSay): A function to send a response back to the channel.
"""
await context.ack()
event_profile = SlackEventProfile(**event)
event_body = SlackEventBody(**body)
logger.info(f"on_message, context: {context}")
logger.info(f"on_message, client: {client}")
logger.info(f"on_message, event_profile: {event_profile}")
logger.info(f"on_message, event_body: {event_body}")
logger.info(f"on_message, say: {say}")
logger.info(f"Received message: {event_profile.text}")
[docs]
def mention_me(
self, context: "AsyncBoltContext", body: SlackEventBody
) -> bool:
r"""Check if the bot is mentioned in the message.
Args:
context (AsyncBoltContext): The Slack Bolt context for the event.
body (SlackEventBody): The body of the Slack event.
Returns:
bool: True if the bot is mentioned in the message, False otherwise.
"""
message = body.event.text
bot_user_id = context.bot_user_id
mention = f"<@{bot_user_id}>"
return mention in message