import asyncio from datetime import datetime, timezone from typing import Annotated, Optional, cast from arcade.sdk import ToolContext, tool from arcade.sdk.auth import Slack from arcade.sdk.errors import RetryableToolError, ToolExecutionError from slack_sdk.errors import SlackApiError from slack_sdk.web.async_client import AsyncWebClient from arcade_slack.constants import MAX_PAGINATION_TIMEOUT_SECONDS from arcade_slack.exceptions import ( ItemNotFoundError, UsernameNotFoundError, ) from arcade_slack.models import ( ConversationType, SlackUserList, ) from arcade_slack.tools.users import get_user_info_by_id, list_users from arcade_slack.utils import ( async_paginate, convert_conversation_type_to_slack_name, convert_datetime_to_unix_timestamp, convert_relative_datetime_to_unix_timestamp, enrich_message_datetime, extract_conversation_metadata, format_users, get_user_by_username, retrieve_conversations_by_user_ids, ) @tool( requires_auth=Slack( scopes=[ "chat:write", "im:write", "users.profile:read", "users:read", ], ) ) async def send_dm_to_user( context: ToolContext, user_name: Annotated[ str, ( "The Slack username of the person you want to message. " "Slack usernames are ALWAYS lowercase." ), ], message: Annotated[str, "The message you want to send"], ) -> Annotated[dict, "The response from the Slack API"]: """Send a direct message to a user in Slack.""" token = ( context.authorization.token if context.authorization and context.authorization.token else "" ) slackClient = AsyncWebClient(token=token) try: # Step 1: Retrieve the user's Slack ID based on their username user_list_response = await slackClient.users_list() user_id = None for user in user_list_response["members"]: response_user_name = ( "" if not isinstance(user.get("name"), str) else user["name"].lower() ) if response_user_name == user_name.lower(): user_id = user["id"] break if not user_id: raise RetryableToolError( "User not found", developer_message=f"User with username '{user_name}' not found.", additional_prompt_content=format_users(cast(SlackUserList, user_list_response)), retry_after_ms=500, # Play nice with Slack API rate limits ) # Step 2: Retrieve the DM channel ID with the user im_response = await slackClient.conversations_open(users=[user_id]) dm_channel_id = im_response["channel"]["id"] # Step 3: Send the message as if it's from you (because we're using a user token) response = await slackClient.chat_postMessage(channel=dm_channel_id, text=message) except SlackApiError as e: error_message = e.response["error"] if "error" in e.response else str(e) raise ToolExecutionError( "Error sending message", developer_message=f"Slack API Error: {error_message}", ) else: return {"response": response.data} @tool( requires_auth=Slack( scopes=[ "chat:write", "channels:read", "groups:read", ], ) ) async def send_message_to_channel( context: ToolContext, channel_name: Annotated[str, "The Slack channel name where you want to send the message. "], message: Annotated[str, "The message you want to send"], ) -> Annotated[dict, "The response from the Slack API"]: """Send a message to a channel in Slack.""" try: slackClient = AsyncWebClient( token=context.authorization.token if context.authorization and context.authorization.token else "" ) channel = await get_channel_metadata_by_name(context=context, channel_name=channel_name) channel_id = channel["id"] response = await slackClient.chat_postMessage(channel=channel_id, text=message) except SlackApiError as e: error_message = e.response["error"] if "error" in e.response else str(e) raise ToolExecutionError( "Error sending message", developer_message=f"Slack API Error: {error_message}", ) else: return {"response": response.data} @tool( requires_auth=Slack( scopes=[ "channels:read", "groups:read", "im:read", "mpim:read", "users:read", "users:read.email", ], ) ) async def get_members_in_conversation_by_id( context: ToolContext, conversation_id: Annotated[str, "The ID of the conversation to get members for"], limit: Annotated[Optional[int], "The maximum number of members to return."] = None, next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None, ) -> Annotated[dict, "Information about each member in the conversation"]: """Get the members of a conversation in Slack by the conversation's ID.""" token = ( context.authorization.token if context.authorization and context.authorization.token else "" ) slackClient = AsyncWebClient(token=token) try: member_ids, next_cursor = await async_paginate( slackClient.conversations_members, "members", limit=limit, next_cursor=next_cursor, channel=conversation_id, ) except SlackApiError as e: if e.response["error"] == "channel_not_found": conversations = await list_conversations_metadata(context) available_conversations = ", ".join( f"{conversation['id']} ({conversation['name']})" for conversation in conversations["conversations"] ) raise RetryableToolError( "Conversation not found", developer_message=f"Conversation with ID '{conversation_id}' not found.", additional_prompt_content=f"Available conversations: {available_conversations}", retry_after_ms=500, ) # Get the members' info # TODO: This will probably hit rate limits. We should probably call list_users() and # then filter the results instead. members = await asyncio.gather(*[ get_user_info_by_id(context, member_id) for member_id in member_ids ]) return { "members": [member for member in members if not member.get("is_bot")], "next_cursor": next_cursor, } @tool( requires_auth=Slack( scopes=[ "channels:read", "groups:read", "im:read", "mpim:read", "users:read", "users:read.email", ], ) ) async def get_members_in_channel_by_name( context: ToolContext, channel_name: Annotated[str, "The name of the channel to get members for"], limit: Annotated[Optional[int], "The maximum number of members to return."] = None, next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None, ) -> Annotated[dict, "The channel members' IDs and Names"]: """Get the members of a conversation in Slack by the conversation's name.""" channel = await get_channel_metadata_by_name(context=context, channel_name=channel_name) return await get_members_in_conversation_by_id( # type: ignore[no-any-return] context=context, conversation_id=channel["id"], limit=limit, next_cursor=next_cursor, ) # TODO: make the function accept a current unix timestamp argument to allow testing without # mocking. Have to wait until arcade.core.annotations.Inferrable is implemented, so that we # can avoid exposing this arg to the LLM. @tool( requires_auth=Slack( scopes=["channels:history", "groups:history", "im:history", "mpim:history"], ) ) async def get_messages_in_conversation_by_id( context: ToolContext, conversation_id: Annotated[str, "The ID of the conversation to get history for"], oldest_relative: Annotated[ Optional[str], ( "The oldest message to include in the results, specified as a time offset from the " "current time in the format 'DD:HH:MM'" ), ] = None, latest_relative: Annotated[ Optional[str], ( "The latest message to include in the results, specified as a time offset from the " "current time in the format 'DD:HH:MM'" ), ] = None, oldest_datetime: Annotated[ Optional[str], ( "The oldest message to include in the results, specified as a datetime object in the " "format 'YYYY-MM-DD HH:MM:SS'" ), ] = None, latest_datetime: Annotated[ Optional[str], ( "The latest message to include in the results, specified as a datetime object in the " "format 'YYYY-MM-DD HH:MM:SS'" ), ] = None, limit: Annotated[Optional[int], "The maximum number of messages to return."] = None, next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None, ) -> Annotated[ dict, ( "The messages in a conversation and next cursor for paginating results (when " "there are additional messages to retrieve)." ), ]: """Get the messages in a conversation by the conversation's ID. A conversation can be a channel, a DM, or a group DM. To filter by an absolute datetime, use 'oldest_datetime' and/or 'latest_datetime'. If only 'oldest_datetime' is provided, it returns messages from the oldest_datetime to the current time. If only 'latest_datetime' is provided, it returns messages since the beginning of the conversation to the latest_datetime. To filter by a relative datetime (e.g. 3 days ago, 1 hour ago, etc.), use 'oldest_relative' and/or 'latest_relative'. If only 'oldest_relative' is provided, it returns messages from the oldest_relative to the current time. If only 'latest_relative' is provided, it returns messages from the current time to the latest_relative. Do not provide both 'oldest_datetime' and 'oldest_relative' or both 'latest_datetime' and 'latest_relative'. Leave all arguments with the default None to get messages without date/time filtering""" error_message = None if oldest_datetime and oldest_relative: error_message = "Cannot specify both 'oldest_datetime' and 'oldest_relative'." if latest_datetime and latest_relative: error_message = "Cannot specify both 'latest_datetime' and 'latest_relative'." if error_message: raise ToolExecutionError(error_message, developer_message=error_message) current_unix_timestamp = int(datetime.now(timezone.utc).timestamp()) if latest_relative: latest_timestamp = convert_relative_datetime_to_unix_timestamp( latest_relative, current_unix_timestamp ) elif latest_datetime: latest_timestamp = convert_datetime_to_unix_timestamp(latest_datetime) else: latest_timestamp = None if oldest_relative: oldest_timestamp = convert_relative_datetime_to_unix_timestamp( oldest_relative, current_unix_timestamp ) elif oldest_datetime: oldest_timestamp = convert_datetime_to_unix_timestamp(oldest_datetime) else: oldest_timestamp = None token = ( context.authorization.token if context.authorization and context.authorization.token else "" ) slackClient = AsyncWebClient(token=token) datetime_args = {} if oldest_timestamp: datetime_args["oldest"] = oldest_timestamp if latest_timestamp: datetime_args["latest"] = latest_timestamp response, next_cursor = await async_paginate( slackClient.conversations_history, "messages", limit=limit, next_cursor=next_cursor, channel=conversation_id, include_all_metadata=True, inclusive=True, # Include messages at the start and end of the time range **datetime_args, ) messages = [enrich_message_datetime(message) for message in response] return {"messages": messages, "next_cursor": next_cursor} # TODO: make the function accept a current unix timestamp argument to allow testing without # mocking. Have to wait until arcade.core.annotations.Inferrable is implemented, so that we # can avoid exposing this arg to the LLM. @tool( requires_auth=Slack( scopes=[ "channels:history", "channels:read", "groups:history", "groups:read", "im:history", "im:read", "mpim:history", "mpim:read", ], ) ) async def get_messages_in_channel_by_name( context: ToolContext, channel_name: Annotated[str, "The name of the channel"], oldest_relative: Annotated[ Optional[str], ( "The oldest message to include in the results, specified as a time offset from the " "current time in the format 'DD:HH:MM'" ), ] = None, latest_relative: Annotated[ Optional[str], ( "The latest message to include in the results, specified as a time offset from the " "current time in the format 'DD:HH:MM'" ), ] = None, oldest_datetime: Annotated[ Optional[str], ( "The oldest message to include in the results, specified as a datetime object in the " "format 'YYYY-MM-DD HH:MM:SS'" ), ] = None, latest_datetime: Annotated[ Optional[str], ( "The latest message to include in the results, specified as a datetime object in the " "format 'YYYY-MM-DD HH:MM:SS'" ), ] = None, limit: Annotated[Optional[int], "The maximum number of messages to return."] = None, next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None, ) -> Annotated[ dict, ( "The messages in a channel and next cursor for paginating results (when " "there are additional messages to retrieve)." ), ]: """Get the messages in a channel by the channel's name. To filter messages by an absolute datetime, use 'oldest_datetime' and/or 'latest_datetime'. If only 'oldest_datetime' is provided, it will return messages from the oldest_datetime to the current time. If only 'latest_datetime' is provided, it will return messages since the beginning of the channel to the latest_datetime. To filter messages by a relative datetime (e.g. 3 days ago, 1 hour ago, etc.), use 'oldest_relative' and/or 'latest_relative'. If only 'oldest_relative' is provided, it will return messages from the oldest_relative to the current time. If only 'latest_relative' is provided, it will return messages from the current time to the latest_relative. Do not provide both 'oldest_datetime' and 'oldest_relative' or both 'latest_datetime' and 'latest_relative'. Leave all arguments with the default None to get messages without date/time filtering""" channel = await get_channel_metadata_by_name(context=context, channel_name=channel_name) return await get_messages_in_conversation_by_id( # type: ignore[no-any-return] context=context, conversation_id=channel["id"], oldest_relative=oldest_relative, latest_relative=latest_relative, oldest_datetime=oldest_datetime, latest_datetime=latest_datetime, limit=limit, next_cursor=next_cursor, ) @tool(requires_auth=Slack(scopes=["im:history", "im:read"])) async def get_messages_in_direct_message_conversation_by_username( context: ToolContext, username: Annotated[str, "The username of the user to get messages from"], oldest_relative: Annotated[ Optional[str], ( "The oldest message to include in the results, specified as a time offset from the " "current time in the format 'DD:HH:MM'" ), ] = None, latest_relative: Annotated[ Optional[str], ( "The latest message to include in the results, specified as a time offset from the " "current time in the format 'DD:HH:MM'" ), ] = None, oldest_datetime: Annotated[ Optional[str], ( "The oldest message to include in the results, specified as a datetime object in the " "format 'YYYY-MM-DD HH:MM:SS'" ), ] = None, latest_datetime: Annotated[ Optional[str], ( "The latest message to include in the results, specified as a datetime object in the " "format 'YYYY-MM-DD HH:MM:SS'" ), ] = None, limit: Annotated[Optional[int], "The maximum number of messages to return."] = None, next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None, ) -> Annotated[ dict, ( "The messages in a direct message conversation and next cursor for paginating results " "when there are additional messages to retrieve." ), ]: """Get the messages in a direct conversation by the user's name. To filter messages by an absolute datetime, use 'oldest_datetime' and/or 'latest_datetime'. If only 'oldest_datetime' is provided, it will return messages from the oldest_datetime to the current time. If only 'latest_datetime' is provided, it will return messages since the beginning of the conversation to the latest_datetime. To filter messages by a relative datetime (e.g. 3 days ago, 1 hour ago, etc.), use 'oldest_relative' and/or 'latest_relative'. If only 'oldest_relative' is provided, it will return messages from the oldest_relative to the current time. If only 'latest_relative' is provided, it will return messages from the current time to the latest_relative. Do not provide both 'oldest_datetime' and 'oldest_relative' or both 'latest_datetime' and 'latest_relative'. Leave all arguments with the default None to get messages without date/time filtering""" direct_conversation = await get_direct_message_conversation_metadata_by_username( context=context, username=username ) return await get_messages_in_conversation_by_id( # type: ignore[no-any-return] context=context, conversation_id=direct_conversation["id"], oldest_relative=oldest_relative, latest_relative=latest_relative, oldest_datetime=oldest_datetime, latest_datetime=latest_datetime, limit=limit, next_cursor=next_cursor, ) @tool(requires_auth=Slack(scopes=["im:history", "im:read"])) async def get_messages_in_multi_person_dm_conversation_by_usernames( context: ToolContext, usernames: Annotated[list[str], "The usernames of the users to get messages from"], oldest_relative: Annotated[ Optional[str], ( "The oldest message to include in the results, specified as a time offset from the " "current time in the format 'DD:HH:MM'" ), ] = None, latest_relative: Annotated[ Optional[str], ( "The latest message to include in the results, specified as a time offset from the " "current time in the format 'DD:HH:MM'" ), ] = None, oldest_datetime: Annotated[ Optional[str], ( "The oldest message to include in the results, specified as a datetime object in the " "format 'YYYY-MM-DD HH:MM:SS'" ), ] = None, latest_datetime: Annotated[ Optional[str], ( "The latest message to include in the results, specified as a datetime object in the " "format 'YYYY-MM-DD HH:MM:SS'" ), ] = None, limit: Annotated[Optional[int], "The maximum number of messages to return."] = None, next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None, ) -> Annotated[ dict, ( "The messages in a multi-person direct message conversation and next cursor for " "paginating results (when there are additional messages to retrieve)." ), ]: """Get the messages in a multi-person direct message conversation by the usernames. To filter messages by an absolute datetime, use 'oldest_datetime' and/or 'latest_datetime'. If only 'oldest_datetime' is provided, it will return messages from the oldest_datetime to the current time. If only 'latest_datetime' is provided, it will return messages since the beginning of the conversation to the latest_datetime. To filter messages by a relative datetime (e.g. 3 days ago, 1 hour ago, etc.), use 'oldest_relative' and/or 'latest_relative'. If only 'oldest_relative' is provided, it will return messages from the oldest_relative to the current time. If only 'latest_relative' is provided, it will return messages from the current time to the latest_relative. Do not provide both 'oldest_datetime' and 'oldest_relative' or both 'latest_datetime' and 'latest_relative'. Leave all arguments with the default None to get messages without date/time filtering""" direct_conversation = await get_multi_person_dm_conversation_metadata_by_usernames( context=context, usernames=usernames ) return await get_messages_in_conversation_by_id( # type: ignore[no-any-return] context=context, conversation_id=direct_conversation["id"], oldest_relative=oldest_relative, latest_relative=latest_relative, oldest_datetime=oldest_datetime, latest_datetime=latest_datetime, limit=limit, next_cursor=next_cursor, ) @tool( requires_auth=Slack( scopes=["channels:read", "groups:read", "im:read", "mpim:read"], ) ) async def get_conversation_metadata_by_id( context: ToolContext, conversation_id: Annotated[str, "The ID of the conversation to get metadata for"], ) -> Annotated[dict, "The conversation metadata"]: """Get the metadata of a conversation in Slack searching by its ID. This tool does not return the messages in a conversation. To get the messages, use the `get_messages_in_conversation_by_id` tool.""" token = ( context.authorization.token if context.authorization and context.authorization.token else "" ) slackClient = AsyncWebClient(token=token) try: response = await slackClient.conversations_info( channel=conversation_id, include_locale=True, include_num_members=True, ) except SlackApiError as e: if e.response.get("error") == "channel_not_found": conversations = await list_conversations_metadata(context) available_conversations = ", ".join( f"{conversation['id']} ({conversation['name']})" for conversation in conversations["conversations"] ) raise RetryableToolError( "Conversation not found", developer_message=f"Conversation with ID '{conversation_id}' not found.", additional_prompt_content=f"Available conversations: {available_conversations}", retry_after_ms=500, ) raise return dict(**extract_conversation_metadata(response["channel"])) @tool(requires_auth=Slack(scopes=["channels:read"])) async def get_channel_metadata_by_name( context: ToolContext, channel_name: Annotated[str, "The name of the channel to get metadata for"], next_cursor: Annotated[ Optional[str], "The cursor to use for pagination, if continuing from a previous search.", ] = None, ) -> Annotated[dict, "The channel metadata"]: """Get the metadata of a channel in Slack searching by its name. This tool does not return the messages in a channel. To get the messages, use the `get_messages_in_channel_by_name` tool.""" channel_names: list[str] = [] async def find_channel() -> dict: nonlocal channel_names, channel_name, next_cursor should_continue = True while should_continue: response = await list_conversations_metadata(context, next_cursor=next_cursor) next_cursor = response.get("next_cursor") for channel in response["conversations"]: response_channel_name = ( "" if not isinstance(channel.get("name"), str) else channel["name"].lower() ) if response_channel_name == channel_name.lower(): return channel # type: ignore[no-any-return] channel_names.append(channel["name"]) if not next_cursor: should_continue = False raise ItemNotFoundError() try: return await asyncio.wait_for(find_channel(), timeout=MAX_PAGINATION_TIMEOUT_SECONDS) except ItemNotFoundError: raise RetryableToolError( "Channel not found", developer_message=f"Channel with name '{channel_name}' not found.", additional_prompt_content=f"Available channel names: {channel_names}", retry_after_ms=500, ) except TimeoutError: raise RetryableToolError( "Channel not found, search timed out.", developer_message=( f"Channel with name '{channel_name}' not found. " f"Search timed out after {MAX_PAGINATION_TIMEOUT_SECONDS} seconds." ), additional_prompt_content=( f"Other channel names found are: {channel_names}. " "The list is potentially non-exhaustive, since the search process timed out. " f"Use the '{list_conversations_metadata.__tool_name__}' tool to get" "a comprehensive list of channels." ), retry_after_ms=500, ) @tool(requires_auth=Slack(scopes=["im:read"])) async def get_direct_message_conversation_metadata_by_username( context: ToolContext, username: Annotated[str, "The username of the user/person to get messages with"], next_cursor: Annotated[ Optional[str], "The cursor to use for pagination, if continuing from a previous search.", ] = None, ) -> Annotated[ Optional[dict], "The direct message conversation metadata.", ]: """Get the metadata of a direct message conversation in Slack by the username. This tool does not return the messages in a conversation. To get the messages, use the `get_messages_in_direct_message_conversation_by_username` tool.""" try: token = ( context.authorization.token if context.authorization and context.authorization.token else "" ) slack_client = AsyncWebClient(token=token) current_user, list_users_response = await asyncio.gather( slack_client.auth_test(), list_users(context) ) other_user = get_user_by_username(username, list_users_response["users"]) conversations_found = await retrieve_conversations_by_user_ids( list_conversations_func=list_conversations_metadata, get_members_in_conversation_func=get_members_in_conversation_by_id, context=context, conversation_types=[ConversationType.DIRECT_MESSAGE], user_ids=[current_user["user_id"], other_user["id"]], exact_match=True, limit=1, next_cursor=next_cursor, ) return None if not conversations_found else conversations_found[0] except UsernameNotFoundError as e: raise RetryableToolError( f"Username '{e.username_not_found}' not found", developer_message=f"User with username '{e.username_not_found}' not found.", additional_prompt_content=f"Available users: {e.usernames_found}", retry_after_ms=500, ) @tool(requires_auth=Slack(scopes=["im:read"])) async def get_multi_person_dm_conversation_metadata_by_usernames( context: ToolContext, usernames: Annotated[list[str], "The usernames of the users/people to get messages with"], next_cursor: Annotated[ Optional[str], "The cursor to use for pagination, if continuing from a previous search.", ] = None, ) -> Annotated[ Optional[dict], "The multi-person direct message conversation metadata.", ]: """Get the metadata of a multi-person direct message conversation in Slack by the usernames. This tool does not return the messages in a conversation. To get the messages, use the `get_messages_in_multi_person_dm_conversation_by_usernames` tool. """ try: token = ( context.authorization.token if context.authorization and context.authorization.token else "" ) slack_client = AsyncWebClient(token=token) current_user, list_users_response = await asyncio.gather( slack_client.auth_test(), list_users(context) ) other_users = [ get_user_by_username(username, list_users_response["users"]) for username in usernames ] conversations_found = await retrieve_conversations_by_user_ids( list_conversations_func=list_conversations_metadata, get_members_in_conversation_func=get_members_in_conversation_by_id, context=context, conversation_types=[ConversationType.MULTI_PERSON_DIRECT_MESSAGE], user_ids=[ current_user["user_id"], *[user["id"] for user in other_users if user["id"] != current_user["user_id"]], ], exact_match=True, limit=1, next_cursor=next_cursor, ) if not conversations_found: raise RetryableToolError( "Conversation not found with the usernames provided", developer_message="Conversation not found with the usernames provided", retry_after_ms=500, ) return conversations_found[0] except UsernameNotFoundError as e: raise RetryableToolError( f"Username '{e.username_not_found}' not found", developer_message=f"User with username '{e.username_not_found}' not found.", additional_prompt_content=f"Available users: {e.usernames_found}", retry_after_ms=500, ) @tool( requires_auth=Slack( scopes=["channels:read", "groups:read", "im:read", "mpim:read"], ) ) async def list_conversations_metadata( context: ToolContext, conversation_types: Annotated[ Optional[list[ConversationType]], "The type(s) of conversations to list. Defaults to all types.", ] = None, limit: Annotated[Optional[int], "The maximum number of conversations to list."] = None, next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None, ) -> Annotated[ dict, ( "The conversations metadata list and a pagination 'next_cursor', if there are more " "conversations to retrieve." ), ]: """ List metadata for Slack conversations (channels and/or direct messages) that the user is a member of. """ if isinstance(conversation_types, ConversationType): conversation_types = [conversation_types] conversation_types_filter = ",".join( convert_conversation_type_to_slack_name(conv_type).value for conv_type in conversation_types or ConversationType ) token = ( context.authorization.token if context.authorization and context.authorization.token else "" ) slackClient = AsyncWebClient(token=token) results, next_cursor = await async_paginate( slackClient.conversations_list, "channels", limit=limit, next_cursor=next_cursor, types=conversation_types_filter, exclude_archived=True, ) return { "conversations": [ dict(**extract_conversation_metadata(conversation)) for conversation in results if conversation.get("is_im") or conversation.get("is_member") ], "next_cursor": next_cursor, } @tool( requires_auth=Slack( scopes=["channels:read"], ) ) async def list_public_channels_metadata( context: ToolContext, limit: Annotated[Optional[int], "The maximum number of channels to list."] = None, ) -> Annotated[dict, "The public channels"]: """List metadata for public channels in Slack that the user is a member of.""" return await list_conversations_metadata( # type: ignore[no-any-return] context, conversation_types=[ConversationType.PUBLIC_CHANNEL], limit=limit, ) @tool( requires_auth=Slack( scopes=["groups:read"], ) ) async def list_private_channels_metadata( context: ToolContext, limit: Annotated[Optional[int], "The maximum number of channels to list."] = None, ) -> Annotated[dict, "The private channels"]: """List metadata for private channels in Slack that the user is a member of.""" return await list_conversations_metadata( # type: ignore[no-any-return] context, conversation_types=[ConversationType.PRIVATE_CHANNEL], limit=limit, ) @tool( requires_auth=Slack( scopes=["mpim:read"], ) ) async def list_group_direct_message_conversations_metadata( context: ToolContext, limit: Annotated[Optional[int], "The maximum number of conversations to list."] = None, ) -> Annotated[dict, "The group direct message conversations metadata"]: """List metadata for group direct message conversations that the user is a member of.""" return await list_conversations_metadata( # type: ignore[no-any-return] context, conversation_types=[ConversationType.MULTI_PERSON_DIRECT_MESSAGE], limit=limit, ) # Note: Bots are included in the results. # Note: Direct messages with no conversation history are included in the results. @tool( requires_auth=Slack( scopes=["im:read"], ) ) async def list_direct_message_conversations_metadata( context: ToolContext, limit: Annotated[Optional[int], "The maximum number of conversations to list."] = None, ) -> Annotated[dict, "The direct message conversations metadata"]: """List metadata for direct message conversations in Slack that the user is a member of.""" response = await list_conversations_metadata( context, conversation_types=[ConversationType.DIRECT_MESSAGE], limit=limit, ) return response # type: ignore[no-any-return]