SlackV2 Deprecated

Deprecated. Use SlackV3 instead.

Messaging and Conferencing · Slack

Details

IDSlackV2
ProviderSlack
CategoryMessaging and Conferencing
From Version5.0.0
Docker Imagedemisto/slack:1.0.0.42956
Supported ModulesAgentix Cortex Cloud Cloud Runtime Security Cloud Posture Security XSIAM EDR

README

Deprecated. Use the Slack v3 integration instead.

Send messages and notifications to your Slack Team. This integration was integrated and tested with version 4.0.1 of Slack, and is available from Cortex XSOAR version 5.0.

Use Cases

  • Mirror Cortex XSOAR investigations War Room to Slack channels and vice-versa.
  • Send notifications, message and files from Cortex XSOAR to Slack.
  • Get notifications in Slack about events in Cortex XSOAR.
  • Manage Cortex XSOAR incidents via direct messages to the Cortex XSOAR bot.
  • Manage Slack channels (create, edit, filter, invite, kick, close).

Detailed Description

  • To allow us access to Slack, the Cortex XSOAR app has to be added to the relevant workspace. Do so by clicking on the following link.
  • After adding the Cortex XSOAR app, you will get an Access Token and Bot Token, which should be inserted in the integration instance configuration's corresponding fields.

Configure SlackV2 on Cortex XSOAR

  1. Navigate to Settings > Integrations  > Servers & Services.
  2. Search for SlackV2.
  3. Click Add instance to create and configure a new integration instance.
    • Name: a textual name for the integration instance.
    • Slack API access token: A token received by adding the application (Starts with xoxp).
    • Slack API bot token: A token received by adding the application (Starts with xoxb).
    • Dedicated Slack channel to receive notifications
    • Send notifications about incidents to the dedicated channel
    • Minimum incident severity to send messages to slack by
    • Type of incidents created in Slack
    • Allow external users to create incidents via DM
    • Use system proxy settings
    • Trust any certificate (not secure)
    • Bot display name in Slack (Cortex XSOAR by default)
    • Bot icon in Slack - Image URL (Demisto icon by default)
    • Maximum time to wait for a rate limited call in seconds - 60 by default
    • Number of objects to return in each paginated call - 200 by default
    • Proxy URL to use in Slack API calls
  1. Click Test to validate the new instance.

Commands

You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook. After you successfully execute a command, a DBot message appears in the War Room with the command details.

  1. mirror-investigation: mirror-investigation
  2. send-notification: send-notification
  3. close-channel: close-channel
  4. slack-send-file: slack-send-file
  5. slack-set-channel-topic: slack-set-channel-topic
  6. slack-create-channel: slack-create-channel
  7. slack-invite-to-channel: slack-invite-to-channel
  8. slack-kick-from-channel: slack-kick-from-channel
  9. slack-rename-channel: slack-rename-channel
  10. slack-get-user-details: slack-get-user-details
  11. slack-filter-channels: slack-filter-channels

1. mirror-investigation

Mirrors the investigation between Slack and the Cortex XSOAR War Room.

Base Command

mirror-investigation

Input
</tr> </tbody> </table>

 

Context Output
There are no context output for this command.

 

Command Example

!mirror-investigation type=all autoclose=true direction=Both channelName=my-mirror channelTopic=my-incident

Human Readable Output

Investigation mirrored successfully, channel: my-mirror

2. send-notification

Sends a message to a user, group, or channel.

Base Command

send-notification

Input
Argument Name Description Required
type The mirroring type. Can be "all", which mirrors everything, "chat", which mirrors only chats (not commands), or "none", which stops all mirroring. Optional
autoclose Whether the channel is auto-closed when an investigation is closed. Can be "true" or "false". Default is "true". Optional
direction The mirroring direction. Can be "FromDemisto", "ToDemisto", or "Both". Default value is "Both". Optional
mirrorTo The channel type. Can be "channel" or "group". The default value is "group". Optional
channelName The name of the channel. The default is "incident-".</td> Optional
channelTopic The topic of the channel. Optional
kickAdmin Whether to remove the Slack administrator (channel creator) from the mirrored channel. Optional
Argument Name Description Required
message The message content. When mentioning another slack user, make sure to do so in the following format: <@user_name>. Optional
to The user to whom to send the message. Can be either the username or email address. Optional
channel The name of the Slack channel to which to send the message. Optional
entry An entry ID to send as a link. Optional
ignoreAddURL Whether to include a URL to the relevant component in Cortex XSOAR. Can be "true" or "false". Default value is "false". Optional
threadID The ID of the thread to which to reply - can be retrieved from a previous send-notification command. Optional
blocks A JSON string of Slack blocks to send in the message. Optional

 

Context Output
Path Type Description
Slack.Thread.ID String b'The Slack thread ID.'

 

Command Example

!send-notification channel=general message="I love Cortex XSOAR"

Context Example
{
    "Slack.Thread": {
        "ID": "1567407432.000500"
    }
}
Human Readable Output

Message sent to Slack successfully. Thread ID is: 1567407432.000500

3. close-channel

Archives a Slack channel.

Base Command

close-channel

Input
Argument Name Description Required
channel The name of the channel to archive. If not provided, the mirrored investigation channel is archived (if the channel exists). Optional

 

Context Output
There are no context output for this command.

 

Command Example

!close-channel

Human Readable Output

Channel successfully archived.

4. slack-send-file

Sends a file to a user, channel, or group. If not specified, the file is sent to the mirrored investigation channel (if the channel exists).

Base Command

slack-send-file

Input
Argument Name Description Required
file The ID of the file entry to send. Required
to The user to whom to send the file. Can be the username or the email address. Optional
group The name of the Slack group (private channel) to which to send the file. Optional
channel The name of the Slack channel to which to send the file. Optional
threadID The ID of the thread to which to reply - can be retrieved from a previous send-notification command. Optional
comment A comment to add to the file. Optional

 

Context Output
There are no context output for this command.

 

Command Example

!slack-send-file file=1@2 channel=general

Human Readable Output

File sent to Slack successfully.

5. slack-set-channel-topic

Sets the topic for a channel.

Base Command

slack-set-channel-topic

Input
Argument Name Description Required
channel The channel name. If not specified, the topic of the mirrored investigation channel is set (if the channel exists). Optional
topic The topic for the channel. Required

 

Context Output
There are no context output for this command.

 

Command Example

!slack-set-channel-topic channel=general topic="Cortex XSOAR rocks"

Human Readable Output

Topic successfully set.

6. slack-create-channel

Creates a channel in Slack.

Base Command

slack-create-channel

Input
Argument Name Description Required
type The channel type. Can be "private" or "public". Optional
name The name of the channel. Required
users A CSV list of user names or email addresses to invite to the channel. For example: "user1, user2...". Optional

 

Context Output
There are no context output for this command.

 

Command Example

!slack-create-channel name=my-channel topic=cool-topic type=private users=demisto_integration

Human Readable Output

Successfully created the channel my-channel.

7. slack-invite-to-channel

Invites users to join a channel.

Base Command

slack-invite-to-channel

Input
Argument Name Description Required
users A CSV list of usernames or email addresses to invite to join the channel. For example: "user1, user2...". Required
channel The name of the channel to which to invite the users. If the name of the channel is not specified, the name of the mirrored investigation channel is used (if the channel exists). Optional

 

Context Output
There are no context output for this command.

 

Command Example

!slack-invite-to-channel channel=my-channel users=cool-user

Human Readable Output

Successfully invited users to the channel.

8. slack-kick-from-channel

Removes users from the specified channel.

Base Command

slack-kick-from-channel

Input
Argument Name Description Required
users A CSV list of usernames or email addresses to remove from the a channel. For example: "user1, user2..." Required
channel The name of the channel from which to remove the users. If the name of the channel is not specified, the mirrored investigation channel is used (if the channel exists). Optional

 

Context Output
There are no context output for this command.

 

Command Example

!slack-kick-from-channel channel=my-channel users=cool-user

Human Readable Output

Successfully kicked users from the channel.

9. slack-rename-channel

Renames a channel in Slack.

Base Command

slack-rename-channel

Input
Argument Name Description Required
name The new name of the channel. Required
channel The current name of the channel. If the name of the channel is not specified, the mirrored investigation channel is used (if the channel exists). Optional

 

Context Output
There are no context output for this command.

 

Command Example

!slack-rename-channel channel=my-channel name=your-channel

Human Readable Output

Channel renamed successfully.

10. slack-get-user-details

Get details about a specified user.

Base Command

slack-get-user-details

Input
Argument Name Description Required
user The Slack user (username or email). Required

 

Context Output
Path Type Description
Slack.User.ID String b'The ID of the user.'
Slack.User.Username String b'The username of the user.'
Slack.User.Name String b'The actual name of the user.'
Slack.User.DisplayName String b'The display name of the user.'
Slack.User.Email String b'The email address of the user.'

 

Command Example

!slack-get-user-details user="cool_user"

Context Example
{
    "Slack.User": {
        "ID": "UXXXXXXXX",
        "Name": "Cool User",
        "Username": "cool_user",
        "Email": "cool_user@coolorg.com"
    }
}
Human Readable Output

Details for Slack user: cool_user

ID Username Name Email
UXXXXXXXX cool_user Cool User cool_user@coolorg.com

11. slack-filter-channels

Get channels matching provided criteria.

Base Command

slack-filter-channels

Input
Argument Name Description Required
name The name of a channel or a regex pattern Optional
creator The member ID who created the channel Optional
is_archived True or False Optional
is_general True or False Optional
is_private True or False Optional
limit The maximum number of channels to return. Default is "20" Optional

 

Context Output
Path Type Description
Slack.Channel.ID String The channel ID
Slack.Channel.Name String The channel name
Slack.Channel.Created Number The channel creation timestamp
Slack.Channel.Creator String The channel creator's member ID
Slack.Channel.IsArchived Boolean Has the channel been archived?
Slack.Channel.IsGeneral Boolean Is the channel the general channel?
Slack.Channel.IsPrivate Boolean Is the channel a private channel?

 

Command Example

!slack-filter-channels name="^general$"

Context Example
{
    "Slack.Channel": {
        "ID": "CXXXXXXXX",
        "Name": "general",
        "Created": 1449252889,
        "Creator": "UXXXXXXXX",
        "IsArchived": false,
        "IsGeneral": true,
        "IsPrivate": false,
    }
}
Human Readable Output

Results

ID Name Created Creator IsArchived IsGeneral IsPrivate
CXXXXXXXX general 1449252889 UXXXXXXXX false true false

