# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# There are 2 ways to use this plugin...
# Method 1: Via Webhook:
#   Visit https://my.slack.com/services/new/incoming-webhook/
#   to create a new incoming webhook for your account. You'll need to
#   follow the wizard to pre-determine the channel(s) you want your
#   message to broadcast to, and when you're complete, you will
#   recieve a URL that looks something like this:
#   https://hooks.slack.com/services/T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7
#                                       ^         ^               ^
#                                       |         |               |
#    These are important <--------------^---------^---------------^
#
# Method 2: Via a Bot:
#   1. visit: https://api.slack.com/apps?new_app=1
#   2. Pick an App Name (such as Apprise) and select your workspace.  Then
#       press 'Create App'
#   3. You'll be able to click on 'Bots' from here where you can then choose
#       to add a 'Bot User'.  Give it a name and choose 'Add Bot User'.
#   4. Now you can choose 'Install App' to which you can choose 'Install App
#       to Workspace'.
#   5. You will need to authorize the app which you get prompted to do.
#   6. Finally you'll get some important information providing you your
#      'OAuth Access Token' and 'Bot User OAuth Access Token' such as:
#        slack://{Oauth Access Token}
#
#        ... which might look something like:
#        slack://xoxp-1234-1234-1234-4ddbc191d40ee098cbaae6f3523ada2d
#        ... or:
#        slack://xoxb-1234-1234-4ddbc191d40ee098cbaae6f3523ada2d
#
#       You must at least give your bot the following access for it to
#       be useful:
#         - chat:write       - MUST be set otherwise you can not post into
#                              a channel
#         - users:read.email - Required if you want to be able to lookup
#                              users by their email address.
#
#      The easiest way to bring a bot into a channel (so that it can send
#      a message to it is to invite it. At this time Apprise does not support
#      an auto-join functionality. To do this:
#        - In the 'Details' section of your channel
#        - Click on the 'More' [...] (elipse icon)
#        - Click 'Add apps'
#        - You will be able to select the Bot App you previously created
#        - Your bot will join your channel.

import contextlib
from json import dumps, loads
from json.decoder import JSONDecodeError
import re
from time import time

import requests

from ..apprise_attachment import AppriseAttachment
from ..common import NotifyFormat, NotifyImageSize, NotifyType
from ..locale import gettext_lazy as _
from ..utils.parse import is_email, parse_bool, parse_list, validate_regex
from ..utils.templates import TemplateType, apply_template
from .base import NotifyBase

# Extend HTTP Error Messages
SLACK_HTTP_ERROR_MAP = {
    401: "Unauthorized - Invalid Token.",
}

# Used to break path apart into list of channels
CHANNEL_LIST_DELIM = re.compile(r"[ \t\r\n,#\\/]+")

# Channel Regular Expression Parsing
CHANNEL_RE = re.compile(
    r"^(?P<channel>[+#@]?[A-Z0-9_-]{1,32})(:(?P<thread_ts>[0-9.]+))?$", re.I
)


class SlackMode:
    """Tracks the mode of which we're using Slack."""

    # We're dealing with a webhook
    # Our token looks like: T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7
    WEBHOOK = "hook"

    # Government Webhook
    # Our token still looks like: T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7
    # however we have a different URL we post to
    WEBHOOK_GOV = "gov-hook"

    # We're dealing with a bot (using the OAuth Access Token)
    # Our token looks like: xoxp-1234-1234-1234-abc124 or
    # Our token looks like: xoxb-1234-1234-abc124 or
    BOT = "bot"

    # Slack Workflow Builder webhook
    # URL: https://hooks.slack.com/workflows/T.../F.../X.../Y...
    WORKFLOW = "workflow"

    # Slack Workflow trigger webhook (newer format)
    # URL: https://hooks.slack.com/triggers/T.../X.../Y...
    WORKFLOW_TRIGGER = "trigger"


# Define our Slack Modes
SLACK_MODES = (
    SlackMode.WEBHOOK,
    SlackMode.WEBHOOK_GOV,
    SlackMode.BOT,
    SlackMode.WORKFLOW,
    SlackMode.WORKFLOW_TRIGGER,
)


