Module slack_bolt.request.internals

Expand source code
import json
from typing import Optional, Dict, Union, Any, Sequence
from urllib.parse import parse_qsl, parse_qs

from slack_bolt.context import BoltContext


def parse_query(query: Optional[Union[str, Dict[str, str], Dict[str, Sequence[str]]]]) -> Dict[str, Sequence[str]]:
    if query is None:
        return {}
    elif isinstance(query, str):
        return parse_qs(query, keep_blank_values=True)
    elif isinstance(query, dict) or hasattr(query, "items"):
        result: Dict[str, Sequence[str]] = {}
        for name, value in query.items():
            if isinstance(value, list):
                result[name] = value
            elif isinstance(value, str):
                result[name] = [value]
            else:
                raise ValueError(f"Unsupported type ({type(value)}) of element in headers ({query})")
        return result  # type: ignore
    else:
        raise ValueError(f"Unsupported type of query detected ({type(query)})")


def parse_body(body: str, content_type: Optional[str]) -> Dict[str, Any]:
    if not body:
        return {}
    if (content_type is not None and content_type == "application/json") or body.startswith("{"):
        return json.loads(body)
    else:
        if "payload" in body:  # This is not JSON format yet
            params = dict(parse_qsl(body, keep_blank_values=True))
            if params.get("payload") is not None:
                return json.loads(params.get("payload"))
            else:
                return {}
        else:
            return dict(parse_qsl(body, keep_blank_values=True))


def extract_is_enterprise_install(payload: Dict[str, Any]) -> Optional[bool]:
    if payload.get("authorizations") is not None and len(payload["authorizations"]) > 0:
        # To make Events API handling functioning also for shared channels,
        # we should use .authorizations[0].is_enterprise_install over .is_enterprise_install
        return extract_is_enterprise_install(payload["authorizations"][0])
    if "is_enterprise_install" in payload:
        is_enterprise_install = payload.get("is_enterprise_install")
        return is_enterprise_install is not None and (is_enterprise_install is True or is_enterprise_install == "true")
    return False


def extract_enterprise_id(payload: Dict[str, Any]) -> Optional[str]:
    org = payload.get("enterprise")
    if org is not None:
        if isinstance(org, str):
            return org
        elif "id" in org:
            return org.get("id")
    if payload.get("authorizations") is not None and len(payload["authorizations"]) > 0:
        # To make Events API handling functioning also for shared channels,
        # we should use .authorizations[0].enterprise_id over .enterprise_id
        return extract_enterprise_id(payload["authorizations"][0])
    if "enterprise_id" in payload:
        return payload.get("enterprise_id")
    if payload.get("team") is not None and "enterprise_id" in payload["team"]:
        # In the case where the type is view_submission
        return payload["team"].get("enterprise_id")
    if payload.get("event") is not None:
        return extract_enterprise_id(payload["event"])
    return None


def extract_actor_enterprise_id(payload: Dict[str, Any]) -> Optional[str]:
    if payload.get("is_ext_shared_channel") is True:
        if payload.get("type") == "event_callback":
            # For safety, we don't set actor IDs for the events like "file_shared",
            # which do not provide any team ID in $.event data. In the case, the IDs cannot be correct.
            event_team_id = payload.get("event", {}).get("user_team") or payload.get("event", {}).get("team")
            if event_team_id is not None and str(event_team_id).startswith("E"):
                return event_team_id
            if event_team_id == payload.get("team_id"):
                return payload.get("enterprise_id")
            return None
    return extract_enterprise_id(payload)


def extract_team_id(payload: Dict[str, Any]) -> Optional[str]:
    if payload.get("view", {}).get("app_installed_team_id") is not None:
        # view_submission payloads can have `view.app_installed_team_id` when a modal view that was opened
        # in a different workspace via some operations inside a Slack Connect channel.
        # Note that the same for enterprise_id does not exist. When you need to know the enterprise_id as well,
        # you have to run some query toward your InstallationStore to know the org where the team_id belongs to.
        return payload.get("view")["app_installed_team_id"]
    if payload.get("team") is not None:
        # With org-wide installations, payload.team in interactivity payloads can be None
        # You need to extract either payload.user.team_id or payload.view.team_id as below
        team = payload.get("team")
        if isinstance(team, str):
            return team
        elif team and "id" in team:
            return team.get("id")
    if payload.get("authorizations") is not None and len(payload["authorizations"]) > 0:
        # To make Events API handling functioning also for shared channels,
        # we should use .authorizations[0].team_id over .team_id
        return extract_team_id(payload["authorizations"][0])
    if "team_id" in payload:
        return payload.get("team_id")
    if payload.get("event") is not None:
        return extract_team_id(payload["event"])
    if payload.get("user") is not None:
        return payload.get("user")["team_id"]
    if payload.get("view") is not None:
        return payload.get("view")["team_id"]
    return None