### slack-get-integration-context *** Returns the integration context as a file. Use this command for debug purposes only. #### Base Command `slack-get-integration-context` #### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | #### Context Output There is no context output for this command.

Additional Information

Change the name of the Cortex XSOAR App

You can change the name and icon of the Cortex XSOAR app in direct messages using the integration configuration settings (parameters). In order to change the name of the application itself, do the following:

  • Go to the app in the **Apps** section in Slack
  • In the app, go to **About > Settings**:
  • Scroll down and click the pencil icon to change the name.

Direct messages

You can send direct messages to the Cortex XSOAR app on Slack using the following commands:

list incidents [page x] - lists the current incidents in Cortex XSOAR. Requires user permissions in Cortex XSOAR.

list my incidents [page x] - lists the current incidents assigned to you in Cortex XSOAR. Requires user permissions in Cortex XSOAR.

list my tasks [page x] - lists the current tasks assigned to you in Cortex XSOAR. Requires user permissions in Cortex XSOAR.

list closed incidents - lists the closed incidents in Cortex XSOAR. Requires user permissions in Cortex XSOAR.

new incident [details] - creates a new incident in Cortex XSOAR. Requires user permissions in Cortex XSOAR, or that the `Allow external users to create incidents via DM` parameter is enabled.

mirror [incident-id] - mirrors an incident in Cortex XSOAR to a Slack channel. Requires user permissions in Cortex XSOAR for the specified incident.

Notifications

The integration sends notifications as they are configured in the notification settings (User Preferences in Cortex XSOAR), and to the dedicated channel configured for incident notifications (according to the integration configuration). If a dedicated channel for incident notifications is configured, the following notifications will be sent there:

  • Incident opened
  • Incident updated
  • Investigation closed
  • Investigation deleted
  • Incident SLA changed
  • Task completed

Blocks and interactive components

The integration supports sending "blocks" to Slack. Blocks are a series of components that can be combined to create visually rich and compellingly interactive messages. In the integration, they can be sent as an array of JSON. More information about that here. You can experiment with and build your own blocks here. The integration also allows some level of interactivity. When a user interacts with an element in a Slack message, Slack sends a request with the relevant information. This request is processed and stored by a dedicated endpoint outside of Cortex XSOAR in the address: https://oproxy.demisto.ninja The integration allows polling this endpoint for user interactions that contain entitlement strings, which are used to perform actions in Cortex XSOAR by external users. See the SlackAsk script for an example. This means that in order to enable interactivity using the integration, connection to this endpoint has to be enabled. The following information is sent to the dedicated endpoint in the request:
Headers
  • Current Cortex XSOAR content version
  • Current Cortex XSOAR server version
  • The name of the integration
  • Team name in Slack - for identification
  • Team ID in Slack - for identification
  • Cortex XSOAR license ID - for identification
Body
  • Entitlement - the unique entitlement string to allow interaction with Cortex XSOAR.

Important! The interactions work only with the Cortex XSOAR Integration bot - the only application that's allowed to send requests to the dedicated endpoint(for security reasons). They will not work with another application.

Known Limitations

  • Due to limitations of the `aiohttp` library, only http proxies are supported.
  • Channels are created by the Slack user who authorized the application. Thus, this user will be in every channel that the integration creates. You cannot kick this user, but they can leave.
  • The integration can only manage channels that the application is a member of. Otherwise those channels will not be found.
  • Currently, the integration does not support working without verifying SSL certificates. The parameter applies only to the endpoint for interactive responses.

Troubleshooting

If messages are not mirrored in Cortex XSOAR, or direct messages are not handled properly, check the integration status on the integration page:


Configuration parameters

  • cred_access_token
  • cred_bot_token
  • access_token — Slack API access token
  • bot_token — Slack API bot token
  • incidentNotificationChannel — Dedicated Slack channel to receive notifications
  • notify_incidents — Send notifications about incidents to the dedicated channel
  • min_severity — Minimum incident severity to send messages to slack by
  • incidentType — Type of incidents created in Slack
  • allow_incidents — Allow external users to create incidents via DM
  • proxy — Use system proxy settings
  • unsecure — Trust any certificate (not secure)
  • longRunning — Long running instance. Required for investigation mirroring and direct messages.
  • bot_name — Bot display name in Slack (Cortex XSOAR by default)
  • bot_icon — Bot icon in Slack - Image URL (Cortex XSOAR icon by default)
  • max_limit_time — Maximum time to wait for a rate limiting call in seconds
  • paginated_count — Number of objects to return in each paginated call
  • proxy_url — Proxy URL to use in Slack API calls
  • filtered_tags — Comma separated list of tags To filter the messages sent from demisto.

Commands (15)

  • close-channel Deprecated

    Archives a Slack channel.

  • mirror-investigation Deprecated

    Mirrors the investigation between Slack and the Demisto War Room.

  • send-notification Deprecated

    Sends a message to a user, group, or channel.

  • slack-close-channel Deprecated

    Deprecated. Use the "close-channel" command instead.

  • slack-create-channel Deprecated

    Creates a channel in Slack.

  • slack-filter-channels Deprecated

    Returns a list of channels matching criteria

  • slack-get-integration-context

    Returns the integration context as a file. Use this command for debug purposes only.

  • slack-get-user-details Deprecated

    Get details about a specified user.

  • slack-invite-to-channel Deprecated

    Invites users to join a channel.

  • slack-kick-from-channel Deprecated

    Removes users from the specified channel.

  • slack-mirror-investigation Deprecated

    Deprecated. Use the "mirror-investigation" command instead.

  • slack-rename-channel Deprecated

    Renames a channel in Slack.

  • slack-send Deprecated

    Deprecated. Use the "send-notification" command instead.

  • slack-send-file Deprecated

    Sends a file to a user, channel, or group. If not specified, the file is sent to the mirrored investigation channel (if the channel exists).

  • slack-set-channel-topic Deprecated

    Sets the topic for a channel.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401
import asyncio
import concurrent
import json
import os
import ssl
import sys
import threading
import traceback
from distutils.util import strtobool
from typing import Any

import requests
import slack
import urllib3
from slack.errors import SlackApiError
from slack.web.slack_response import SlackResponse


# disable unsecure warnings
urllib3.disable_warnings()

""" CONSTANTS """

SEVERITY_DICT = {"Unknown": 0, "Low": 1, "Medium": 2, "High": 3, "Critical": 4}

USER_TAG_EXPRESSION = "<@(.*?)>"
CHANNEL_TAG_EXPRESSION = "<#(.*?)>"
URL_EXPRESSION = r"<(https?://.+?)(?:\|.+)?>"
GUID_REGEX = r"(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}"
ENTITLEMENT_REGEX = rf"{GUID_REGEX}@(({GUID_REGEX})|(?:[\d_]+))_*(\|\S+)?\b"
MESSAGE_FOOTER = "\n**From Slack**"
MIRROR_TYPE = "mirrorEntry"
INCIDENT_OPENED = "incidentOpened"
INCIDENT_NOTIFICATION_CHANNEL = "incidentNotificationChannel"
PLAYGROUND_INVESTIGATION_TYPE = 9
WARNING_ENTRY_TYPE = 11
ENDPOINT_URL = "https://oproxy.demisto.ninja/slack-poll"
POLL_INTERVAL_MINUTES: dict[tuple, float] = {(0, 15): 1, (15, 60): 2, (60,): 5}
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
OBJECTS_TO_KEYS = {"mirrors": "investigation_id", "questions": "entitlement", "users": "id"}
SYNC_CONTEXT = True

""" GLOBALS """

BOT_TOKEN: str
ACCESS_TOKEN: str
PROXY_URL: str | None
PROXIES: dict
DEDICATED_CHANNEL: str
CLIENT: slack.WebClient
CHANNEL_CLIENT: slack.WebClient
ALLOW_INCIDENTS: bool
NOTIFY_INCIDENTS: bool
INCIDENT_TYPE: str
SEVERITY_THRESHOLD: int
VERIFY_CERT: bool
SSL_CONTEXT: ssl.SSLContext | None
QUESTION_LIFETIME: int
BOT_NAME: str
BOT_ICON_URL: str
MAX_LIMIT_TIME: int
PAGINATED_COUNT: int

""" HELPER FUNCTIONS """


def get_bot_id() -> str:
    """
    Gets the app bot ID

    Returns:
        The app bot ID
    """
    response = send_slack_request_sync(CLIENT, "auth.test")

    return response.get("user_id")


def test_module():
    """
    Sends a test message to the dedicated slack channel.
    """
    if not DEDICATED_CHANNEL:
        return_error("A dedicated slack channel must be provided.")
    channel = get_conversation_by_name(DEDICATED_CHANNEL)
    if not channel:
        return_error("Dedicated channel not found.")
    message = "Hi there! This is a test message."

    body = {"text": message, "channel": channel.get("id")}

    send_slack_request_sync(CLIENT, "chat.postMessage", body=body)

    demisto.results("ok")


def get_current_utc_time() -> datetime:
    """
    Returns:
        The current UTC time.
    """
    return datetime.utcnow()


def get_user_by_name(user_to_search: str, add_to_context: bool = True) -> dict:
    """
    Gets a slack user by a user name

    Args:
        user_to_search: The user name or email
        add_to_context: Whether to update the integration context

    Returns:
        A slack user object
    """

    user: dict = {}
    users: list = []
    integration_context = get_integration_context(SYNC_CONTEXT)

    user_to_search = user_to_search.lower()
    if integration_context.get("users"):
        users = json.loads(integration_context["users"])
        users_filter = list(
            filter(
                lambda u: u.get("name", "").lower() == user_to_search
                or u.get("profile", {}).get("email", "").lower() == user_to_search
                or u.get("real_name", "").lower() == user_to_search,
                users,
            )
        )
        if users_filter:
            user = users_filter[0]
    if not user:
        body = {"limit": PAGINATED_COUNT}
        response = send_slack_request_sync(CLIENT, "users.list", http_verb="GET", body=body)
        while True:
            workspace_users = response["members"] if response and response.get("members", []) else []
            cursor = response.get("response_metadata", {}).get("next_cursor")
            users_filter = list(
                filter(
                    lambda u: u.get("name", "").lower() == user_to_search
                    or u.get("profile", {}).get("email", "").lower() == user_to_search
                    or u.get("real_name", "").lower() == user_to_search,
                    workspace_users,
                )
            )
            if users_filter:
                break
            if not cursor:
                break
            body = body.copy()
            body.update({"cursor": cursor})
            response = send_slack_request_sync(CLIENT, "users.list", http_verb="GET", body=body)

        if users_filter:
            user = users_filter[0]
            if add_to_context:
                users.append(user)
                set_to_integration_context_with_retries({"users": users}, OBJECTS_TO_KEYS, SYNC_CONTEXT)
        else:
            return {}

    return user