class NotifySlack(NotifyBase):
    """A wrapper for Slack Notifications."""

    # The default descriptive name associated with the Notification
    service_name = "Slack"

    # The services URL
    service_url = "https://slack.com/"

    # The default secure protocol
    secure_protocol = "slack"

    # Allow 50 requests per minute (Tier 2).
    # 60/50 = 0.2
    request_rate_per_sec = 1.2

    # A URL that takes you to the setup/help of the specific protocol
    setup_url = "https://appriseit.com/services/slack/"

    # Support attachments
    attachment_support = True

    # The maximum targets to include when doing batch transfers
    # Slack Webhook URLs
    webhook_url = "https://hooks.slack.com/services"
    webhook_gov_url = "https://hooks.slack-gov.com/services"

    # Slack Workflow Builder webhook URL bases
    # See: https://slack.com/help/articles/360041352714
    workflow_url = "https://hooks.slack.com/workflows"
    workflow_trigger_url = "https://hooks.slack.com/triggers"

    # Slack API URL (used with Bots)
    api_url = "https://slack.com/api/{}"

    # Allows the user to specify the NotifyImageSize object
    image_size = NotifyImageSize.XY_72

    # The maximum allowable characters allowed in the body per message
    body_maxlen = 35000

    # There is no reason we should exceed 35KB when reading in a JSON file.
    # If it is more than this, then it is not accepted
    max_slack_template_size = 35000

    # Default Notification Format
    notify_format = NotifyFormat.MARKDOWN

    # Bot's do not have default channels to notify; so #general
    # becomes the default channel in BOT mode
    default_notification_channel = "#general"

    # Define object templates
    templates = (
        # Webhook
        "{schema}://{token_a}/{token_b}/{token_c}",
        "{schema}://{botname}@{token_a}/{token_b}/{token_c}",
        "{schema}://{token_a}/{token_b}/{token_c}/{targets}",
        "{schema}://{botname}@{token_a}/{token_b}/{token_c}/{targets}",
        # Bot
        "{schema}://{access_token}/",
        "{schema}://{access_token}/{targets}",
    )

    # Define our template tokens
    template_tokens = dict(
        NotifyBase.template_tokens,
        **{
            "botname": {
                "name": _("Bot Name"),
                "type": "string",
                "map_to": "user",
            },
            # Bot User OAuth Access Token
            # which always starts with xoxp- e.g.:
            #     xoxb-1234-1234-4ddbc191d40ee098cbaae6f3523ada2d
            "access_token": {
                "name": _("OAuth Access Token"),
                "type": "string",
                "private": True,
                "required": True,
                "regex": (r"^(?:xoxe\.)?xox[abp]-[A-Z0-9-]+$", "i"),
            },
            # Token required as part of the Webhook request
            #  /AAAAAAAAA/........./........................
            "token_a": {
                "name": _("Token A"),
                "type": "string",
                "private": True,
                "required": True,
                "regex": (r"^[A-Z0-9]+$", "i"),
            },
            # Token required as part of the Webhook request
            #  /........./BBBBBBBBB/........................
            "token_b": {
                "name": _("Token B"),
                "type": "string",
                "private": True,
                "required": True,
                "regex": (r"^[A-Z0-9]+$", "i"),
            },
            # Token required as part of the Webhook request
            #  /........./........./CCCCCCCCCCCCCCCCCCCCCCCC
            "token_c": {
                "name": _("Token C"),
                "type": "string",
                "private": True,
                "required": True,
                "regex": (r"^[A-Za-z0-9]+$", "i"),
            },
            "target_encoded_id": {
                "name": _("Target Encoded ID"),
                "type": "string",
                "prefix": "+",
                "map_to": "targets",
            },
            "target_email": {
                "name": _("Target Email"),
                "type": "string",
                "map_to": "targets",
            },
            "target_user": {
                "name": _("Target User"),
                "type": "string",
                "prefix": "@",
                "map_to": "targets",
            },
            "target_channels": {
                "name": _("Target Channel"),
                "type": "string",
                "prefix": "#",
                "map_to": "targets",
            },
            "targets": {
                "name": _("Targets"),
                "type": "list:string",
            },
            # Workflow Builder path: all URL segments joined with slashes
            # e.g. T05XXXX/Ft07XXXX/XXXXXXXX/YYYYYYYY
            "workflow_path": {
                "name": _("Workflow Path"),
                "type": "string",
                "private": True,
            },
        },
    )

    # Define our template arguments
    template_args = dict(
        NotifyBase.template_args,
        **{
            "image": {
                "name": _("Include Image"),
                "type": "bool",
                "default": True,
                "map_to": "include_image",
            },
            "footer": {
                "name": _("Include Footer"),
                "type": "bool",
                "default": True,
                "map_to": "include_footer",
            },
            # Use Payload in Blocks (vs legacy way):
            #  See: https://api.slack.com/reference/messaging/payload
            "blocks": {
                "name": _("Use Blocks"),
                "type": "bool",
                "default": False,
                "map_to": "use_blocks",
            },
            "timestamp": {
                "name": _("Include Timestamp"),
                "type": "bool",
                "default": True,
                "map_to": "include_timestamp",
            },
            "mode": {
                "name": _("Message Mode"),
                "type": "choice:string",
                "values": SLACK_MODES,
                # mode is detected if not specified
            },
            "token": {
                "name": _("Token"),
                "alias_of": ("access_token", "token_a", "token_b", "token_c"),
            },
            "to": {
                "alias_of": "targets",
            },
            "template": {
                "name": _("Template Path"),
                "type": "string",
                "private": True,
            },
        },
    )

    # Define our token control
    template_kwargs = {
        "tokens": {
            "name": _("Template Tokens"),
            "prefix": ":",
        },
    }

    # Formatting requirements are defined here:
    # https://api.slack.com/docs/message-formatting
    _re_formatting_map = {
        # New lines must become the string version
        r"\r\*\n": "\\n",
        # Escape other special characters
        r"&": "&amp;",
        r"<": "&lt;",
        r">": "&gt;",
    }

    # To notify a channel, one uses <!channel|channel>
    _re_channel_support = re.compile(
        r"(?P<match>(?:<|\&lt;)?[ \t]*"
        r"!(?P<channel>[^| \n]+)"
        r"(?:[ \t]*\|[ \t]*(?:(?P<val>[^\n]+?)[ \t]*)?(?:>|\&gt;)"
        r"|(?:>|\&gt;)))",
        re.IGNORECASE,
    )

    # To notify a user by their ID, one uses <@U6TTX1F9R>
    _re_user_id_support = re.compile(
        r"(?P<match>(?:<|\&lt;)?[ \t]*"
        r"@(?P<userid>[^| \n]+)"
        r"(?:[ \t]*\|[ \t]*(?:(?P<val>[^\n]+?)[ \t]*)?(?:>|\&gt;)"
        r"|(?:>|\&gt;)))",
        re.IGNORECASE,
    )

    # The markdown in slack isn't [desc](url), it's <url|desc>
    #
    # To accommodate this, we need to ensure we don't escape URLs that match
    _re_url_support = re.compile(
        r"(?P<match>(?:<|\&lt;)?[ \t]*"
        r"(?P<url>(?:https?|mailto)://[^| \n]+)"
        r"(?:[ \t]*\|[ \t]*(?:(?P<val>[^\n]+?)[ \t]*)?(?:>|\&gt;)"
        r"|(?:>|\&gt;)))",
        re.IGNORECASE,
    )

    def __init__(
        self,
        access_token=None,
        token_a=None,
        token_b=None,
        token_c=None,
        targets=None,
        include_image=None,
        include_footer=None,
        include_timestamp=None,
        use_blocks=None,
        mode=None,
        template=None,
        tokens=None,
        workflow_path=None,
        **kwargs,
    ):
        """Initialize Slack Object."""
        super().__init__(**kwargs)

        # Store our webhook mode
        # Track whether the caller supplied an explicit mode so we can
        # decide later whether to validate exact segment count or
        # auto-detect the workflow sub-mode from segment count.
        _mode_explicit = bool(mode and isinstance(mode, str))
        if _mode_explicit:
            self.mode = next(
                (a for a in SLACK_MODES if a.startswith(mode)), None
            )
            if self.mode not in SLACK_MODES:
                msg = f"The Slack mode specified ({mode}) is invalid."
                self.logger.warning(msg)
                raise TypeError(msg)

        elif workflow_path:
            # Mode will be determined by segment count below;
            # use WORKFLOW as a sentinel so we enter the right branch.
            self.mode = SlackMode.WORKFLOW

        else:  # Detect
            self.mode = SlackMode.BOT if access_token else SlackMode.WEBHOOK

        if self.mode in (
            SlackMode.WORKFLOW,
            SlackMode.WORKFLOW_TRIGGER,
        ):
            # Workflow Builder mode: no OAuth token, no webhook tokens
            self.access_token = None
            self.token_a = None
            self.token_b = None
            self.token_c = None

            # Parse workflow path segments from string or list
            if isinstance(workflow_path, (list, tuple)):
                self.workflow_path = [str(s) for s in workflow_path if s]
            elif isinstance(workflow_path, str):
                self.workflow_path = [s for s in workflow_path.split("/") if s]
            else:
                self.workflow_path = []

            # Segment counts are fixed by Slack:
            #   /workflows/ URLs have exactly 4 segments
            #   /triggers/  URLs have exactly 3 segments
            seg_count = len(self.workflow_path)
            if _mode_explicit:
                # Validate exact count for the declared mode
                expected = 4 if self.mode == SlackMode.WORKFLOW else 3
                if seg_count != expected:
                    msg = (
                        f"A Slack {self.mode!r} URL requires exactly"
                        f" {expected} path segments"
                        f" ({workflow_path!r} has {seg_count})."
                    )
                    self.logger.warning(msg)
                    raise TypeError(msg)
            else:
                # Auto-detect sub-mode from segment count
                if seg_count == 4:
                    self.mode = SlackMode.WORKFLOW
                elif seg_count == 3:
                    self.mode = SlackMode.WORKFLOW_TRIGGER
                else:
                    msg = (
                        "A Slack Workflow URL requires exactly 3 or 4"
                        f" path segments ({workflow_path!r} has"
                        f" {seg_count})."
                    )
                    self.logger.warning(msg)
                    raise TypeError(msg)

        elif self.mode in (SlackMode.WEBHOOK, SlackMode.WEBHOOK_GOV):
            self.workflow_path = []
            self.access_token = None
            self.token_a = validate_regex(
                token_a, *self.template_tokens["token_a"]["regex"]
            )
            if not self.token_a:
                msg = (
                    "An invalid Slack (first) Token "
                    f"({token_a}) was specified."
                )
                self.logger.warning(msg)
                raise TypeError(msg)

            self.token_b = validate_regex(
                token_b, *self.template_tokens["token_b"]["regex"]
            )
            if not self.token_b:
                msg = (
                    "An invalid Slack (second) Token "
                    f"({token_b}) was specified."
                )
                self.logger.warning(msg)
                raise TypeError(msg)

            self.token_c = validate_regex(
                token_c, *self.template_tokens["token_c"]["regex"]
            )
            if not self.token_c:
                msg = (
                    "An invalid Slack (third) Token "
                    f"({token_c}) was specified."
                )
                self.logger.warning(msg)
                raise TypeError(msg)
        else:
            self.workflow_path = []
            self.token_a = None
            self.token_b = None
            self.token_c = None
            self.access_token = validate_regex(
                access_token, *self.template_tokens["access_token"]["regex"]
            )
            if not self.access_token:
                msg = (
                    "An invalid Slack OAuth Access Token "
                    f"({access_token}) was specified."
                )
                self.logger.warning(msg)
                raise TypeError(msg)

        # Look the users up by their email address and map them back to their
        # id here for future queries (if needed). This allows people to
        # specify a full email as a recipient via slack
        self._lookup_users = {}

        self.use_blocks = (
            parse_bool(use_blocks, self.template_args["blocks"]["default"])
            if use_blocks is not None
            else self.template_args["blocks"]["default"]
        )

        # Build list of channels
        self.channels = parse_list(targets)
        if len(self.channels) == 0:
            # No problem; the webhook is smart enough to just notify the
            # channel it was created for; adding 'None' is just used as
            # a flag lower to not set the channels
            self.channels.append(
                None
                if self.mode
                in (
                    SlackMode.WEBHOOK,
                    SlackMode.WEBHOOK_GOV,
                    # Workflow mode has no channel concept
                    SlackMode.WORKFLOW,
                    SlackMode.WORKFLOW_TRIGGER,
                )
                else self.default_notification_channel
            )

        # Iterate over above list and store content accordingly
        self._re_formatting_rules = re.compile(
            r"(" + "|".join(self._re_formatting_map.keys()) + r")",
            re.IGNORECASE,
        )
        # Place a thumbnail image inline with the message body
        self.include_image = (
            self.template_args["image"]["default"]
            if include_image is None
            else include_image
        )

        # Place a footer with each post
        self.include_footer = (
            self.template_args["footer"]["default"]
            if include_footer is None
            else include_footer
        )

        # timestamp inclusion (only applicable if footer also defined
        self.include_timestamp = (
            self.template_args["timestamp"]["default"]
            if include_timestamp is None
            else include_timestamp
        )

        # Our template object is just an AppriseAttachment object
        self.template = AppriseAttachment(asset=self.asset)
        if template:
            # Add our definition to our template
            self.template.add(template)
            if not len(self.template):
                # add() failed (unsupported schema, unparseable URL, etc.)
                msg = f"The Slack template ({template!r}) could not be loaded."
                self.logger.warning(msg)
                raise TypeError(msg)
            # Enforce maximum file size
            self.template[0].max_file_size = self.max_slack_template_size

        # Template functionality
        self.tokens = {}
        if isinstance(tokens, dict):
            self.tokens.update(tokens)

        elif tokens:
            msg = (
                "The specified Slack Template Tokens "
                f"({tokens}) are not identified as a dictionary."
            )
            self.logger.warning(msg)
            raise TypeError(msg)

        # A template always implies Block Kit mode; there is no meaningful
        # use for templates outside of blocks mode.
        if self.template:
            self.use_blocks = True

        return

    def gen_payload(
        self, body, title="", notify_type=NotifyType.INFO, **kwargs
    ):
        """Generate our payload from an external Block Kit JSON template.

        Returns the validated attachment content dict (which must contain
        a non-empty 'blocks' list with typed entries) to be placed into
        the Slack attachments array.  Returns False on any failure.
        """
        # Acquire the first template attachment
        template = self.template[0]
        if not template:
            # We could not access the attachment
            self.logger.error(
                "Could not access Slack template"
                f" {template.url(privacy=True)}."
            )
            return False

        # Acquire our to-be footer icon if configured to do so
        image_url = (
            None if not self.include_image else self.image_url(notify_type)
        )

        # Take a copy of our token dictionary
        tokens = self.tokens.copy()

        # Apply some defaults template values
        tokens["app_body"] = body
        tokens["app_title"] = title
        tokens["app_type"] = notify_type.value
        tokens["app_id"] = self.app_id
        tokens["app_desc"] = self.app_desc
        tokens["app_color"] = self.color(notify_type)
        tokens["app_image_url"] = image_url
        tokens["app_url"] = self.app_url

        # Templates are always Block Kit JSON; enforce JSON escaping
        tokens["app_mode"] = TemplateType.JSON

        # Coerce all substitution values to str before JSON-escaping.
        # apply_template's _escape_json() calls json.dumps(v)[1:-1] which
        # is only correct for strings; None produces "ul" and other non-
        # string types produce similarly corrupted output.  app_mode is a
        # TemplateType sentinel passed as a named parameter, not a
        # substitution value, so it is left as-is.
        safe_tokens = {
            k: (
                v
                if k == "app_mode" or isinstance(v, str)
                else ("" if v is None else str(v))
            )
            for k, v in tokens.items()
        }

        try:
            with open(template.path) as fp:
                content = apply_template(fp.read(), **safe_tokens)

        except OSError:
            self.logger.error(
                "Slack template"
                f" {template.url(privacy=True)} could not be read."
            )
            return False

        # Parse and validate as JSON
        try:
            content = loads(content)

        except JSONDecodeError as e:
            self.logger.error(
                "Slack template"
                f" {template.url(privacy=True)} contains invalid JSON."
            )
            self.logger.debug(f"JSONDecodeError: {e}")
            return False

        # Template must parse to a JSON object, not an array or scalar
        if not isinstance(content, dict):
            self.logger.error(
                "Slack template"
                f" {template.url(privacy=True)} must be a JSON object"
                " (got {}).".format(type(content).__name__)
            )
            return False

        # 'blocks' must be a non-empty list (Block Kit requirement)
        if (
            not isinstance(content.get("blocks"), list)
            or not content["blocks"]
        ):
            self.logger.error(
                "Slack template"
                f" {template.url(privacy=True)} must contain"
                " a non-empty 'blocks' list."
            )
            return False

        # Every block must be a dict with a 'type' string (Block Kit)
        if not all(
            isinstance(b, dict) and isinstance(b.get("type"), str)
            for b in content["blocks"]
        ):
            self.logger.error(
                "Slack template"
                f" {template.url(privacy=True)} contains"
                " a block missing a 'type' string."
            )
            return False

        # Return the validated attachment content dict
        return content

    def send(
        self,
        body,
        title="",
        notify_type=NotifyType.INFO,
        attach=None,
        **kwargs,
    ):
        """Perform Slack Notification."""

        # error tracking (used for function return)
        has_error = False

        #
        # Workflow Builder mode: fixed endpoint, no channel iteration
        #
        if self.mode in (SlackMode.WORKFLOW, SlackMode.WORKFLOW_TRIGGER):
            if self.template:
                # Use external Block Kit JSON template
                payload = self.gen_payload(
                    body, title, notify_type=notify_type, **kwargs
                )
                if payload is False:
                    return False

            else:
                # Default payload: combine title and body into a single
                # 'text' field -- the workflow must accept this variable.
                payload = {
                    "text": f"{title}: {body}" if title else body,
                }

            # Construct the workflow POST URL from stored path segments
            wf_base = (
                self.workflow_trigger_url
                if self.mode is SlackMode.WORKFLOW_TRIGGER
                else self.workflow_url
            )
            url = "{}/{}".format(wf_base, "/".join(self.workflow_path))
            # _send() returns False on error, non-False on success
            return self._send(url, payload) is not False

        #
        # Prepare JSON Object (applicable to both WEBHOOK and BOT mode)
        #
        if self.use_blocks:
            if self.template:
                # Template mode: generate blocks attachment from template
                template_content = self.gen_payload(
                    body, title, notify_type=notify_type, **kwargs
                )
                if template_content is False:
                    # Template loading or parsing failed; abort early
                    return False

                # Wrap the template attachment content in a full payload
                payload = {
                    "username": self.user if self.user else self.app_id,
                    "attachments": [template_content],
                }

            else:
                # Our slack format
                slack_format = (
                    "mrkdwn"
                    if self.notify_format == NotifyFormat.MARKDOWN
                    else "plain_text"
                )

                payload = {
                    "username": self.user if self.user else self.app_id,
                    "attachments": [
                        {
                            "blocks": [
                                {
                                    "type": "section",
                                    "text": {
                                        "type": slack_format,
                                        "text": body,
                                    },
                                }
                            ],
                            "color": self.color(notify_type),
                        }
                    ],
                }

                # Slack only accepts non-empty header sections
                if title:
                    payload["attachments"][0]["blocks"].insert(
                        0,
                        {
                            "type": "header",
                            "text": {
                                "type": "plain_text",
                                "text": title,
                                "emoji": True,
                            },
                        },
                    )

                # Include the footer only if specified to do so
                if self.include_footer:
                    # Acquire our to-be footer icon if configured to do so
                    image_url = (
                        None
                        if not self.include_image
                        else self.image_url(notify_type)
                    )

                    # Prepare our footer based on the block structure
                    footer = {
                        "type": "context",
                        "elements": [
                            {"type": slack_format, "text": self.app_id}
                        ],
                    }

                    if image_url:
                        payload["icon_url"] = image_url

                        footer["elements"].insert(
                            0,
                            {
                                "type": "image",
                                "image_url": image_url,
                                "alt_text": notify_type,
                            },
                        )

                    payload["attachments"][0]["blocks"].append(footer)

        else:
            #
            # Legacy API Formatting
            #
            if self.notify_format == NotifyFormat.MARKDOWN:
                body = self._re_formatting_rules.sub(  # pragma: no branch
                    lambda x: self._re_formatting_map[x.group()],
                    body,
                )

                # Support <!channel|desc>, <!channel> entries
                for match in self._re_channel_support.findall(body):
                    # Swap back any ampersands previously updaated
                    channel = match[1].strip()
                    desc = match[2].strip()

                    # Update our string
                    body = re.sub(
                        re.escape(match[0]),
                        f"<!{channel}|{desc}>" if desc else f"<!{channel}>",
                        body,
                        flags=re.IGNORECASE,
                    )

                # Support <@userid|desc>, <@channel> entries
                for match in self._re_user_id_support.findall(body):
                    # Swap back any ampersands previously updaated
                    user = match[1].strip()
                    desc = match[2].strip()

                    # Update our string
                    body = re.sub(
                        re.escape(match[0]),
                        f"<@{user}|{desc}>" if desc else f"<@{user}>",
                        body,
                        flags=re.IGNORECASE,
                    )

                # Support <url|desc>, <url> entries
                for match in self._re_url_support.findall(body):
                    # Swap back any ampersands previously updaated
                    url = match[1].replace("&amp;", "&")
                    desc = match[2].strip()

                    # Update our string
                    body = re.sub(
                        re.escape(match[0]),
                        f"<{url}|{desc}>" if desc else f"<{url}>",
                        body,
                        flags=re.IGNORECASE,
                    )

            # Perform Formatting on title here; this is not needed for block
            # mode above
            title = self._re_formatting_rules.sub(  # pragma: no branch
                lambda x: self._re_formatting_map[x.group()],
                title,
            )

            # Prepare JSON Object (applicable to both WEBHOOK and BOT mode)
            payload = {
                "username": self.user if self.user else self.app_id,
                # Use Markdown language
                "mrkdwn": self.notify_format == NotifyFormat.MARKDOWN,
                "attachments": [
                    {
                        "title": title,
                        "text": body,
                        "color": self.color(notify_type),
                    }
                ],
            }

            # Acquire our to-be footer icon if configured to do so
            image_url = (
                None if not self.include_image else self.image_url(notify_type)
            )

            if image_url:
                payload["icon_url"] = image_url

            # Include the footer only if specified to do so
            if self.include_footer:
                if image_url:
                    payload["attachments"][0]["footer_icon"] = image_url

                # Include the footer only if specified to do so
                payload["attachments"][0]["footer"] = self.app_id

                if self.include_timestamp:
                    # Timestamp
                    payload["attachments"][0]["ts"] = time()
        if (
            attach
            and self.attachment_support
            and self.mode in (SlackMode.WEBHOOK, SlackMode.WEBHOOK_GOV)
        ):
            # Be friendly; let the user know why they can't send their
            # attachments if using the Webhook mode
            self.logger.warning("Slack Webhooks do not support attachments.")

        # Prepare our Slack URL (depends on mode)
        if self.mode is SlackMode.WEBHOOK:
            url = (
                f"{self.webhook_url}/{self.token_a}"
                f"/{self.token_b}/{self.token_c}"
            )

        elif self.mode is SlackMode.WEBHOOK_GOV:
            url = (
                f"{self.webhook_gov_url}/{self.token_a}"
                f"/{self.token_b}/{self.token_c}"
            )

        else:  # SlackMode.BOT
            url = self.api_url.format("chat.postMessage")

        # Create a copy of the channel list
        channels = list(self.channels)

        attach_channel_list = []
        while len(channels):
            channel = channels.pop(0)
            if channel is not None:
                # We'll perform a user lookup if we detect an email
                email = is_email(channel)
                if email:
                    payload["channel"] = self.lookup_userid(
                        email["full_email"]
                    )

                    if not payload["channel"]:
                        # Move along; any notifications/logging would have
                        # come from lookup_userid()
                        has_error = True
                        continue

                else:  # Channel
                    result = CHANNEL_RE.match(channel)

                    if not result:
                        # Channel over-ride was specified
                        self.logger.warning(
                            f"The specified Slack target {channel} is invalid;"
                            "skipping."
                        )

                        # Mark our failure
                        has_error = True
                        continue

                    # Store oure content
                    channel, thread_ts = (
                        result.group("channel"),
                        result.group("thread_ts"),
                    )
                    if thread_ts:
                        payload["thread_ts"] = thread_ts

                    elif "thread_ts" in payload:
                        # Handle situations where one channel has a thread_id
                        # specified, and the next does not.  We do not want to
                        # cary forward the last value specified
                        del payload["thread_ts"]

                    if channel[0] == "+":
                        # Treat as encoded id if prefixed with a +
                        payload["channel"] = channel[1:]

                    elif channel[0] == "@":
                        # Treat @ value 'as is'
                        payload["channel"] = channel

                    else:
                        # Prefix with channel hash tag (if not already)
                        payload["channel"] = (
                            channel if channel[0] == "#" else f"#{channel}"
                        )

            response = self._send(url, payload)
            if not response:
                # Handle any error
                has_error = True
                continue

            # Store the valid channel or chat ID (for DMs) that will
            # be accepted by Slack's attachment method later.
            if response.get("channel"):
                attach_channel_list.append(response.get("channel"))

            self.logger.info(
                "Sent Slack notification{}.".format(
                    f" to {channel}" if channel is not None else ""
                )
            )

        if (
            attach
            and self.attachment_support
            and self.mode is SlackMode.BOT
            and attach_channel_list
        ):
            # Send our attachments (can only be done in bot mode)
            for no, attachment in enumerate(attach, start=1):
                # Perform some simple error checking
                if not attachment:
                    # We could not access the attachment
                    self.logger.error(
                        "Could not access attachment"
                        f" {attachment.url(privacy=True)}."
                    )
                    return False

                self.logger.debug(
                    f"Posting Slack attachment {attachment.url(privacy=True)}"
                )

                # Get the URL to which to upload the file.
                # https://api.slack.com/methods/files.getUploadURLExternal
                params = {
                    "filename": (
                        attachment.name
                        if attachment.name
                        else f"file{no:03}.dat"
                    ),
                    "length": len(attachment),
                }
                url_ = self.api_url.format("files.getUploadURLExternal")
                response = self._send(
                    url_, {}, http_method="get", params=params
                )
                if not (
                    response
                    and response.get("file_id")
                    and response.get("upload_url")
                ):
                    self.logger.error("Could retrieve file upload URL.")
                    # We failed to get an upload URL, take an early exit
                    return False

                file_id = response.get("file_id")
                upload_url = response.get("upload_url")

                # Upload file
                response = self._send(upload_url, {}, attach=attachment)

                # Send file to channels
                # https://api.slack.com/methods/files.completeUploadExternal
                for channel_id in attach_channel_list:
                    payload_ = {
                        "files": [
                            {
                                "id": file_id,
                                "title": attachment.name,
                            }
                        ],
                        "channel_id": channel_id,
                    }
                    url_ = self.api_url.format("files.completeUploadExternal")
                    response = self._send(url_, payload_)
                    # Expected response
                    # {
                    #     "ok": true,
                    #     "files": [
                    #         {
                    #             "id": "F123ABC456",
                    #             "title": "slack-test"
                    #         }
                    #     ]
                    # }
                    if not (response and response.get("files")):
                        self.logger.error("Failed to send file to channel.")
                        # We failed to send the file to the channel,
                        # take an early exit
                        return False

        return not has_error

    def lookup_userid(self, email):
        """Takes an email address and attempts to resolve/acquire it's user id
        for notification purposes."""
        if email in self._lookup_users:
            # We're done as entry has already been retrieved
            return self._lookup_users[email]

        if self.mode is not SlackMode.BOT:
            # You can not look up
            self.logger.warning(
                "Emails can not be resolved to Slack User IDs unless you "
                "have a bot configured."
            )
            return None

        lookup_url = self.api_url.format("users.lookupByEmail")
        headers = {
            "User-Agent": self.app_id,
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": f"Bearer {self.access_token}",
        }

        # we pass in our email address as the argument
        params = {
            "email": email,
        }

        self.logger.debug(
            "Slack User Lookup POST URL:"
            f" {lookup_url} (cert_verify={self.verify_certificate!r})"
        )
        self.logger.debug(f"Slack User Lookup Parameters: {params!s}")

        # Initialize our HTTP JSON response
        response = {"ok": False}

        # Initialize our detected user id (also the response to this function)
        user_id = None

        # Always call throttle before any remote server i/o is made
        self.throttle()
        try:
            r = requests.get(
                lookup_url,
                headers=headers,
                params=params,
                verify=self.verify_certificate,
                timeout=self.request_timeout,
                allow_redirects=self.redirects,
            )

            # Attachment posts return a JSON string
            with contextlib.suppress(AttributeError, TypeError, ValueError):
                # Load our JSON object if we can

                # ValueError = r.content is Unparsable
                # TypeError = r.content is None
                # AttributeError = r is None
                response = loads(r.content)

            # We can get a 200 response, but still fail.  A failure message
            # might look like this (missing bot permissions):
            #    {
            #      'ok': False,
            #      'error': 'missing_scope',
            #      'needed': 'users:read.email',
            #      'provided': 'calls:write,chat:write'
            #    }

            if r.status_code != requests.codes.ok or not (
                response and response.get("ok", False)
            ):
                # We had a problem
                status_str = NotifySlack.http_response_code_lookup(
                    r.status_code, SLACK_HTTP_ERROR_MAP
                )

                self.logger.warning(
                    "Failed to send Slack User Lookup:{}{}error={}.".format(
                        status_str, ", " if status_str else "", r.status_code
                    )
                )

                self.logger.debug(
                    "Response Details:\r\n%r", (r.content or b"")[:2000]
                )

                # Return; we're done
                return False

            # If we reach here, then we were successful in looking up
            # the user. A response generally looks like this:
            # {
            #   'ok': True,
            #   'user': {
            #     'id': 'J1ZQB9T9Y',
            #     'team_id': 'K1WR6TML2',
            #     'name': 'l2g',
            #     'deleted': False,
            #     'color': '9f69e7',
            #     'real_name': 'Chris C',
            #     'tz': 'America/New_York',
            #     'tz_label': 'Eastern Standard Time',
            #     'tz_offset': -18000,
            #     'profile': {
            #       'title': '',
            #       'phone': '',
            #       'skype': '',
            #       'real_name': 'Chris C',
            #       'real_name_normalized':
            #       'Chris C',
            #       'display_name': 'l2g',
            #       'display_name_normalized': 'l2g',
            #       'fields': None,
            #       'status_text': '',
            #       'status_emoji': '',
            #       'status_expiration': 0,
            #       'avatar_hash': 'g785e9c0ddf6',
            #       'email': 'lead2gold@gmail.com',
            #       'first_name': 'Chris',
            #       'last_name': 'C',
            #       'image_24': 'https://secure.gravatar.com/...',
            #       'image_32': 'https://secure.gravatar.com/...',
            #       'image_48': 'https://secure.gravatar.com/...',
            #       'image_72': 'https://secure.gravatar.com/...',
            #       'image_192': 'https://secure.gravatar.com/...',
            #       'image_512': 'https://secure.gravatar.com/...',
            #       'status_text_canonical': '',
            #       'team': 'K1WR6TML2'
            #     },
            #     'is_admin': True,
            #     'is_owner': True,
            #     'is_primary_owner': True,
            #     'is_restricted': False,
            #     'is_ultra_restricted': False,
            #     'is_bot': False,
            #     'is_app_user': False,
            #     'updated': 1603904274
            #   }
            # }
            # We're only interested in the id
            user_id = response["user"]["id"]

            # Cache it for future
            self._lookup_users[email] = user_id
            self.logger.info(
                "Email %s resolves to the Slack User ID: %s.", email, user_id
            )

        except requests.RequestException as e:
            self.logger.warning(
                "A Connection error occurred looking up Slack User.",
            )
            self.logger.debug(f"Socket Exception: {e!s}")
            # Return; we're done
            return None

        return user_id

    def _send(
        self,
        url,
        payload,
        attach=None,
        http_method="post",
        params=None,
        **kwargs,
    ):
        """Wrapper to the requests (post) object."""
        self.logger.debug(
            f"Slack POST URL: {url} (cert_verify={self.verify_certificate!r})"
        )
        self.logger.debug(f"Slack Payload: {payload!s}")

        headers = {
            "User-Agent": self.app_id,
            "Accept": "application/json",
        }

        if not attach:
            headers["Content-Type"] = "application/json; charset=utf-8"

        if self.mode is SlackMode.BOT:
            headers["Authorization"] = f"Bearer {self.access_token}"

        # Our response object
        response = {"ok": False}

        # Always call throttle before any remote server i/o is made
        self.throttle()

        # Our attachment path (if specified)
        files = None

        try:
            # Open our attachment path if required:
            if attach:
                files = {
                    "file": (
                        attach.name,
                        # file handle is safely closed in `finally`; inline
                        # open is intentional; attach.open() dispatches to
                        # BytesIO for memory attachments
                        attach.open(),
                    ),
                }

            r = requests.request(
                http_method,
                url,
                data=payload if attach else dumps(payload),
                headers=headers,
                files=files,
                verify=self.verify_certificate,
                timeout=self.request_timeout,
                allow_redirects=self.redirects,
                params=params if params else None,
            )

            # Posts return a JSON string
            with contextlib.suppress(AttributeError, TypeError, ValueError):
                # Load our JSON object if we can
                # ValueError = r.content is Unparsable
                # TypeError = r.content is None
                # AttributeError = r is None
                response = loads(r.content)

            # Another response type is:
            # {
            #   'ok': False,
            #   'error': 'not_in_channel',
            # }
            status_okay = False
            if self.mode is SlackMode.BOT:
                status_okay = (
                    (response and response.get("ok", False))
                    or
                    # Responses for file uploads look like this
                    # 'OK - <file length>'
                    (
                        r.content
                        and isinstance(r.content, bytes)
                        and b"OK" in r.content
                    )
                )
            elif self.mode in (
                SlackMode.WORKFLOW,
                SlackMode.WORKFLOW_TRIGGER,
            ):
                # Workflow trigger webhooks confirm success via HTTP 200;
                # the response body may be empty or non-standard JSON.
                status_okay = r.status_code == requests.codes.ok
            elif r.content == b"ok":
                # The text 'ok' is returned if this is a Webhook request
                # So the below captures that as well.
                status_okay = True

            if r.status_code != requests.codes.ok or not status_okay:
                # We had a problem
                status_str = NotifySlack.http_response_code_lookup(
                    r.status_code, SLACK_HTTP_ERROR_MAP
                )

                self.logger.warning(
                    "Failed to send{} to Slack: {}{}error={}.".format(
                        (" " + attach.name) if attach else "",
                        status_str,
                        ", " if status_str else "",
                        r.status_code,
                    )
                )

                self.logger.debug(f"Response Details:\r\n{r.content}")
                return False

            # Message Post Response looks like this:
            # {
            #   "attachments": [
            #     {
            #       "color": "3AA3E3",
            #       "fallback": "test",
            #       "id": 1,
            #       "text": "my body",
            #       "title": "my title",
            #       "ts": 1573694687
            #     }
            #   ],
            #   "bot_id": "BAK4K23G5",
            #   "icons": {
            #     "image_48": "https://s3-us-west-2.amazonaws.com/...
            #   },
            #   "subtype": "bot_message",
            #   "text": "",
            #   "ts": "1573694689.003700",
            #   "type": "message",
            #   "username": "Apprise"
            # }

            # files.completeUploadExternal responses look like this:
            # {
            #     "ok": true,
            #     "files": [
            #         {
            #             "id": "F123ABC456",
            #             "title": "slack-test"
            #         }
            #     ]
            # }
        except requests.RequestException as e:
            self.logger.warning(
                "A Connection error occurred posting {}to Slack.".format(
                    attach.name if attach else ""
                )
            )
            self.logger.debug(f"Socket Exception: {e!s}")
            return False

        except OSError as e:
            self.logger.warning(
                "An I/O error occurred while reading {}.".format(
                    attach.name if attach else "attachment"
                )
            )
            self.logger.debug(f"I/O Exception: {e!s}")
            return False

        finally:
            # Close our file (if it's open) stored in the second element
            # of our files tuple (index 1)
            if files:
                files["file"][1].close()

        # Return the response for processing
        return response

    @property
    def url_identifier(self):
        """Returns all of the identifiers that make this URL unique from
        another simliar one.

        Targets or end points should never be identified here.
        """
        if self.mode in (SlackMode.WORKFLOW, SlackMode.WORKFLOW_TRIGGER):
            # Workflow mode is identified by its path segments
            return (self.secure_protocol, self.mode, *self.workflow_path)

        return (
            self.secure_protocol,
            self.token_a,
            self.token_b,
            self.token_c,
            self.access_token,
        )

    def url(self, privacy=False, *args, **kwargs):
        """Returns the URL built dynamically based on specified arguments."""

        # Define any URL parameters
        params = {
            "image": "yes" if self.include_image else "no",
            "footer": "yes" if self.include_footer else "no",
            "timestamp": "yes" if self.include_timestamp else "no",
            "blocks": "yes" if self.use_blocks else "no",
            "mode": self.mode,
        }

        if self.template:
            params["template"] = NotifySlack.quote(
                self.template[0].url(), safe=""
            )

        # Extend our parameters
        params.update(self.url_parameters(privacy=privacy, *args, **kwargs))

        # Store any template token entries if specified
        params.update({f":{k}": v for k, v in self.tokens.items()})

        # Determine if there is a botname present
        botname = ""
        if self.user:
            botname = "{botname}@".format(
                botname=NotifySlack.quote(self.user, safe=""),
            )

        if self.mode in (SlackMode.WORKFLOW, SlackMode.WORKFLOW_TRIGGER):
            return "{schema}://{path}/?{params}".format(
                schema=self.secure_protocol,
                path="/".join(
                    self.pprint(s, privacy, safe="")
                    for s in self.workflow_path
                ),
                params=NotifySlack.urlencode(params),
            )

        if self.mode in (SlackMode.WEBHOOK, SlackMode.WEBHOOK_GOV):
            return (
                "{schema}://{botname}{token_a}/{token_b}/{token_c}/"
                "{targets}/?{params}".format(
                    schema=self.secure_protocol,
                    botname=botname,
                    token_a=self.pprint(self.token_a, privacy, safe=""),
                    token_b=self.pprint(self.token_b, privacy, safe=""),
                    token_c=self.pprint(self.token_c, privacy, safe=""),
                    targets="/".join(
                        [NotifySlack.quote(x, safe="") for x in self.channels]
                    ),
                    params=NotifySlack.urlencode(params),
                )
            )
        # else -> self.mode == SlackMode.BOT:
        return "{schema}://{botname}{access_token}/{targets}/?{params}".format(
            schema=self.secure_protocol,
            botname=botname,
            access_token=self.pprint(self.access_token, privacy, safe=""),
            targets="/".join(
                [NotifySlack.quote(x, safe="") for x in self.channels]
            ),
            params=NotifySlack.urlencode(params),
        )

    def __len__(self):
        """Returns the number of targets associated with this notification."""
        return len(self.channels)

    @staticmethod
    def parse_url(url):
        """Parses the URL and returns enough arguments that can allow us to re-
        instantiate this object."""
        results = NotifyBase.parse_url(url, verify_host=False)
        if not results:
            # We're done early as we couldn't load the results
            return results

        # The first token is stored in the hostname
        token = NotifySlack.unquote(results["host"])

        # Get unquoted path entries
        entries = NotifySlack.split_path(results["fullpath"])

        # Peek at mode early to guide path parsing.
        # Resolve via the same prefix-matching used in __init__ so that
        # abbreviated inputs (e.g. mode=tri or mode=work) are handled
        # identically in both places.
        _mode_raw = results["qsd"].get("mode", "")
        _mode = (
            next(
                (a for a in SLACK_MODES if a.startswith(_mode_raw)),
                "",
            )
            if _mode_raw
            else ""
        )

        if _mode in (SlackMode.WORKFLOW, SlackMode.WORKFLOW_TRIGGER):
            # All path segments form the workflow_path
            results["workflow_path"] = "/".join([token, *entries])
            results["targets"] = []

        # Verify if our token is a bot token or part of a webhook:
        elif token.startswith("xo"):
            # We're dealing with a bot
            results["access_token"] = token
            results["targets"] = entries

        else:
            # We're dealing with a webhook
            results["token_a"] = token
            results["token_b"] = entries.pop(0) if entries else None
            results["token_c"] = entries.pop(0) if entries else None
            # assign remaining entries to the channels we wish to notify
            results["targets"] = entries

        # Support the token flag where you can set it to the bot token
        # or the webhook token (with slash delimiters)
        if "token" in results["qsd"] and len(results["qsd"]["token"]):
            # Break our entries up into a list; we can ue the Channel
            # list delimiter above since it doesn't contain any characters
            # we don't otherwise accept anyway in our token
            entries = list(
                filter(
                    bool,
                    CHANNEL_LIST_DELIM.split(
                        NotifySlack.unquote(results["qsd"]["token"])
                    ),
                )
            )

            # check to see if we're dealing with a bot/user token
            if entries and entries[0].startswith("xo"):
                # We're dealing with a bot
                results["access_token"] = entries[0]
                results["token_a"] = None
                results["token_b"] = None
                results["token_c"] = None

            else:  # Webhook
                results["access_token"] = None
                results["token_a"] = entries.pop(0) if entries else None
                results["token_b"] = entries.pop(0) if entries else None
                results["token_c"] = entries.pop(0) if entries else None

        # Support the 'to' variable so that we can support rooms this way too
        # The 'to' makes it easier to use yaml configuration
        if "to" in results["qsd"] and len(results["qsd"]["to"]):
            results["targets"] += list(
                filter(
                    bool,
                    CHANNEL_LIST_DELIM.split(
                        NotifySlack.unquote(results["qsd"]["to"])
                    ),
                )
            )

        # Get Image Flag
        results["include_image"] = parse_bool(
            results["qsd"].get(
                "image", NotifySlack.template_args["image"]["default"]
            )
        )

        results["include_timestamp"] = parse_bool(
            results["qsd"].get(
                "timestamp", NotifySlack.template_args["timestamp"]["default"]
            )
        )

        # Get Payload structure (use blocks?)
        if "blocks" in results["qsd"] and len(results["qsd"]["blocks"]):
            results["use_blocks"] = parse_bool(results["qsd"]["blocks"])

        # Get Footer Flag
        results["include_footer"] = parse_bool(
            results["qsd"].get(
                "footer", NotifySlack.template_args["footer"]["default"]
            )
        )

        # Get Mode
        if "mode" in results["qsd"] and len(results["qsd"]["mode"]):
            results["mode"] = NotifySlack.unquote(results["qsd"]["mode"])

        # Template Handling
        if "template" in results["qsd"] and results["qsd"]["template"]:
            results["template"] = NotifySlack.unquote(
                results["qsd"]["template"]
            )

        # Store our tokens
        results["tokens"] = results["qsd:"]

        return results

    @staticmethod
    def parse_native_url(url):
        """
        Supports:
          - https://hooks.slack.com/services/TOKEN_A/TOKEN_B/TOKEN_C
          - https://hooks.slack-gov.com/services/TOKEN_A/TOKEN_B/TOKEN_C
          - https://hooks.slack.com/workflows/T.../F.../X.../Y...
          - https://hooks.slack.com/triggers/T.../X.../Y...
        """

        # Workflow Builder and trigger webhook URLs
        wf_result = re.match(
            r"^https?://hooks\.slack\.com/"
            r"(?P<wf_type>workflows|triggers)/"
            r"(?P<path>[A-Z0-9][A-Z0-9/_-]+)/?"
            r"(?P<params>\?.+)?$",
            url,
            re.I,
        )
        if wf_result:
            wf_type = wf_result.group("wf_type").lower()
            # Map URL path type to Apprise mode string
            mode = (
                SlackMode.WORKFLOW_TRIGGER
                if wf_type == "triggers"
                else SlackMode.WORKFLOW
            )
            path = wf_result.group("path").strip("/")
            params = wf_result.group("params") or ""
            sep = "&" if params else "?"
            return NotifySlack.parse_url(
                "{schema}://{path}/{params}{sep}mode={mode}".format(
                    schema=NotifySlack.secure_protocol,
                    path=path,
                    params=params,
                    sep=sep,
                    mode=mode,
                )
            )

        # Standard incoming webhook and government webhook URLs
        result = re.match(
            r"^https?://(?P<host>hooks\.slack(?P<gov>-gov)?\.com)/services/"
            r"(?P<token_a>[A-Z0-9]+)/"
            r"(?P<token_b>[A-Z0-9]+)/"
            r"(?P<token_c>[A-Z0-9]+)/?"
            r"(?P<params>\?.+)?$",
            url,
            re.I,
        )

        if result:
            params = (
                "" if not result.group("params") else result.group("params")
            )

            if result.group("gov"):
                # provide gov parameters
                params = (
                    "?" if not params else "&"
                ) + f"mode={SlackMode.WEBHOOK_GOV}"

            return NotifySlack.parse_url(
                "{schema}://{token_a}/{token_b}/{token_c}/{params}".format(
                    schema=NotifySlack.secure_protocol,
                    token_a=result.group("token_a"),
                    token_b=result.group("token_b"),
                    token_c=result.group("token_c"),
                    params=params,
                )
            )

        return None