def extract_actor_team_id(payload: Dict[str, Any]) -> Optional[str]:
    if payload.get("is_ext_shared_channel") is True:
        if payload.get("type") == "event_callback":
            event_type = payload.get("event", {}).get("type")
            if event_type == "app_mention":
                # The $.event.user_team can be an enterprise_id in app_mention events.
                # In the scenario, there is no way to retrieve actor_team_id as of March 2023
                user_team = payload.get("event", {}).get("user_team")
                if user_team is None:
                    # working with an app installed in this user's org/workspace side
                    return payload.get("event", {}).get("team")
                if str(user_team).startswith("T"):
                    # interacting from a connected non-grid workspace
                    return user_team
                # Interacting from a connected grid workspace; in this case, team_id cannot be resolved as of March 2023
                return None
            # For safety, we don't set actor IDs for the events like "file_shared",
            # which do not provide any team ID in $.event data. In the case, the IDs cannot be correct.
            event_user_team = payload.get("event", {}).get("user_team")
            if event_user_team is not None:
                if str(event_user_team).startswith("T"):
                    return event_user_team
                elif str(event_user_team).startswith("E"):
                    if event_user_team == payload.get("enterprise_id"):
                        return payload.get("team_id")
                    elif event_user_team == payload.get("context_enterprise_id"):
                        return payload.get("context_team_id")

            event_team = payload.get("event", {}).get("team")
            if event_team is not None:
                if str(event_team).startswith("T"):
                    return event_team
                elif str(event_team).startswith("E"):
                    if event_team == payload.get("enterprise_id"):
                        return payload.get("team_id")
                    elif event_team == payload.get("context_enterprise_id"):
                        return payload.get("context_team_id")
            return None

    return extract_team_id(payload)


def extract_user_id(payload: Dict[str, Any]) -> Optional[str]:
    user = payload.get("user")
    if user is not None:
        if isinstance(user, str):
            return user
        elif "id" in user:
            return user.get("id")
    if "user_id" in payload:
        return payload.get("user_id")
    if payload.get("event") is not None:
        return extract_user_id(payload["event"])
    if payload.get("message") is not None:
        # message_changed: body["event"]["message"]
        return extract_user_id(payload["message"])
    if payload.get("previous_message") is not None:
        # message_deleted: body["event"]["previous_message"]
        return extract_user_id(payload["previous_message"])
    return None


def extract_actor_user_id(payload: Dict[str, Any]) -> Optional[str]:
    if payload.get("is_ext_shared_channel") is True:
        if payload.get("type") == "event_callback":
            event = payload.get("event")
            if event is None:
                return None
            if extract_actor_enterprise_id(payload) is None and extract_actor_team_id(payload) is None:
                # When both enterprise_id and team_id are not identified, we skip returning user_id too for safety
                return None
            return event.get("user") or event.get("user_id")
    return extract_user_id(payload)


def extract_channel_id(payload: Dict[str, Any]) -> Optional[str]:
    channel = payload.get("channel")
    if channel is not None:
        if isinstance(channel, str):
            return channel
        elif "id" in channel:
            return channel.get("id")
    if "channel_id" in payload:
        return payload.get("channel_id")
    if payload.get("event") is not None:
        return extract_channel_id(payload["event"])
    if payload.get("item") is not None:
        # reaction_added: body["event"]["item"]
        return extract_channel_id(payload["item"])
    return None


def build_context(context: BoltContext, body: Dict[str, Any]) -> BoltContext:
    context["is_enterprise_install"] = extract_is_enterprise_install(body)
    enterprise_id = extract_enterprise_id(body)
    if enterprise_id:
        context["enterprise_id"] = enterprise_id
    team_id = extract_team_id(body)
    if team_id:
        context["team_id"] = team_id
    user_id = extract_user_id(body)
    if user_id:
        context["user_id"] = user_id
    # Actor IDs are useful for Events API on a Slack Connect channel
    actor_enterprise_id = extract_actor_enterprise_id(body)
    if actor_enterprise_id:
        context["actor_enterprise_id"] = actor_enterprise_id
    actor_team_id = extract_actor_team_id(body)
    if actor_team_id:
        context["actor_team_id"] = actor_team_id
    actor_user_id = extract_actor_user_id(body)
    if actor_user_id:
        context["actor_user_id"] = actor_user_id
    channel_id = extract_channel_id(body)
    if channel_id:
        context["channel_id"] = channel_id
    if "response_url" in body:
        context["response_url"] = body["response_url"]
    elif "response_urls" in body:
        # In the case where response_url_enabled: true in a modal exists
        response_urls = body["response_urls"]
        if len(response_urls) >= 1:
            if len(response_urls) > 1:
                context.logger.debug(debug_multiple_response_urls_detected())
            response_url = response_urls[0].get("response_url")
            context["response_url"] = response_url
    return context