def search_slack_users(users: list | str) -> list:
    """
    Search given users in Slack

    Args:
        users: The users to find

    Returns:
        The slack users
    """
    slack_users = []

    if not isinstance(users, list):
        users = [users]

    for user in users:
        slack_user = get_user_by_name(user)
        if not slack_user:
            demisto.results(
                {"Type": WARNING_ENTRY_TYPE, "Contents": f"User {user} not found in Slack", "ContentsFormat": formats["text"]}
            )
        else:
            slack_users.append(slack_user)
    return slack_users


def find_mirror_by_investigation() -> dict:
    """
    Finds a mirrored channel by the mirrored investigation

    Returns:
        The mirror object
    """
    mirror: dict = {}
    investigation = demisto.investigation()
    if investigation:
        integration_context = get_integration_context(SYNC_CONTEXT)
        if integration_context.get("mirrors"):
            mirrors = json.loads(integration_context["mirrors"])
            investigation_filter = list(filter(lambda m: investigation.get("id") == m["investigation_id"], mirrors))
            if investigation_filter:
                mirror = investigation_filter[0]

    return mirror


def set_name_and_icon(body: dict, method: str):
    """
    If provided, sets a name and an icon for the bot if a message is sent.
    Args:
        body: The message body.
        method: The current API method.
    """
    if method == "chat.postMessage":
        if BOT_NAME:
            body["username"] = BOT_NAME
        if BOT_ICON_URL:
            body["icon_url"] = BOT_ICON_URL


def send_slack_request_sync(
    client: slack.WebClient, method: str, http_verb: str = "POST", file_: str = "", body: dict = None
) -> SlackResponse:
    """
    Sends a request to slack API while handling rate limit errors.

    Args:
        client: The slack client.
        method: The method to use.
        http_verb: The HTTP method to use.
        file_: A file path to send.
        body: The request body.

    Returns:
        The slack API response.
    """
    if body is None:
        body = {}

    set_name_and_icon(body, method)
    total_try_time = 0
    while True:
        try:
            demisto.debug(f"Sending slack {method} (sync). Body is: {str(body)}")
            if http_verb == "POST":
                if file_:
                    response = client.api_call(method, files={"file": file_}, data=body)
                else:
                    response = client.api_call(method, json=body)
            else:
                response = client.api_call(method, http_verb="GET", params=body)
        except SlackApiError as api_error:
            demisto.debug(f"Got rate limit error (sync). Body is: {str(body)}\n{api_error}")
            response = api_error.response
            headers = response.headers  # type: ignore
            if "Retry-After" in headers:
                retry_after = int(headers["Retry-After"])
                total_try_time += retry_after
                if total_try_time < MAX_LIMIT_TIME:
                    time.sleep(retry_after)
                    continue
            raise
        break

    return response  # type: ignore


async def send_slack_request_async(
    client: slack.WebClient, method: str, http_verb: str = "POST", file_: str = "", body: dict = None
) -> SlackResponse:
    """
    Sends an async request to slack API while handling rate limit errors.

    Args:
        client: The slack client.
        method: The method to use.
        http_verb: The HTTP method to use.
        file_: A file path to send.
        body: The request body.

    Returns:
        The slack API response.
    """
    if body is None:
        body = {}

    set_name_and_icon(body, method)
    total_try_time = 0
    while True:
        try:
            demisto.debug(f"Sending slack {method} (async). Body is: {str(body)}")
            if http_verb == "POST":
                if file_:
                    response = await client.api_call(method, files={"file": file_}, data=body)  # type: ignore
                else:
                    response = await client.api_call(method, json=body)  # type: ignore
            else:
                response = await client.api_call(method, http_verb="GET", params=body)  # type: ignore
        except SlackApiError as api_error:
            demisto.debug(f"Got rate limit error (async). Body is: {str(body)}\n{api_error}")
            response = api_error.response
            headers = response.headers
            if "Retry-After" in headers:
                retry_after = int(headers["Retry-After"])
                total_try_time += retry_after
                if total_try_time < MAX_LIMIT_TIME:
                    await asyncio.sleep(retry_after)
                    continue
            raise
        break

    return response


""" MIRRORING """


async def get_slack_name(slack_id: str, client) -> str:
    """
    Get the slack name of a provided user or channel by its ID

    Args:
        client: The slack client
        slack_id: The slack user or channel ID

    Returns:
        The slack user or channel name
    """
    if not slack_id:
        return ""

    integration_context = get_integration_context(SYNC_CONTEXT)
    prefix = slack_id[0]
    slack_name = ""

    if prefix in ["C", "D", "G"]:
        slack_id = slack_id.split("|")[0]
        conversation: dict = {}
        if integration_context.get("conversations"):
            conversations = list(filter(lambda c: c["id"] == slack_id, json.loads(integration_context["conversations"])))
            if conversations:
                conversation = conversations[0]
        if not conversation:
            body = {"channel": slack_id}

            conversation = (await send_slack_request_async(client, "conversations.info", http_verb="GET", body=body)).get(
                "channel", {}
            )
        slack_name = conversation.get("name", "")
    elif prefix == "U":
        user: dict = {}
        if integration_context.get("users"):
            users = list(filter(lambda u: u["id"] == slack_id, json.loads(integration_context["users"])))
            if users:
                user = users[0]
        if not user:
            body = {"user": slack_id}
            user = (await send_slack_request_async(client, "users.info", http_verb="GET", body=body)).get("user", {})

        slack_name = user.get("name", "")

    return slack_name


async def clean_message(message: str, client: slack.WebClient) -> str:
    """
    Prettifies a slack message - replaces tags and URLs with clean expressions

    Args:
        message: The slack message
        client: The slack client

    Returns:
        The clean slack message
    """
    matches = re.findall(USER_TAG_EXPRESSION, message)
    matches += re.findall(CHANNEL_TAG_EXPRESSION, message)
    message = re.sub(USER_TAG_EXPRESSION, r"\1", message)
    message = re.sub(CHANNEL_TAG_EXPRESSION, r"\1", message)
    for match in matches:
        slack_name = await get_slack_name(match, client)
        message = message.replace(match, slack_name)

    resolved_message = re.sub(URL_EXPRESSION, r"\1", message)

    return resolved_message


def invite_users_to_conversation(conversation_id: str, users_to_invite: list):
    """
    Invites users to a provided conversation using a provided slack client with a channel token.

    Args:
        conversation_id: The slack conversation ID to invite the users to.
        users_to_invite: The user slack IDs to invite.
    """
    for user in users_to_invite:
        try:
            body = {"channel": conversation_id, "users": user}
            send_slack_request_sync(CHANNEL_CLIENT, "conversations.invite", body=body)
        except SlackApiError as e:
            message = str(e)
            if "already_in_channel" in message:
                continue
            elif message.find("cant_invite_self") == -1:
                raise


def kick_users_from_conversation(conversation_id: str, users_to_kick: list):
    """
    Kicks users from a provided conversation using a provided slack client with a channel token.

    Args:
        conversation_id: The slack conversation ID to kick the users from.
        users_to_kick: The user slack IDs to kick.
    """
    for user in users_to_kick:
        try:
            body = {"channel": conversation_id, "user": user}
            send_slack_request_sync(CHANNEL_CLIENT, "conversations.kick", body=body)
        except SlackApiError as e:
            message = str(e)
            if message.find("cant_invite_self") == -1:
                raise


def mirror_investigation():
    """
    Updates the integration context with a new or existing mirror.
    """
    mirror_type = demisto.args().get("type", "all")
    auto_close = demisto.args().get("autoclose", "true")
    mirror_direction = demisto.args().get("direction", "both")
    mirror_to = demisto.args().get("mirrorTo", "group")
    channel_name = demisto.args().get("channelName", "")
    channel_topic = demisto.args().get("channelTopic", "")
    kick_admin = bool(strtobool(demisto.args().get("kickAdmin", "false")))

    investigation = demisto.investigation()

    if investigation.get("type") == PLAYGROUND_INVESTIGATION_TYPE:
        return_error("Can not perform this action in playground.")

    integration_context = get_integration_context(SYNC_CONTEXT)

    if not integration_context or not integration_context.get("mirrors", []):
        mirrors: list = []
    else:
        mirrors = json.loads(integration_context["mirrors"])
    if not integration_context or not integration_context.get("conversations", []):
        conversations: list = []
    else:
        conversations = json.loads(integration_context["conversations"])

    investigation_id = investigation.get("id")
    send_first_message = False
    current_mirror = list(filter(lambda m: m["investigation_id"] == investigation_id, mirrors))
    channel_filter: list = []
    if channel_name:
        channel_filter = list(filter(lambda m: m["channel_name"] == channel_name, mirrors))

    if not current_mirror:
        channel_name = channel_name or f"incident-{investigation_id}"

        if not channel_filter:
            body = {"name": channel_name}

            if mirror_to != "channel":
                body["is_private"] = True

            conversation = send_slack_request_sync(CHANNEL_CLIENT, "conversations.create", body=body).get("channel", {})

            conversation_name = conversation.get("name")
            conversation_id = conversation.get("id")
            conversations.append(conversation)

            # Get the bot ID so we can invite him
            if integration_context.get("bot_id"):
                bot_id = integration_context["bot_id"]
            else:
                bot_id = get_bot_id()
                set_to_integration_context_with_retries({"bot_id": bot_id}, OBJECTS_TO_KEYS, SYNC_CONTEXT)

            invite_users_to_conversation(conversation_id, [bot_id])

            send_first_message = True
        else:
            mirrored_channel = channel_filter[0]
            conversation_id = mirrored_channel["channel_id"]
            conversation_name = mirrored_channel["channel_name"]

        mirror = {
            "channel_id": conversation_id,
            "channel_name": conversation_name,
            "investigation_id": investigation.get("id"),
            "mirror_type": mirror_type,
            "mirror_direction": mirror_direction,
            "mirror_to": mirror_to,
            "auto_close": bool(strtobool(auto_close)),
            "mirrored": False,
        }
    else:
        mirror = mirrors.pop(mirrors.index(current_mirror[0]))
        conversation_id = mirror["channel_id"]
        if mirror_type:
            mirror["mirror_type"] = mirror_type
        if auto_close:
            mirror["auto_close"] = bool(strtobool(auto_close))
        if mirror_direction:
            mirror["mirror_direction"] = mirror_direction
        if mirror_to and mirror["mirror_to"] != mirror_to:
            return_error("Cannot change the Slack channel type from Demisto.")
        if channel_name:
            return_error("Cannot change the Slack channel name.")
        if channel_topic:
            return_error("Cannot change the Slack channel topic.")
        conversation_name = mirror["channel_name"]
        mirror["mirrored"] = False

    set_topic = False
    if channel_topic:
        set_topic = True
    else:
        mirror_name = f"incident-{investigation_id}"
        channel_filter = list(filter(lambda m: m["channel_name"] == conversation_name, mirrors))
        if "channel_topic" in mirror:
            channel_topic = mirror["channel_topic"]
        elif channel_filter:
            channel_mirror = channel_filter[0]
            channel_topic = channel_mirror["channel_topic"]
        else:
            channel_topic = ""
        mirrored_investigations_ids = [f'incident-{m["investigation_id"]}' for m in channel_filter]
        if not channel_topic or channel_topic.find("incident-") != -1:
            new_topic = ", ".join(mirrored_investigations_ids + [mirror_name])
            if channel_topic != new_topic:
                channel_topic = new_topic
                set_topic = True

    if set_topic:
        body = {"channel": conversation_id, "topic": channel_topic}
        send_slack_request_sync(CHANNEL_CLIENT, "conversations.setTopic", body=body)
    mirror["channel_topic"] = channel_topic

    mirrors.append(mirror)

    set_to_integration_context_with_retries({"mirrors": mirrors, "conversations": conversations}, OBJECTS_TO_KEYS, SYNC_CONTEXT)

    if kick_admin:
        body = {"channel": conversation_id}
        send_slack_request_sync(CHANNEL_CLIENT, "conversations.leave", body=body)
    if send_first_message:
        server_links = demisto.demistoUrls()
        server_link = server_links.get("server")
        message = (
            f"This channel was created to mirror incident {investigation_id}."
            f" \n View it on: {server_link}#/WarRoom/{investigation_id}"
        )
        body = {"text": message, "channel": conversation_id}

        send_slack_request_sync(CLIENT, "chat.postMessage", body=body)

    demisto.results(f"Investigation mirrored successfully, channel: {conversation_name}")


def long_running_loop():
    """
    Runs in a long running container - checking for newly mirrored investigations and answered questions.
    """
    while True:
        error = ""
        try:
            check_for_mirrors()
            check_for_answers()
        except requests.exceptions.ConnectionError as e:
            error = f"Could not connect to the Slack endpoint: {str(e)}"
        except Exception as e:
            error = f"An error occurred: {str(e)}"
        finally:
            if error:
                demisto.error(error)
                demisto.updateModuleHealth(error)
            time.sleep(5)


def check_for_answers():
    """
    Checks for answered questions
    """

    integration_context = get_integration_context(SYNC_CONTEXT)
    questions = integration_context.get("questions", [])
    users = integration_context.get("users", [])
    if questions:
        questions = json.loads(questions)
    if users:
        users = json.loads(users)
    now = get_current_utc_time()
    now_string = datetime.strftime(now, DATE_FORMAT)
    updated_questions = []

    for question in questions:
        if question.get("last_poll_time"):
            if question.get("expiry"):
                # Check if the question expired - if it did, answer it with the default response and remove it
                expiry = datetime.strptime(question["expiry"], DATE_FORMAT)
                if expiry < now:
                    answer_question(question.get("default_response"), question)
                    updated_questions.append(question)
                    continue
            # Check if it has been enough time(determined by the POLL_INTERVAL_MINUTES parameter)
            # since the last polling time. if not, continue to the next question until it has.
            last_poll_time = datetime.strptime(question["last_poll_time"], DATE_FORMAT)
            delta = now - last_poll_time
            minutes = delta.total_seconds() / 60
            sent = question.get("sent")
            poll_time_minutes = get_poll_minutes(now, sent)

            if minutes < poll_time_minutes:
                continue
        entitlement = question.get("entitlement", "")
        demisto.info(f"Slack - polling for an answer for entitlement {entitlement}")
        question["last_poll_time"] = now_string
        updated_questions.append(question)

        headers = {"Accept": "application/json", "Content-Type": "application/json"}
        add_info_headers(headers, question.get("expiry"))

        body = {"entitlement": entitlement}
        res = requests.post(ENDPOINT_URL, data=json.dumps(body), headers=headers, proxies=PROXIES, verify=VERIFY_CERT, timeout=30)
        if res.status_code != 200:
            demisto.error(f"Slack - failed to poll for answers: {res.content!r}, status code: {res.status_code!r}")
            continue
        answer: dict = {}
        try:
            answer = res.json()
        except Exception:
            demisto.info(f"Slack - Could not parse response for entitlement {entitlement!r}: {res.content!r}")
        if not answer:
            continue
        payload_json: str = answer.get("payload", "")
        if not payload_json:
            continue
        payload = json.loads(payload_json)

        actions = payload.get("actions", [])
        if actions:
            demisto.info(f"Slack - received answer from user for entitlement {entitlement}.")
            user_id = payload.get("user", {}).get("id")
            user_filter = list(filter(lambda u: u["id"] == user_id, users))
            if user_filter:
                user = user_filter[0]
            else:
                body = {"user": user_id}
                user = send_slack_request_sync(CLIENT, "users.info", http_verb="GET", body=body).get("user", {})
                users.append(user)

            answer_question(actions[0].get("text", {}).get("text"), question, user.get("profile", {}).get("email"))

    if updated_questions:
        set_to_integration_context_with_retries({"users": users, "questions": questions}, OBJECTS_TO_KEYS, SYNC_CONTEXT)


def get_poll_minutes(current_time: datetime, sent: str | None) -> float:
    """
    Get the interval to wait before polling again in minutes.

    Args:
        current_time: The current time.
        sent: The time when the polling request was sent.

    Returns:
        Total minutes to wait before polling.
    """
    poll_time_minutes = 1.0
    if sent:
        sent_time = datetime.strptime(sent, DATE_FORMAT)
        total_delta = current_time - sent_time
        total_minutes = total_delta.total_seconds() / 60

        for minute_range, interval in POLL_INTERVAL_MINUTES.items():
            if len(minute_range) > 1 and total_minutes > minute_range[1]:
                continue
            poll_time_minutes = interval
            break

    return poll_time_minutes


def add_info_headers(headers: dict, expiry):
    # pylint: disable=no-member
    try:
        auth = send_slack_request_sync(CLIENT, "auth.test")
        team_name = auth.get("team", "")
        team_id = auth.get("team_id", "")
        headers["X-Content-TeamName"] = team_name
        headers["X-Content-TeamID"] = team_id
        headers["X-Content-Expiry"] = expiry if expiry else "No expiry"
        headers.update(get_x_content_info_headers())
    except Exception as e:
        demisto.error(f"Failed getting integration info: {str(e)}")


def answer_question(text: str, question: dict, email: str = ""):
    entitlement = question.get("entitlement", "")
    content, guid, incident_id, task_id = extract_entitlement(entitlement, text)
    try:
        demisto.handleEntitlementForUser(incident_id, guid, email, content, task_id)
    except Exception as e:
        demisto.error(f"Failed handling entitlement {entitlement}: {str(e)}")
    question["remove"] = True


def check_for_mirrors():
    """
    Checks for newly created mirrors and handles the mirroring process
    """
    integration_context = get_integration_context(SYNC_CONTEXT)
    if integration_context.get("mirrors"):
        mirrors = json.loads(integration_context["mirrors"])
        updated_mirrors = []
        updated_users = []
        for mirror in mirrors:
            if not mirror["mirrored"]:
                investigation_id = mirror["investigation_id"]
                demisto.info(f"Mirroring: {investigation_id}")
                mirror = mirrors.pop(mirrors.index(mirror))
                if mirror["mirror_to"] and mirror["mirror_direction"] and mirror["mirror_type"]:
                    mirror_type = mirror["mirror_type"]
                    auto_close = mirror["auto_close"]
                    direction = mirror["mirror_direction"]
                    channel_id = mirror["channel_id"]
                    if isinstance(auto_close, str):
                        auto_close = bool(strtobool(auto_close))
                    users: list[dict] = demisto.mirrorInvestigation(investigation_id, f"{mirror_type}:{direction}", auto_close)
                    if mirror_type != "none":
                        try:
                            invited_users = invite_to_mirrored_channel(channel_id, users)
                            updated_users.extend(invited_users)
                        except Exception as error:
                            demisto.error(f"Could not invite investigation users to the mirrored channel: {error}")

                    mirror["mirrored"] = True
                    updated_mirrors.append(mirror)
                else:
                    demisto.info(f"Could not mirror {investigation_id}")

        if updated_mirrors:
            context = {"mirrors": updated_mirrors}
            if updated_users:
                context["users"] = updated_users

            set_to_integration_context_with_retries(context, OBJECTS_TO_KEYS, SYNC_CONTEXT)


def invite_to_mirrored_channel(channel_id: str, users: list[dict]) -> list:
    """
    Invite the relevant users to a mirrored channel
    Args:
        channel_id: The mirrored channel
        users: The users to invite, each a dict of username and email

    Returns:
        users: The slack users that were invited
    """
    slack_users = []
    for user in users:
        slack_user: dict = {}
        # Try to invite by Demisto email
        user_email = user.get("email", "")
        user_name = user.get("username", "")
        if user_email:
            slack_user = get_user_by_name(user_email, False)
        # Try to invite by Demisto user name
        if not slack_user and user_name:
            slack_user = get_user_by_name(user_name, False)
        if slack_user:
            slack_users.append(slack_user)
        else:
            demisto.results(
                {
                    "Type": WARNING_ENTRY_TYPE,
                    "Contents": f"User {user_name} not found in Slack",
                    "ContentsFormat": formats["text"],
                }
            )

    users_to_invite = [user.get("id") for user in slack_users]
    invite_users_to_conversation(channel_id, users_to_invite)

    return slack_users


def extract_entitlement(entitlement: str, text: str) -> tuple[str, str, str, str]:
    """
    Extracts entitlement components from an entitlement string
    Args:
        entitlement: The entitlement itself
        text: The actual reply text

    Returns:
        Entitlement components
    """
    parts = entitlement.split("@")
    guid = parts[0]
    id_and_task = parts[1].split("|")
    incident_id = id_and_task[0]
    task_id = ""
    if len(id_and_task) > 1:
        task_id = id_and_task[1]
    content = text.replace(entitlement, "", 1)

    return content, guid, incident_id, task_id