def extract_content_type(headers: Dict[str, Sequence[str]]) -> Optional[str]:
    content_type: Optional[str] = headers.get("content-type", [None])[0]
    if content_type:
        return content_type.split(";")[0]
    return None


def build_normalized_headers(headers: Optional[Dict[str, Union[str, Sequence[str]]]]) -> Dict[str, Sequence[str]]:
    normalized_headers: Dict[str, Sequence[str]] = {}
    if headers is not None:
        for key, value in headers.items():
            normalized_name = key.lower()
            if isinstance(value, list):
                normalized_headers[normalized_name] = value
            elif isinstance(value, str):
                normalized_headers[normalized_name] = [value]
            else:
                raise ValueError(f"Unsupported type ({type(value)}) of element in headers ({headers})")
    return normalized_headers  # type: ignore


def error_message_raw_body_required_in_http_mode() -> str:
    return "`body` must be a raw string data when running in the HTTP server mode"


def debug_multiple_response_urls_detected() -> str:
    return (
        "`response_urls` in the body has multiple URLs in it. "
        "If you would like to use non-primary one, "
        "please manually extract the one from body['response_urls']."
    )

Functions

def build_context(context: BoltContext, body: Dict[str, Any]) ‑> BoltContext
Expand source code
def build_context(context: BoltContext, body: Dict[str, Any]) -> BoltContext:
    context["is_enterprise_install"] = extract_is_enterprise_install(body)
    enterprise_id = extract_enterprise_id(body)
    if enterprise_id:
        context["enterprise_id"] = enterprise_id
    team_id = extract_team_id(body)
    if team_id:
        context["team_id"] = team_id
    user_id = extract_user_id(body)
    if user_id:
        context["user_id"] = user_id
    # Actor IDs are useful for Events API on a Slack Connect channel
    actor_enterprise_id = extract_actor_enterprise_id(body)
    if actor_enterprise_id:
        context["actor_enterprise_id"] = actor_enterprise_id
    actor_team_id = extract_actor_team_id(body)
    if actor_team_id:
        context["actor_team_id"] = actor_team_id
    actor_user_id = extract_actor_user_id(body)
    if actor_user_id:
        context["actor_user_id"] = actor_user_id
    channel_id = extract_channel_id(body)
    if channel_id:
        context["channel_id"] = channel_id
    if "response_url" in body:
        context["response_url"] = body["response_url"]
    elif "response_urls" in body:
        # In the case where response_url_enabled: true in a modal exists
        response_urls = body["response_urls"]
        if len(response_urls) >= 1:
            if len(response_urls) > 1:
                context.logger.debug(debug_multiple_response_urls_detected())
            response_url = response_urls[0].get("response_url")
            context["response_url"] = response_url
    return context
def build_normalized_headers(headers: Optional[Dict[str, Union[str, Sequence[str]]]]) ‑> Dict[str, Sequence[str]]
Expand source code
def build_normalized_headers(headers: Optional[Dict[str, Union[str, Sequence[str]]]]) -> Dict[str, Sequence[str]]:
    normalized_headers: Dict[str, Sequence[str]] = {}
    if headers is not None:
        for key, value in headers.items():
            normalized_name = key.lower()
            if isinstance(value, list):
                normalized_headers[normalized_name] = value
            elif isinstance(value, str):
                normalized_headers[normalized_name] = [value]
            else:
                raise ValueError(f"Unsupported type ({type(value)}) of element in headers ({headers})")
    return normalized_headers  # type: ignore
def debug_multiple_response_urls_detected() ‑> str
Expand source code
def debug_multiple_response_urls_detected() -> str:
    return (
        "`response_urls` in the body has multiple URLs in it. "
        "If you would like to use non-primary one, "
        "please manually extract the one from body['response_urls']."
    )
def error_message_raw_body_required_in_http_mode() ‑> str
Expand source code
def error_message_raw_body_required_in_http_mode() -> str:
    return "`body` must be a raw string data when running in the HTTP server mode"