async def slack_loop():
    """
    Starts a Slack RTM client while checking the connection.
    """
    while True:
        loop = asyncio.get_running_loop()
        rtm_client = None
        try:
            rtm_client = slack.RTMClient(
                token=BOT_TOKEN, run_async=True, loop=loop, auto_reconnect=False, proxy=PROXY_URL, ssl=SSL_CONTEXT
            )

            client_future = rtm_client.start()
            while True:
                await asyncio.sleep(10)
                if rtm_client._websocket is None or rtm_client._websocket.closed or client_future.done():
                    ex = client_future.exception()
                    if ex:
                        demisto.error(f"Slack client raised an exception: {ex}")
                    demisto.info("Slack - websocket is closed or done")
                    break
        except Exception as e:
            error = f"Slack client raised an exception: {e}"
            await handle_listen_error(error)
        finally:
            # If we got here, the websocket is closed or the client can't connect. Will try to connect every 5 seconds.
            if rtm_client and not rtm_client._stopped:
                rtm_client.stop()
            await asyncio.sleep(5)


async def handle_listen_error(error: str):
    """
    Logs an error and updates the module health accordingly.

    Args:
        error: The error string.
    """
    demisto.error(error)
    demisto.updateModuleHealth(error)


async def start_listening():
    """
    Starts a Slack RTM client and checks for mirrored incidents.
    """
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    loop.run_in_executor(executor, long_running_loop)
    await slack_loop()


async def handle_dm(user: dict, text: str, client: slack.WebClient):
    """
    Handles a direct message sent to the bot

    Args:
        user: The user who sent the message
        text: The message text
        client: The Slack client

    Returns:
        Text to return to the user
    """
    demisto.info("Slack - handling direct message.")
    message: str = text.lower()
    if message.find("incident") != -1 and (
        message.find("create") != -1 or message.find("open") != -1 or message.find("new") != -1
    ):
        user_email = user.get("profile", {}).get("email", "")
        user_name = user.get("name", "")
        if user_email:
            demisto_user = demisto.findUser(email=user_email)
        else:
            demisto_user = demisto.findUser(username=user.get("name"))

        if not demisto_user and not ALLOW_INCIDENTS:
            data = "You are not allowed to create incidents."
        else:
            try:
                data = await translate_create(text, user_name, user_email, demisto_user)
            except Exception as e:
                data = f"Failed creating incidents: {str(e)}"
    else:
        try:
            data = demisto.directMessage(text, user.get("name"), user.get("profile", {}).get("email"), ALLOW_INCIDENTS)
        except Exception as e:
            data = str(e)

    if not data:
        data = "Sorry, I could not perform the selected operation."
    body = {"users": user.get("id")}
    im = await send_slack_request_async(client, "conversations.open", body=body)
    channel = im.get("channel", {}).get("id")
    body = {"text": data, "channel": channel}

    await send_slack_request_async(client, "chat.postMessage", body=body)


async def translate_create(message: str, user_name: str, user_email: str, demisto_user: dict) -> str:
    """
    Processes an incident creation message
    Args:
        message: The creation message
        user_name: The name of the user in Slack
        user_email: The email of the user in Slack
        demisto_user: The demisto user associated with the request (if exists)

    Returns:
        Creation result
    """
    json_pattern = r"(?<=json=).*"
    name_pattern = r"(?<=name=).*"
    type_pattern = r"(?<=type=).*"
    message = message.replace("\n", "").replace("`", "")
    json_match = re.search(json_pattern, message)
    created_incident = None
    data = ""
    user_demisto_id = ""
    if demisto_user:
        user_demisto_id = demisto_user.get("id", "")

    if json_match:
        if re.search(name_pattern, message) or re.search(type_pattern, message):
            data = "No other properties other than json should be specified."
        else:
            incidents_json = json_match.group()
            incidents = json.loads(incidents_json.replace("", '"').replace("", '"'))
            if not isinstance(incidents, list):
                incidents = [incidents]
            created_incident = await create_incidents(incidents, user_name, user_email, user_demisto_id)

            if not created_incident:
                data = "Failed creating incidents."
    else:
        name_match = re.search(name_pattern, message)
        if not name_match:
            data = "Please specify arguments in the following manner: name=<name> type=[type] or json=<json>."
        else:
            incident_name = re.sub("type=.*", "", name_match.group()).strip()
            incident_type = ""

            type_match = re.search(type_pattern, message)
            if type_match:
                incident_type = re.sub("name=.*", "", type_match.group()).strip()

            incident = {"name": incident_name}

            incident_type = incident_type or INCIDENT_TYPE
            if incident_type:
                incident["type"] = incident_type

            created_incident = await create_incidents([incident], user_name, user_email, user_demisto_id)
            if not created_incident:
                data = "Failed creating incidents."

    if created_incident:
        if isinstance(created_incident, list):
            created_incident = created_incident[0]
        server_links = demisto.demistoUrls()
        server_link = server_links.get("server")
        incident_name = created_incident["name"]
        incident_id = created_incident["id"]
        data = f"Successfully created incident {incident_name}.\n View it on: {server_link}#/WarRoom/{incident_id}"

    return data


async def create_incidents(incidents: list, user_name: str, user_email: str, user_demisto_id: str = "") -> dict:
    """
    Creates incidents according to a provided JSON object
    Args:
        incidents: The incidents JSON
        user_name The name of the user in Slack
        user_email The email of the user in Slack
        user_demisto_id: The id of demisto user associated with the request (if exists)

    Returns:
        The creation result
    """

    for incident in incidents:
        # Add relevant labels to context
        labels = incident.get("labels", [])
        keys = [label.get("type") for label in labels]
        if "Reporter" not in keys:
            labels.append({"type": "Reporter", "value": user_name})
        if "ReporterEmail" not in keys:
            labels.append({"type": "ReporterEmail", "value": user_email})
        if "Source" not in keys:
            labels.append({"type": "Source", "value": "Slack"})
        incident["labels"] = labels

    if user_demisto_id:
        data = demisto.createIncidents(incidents, userID=user_demisto_id)
    else:
        data = demisto.createIncidents(incidents)

    return data


@slack.RTMClient.run_on(event="message")
async def listen(**payload: Any):
    """
    Listens to Slack RTM messages

    Args:
        payload: The message payload
    """
    data: dict = payload.get("data", {})
    data_type: str = payload.get("type", "")
    client: slack.WebClient = payload.get("web_client", CLIENT)

    if data_type == "error":
        error = payload.get("error", {})
        error_code = error.get("code")
        error_msg = error.get("msg")
        await handle_listen_error(f"Slack API has thrown an error. Code: {error_code}, Message: {error_msg}.")
        return
    try:
        subtype = data.get("subtype", "")
        text = data.get("text", "")
        user_id = data.get("user", "")
        channel = data.get("channel", "")
        message_bot_id = data.get("bot_id", "")
        thread = data.get("thread_ts", "")
        message = data.get("message", {})

        if subtype == "bot_message" or message_bot_id or message.get("subtype") == "bot_message":
            return

        user = await get_user_by_id_async(client, user_id)
        entitlement_reply = await check_and_handle_entitlement(text, user, thread)
        if entitlement_reply:
            body = {"text": entitlement_reply, "thread_ts": thread, "channel": channel}

            await send_slack_request_async(client, "chat.postMessage", body=body)
        elif channel and channel[0] == "D":
            # DM
            await handle_dm(user, text, client)
        else:
            channel_id = data.get("channel")
            integration_context = get_integration_context(SYNC_CONTEXT)
            if not integration_context or "mirrors" not in integration_context:
                return

            mirrors = json.loads(integration_context["mirrors"])
            mirror_filter = list(filter(lambda m: m["channel_id"] == channel_id, mirrors))
            if not mirror_filter:
                return

            for mirror in mirror_filter:
                if mirror["mirror_direction"] == "FromDemisto" or mirror["mirror_type"] == "none":
                    return

                if not mirror["mirrored"]:
                    # In case the investigation is not mirrored yet
                    mirror = mirrors.pop(mirrors.index(mirror))
                    if mirror["mirror_to"] and mirror["mirror_direction"] and mirror["mirror_type"]:
                        investigation_id = mirror["investigation_id"]
                        mirror_type = mirror["mirror_type"]
                        auto_close = mirror["auto_close"]
                        direction = mirror["mirror_direction"]
                        if isinstance(auto_close, str):
                            auto_close = bool(strtobool(auto_close))
                        demisto.info(f"Mirroring: {investigation_id}")
                        demisto.mirrorInvestigation(investigation_id, f"{mirror_type}:{direction}", auto_close)
                        mirror["mirrored"] = True
                        mirrors.append(mirror)
                        set_to_integration_context_with_retries({"mirrors": mirrors}, OBJECTS_TO_KEYS, SYNC_CONTEXT)

                investigation_id = mirror["investigation_id"]
                await handle_text(client, investigation_id, text, user)
        # Reset module health
        demisto.updateModuleHealth("")
    except Exception as e:
        await handle_listen_error(f"Error occurred while listening to Slack: {str(e)}")


async def get_user_by_id_async(client: slack.WebClient, user_id: str) -> dict:
    """
    Get the details of a slack user by id asynchronously.
    Args:
        client: The slack web client to use.
        user_id: The id of the user.

    Returns:
        The slack user.
    """
    user: dict = {}
    users: list = []
    integration_context = get_integration_context(SYNC_CONTEXT)
    if integration_context.get("users"):
        users = json.loads(integration_context["users"])
        user_filter = list(filter(lambda u: u["id"] == user_id, users))
        if user_filter:
            user = user_filter[0]
    if not user:
        body = {"user": user_id}
        user = (await send_slack_request_async(client, "users.info", http_verb="GET", body=body)).get("user", {})
        users.append(user)
        set_to_integration_context_with_retries({"users": users}, OBJECTS_TO_KEYS, SYNC_CONTEXT)

    return user


async def handle_text(client: slack.WebClient, investigation_id: str, text: str, user: dict):
    """
    Handles text received in the Slack workspace (not DM)

    Args:
        client: The Slack client
        investigation_id: The mirrored investigation ID
        text: The received text
        user: The sender
    """
    demisto.info(f"Slack - adding entry to incident {investigation_id}")
    if text:
        demisto.addEntry(
            id=investigation_id,
            entry=await clean_message(text, client),
            username=user.get("name", ""),
            email=user.get("profile", {}).get("email", ""),
            footer=MESSAGE_FOOTER,
        )


async def check_and_handle_entitlement(text: str, user: dict, thread_id: str) -> str:
    """
    Handles an entitlement message (a reply to a question)
    Args:
        text: The message text
        user: The user who sent the reply
        thread_id: The thread ID

    Returns:
        If the message contains entitlement, return a reply.
    """

    entitlement_match = re.search(ENTITLEMENT_REGEX, text)
    if entitlement_match:
        demisto.info("Slack - handling entitlement in message.")
        content, guid, incident_id, task_id = extract_entitlement(entitlement_match.group(), text)
        demisto.handleEntitlementForUser(incident_id, guid, user.get("profile", {}).get("email"), content, task_id)

        return "Thank you for your response."
    else:
        integration_context = get_integration_context(SYNC_CONTEXT)
        questions = integration_context.get("questions", [])
        if questions and thread_id:
            questions = json.loads(questions)
            question_filter = list(filter(lambda q: q.get("thread") == thread_id, questions))
            if question_filter:
                question = question_filter[0]
                demisto.info("Slack - handling entitlement in thread.")
                entitlement = question.get("entitlement")
                reply = question.get("reply", "Thank you for your response.")
                content, guid, incident_id, task_id = extract_entitlement(entitlement, text)
                demisto.handleEntitlementForUser(incident_id, guid, user.get("profile", {}).get("email"), content, task_id)
                question["remove"] = True
                set_to_integration_context_with_retries({"questions": questions}, OBJECTS_TO_KEYS, SYNC_CONTEXT)

                return reply

    return ""


""" SEND """


def get_conversation_by_name(conversation_name: str) -> dict:
    """
    Get a slack conversation by its name

    Args:
        conversation_name: The conversation name

    Returns:
        The slack conversation
    """
    integration_context = get_integration_context(SYNC_CONTEXT)

    conversation_to_search = conversation_name.lower()
    # Find conversation in the cache
    conversations = integration_context.get("conversations")
    if conversations:
        conversations = json.loads(conversations)
        conversation_filter = list(filter(lambda c: conversation_to_search == c.get("name", "").lower(), conversations))
        if conversation_filter:
            return conversation_filter[0]

    demisto.debug(f'could not find slack channel "{conversation_to_search}" in integration context, searching via API')
    # If not found in cache, search for it
    body = {"types": "private_channel,public_channel", "limit": PAGINATED_COUNT}
    response = send_slack_request_sync(CLIENT, "conversations.list", http_verb="GET", body=body)
    conversation: dict = {}
    while True:
        conversations = response["channels"] if response and response.get("channels") else []
        cursor = response.get("response_metadata", {}).get("next_cursor")
        conversation_filter = [c for c in conversations if c.get("name") == conversation_name]
        if conversation_filter:
            break
        if not cursor:
            break

        body = body.copy()  # strictly for unit-test purposes (test_get_conversation_by_name_paging)
        body.update({"cursor": cursor})
        response = send_slack_request_sync(CLIENT, "conversations.list", http_verb="GET", body=body)

    if conversation_filter:
        conversation = conversation_filter[0]

    # Save conversations to cache
    if conversation:
        conversations = integration_context.get("conversations")
        if conversations:
            conversations = json.loads(conversations)
            conversations.append(conversation)
        else:
            conversations = [conversation]
        set_to_integration_context_with_retries({"conversations": conversations}, OBJECTS_TO_KEYS, SYNC_CONTEXT)

    return conversation


def filter_conversations(
    name: str = "",
    creator: str = "",
    is_archived: bool | None = None,
    is_general: bool | None = None,
    is_private: bool | None = None,
) -> list:
    """
    Get a filtered list of slack conversations

    Args:
        name: The name of a channel or a regex pattern
        creator: The member ID who created the channel
        is_archived: True, False, or None to ignore
        is_general: True, False, or None to ignore
        is_private: True, False, or None to ignore

    Returns:
        A filtered list of slack conversations
    """

    integration_context = get_integration_context(SYNC_CONTEXT)

    # Find conversation in the cache
    conversations = integration_context.get("conversations")
    if conversations:
        conversations = json.loads(conversations)
        conversation_filter = []
        for c in conversations:
            if name and not re.search(name, c.get("name"), re.IGNORECASE):
                continue
            if creator and creator.upper() != c.get("creator"):
                continue
            if is_archived is not None and is_archived != c.get("is_archived"):
                continue
            if is_general is not None and is_general != c.get("is_general"):
                continue
            if is_private is not None and is_private != c.get("is_private"):
                continue

            conversation_filter.append(c)

        if conversation_filter:
            return conversation_filter

    filters: dict[str, str | bool] = {}
    if name:
        filters.update({"name": name})
    if creator:
        filters.update({"creator": creator})
    if is_archived:
        filters.update({"is_archived": is_archived})
    if is_general:
        filters.update({"is_general": is_general})
    if is_private:
        filters.update({"is_private": is_private})
    filter_string = ", ".join([f"{k}={v}" for k, v in filters.items()])
    demisto.debug(f"could not find channels matching filters {filter_string} in integration context, searching via API")

    # If not found in cache, search for it
    body = {"types": "private_channel,public_channel", "limit": PAGINATED_COUNT}
    response = send_slack_request_sync(CLIENT, "conversations.list", http_verb="GET", body=body)
    filtered_conversations: list = []
    while True:
        conversations = response["channels"] if response and response.get("channels") else []
        cursor = response.get("response_metadata", {}).get("next_cursor")
        conversation_filter = []
        for c in conversations:
            if name and not re.search(name, c.get("name"), re.IGNORECASE):
                continue
            if creator and creator.upper() != c.get("creator"):
                continue
            if is_archived is not None and is_archived != c.get("is_archived"):
                continue
            if is_general is not None and is_general != c.get("is_general"):
                continue
            if is_private is not None and is_private != c.get("is_private"):
                continue

            conversation_filter.append(c)

        if conversation_filter:
            filtered_conversations.extend(conversation_filter)
        if not cursor:
            break

        body = body.copy()  # strictly for unit-test purposes (test_filter_conversations_by_regex_paging)
        body.update({"cursor": cursor})
        response = send_slack_request_sync(CLIENT, "conversations.list", http_verb="GET", body=body)

    # Save conversations to cache
    if filtered_conversations:
        conversations = integration_context.get("conversations")
        if conversations:
            conversations = json.loads(conversations)
            conversations.extend(filtered_conversations)
        else:
            conversations = filtered_conversations
        set_to_integration_context_with_retries({"conversations": conversations}, OBJECTS_TO_KEYS, SYNC_CONTEXT)

    return filtered_conversations


def slack_send():
    """
    Sends a message to slack
    """

    args = demisto.args()
    demisto.debug(f"Got slack send request. args: {args}")
    message = args.get("message", "")
    to = args.get("to")
    original_channel = args.get("channel")
    group = args.get("group")
    message_type = args.get("messageType", "")  # From server
    original_message = args.get("originalMessage", "")  # From server
    entry = args.get("entry")
    ignore_add_url = args.get("ignoreAddURL", False) or args.get("IgnoreAddURL", False)
    thread_id = args.get("threadID", "")
    severity = args.get("severity")  # From server
    blocks = args.get("blocks")
    entry_object = args.get("entryObject")  # From server, available from demisto v6.1 and above
    entitlement = ""

    if message_type == MIRROR_TYPE and original_message.find(MESSAGE_FOOTER) != -1:
        # return so there will not be a loop of messages
        return

    if message_type == MIRROR_TYPE:
        tags = argToList(demisto.params().get("filtered_tags", []))
        entry_tags = entry_object.get("tags", [])

        if tags and not entry_tags:
            return

        # return if the entry tags is not containing any of the filtered_tags
        if tags and not any(elem in entry_tags for elem in tags):
            return

    if (to and group) or (to and original_channel) or (to and original_channel and group):
        return_error("Only one destination can be provided.")

    if severity:
        try:
            severity = int(severity)
        except Exception:
            severity = None

    channel = original_channel
    if original_channel == INCIDENT_NOTIFICATION_CHANNEL or (not original_channel and message_type == INCIDENT_OPENED):
        original_channel = INCIDENT_NOTIFICATION_CHANNEL
        channel = DEDICATED_CHANNEL
        demisto.debug(f"trying to send message to channel {original_channel}, changing slack channel to {channel}")

    if (
        channel == DEDICATED_CHANNEL
        and original_channel == INCIDENT_NOTIFICATION_CHANNEL
        and ((severity is not None and severity < SEVERITY_THRESHOLD) or not NOTIFY_INCIDENTS)
    ):
        channel = None

    if not (to or group or channel):
        return_error("Either a user, group or channel must be provided.")

    reply = ""
    expiry = ""
    default_response = ""
    if blocks:
        entitlement_match = re.search(ENTITLEMENT_REGEX, blocks)
        if entitlement_match:
            try:
                parsed_message = json.loads(blocks)
                entitlement = parsed_message.get("entitlement")
                blocks = parsed_message.get("blocks")
                reply = parsed_message.get("reply")
                expiry = parsed_message.get("expiry")
                default_response = parsed_message.get("default_response")
            except Exception:
                demisto.info("Slack - could not parse JSON from entitlement blocks.")
    elif message:
        entitlement_match = re.search(ENTITLEMENT_REGEX, message)
        if entitlement_match:
            try:
                parsed_message = json.loads(message)
                entitlement = parsed_message.get("entitlement")
                message = parsed_message.get("message")
                reply = parsed_message.get("reply")
                expiry = parsed_message.get("expiry")
                default_response = parsed_message.get("default_response")
            except Exception:
                demisto.info("Slack - could not parse JSON from entitlement message.")

    response = slack_send_request(to, channel, group, entry, ignore_add_url, thread_id, message=message, blocks=blocks)

    if response:
        thread = response.get("ts")
        if entitlement:
            save_entitlement(entitlement, thread, reply, expiry, default_response)

        demisto.results(
            {
                "Type": entryTypes["note"],
                "HumanReadable": f"Message sent to Slack successfully.\nThread ID is: {thread}",
                "Contents": response.data,
                "ContentsFormat": formats["json"],
                "EntryContext": {
                    "Slack.Thread(val.ID===obj.ID)": {"ID": thread},
                },
            }
        )
    else:
        demisto.results("Could not send the message to Slack.")


def save_entitlement(entitlement, thread, reply, expiry, default_response):
    """
    Saves an entitlement with its thread

    Args:
        entitlement: The entitlement
        thread: The thread
        reply: The reply to send to the user.
        expiry: The question expiration date.
        default_response: The response to send if the question times out.
    """
    integration_context = get_integration_context(SYNC_CONTEXT)
    questions = integration_context.get("questions", [])
    if questions:
        questions = json.loads(integration_context["questions"])
    questions.append(
        {
            "thread": thread,
            "entitlement": entitlement,
            "reply": reply,
            "expiry": expiry,
            "sent": datetime.strftime(get_current_utc_time(), DATE_FORMAT),
            "default_response": default_response,
        }
    )

    set_to_integration_context_with_retries({"questions": questions}, OBJECTS_TO_KEYS, SYNC_CONTEXT)