def extract_actor_enterprise_id(payload: Dict[str, Any]) ‑> Optional[str]
Expand source code
def extract_actor_enterprise_id(payload: Dict[str, Any]) -> Optional[str]:
    if payload.get("is_ext_shared_channel") is True:
        if payload.get("type") == "event_callback":
            # For safety, we don't set actor IDs for the events like "file_shared",
            # which do not provide any team ID in $.event data. In the case, the IDs cannot be correct.
            event_team_id = payload.get("event", {}).get("user_team") or payload.get("event", {}).get("team")
            if event_team_id is not None and str(event_team_id).startswith("E"):
                return event_team_id
            if event_team_id == payload.get("team_id"):
                return payload.get("enterprise_id")
            return None
    return extract_enterprise_id(payload)
def extract_actor_team_id(payload: Dict[str, Any]) ‑> Optional[str]
Expand source code
def extract_actor_team_id(payload: Dict[str, Any]) -> Optional[str]:
    if payload.get("is_ext_shared_channel") is True:
        if payload.get("type") == "event_callback":
            event_type = payload.get("event", {}).get("type")
            if event_type == "app_mention":
                # The $.event.user_team can be an enterprise_id in app_mention events.
                # In the scenario, there is no way to retrieve actor_team_id as of March 2023
                user_team = payload.get("event", {}).get("user_team")
                if user_team is None:
                    # working with an app installed in this user's org/workspace side
                    return payload.get("event", {}).get("team")
                if str(user_team).startswith("T"):
                    # interacting from a connected non-grid workspace
                    return user_team
                # Interacting from a connected grid workspace; in this case, team_id cannot be resolved as of March 2023
                return None
            # For safety, we don't set actor IDs for the events like "file_shared",
            # which do not provide any team ID in $.event data. In the case, the IDs cannot be correct.
            event_user_team = payload.get("event", {}).get("user_team")
            if event_user_team is not None:
                if str(event_user_team).startswith("T"):
                    return event_user_team
                elif str(event_user_team).startswith("E"):
                    if event_user_team == payload.get("enterprise_id"):
                        return payload.get("team_id")
                    elif event_user_team == payload.get("context_enterprise_id"):
                        return payload.get("context_team_id")

            event_team = payload.get("event", {}).get("team")
            if event_team is not None:
                if str(event_team).startswith("T"):
                    return event_team
                elif str(event_team).startswith("E"):
                    if event_team == payload.get("enterprise_id"):
                        return payload.get("team_id")
                    elif event_team == payload.get("context_enterprise_id"):
                        return payload.get("context_team_id")
            return None

    return extract_team_id(payload)
def extract_actor_user_id(payload: Dict[str, Any]) ‑> Optional[str]
Expand source code
def extract_actor_user_id(payload: Dict[str, Any]) -> Optional[str]:
    if payload.get("is_ext_shared_channel") is True:
        if payload.get("type") == "event_callback":
            event = payload.get("event")
            if event is None:
                return None
            if extract_actor_enterprise_id(payload) is None and extract_actor_team_id(payload) is None:
                # When both enterprise_id and team_id are not identified, we skip returning user_id too for safety
                return None
            return event.get("user") or event.get("user_id")
    return extract_user_id(payload)
def extract_channel_id(payload: Dict[str, Any]) ‑> Optional[str]
Expand source code
def extract_channel_id(payload: Dict[str, Any]) -> Optional[str]:
    channel = payload.get("channel")
    if channel is not None:
        if isinstance(channel, str):
            return channel
        elif "id" in channel:
            return channel.get("id")
    if "channel_id" in payload:
        return payload.get("channel_id")
    if payload.get("event") is not None:
        return extract_channel_id(payload["event"])
    if payload.get("item") is not None:
        # reaction_added: body["event"]["item"]
        return extract_channel_id(payload["item"])
    return None
def extract_content_type(headers: Dict[str, Sequence[str]]) ‑> Optional[str]
Expand source code
def extract_content_type(headers: Dict[str, Sequence[str]]) -> Optional[str]:
    content_type: Optional[str] = headers.get("content-type", [None])[0]
    if content_type:
        return content_type.split(";")[0]
    return None
def extract_enterprise_id(payload: Dict[str, Any]) ‑> Optional[str]
Expand source code
def extract_enterprise_id(payload: Dict[str, Any]) -> Optional[str]:
    org = payload.get("enterprise")
    if org is not None:
        if isinstance(org, str):
            return org
        elif "id" in org:
            return org.get("id")
    if payload.get("authorizations") is not None and len(payload["authorizations"]) > 0:
        # To make Events API handling functioning also for shared channels,
        # we should use .authorizations[0].enterprise_id over .enterprise_id
        return extract_enterprise_id(payload["authorizations"][0])
    if "enterprise_id" in payload:
        return payload.get("enterprise_id")
    if payload.get("team") is not None and "enterprise_id" in payload["team"]:
        # In the case where the type is view_submission
        return payload["team"].get("enterprise_id")
    if payload.get("event") is not None:
        return extract_enterprise_id(payload["event"])
    return None