def slack_send_file():
    """
    Sends a file to slack
    """
    to = demisto.args().get("to")
    channel = demisto.args().get("channel")
    group = demisto.args().get("group")
    entry_id = demisto.args().get("file")
    thread_id = demisto.args().get("threadID")
    comment = demisto.args().get("comment", "")

    if not (to or channel or group):
        mirror = find_mirror_by_investigation()
        if mirror:
            channel = mirror.get("channel_name")

    if not (to or channel or group):
        return_error("Either a user, group or channel must be provided.")

    file_path = demisto.getFilePath(entry_id)

    file_dict = {"path": file_path["path"], "name": file_path["name"], "comment": comment}

    response = slack_send_request(to, channel, group, thread_id=thread_id, file_dict=file_dict)
    if response:
        demisto.results("File sent to Slack successfully.")
    else:
        demisto.results("Could not send the file to Slack.")


def send_message(
    destinations: list, entry: str, ignore_add_url: bool, integration_context: dict, message: str, thread_id: str, blocks: str
):
    """
    Sends a message to Slack.
    Args:
        destinations: The destinations to send to.
        entry: A WarRoom entry to send.
        ignore_add_url: Do not add a Demisto URL to the message.
        integration_context: Current integration context.
        message: The message to send.
        thread_id: The Slack thread ID to send the message to.
        blocks: Message blocks to send

    Returns:
        The Slack send response.
    """
    if not message:
        if blocks:
            message = "New message from SOC Bot"
            # This is shown in the notification bubble from Slack
        else:
            message = "\n"

    if message and not blocks:
        if ignore_add_url and isinstance(ignore_add_url, str):
            ignore_add_url = bool(strtobool(ignore_add_url))
        if not ignore_add_url:
            investigation = demisto.investigation()
            server_links = demisto.demistoUrls()
            if investigation:
                if investigation.get("type") != PLAYGROUND_INVESTIGATION_TYPE:
                    link = server_links.get("warRoom")
                    if link:
                        if entry:
                            link += "/" + entry
                        message += f"\nView it on: {link}"
                else:
                    link = server_links.get("server", "")
                    if link:
                        message += f"\nView it on: {link}#/home"
    try:
        response = send_message_to_destinations(destinations, message, thread_id, blocks)
    except SlackApiError as e:
        if str(e).find("not_in_channel") == -1 and str(e).find("channel_not_found") == -1:
            raise
        bot_id = integration_context.get("bot_id")
        if not bot_id:
            bot_id = get_bot_id()
        for dest in destinations:
            invite_users_to_conversation(dest, [bot_id])
        response = send_message_to_destinations(destinations, message, thread_id, blocks)
    return response


def send_message_to_destinations(destinations: list, message: str, thread_id: str, blocks: str = "") -> SlackResponse | None:
    """
    Sends a message to provided destinations Slack.

    Args:
        destinations: Destinations to send to.
        message: The message to send.
        thread_id: Slack thread ID to send to.
        blocks: Message blocks to send

    Returns:
        The Slack send response.
    """
    response: SlackResponse | None = None
    body: dict = {}

    if message:
        body["text"] = message
    if blocks:
        block_list = json.loads(blocks, strict=False)
        body["blocks"] = block_list
    if thread_id:
        body["thread_ts"] = thread_id

    for destination in destinations:
        body["channel"] = destination
        response = send_slack_request_sync(CLIENT, "chat.postMessage", body=body)

    return response


def send_file(destinations: list, file_dict: dict, integration_context: dict, thread_id: str) -> SlackResponse | None:
    """
    Sends a file to Slack.

    Args:
        destinations: Destinations to send the file to.
        file_dict: The file to send.
        integration_context: The current integration context.
        thread_id: A Slack thread to send to.

    Returns:
        The Slack send response.
    """
    try:
        response = send_file_to_destinations(destinations, file_dict, thread_id)
    except SlackApiError as e:
        if str(e).find("not_in_channel") == -1 and str(e).find("channel_not_found") == -1:
            raise
        bot_id = integration_context.get("bot_id")
        if not bot_id:
            bot_id = get_bot_id()
            integration_context["bot_id"] = bot_id
        for dest in destinations:
            invite_users_to_conversation(dest, [bot_id])
        response = send_file_to_destinations(destinations, file_dict, thread_id)

    return response


def send_file_to_destinations(destinations: list, file_dict: dict, thread_id: str) -> SlackResponse | None:
    """
    Sends a file to provided destinations in Slack.

    Args:
        destinations: The destinations to send to.
        file_dict: The file to send.
        thread_id: A thread ID to send to.

    Returns:
        The Slack send response.
    """
    response: SlackResponse | None = None
    body = {"filename": file_dict["name"]}

    if "comment" in file_dict:
        body["initial_comment"] = file_dict["comment"]

    for destination in destinations:
        body["channels"] = destination
        if thread_id:
            body["thread_ts"] = thread_id

        response = send_slack_request_sync(CLIENT, "files.upload", file_=file_dict["path"], body=body)

    return response


def slack_send_request(
    to: str,
    channel: str,
    group: str,
    entry: str = "",
    ignore_add_url: bool = False,
    thread_id: str = "",
    message: str = "",
    blocks: str = "",
    file_dict: dict = None,
) -> SlackResponse | None:
    """
    Requests to send a message or a file to Slack.

    Args:
        to: A Slack user to send to.
        channel: A Slack channel to send to.
        group: A Slack private channel to send to.
        entry: WarRoom entry to send.
        ignore_add_url: Do not add a Demisto URL to the message.
        thread_id: The Slack thread ID to send to.
        message: A message to send.
        blocks: Blocks to send with a slack message
        file_dict: A file to send.

    Returns:
        The Slack send response.
    """

    integration_context = get_integration_context(SYNC_CONTEXT)
    mirrors: list = []
    if integration_context and "mirrors" in integration_context:
        mirrors = json.loads(integration_context["mirrors"])

    destinations = []

    if to:
        if isinstance(to, list):
            to = to[0]
        user = get_user_by_name(to)
        if not user:
            demisto.error(f"Could not find the Slack user {to}")
        else:
            body = {"users": user.get("id")}
            im = send_slack_request_sync(CLIENT, "conversations.open", body=body)
            destinations.append(im.get("channel", {}).get("id"))
    if channel or group and not destinations:
        destination_name = channel or group
        mirrored_channel_filter = list(filter(lambda m: f'incident-{m["investigation_id"]}' == destination_name, mirrors))
        if mirrored_channel_filter:
            channel_mirror = mirrored_channel_filter[0]
            conversation_id = channel_mirror["channel_id"]
        else:
            conversation = get_conversation_by_name(destination_name)
            if not conversation:
                return_error(f"Could not find the Slack conversation {destination_name}")
            conversation_id = conversation.get("id")

        if conversation_id:
            destinations.append(conversation_id)

    if not destinations:
        return_error("Could not find any destination to send to.")

    if file_dict:
        response = send_file(destinations, file_dict, integration_context, thread_id)
        return response

    response = send_message(destinations, entry, ignore_add_url, integration_context, message, thread_id, blocks)

    return response


def set_channel_topic():
    """
    Sets a topic for a slack channel
    """

    channel = demisto.args().get("channel")
    topic = demisto.args().get("topic")

    channel_id = ""

    if not channel:
        mirror = find_mirror_by_investigation()
        if mirror:
            channel_id = mirror.get("channel_id", "")
            # We need to update the topic in the mirror
            integration_context = get_integration_context(SYNC_CONTEXT)
            mirrors = json.loads(integration_context["mirrors"])
            mirror = mirrors.pop(mirrors.index(mirror))
            mirror["channel_topic"] = topic
            mirrors.append(mirror)
            set_to_integration_context_with_retries({"mirrors": mirrors}, OBJECTS_TO_KEYS, SYNC_CONTEXT)
    else:
        channel = get_conversation_by_name(channel)
        channel_id = channel.get("id")

    if not channel_id:
        return_error("Channel not found - the Demisto app needs to be a member of the channel in order to look it up.")

    body = {"channel": channel_id, "topic": topic}
    send_slack_request_sync(CHANNEL_CLIENT, "conversations.setTopic", body=body)

    demisto.results("Topic successfully set.")


def rename_channel():
    """
    Renames a slack channel
    """

    channel = demisto.args().get("channel")
    new_name = demisto.args().get("name")

    channel_id = ""

    if not channel:
        mirror = find_mirror_by_investigation()
        if mirror:
            channel_id = mirror.get("channel_id", "")
            # We need to update the name in the mirror
            integration_context = get_integration_context(SYNC_CONTEXT)
            mirrors = json.loads(integration_context["mirrors"])
            mirror = mirrors.pop(mirrors.index(mirror))
            mirror["channel_name"] = new_name
            mirrors.append(mirror)
            set_to_integration_context_with_retries({"mirrors": mirrors}, OBJECTS_TO_KEYS, SYNC_CONTEXT)
    else:
        channel = get_conversation_by_name(channel)
        channel_id = channel.get("id")

    if not channel_id:
        return_error("Channel not found - the Demisto app needs to be a member of the channel in order to look it up.")

    body = {"channel": channel_id, "name": new_name}
    send_slack_request_sync(CHANNEL_CLIENT, "conversations.rename", body=body)

    demisto.results("Channel renamed successfully.")


def close_channel():
    """
    Archives a slack channel by name or its incident ID if mirrored.
    """
    channel = demisto.args().get("channel")
    channel_id = ""

    if not channel:
        mirror = find_mirror_by_investigation()
        if mirror:
            channel_id = mirror.get("channel_id", "")
            # We need to update the topic in the mirror
            integration_context = get_integration_context(SYNC_CONTEXT)
            mirrors = json.loads(integration_context["mirrors"])
            channel_id = mirror["channel_id"]
            # Check for mirrors on the archived channel
            channel_mirrors = list(filter(lambda m: channel_id == m["channel_id"], mirrors))
            for mirror in channel_mirrors:
                mirror["remove"] = True
                demisto.mirrorInvestigation(
                    mirror["investigation_id"], f'none:{mirror["mirror_direction"]}', mirror["auto_close"]
                )

            set_to_integration_context_with_retries({"mirrors": mirrors}, OBJECTS_TO_KEYS, SYNC_CONTEXT)
    else:
        channel = get_conversation_by_name(channel)
        channel_id = channel.get("id")

    if not channel_id:
        return_error("Channel not found - the Demisto app needs to be a member of the channel in order to look it up.")

    body = {"channel": channel_id}
    send_slack_request_sync(CHANNEL_CLIENT, "conversations.archive", body=body)

    demisto.results("Channel successfully archived.")


def create_channel():
    """
    Creates a channel in Slack using the provided arguments.
    """
    channel_type = demisto.args().get("type", "private")
    channel_name = demisto.args()["name"]
    users = argToList(demisto.args().get("users", []))
    topic = demisto.args().get("topic")

    body = {"name": channel_name}

    if channel_type == "private":
        body["is_private"] = True

    conversation = send_slack_request_sync(CHANNEL_CLIENT, "conversations.create", body=body).get("channel", {})

    if users:
        slack_users = search_slack_users(users)
        invite_users_to_conversation(conversation.get("id"), [u.get("id") for u in slack_users])
    if topic:
        body = {"channel": conversation.get("id"), "topic": topic}
        send_slack_request_sync(CHANNEL_CLIENT, "conversations.setTopic", body=body)

    created_channel_name = conversation.get("name")
    created_channel_id = conversation.get("id")
    demisto.results(f"Successfully created the channel {created_channel_name}")

    # Save the newly created channel to the context for fast future lookups
    integration_context = get_integration_context(SYNC_CONTEXT)
    context_conversations = integration_context.get("conversations", "[]")
    conversations = json.loads(context_conversations)
    conversations.append({"name": created_channel_name, "id": created_channel_id})
    set_to_integration_context_with_retries({"conversations": conversations}, OBJECTS_TO_KEYS, SYNC_CONTEXT)


def invite_to_channel():
    channel = demisto.args().get("channel")
    users = argToList(demisto.args().get("users", []))

    channel_id = ""

    if not channel:
        mirror = find_mirror_by_investigation()
        if mirror:
            channel_id = mirror["channel_id"]
    else:
        channel = get_conversation_by_name(channel)
        channel_id = channel.get("id")

    if not channel_id:
        return_error("Channel not found - the Demisto app needs to be a member of the channel in order to look it up.")

    slack_users = search_slack_users(users)
    if slack_users:
        invite_users_to_conversation(channel_id, [u.get("id") for u in slack_users])
    else:
        return_error("No users found")

    demisto.results("Successfully invited users to the channel.")


def kick_from_channel():
    channel = demisto.args().get("channel")
    users = argToList(demisto.args().get("users", []))

    channel_id = ""

    if not channel:
        mirror = find_mirror_by_investigation()
        if mirror:
            channel_id = mirror["channel_id"]
    else:
        channel = get_conversation_by_name(channel)
        channel_id = channel.get("id")

    if not channel_id:
        return_error("Channel not found - the Demisto app needs to be a member of the channel in order to look it up.")

    slack_users = search_slack_users(users)
    if slack_users:
        kick_users_from_conversation(channel_id, [u.get("id") for u in slack_users])
    else:
        return_error("No users found")

    demisto.results("Successfully kicked users from the channel.")


def get_user():
    user = demisto.args()["user"]

    slack_user = get_user_by_name(user)
    if not slack_user:
        return_error("User not found")

    profile = slack_user.get("profile", {})
    result_user = {
        "ID": slack_user.get("id"),
        "Username": slack_user.get("name"),
        "Name": profile.get("real_name_normalized") or profile.get("real_name"),
        "DisplayName": profile.get("display_name"),
        "Email": profile.get("email"),
    }

    hr = tableToMarkdown(
        "Details for Slack user: " + user,
        result_user,
        headers=["ID", "Username", "Name", "DisplayName", "Email"],
        headerTransform=pascalToSpace,
        removeNull=True,
    )
    context = {"Slack.User(val.ID === obj.ID)": createContext(result_user, removeNull=True)}

    return_outputs(hr, context, slack_user)


def long_running_main():
    """
    Starts the long running thread.
    """
    asyncio.run(start_listening())


def init_globals(command_name: str = ""):
    """
    Initializes global variables according to the integration parameters
    """
    global BOT_TOKEN, ACCESS_TOKEN, PROXY_URL, PROXIES, DEDICATED_CHANNEL, CLIENT, CHANNEL_CLIENT
    global SEVERITY_THRESHOLD, ALLOW_INCIDENTS, NOTIFY_INCIDENTS, INCIDENT_TYPE, VERIFY_CERT
    global BOT_NAME, BOT_ICON_URL, MAX_LIMIT_TIME, PAGINATED_COUNT, SSL_CONTEXT

    VERIFY_CERT = not demisto.params().get("unsecure", False)
    if not VERIFY_CERT:
        SSL_CONTEXT = ssl.create_default_context()
        SSL_CONTEXT.check_hostname = False
        SSL_CONTEXT.verify_mode = ssl.CERT_NONE
    else:
        # Use default SSL context
        SSL_CONTEXT = None

    if command_name != "long-running-execution":
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        if not loop._default_executor:  # type: ignore[attr-defined]
            demisto.info(f"setting _default_executor on loop: {loop} id: {id(loop)}")
            loop.set_default_executor(concurrent.futures.ThreadPoolExecutor(max_workers=4))

    BOT_TOKEN = demisto.params().get("cred_bot_token", {}).get("password") or demisto.params().get("bot_token", "")
    ACCESS_TOKEN = demisto.params().get("cred_access_token", {}).get("password") or demisto.params().get("access_token", "")
    PROXIES = handle_proxy()
    proxy_url = demisto.params().get("proxy_url")
    PROXY_URL = proxy_url or PROXIES.get("http")  # aiohttp only supports http proxy
    DEDICATED_CHANNEL = demisto.params().get("incidentNotificationChannel")
    CLIENT = slack.WebClient(token=BOT_TOKEN, proxy=PROXY_URL, ssl=SSL_CONTEXT)
    CHANNEL_CLIENT = slack.WebClient(token=ACCESS_TOKEN, proxy=PROXY_URL, ssl=SSL_CONTEXT)
    SEVERITY_THRESHOLD = SEVERITY_DICT.get(demisto.params().get("min_severity", "Low"), 1)
    ALLOW_INCIDENTS = demisto.params().get("allow_incidents", False)
    NOTIFY_INCIDENTS = demisto.params().get("notify_incidents", True)
    INCIDENT_TYPE = demisto.params().get("incidentType")
    BOT_NAME = demisto.params().get("bot_name")  # Bot default name defined by the slack plugin (3-rd party)
    BOT_ICON_URL = demisto.params().get("bot_icon")  # Bot default icon url defined by the slack plugin (3-rd party)
    MAX_LIMIT_TIME = int(demisto.params().get("max_limit_time", "60"))
    PAGINATED_COUNT = int(demisto.params().get("paginated_count", "200"))


def print_thread_dump():
    demisto.info(f"current thread: {threading.current_thread().name}")
    for threadId, stack in sys._current_frames().items():
        stack_str = "\n".join(traceback.format_stack(stack))
        demisto.info(f"{threadId} stack: {stack_str}")


def loop_info(loop: asyncio.AbstractEventLoop):
    if not loop:
        return "loop is None"
    info = f"loop: {loop}. id: {id(loop)}."
    info += f"executor: {loop._default_executor} id: {id(loop._default_executor)}"  # type: ignore[attr-defined]
    if loop._default_executor:  # type: ignore[attr-defined]
        info += f" executor threads size: {len(loop._default_executor._threads)}"  # type: ignore[attr-defined]
        info += f" max: {loop._default_executor._max_workers} {loop._default_executor._threads}"  # type: ignore[attr-defined]
    return info


def slack_get_integration_context():
    integration_context = get_integration_context()
    return_results(fileResult("slack_integration_context", json.dumps(integration_context), EntryType.ENTRY_INFO_FILE))


def filter_channels_command():
    """
    Gets channels that match criteria
    """
    name = demisto.args().get("name", "")
    creator = demisto.args().get("creator", "")
    is_archived = demisto.args().get("is_archived")
    is_general = demisto.args().get("is_general")
    is_private = demisto.args().get("is_private")
    limit = int(demisto.args().get("limit", "20"))

    is_archived = bool(strtobool(is_archived)) if is_archived else None
    is_general = bool(strtobool(is_general)) if is_general else None
    is_private = bool(strtobool(is_private)) if is_private else None

    command_results_list: list[dict[str, bool | str]] = []
    filtered_conversations = filter_conversations(
        name=name, creator=creator, is_archived=is_archived, is_general=is_general, is_private=is_private
    )
    demisto.debug(f"found {len(filtered_conversations)} filtered conversations; limiting results to {limit}")
    for conversation in filtered_conversations[:limit]:
        result_conversation = {
            "ID": conversation.get("id"),
            "Name": conversation.get("name"),
            "Created": conversation.get("created"),
            "Creator": conversation.get("creator"),
            "IsArchived": conversation.get("is_archived"),
            "IsGeneral": conversation.get("is_general"),
            "IsPrivate": conversation.get("is_private"),
        }
        command_results_list.append(result_conversation)

    if command_results_list:
        human_readable = tableToMarkdown(
            "Channels: ",
            command_results_list,
            headers=["ID", "Name", "Created", "Creator", "IsArchived", "IsGeneral", "IsPrivate"],
            removeNull=True,
        )

    else:
        human_readable = "No channels found."

    return_results(
        CommandResults(
            outputs_prefix="Slack.Channel", outputs_key_field="ID", outputs=command_results_list, readable_output=human_readable
        )
    )


def main():
    """
    Main
    """
    global CLIENT
    if is_debug_mode():
        os.environ["PYTHONASYNCIODEBUG"] = "1"

    commands = {
        "test-module": test_module,
        "long-running-execution": long_running_main,
        "slack-mirror-investigation": mirror_investigation,
        "mirror-investigation": mirror_investigation,
        "slack-send": slack_send,
        "send-notification": slack_send,
        "slack-send-file": slack_send_file,
        "slack-set-channel-topic": set_channel_topic,
        "close-channel": close_channel,
        "slack-close-channel": close_channel,
        "slack-create-channel": create_channel,
        "slack-invite-to-channel": invite_to_channel,
        "slack-kick-from-channel": kick_from_channel,
        "slack-rename-channel": rename_channel,
        "slack-get-user-details": get_user,
        "slack-get-integration-context": slack_get_integration_context,
        "slack-filter-channels": filter_channels_command,
    }

    command_name = demisto.command()
    init_globals(command_name)

    try:
        command_func = commands[command_name]
        command_func()
    except Exception as e:
        LOG(e)
        return_error(str(e))
    finally:
        demisto.info(f"{command_name} completed. loop: {loop_info(CLIENT._event_loop)}")  # type: ignore
        if is_debug_mode():
            print_thread_dump()


if __name__ in ["__main__", "__builtin__", "builtins"]:
    main()