def extract_is_enterprise_install(payload: Dict[str, Any]) ‑> Optional[bool]
Expand source code
def extract_is_enterprise_install(payload: Dict[str, Any]) -> Optional[bool]:
    if payload.get("authorizations") is not None and len(payload["authorizations"]) > 0:
        # To make Events API handling functioning also for shared channels,
        # we should use .authorizations[0].is_enterprise_install over .is_enterprise_install
        return extract_is_enterprise_install(payload["authorizations"][0])
    if "is_enterprise_install" in payload:
        is_enterprise_install = payload.get("is_enterprise_install")
        return is_enterprise_install is not None and (is_enterprise_install is True or is_enterprise_install == "true")
    return False
def extract_team_id(payload: Dict[str, Any]) ‑> Optional[str]
Expand source code
def extract_team_id(payload: Dict[str, Any]) -> Optional[str]:
    if payload.get("view", {}).get("app_installed_team_id") is not None:
        # view_submission payloads can have `view.app_installed_team_id` when a modal view that was opened
        # in a different workspace via some operations inside a Slack Connect channel.
        # Note that the same for enterprise_id does not exist. When you need to know the enterprise_id as well,
        # you have to run some query toward your InstallationStore to know the org where the team_id belongs to.
        return payload.get("view")["app_installed_team_id"]
    if payload.get("team") is not None:
        # With org-wide installations, payload.team in interactivity payloads can be None
        # You need to extract either payload.user.team_id or payload.view.team_id as below
        team = payload.get("team")
        if isinstance(team, str):
            return team
        elif team and "id" in team:
            return team.get("id")
    if payload.get("authorizations") is not None and len(payload["authorizations"]) > 0:
        # To make Events API handling functioning also for shared channels,
        # we should use .authorizations[0].team_id over .team_id
        return extract_team_id(payload["authorizations"][0])
    if "team_id" in payload:
        return payload.get("team_id")
    if payload.get("event") is not None:
        return extract_team_id(payload["event"])
    if payload.get("user") is not None:
        return payload.get("user")["team_id"]
    if payload.get("view") is not None:
        return payload.get("view")["team_id"]
    return None
def extract_user_id(payload: Dict[str, Any]) ‑> Optional[str]
Expand source code
def extract_user_id(payload: Dict[str, Any]) -> Optional[str]:
    user = payload.get("user")
    if user is not None:
        if isinstance(user, str):
            return user
        elif "id" in user:
            return user.get("id")
    if "user_id" in payload:
        return payload.get("user_id")
    if payload.get("event") is not None:
        return extract_user_id(payload["event"])
    if payload.get("message") is not None:
        # message_changed: body["event"]["message"]
        return extract_user_id(payload["message"])
    if payload.get("previous_message") is not None:
        # message_deleted: body["event"]["previous_message"]
        return extract_user_id(payload["previous_message"])
    return None
def parse_body(body: str, content_type: Optional[str]) ‑> Dict[str, Any]
Expand source code
def parse_body(body: str, content_type: Optional[str]) -> Dict[str, Any]:
    if not body:
        return {}
    if (content_type is not None and content_type == "application/json") or body.startswith("{"):
        return json.loads(body)
    else:
        if "payload" in body:  # This is not JSON format yet
            params = dict(parse_qsl(body, keep_blank_values=True))
            if params.get("payload") is not None:
                return json.loads(params.get("payload"))
            else:
                return {}
        else:
            return dict(parse_qsl(body, keep_blank_values=True))
def parse_query(query: Union[str, Dict[str, str], Dict[str, Sequence[str]], ForwardRef(None)]) ‑> Dict[str, Sequence[str]]
Expand source code
def parse_query(query: Optional[Union[str, Dict[str, str], Dict[str, Sequence[str]]]]) -> Dict[str, Sequence[str]]:
    if query is None:
        return {}
    elif isinstance(query, str):
        return parse_qs(query, keep_blank_values=True)
    elif isinstance(query, dict) or hasattr(query, "items"):
        result: Dict[str, Sequence[str]] = {}
        for name, value in query.items():
            if isinstance(value, list):
                result[name] = value
            elif isinstance(value, str):
                result[name] = [value]
            else:
                raise ValueError(f"Unsupported type ({type(value)}) of element in headers ({query})")
        return result  # type: ignore
    else:
        raise ValueError(f"Unsupported type of query detected ({type(query)})")