MattermostV2

Mattermost is an open-source, self-hostable online chat service with file sharing, search, and integrations. It is designed as an internal chat for organizations and companies.

Messaging and Conferencing · Mattermost

Details

IDMattermostV2
ProviderMattermost Inc
CategoryMessaging and Conferencing
From Version6.10.0
Docker Imagedemisto/netutils:1.0.0.10187688
Supported ModulesAgentix XSIAM

README

Mattermost is an open-source, self-hostable online chat service with file sharing, search, and integrations. It is designed as an internal chat for organizations and companies.
This integration was integrated and tested with version 4.0.0 of Mattermost API.

Some changes have been made that might affect your existing content.
If you are upgrading from a previous version of this integration, see Breaking Changes.

Configure Mattermost v2 in Cortex

Parameter Description Required
Server URL   True
Bot Access Token The Bot Access Token to use for connection. True
Personal Access Token The Personal Access Token to use for connection. True
Team Name   True
Default Notifications Channel If Notifications Channel name is not specified, will send notification to incidentNotificationChannel channel. False
Enable Incident Mirroring   False
Allow external users to create incidents via DM.   False
Types of Notifications to Send Notifications of type ‘externalAskSubmit’ or ‘externalFormSubmit’ are not configurable because they are required to allow Ask tasks to be sent correctly. False
Long running instance. Required for investigation mirroring and direct messages.   False
Trust any certificate (not secure)   False
Use system proxy settings   False

Commands

You can execute these commands from the 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.

mattermost-get-team


Gets a team’s details.

Required Permissions

Must be authenticated and have the view_team permission.

Base Command

mattermost-get-team

Input

Argument Name Description Required
team_name The name of the team to retrieve. Required

Context Output

Path Type Description
Mattermost.Team.id String The ID of the team.
Mattermost.Team.create_at Unknown When was the team created.
Mattermost.Team.update_at Unknown When was the team updated.
Mattermost.Team.delete_at Unknown When was the team deleted.
Mattermost.Team.display_name String The display name of the team.
Mattermost.Team.name String The name of the team.
Mattermost.Team.description String The description of the team.
Mattermost.Team.email String The email of the team.
Mattermost.Team.type String The type of the team.
Mattermost.Team.company_name String The company name of the team.
Mattermost.Team.allowed_domains String The allowed domains of the team.
Mattermost.Team.invite_id String The allowed domains of the team.
Mattermost.Team.allow_open_invite Unknown Does the team allow open invites.
Mattermost.Team.scheme_id String The scheme ID of the team.
Mattermost.Team.policy_id String The policy ID of the team.

Command example

!mattermost-get-team team_name=panw

Context Example

{
    "Mattermost": {
        "Team": {
            "allow_open_invite": false,
            "allowed_domains": "",
            "cloud_limits_archived": false,
            "company_name": "",
            "create_at": 1696486762638,
            "delete_at": 0,
            "description": "",
            "display_name": "PANW",
            "email": "rrapoport@example.com",
            "group_constrained": false,
            "id": "6ie46zmi4fdqiqqe7p5gfki9hr",
            "invite_id": "ocoh4fcrw7dzxgfu5bdtqpy7cr",
            "name": "panw",
            "policy_id": null,
            "scheme_id": "",
            "type": "O",
            "update_at": 1696486762638
        }
    }
}

Human Readable Output

Team details

allow_open_invite allowed_domains cloud_limits_archived company_name create_at delete_at description display_name email group_constrained id invite_id name policy_id scheme_id type update_at
false   false   1696486762638 0   PANW email false id id panw     O 1696486762638

mattermost-list-channels


Lists channels.

Required Permissions

manage_system

Base Command

mattermost-list-channels

Input

Argument Name Description Required
team The name of the team to list channels from. Default is the team name from the integration configuration. Optional
include_private_channels Whether to include private channels. Default is false. Possible values are: true, false. Optional
page The page number to retrieve. Default value is 0. Optional
page_size The size of the page to retrieve. Default value is 50. Optional
limit How many results to retrieve. Will override the page and page_size arguments if given. Optional

Context Output

Path Type Description
Mattermost.Channel.id String The ID of the channel.
Mattermost.Channel.create_at Unknown When was the channel created.
Mattermost.Channel.update_at Unknown When was the channel updated.
Mattermost.Channel.delete_at Unknown When was the channel deleted.
Mattermost.Channel.display_name String The display name of the channel.
Mattermost.Channel.name String The name of the channel.
Mattermost.Channel.description String The description of the channel.
Mattermost.Channel.header String The header of the channel.
Mattermost.Channel.purpose String The purpose of the channel.
Mattermost.Channel.last_post_at Unknown When was the last post to the channel made.
Mattermost.Channel.total_msg_count Unknown The total massage count of the channel.
Mattermost.Channel.extra_update_at Unknown When was the channel updated.
Mattermost.Channel.creator_id String The creator ID of the channel.

Command example

!mattermost-list-channels limit=2 include_private_channels=true

Context Example

{
    "Mattermost": {
        "Channel": [
            {
                "create_at": 1697024204532,
                "creator_id": "creator_id",
                "delete_at": 0,
                "display_name": "Name",
                "extra_update_at": 0,
                "group_constrained": null,
                "header": "",
                "id": "id",
                "last_post_at": 1712503619042,
                "last_root_post_at": 1712503619042,
                "name": "name",
                "policy_id": null,
                "props": null,
                "purpose": "",
                "scheme_id": null,
                "shared": null,
                "team_id": "team_id",
                "total_msg_count": 58,
                "total_msg_count_root": 56,
                "type": "O",
                "update_at": 1697024204532
            },
            {
                "create_at": 1696486762650,
                "creator_id": "",
                "delete_at": 0,
                "display_name": "Off-Topic",
                "extra_update_at": 0,
                "group_constrained": null,
                "header": "",
                "id": "id",
                "last_post_at": 1712501916866,
                "last_root_post_at": 1712501916866,
                "name": "off-topic",
                "policy_id": null,
                "props": null,
                "purpose": "",
                "scheme_id": null,
                "shared": null,
                "team_id": "team_id",
                "total_msg_count": 4,
                "total_msg_count_root": 4,
                "type": "O",
                "update_at": 1696486762650
            }
        ]
    }
}

Human Readable Output

Channels

name display_name type id
name Display_Name O id
off-topic Off-Topic O id

mattermost-list-private-channels-for-user


Get all the private channels on a team for a user.

Required Permissions

Logged in as the user, or have edit_other_users permission, and view_team permission for the team.

Base Command

mattermost-list-private-channels-for-user

Input

Argument Name Description Required
team_name The name of the team to list channels from. Default is the team name from the integration configuration. Optional
user_id User ID to get channel memberships for. Required

Context Output

Path Type Description
Mattermost.User.Channels Unknown List of channels where the user is member of.
Mattermost.User.id String The ID of the user.

Command example

!mattermost-list-private-channels-for-user user_id=user_id team_name=panw

Context Example

{
    "Mattermost": {
        "User": [
            {
                "channel_id": "channel_id",
                "user_id": "user_id",
                "roles": "Admin",
                "last_viewed_at": 0,
                "msg_count": 0,
                "mention_count": 0,
                "notify_props": {},
                "last_update_at": 0
            }
        ]
    }
}

Human Readable Output

Channels for UserName

name display_name type id
name Display_Name O channel_id

mattermost-create-channel


Creates a channel.

Required Permissions

If creating a public channel, create_public_channel permission is required. If creating a private channel, create_private_channel permission is required.

Base Command

mattermost-create-channel

Input

Argument Name Description Required
display_name The display name of the channel to create. Required
name The name of the channel to create. Required
type The type of the channel to create. Possible values are: public, private. Default is public. Optional
purpose The purpose of the channel to create. Optional
header The header of the channel to create. Optional
team The team name of the channel to create. Default is the team name from the integration configuration. Optional

Context Output

Path Type Description
Mattermost.Channel.id String The ID of the channel.
Mattermost.Channel.create_at Unknown When was the channel created.
Mattermost.Channel.update_at Unknown When was the channel updated.
Mattermost.Channel.delete_at Unknown When was the channel deleted.
Mattermost.Channel.display_name String The display name of the channel.
Mattermost.Channel.name String The name of the channel.
Mattermost.Channel.description String The description of the channel.
Mattermost.Channel.header String The header of the channel.
Mattermost.Channel.purpose String The purpose of the channel.
Mattermost.Channel.last_post_at Unknown When was the last post to the channel made.
Mattermost.Channel.total_msg_count Unknown The total massage count of the channel.
Mattermost.Channel.extra_update_at Unknown When was the channel updated.
Mattermost.Channel.creator_id String The creator ID of the channel.
Mattermost.Channel.scheme_id String The scheme ID of the channel.

Command example

!mattermost-create-channel display_name=channel_name name=channel_name type=Private

Context Example

{
    "Mattermost": {
        "Channel": {
            "create_at": 1712649608411,
            "creator_id": "creator_id",
            "delete_at": 0,
            "display_name": "channel_name",
            "extra_update_at": 0,
            "group_constrained": null,
            "header": "",
            "id": "id",
            "last_post_at": 1712649608426,
            "last_root_post_at": 1712649608426,
            "name": "channel_name",
            "policy_id": null,
            "props": null,
            "purpose": "",
            "scheme_id": null,
            "shared": null,
            "team_id": "team_id",
            "total_msg_count": 0,
            "total_msg_count_root": 0,
            "type": "P",
            "update_at": 1712649608411
        }
    }
}

Human Readable Output

Channel channel_name was created successfully.

mattermost-add-channel-member


Adds a channel member.

Required Permissions

No permissions required.

Base Command

mattermost-add-channel-member

Input

Argument Name Description Required
team The team name of the channel to add the user to. Default is the team name from the integration configuration. Optional
channel The name of the channel to add the user to. Required
user_id The ID of the user to add. Use the command ‘mattermost-list-users’ to fetch the user ID. Required

Context Output

There is no context output for this command.

mattermost-remove-channel-member


Removes a channel member.

Required Permissions

manage_public_channel_members permission if the channel is public. manage_private_channel_members permission if the channel is private.

Base Command

mattermost-remove-channel-member

Input

Argument Name Description Required
team The team name of the channel to add the user to. Default is the team name from the integration configuration. Optional
channel The channel name of the channel to remove the user from. Required
user_id The ID of the user to remove. Use the command ‘mattermost-list-users’ to fetch the user ID. Required

Context Output

There is no context output for this command.

mattermost-list-users


Lists users.

Required Permissions

Requires an active session and (if specified) membership to the channel or team being selected from.

Base Command

mattermost-list-users

Input

Argument Name Description Required
team_name The name of the team to filter users by. Optional
channel The name of the channel to filters users by. If mentioned, a team name must be mentioned as well. Optional
page The page number to retrieve. Should be provided with the page_size argument. Default value is 0. Optional
page_size The size of the page to retrieve. Should be provided with the page argument. Default value is 50. Optional
limit How many results to retrieve. If provided, overrides the page and page_size arguments. Optional

Context Output

Path Type Description
Mattermost.User.id String The ID of the user.
Mattermost.User.create_at Unknown When was the user created.
Mattermost.User.update_at Unknown When was the user updated.
Mattermost.User.delete_at Unknown When was the user deleted.
Mattermost.User.username String The username of the user.
Mattermost.User.auth_data String The authorization data of the user.
Mattermost.User.auth_service String The authorization service of the user.
Mattermost.User.email String The email of the user.
Mattermost.User.nickname String The nickname of the user.
Mattermost.User.first_name Unknown The first name of the user.
Mattermost.User.last_name Unknown The last name of the user.
Mattermost.User.position Unknown The position of the user.
Mattermost.User.roles String The roles of the channel.
Mattermost.User.locale String The locale of the channel.
Mattermost.User.timezone Unknown The timezone of the user.

Command example

!mattermost-list-users limit=2 team_name=panw

Context Example

{
    "Mattermost": {
        "User": [
            {
                "auth_data": "",
                "auth_service": "",
                "create_at": 1696486752272,
                "delete_at": 0,
                "disable_welcome_email": false,
                "email": "email",
                "first_name": "",
                "id": "id",
                "last_name": "",
                "locale": "en",
                "nickname": "",
                "position": "",
                "roles": "system_admin system_user",
                "timezone": {
                    "automaticTimezone": "Asia/Jerusalem",
                    "manualTimezone": "",
                    "useAutomaticTimezone": "true"
                },
                "update_at": 1696486762658,
                "username": "admin"
            },
            {
                "auth_data": "",
                "auth_service": "",
                "create_at": 1696500307646,
                "delete_at": 0,
                "disable_welcome_email": false,
                "email": "email",
                "first_name": "",
                "id": "id",
                "last_name": "",
                "locale": "en",
                "nickname": "",
                "position": "",
                "roles": "system_user system_admin",
                "timezone": {
                    "automaticTimezone": "Asia/Jerusalem",
                    "manualTimezone": "",
                    "useAutomaticTimezone": "true"
                },
                "update_at": 1697354262697,
                "username": "username"
            }
        ]
    }
}

Human Readable Output

Users

username email role id
admin admin@admin.com   8a6t7whumbdbxrawretujh6rre
dev admin@ddev.com   o9hpcwz73fdwxe9adue8jxo16o

mattermost-send-file


Sends a file.

Required Permissions

Must have upload_file permission.

Base Command

mattermost-send-file

Command example

!mattermost-send-file message=check entry_id=85@109 channel=test

Input

Argument Name Description Required
team_name The team name of the channel to send the file to. Default is the team name from the integration configuration. Optional
channel The channel name of the channel to send the file to. Cannot be combined with the to argument. Optional
message The message to send to the channel along with the file. Required
entry_id The entry ID of the file. Required
to The username or email of the user to send the file to. Optional

Context Output

There is no context output for this command.

Human Readable Output

file test.txt was successfully sent to channel test

send-notification


Send a message using a chatbot app.

Required Permissions

Must have create_post permission for the channel the post is being created in.

Base Command

send-notification

Input

Argument Name Description Required
message The message to send. Required
channel The channel name to send the notification to. Default value is the channel configuration parameter. Optional
entry An entry ID to send as a link. Optional
to The username or email of the user to send the file to. Optional
ignoreAddURL Adds the War Room link to the message. Possible values are: true, false. Optional
mattermost_ask The message as a JSON for asking questions to the user. Default value is false. Possible values are: true, false. Optional

Context Output

There is no context output for this command.

mattermost-close-channel


Closes a channel.

Required Permissions

delete_public_channel permission if the channel is public. delete_private_channel permission if the channel is private, or has manage_system permission.

Base Command

mattermost-close-channel

Input

Argument Name Description Required
team_name The team name of the channel to close. Default value is the team name from the integration configuration. Optional
channel The channel name of the channel to close. If not provided, the mirrored investigation channel is archived (if the channel exists). Optional

Context Output

There is no context output for this command.

close-channel


Closes a mirrored MatterMost channel. If not provided, the mirrored investigation channel is archived (if the channel exists).

Required Permissions

delete_public_channel permission if the channel is public. delete_private_channel permission if the channel is private, or has manage_system permission.

Base Command

close-channel

Input

Argument Name Description Required
team_name The team name of the channel to delete. Default value is the team name from the integration configuration. Optional
channel The channel name of the channel to close. Optional

Context Output

There is no context output for this command.

mirror-investigation


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

Required Permissions

No permissions required.

Base Command

mirror-investigation

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. Possible values are: all, chat, none. Default is all. Optional
autoclose Whether the channel is auto-closed when an investigation is closed. Possible values are: true, false. Default is true. Optional
direction The mirroring direction. Possible values are: Both, FromDemisto, ToDemisto. Default is Both. Optional
channel The name of the channel. The default is “incident-<incidentID>”. Optional
kickAdmin Whether to remove the admin from the newly created channel. Default value is false. Possible values are: true, false. Default is false. Optional
mirrorTo Mirrors the investigation to a group (private channel) or a public channel. Default is group. Possible values are: group, channel. Default is group. Optional

Context Output

There is no context output for this command.

close-channel


Closes a mirrored Mattermost channel. If not provided, the mirrored investigation channel is archived (if the channel exists).

Base Command

close-channel

Input

Argument Name Description Required
team_name The team name of the channel to delete. Default value is the team name from the integration configuration. Optional
channel_name The channel name of the channel to delete. Optional

Context Output

There is no context output for this command.

mattermost-mirror-investigation


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

Required Permissions

No permissions channel.

Base Command

mattermost-mirror-investigation

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. Possible values are: all, chat, none. Default is all. Optional
autoclose Whether the channel is auto-closed when an investigation is closed. Possible values are: true, false. Default is true. Optional
direction The mirroring direction. Possible values are: Both, FromDemisto, ToDemisto. Default is Both. Optional
channel The name of the channel. The default is “incident-<incidentID>”. Optional
kickAdmin Whether to remove the admin from the newly created channel. Default value is false. Possible values are: true, false. Default is false. Optional
mirrorTo Mirrors the investigation to a group (private channel) or a public channel. Possible values are: group, channel. Default is group. Optional

Context Output

There is no context output for this command.

mattermost-list-groups


Lists groups. Groups are available on Enterprise and Professional plans. Minimum server version: 5.11

Required Permissions

No permissions required.

Base Command

mattermost-list-groups

Input

Argument Name Description Required
page The page number to retrieve. Default value is 0. Optional
page_size The size of the page to retrieve. Default value is 50. Optional
limit How many results to retrieve. Will override the page and page_size arguments if given. Optional
group Search for a specific user by this pattern. Optional

Context Output

Path Type Description
Mattermost.Groups.id String The ID of the group.
Mattermost.Groups.create_at Unknown The datetime the group was created.
Mattermost.Groups.update_at Unknown The datetime the group was updated.
Mattermost.Groups.delete_at Unknown The datetime the group was deleted.
Mattermost.Groups.display_name String The display name of the group.
Mattermost.Groups.name String The name of the group.
Mattermost.Groups.description String The description of the group.
Mattermost.Groups.source String The source of the group.
Mattermost.Groups.remote_id String The remote ID of the group.
Mattermost.Groups.has_syncables boolean If the group has any syncables.

Command example


#### Context Example

```json
{
    "Mattermost": {
        "Groups": [
            {
                "id": "0815xyz",
                "name": "name",
                "display_name": "display name",
                "description": "description",
                "source": "custom",
                "remote_id": null,
                "create_at": 0,
                "update_at": 0,
                "delete_at": 0,
                "has_syncables": false
            },
            {
                "id": "0815abc",
                "name": "name",
                "display_name": "display name",
                "description": "",
                "source": "custom",
                "remote_id": null,
                "create_at": 0,
                "update_at": 0,
                "delete_at": 0,
                "has_syncables": false
            }
        ]
    }
}

Human Readable Output

Groups

name display_name description id
name display name   0815abc
name display name description 0815xyz

mattermost-list-group-members


Lists group members. Groups are available on Enterprise and Professional plans. Minimum server version: 5.11

Required Permissions

Must have manage_system permission.

Base Command

mattermost-list-group-members

Input

Argument Name Description Required
page The page number to retrieve. Default value is 0. Optional
page_size The size of the page to retrieve. Default value is 50. Optional
limit How many results to retrieve. Will override the page and page_size arguments if given. Optional
group_id The group ID of the user group to list members for. Use the command ‘mattermost-list-groups’ to fetch the group ID. Required

Context Output

Path Type Description
Mattermost.Members.id String The ID of the group member.
Mattermost.Members.first_name String The first name of the group member.
Mattermost.Members.last_name String The last name of the group member.
Mattermost.Members.username String The username of the group member.

Command example

!mattermost-list-group-members group_id=group_id

Human Readable Output

Group moi9ygz8qby1pr1xgkcfuqww9r members (2/2)

username email id
username1 admin@admin.com 8a6t7whumbdbxrawretujh6rre
username2 admin@ddev.com o9hpcwz73fdwxe9adue8jxo16o

mattermost-add-group-member


Add group member(s). Groups are available on Enterprise and Professional plans. Minimum server version: 6.3

Required Permissions

Must have custom_group_manage_members permission for the given group.

Base Command

mattermost-add-group-member

Input

Argument Name Description Required
group_id The group ID of the user group to list members for. Use the command ‘mattermost-list-groups’ to fetch the group ID. Required
user_ids A comma-separated list of IDs of the users to add. Use the command ‘mattermost-list-users’ to fetch the user ID(s). Required

Context Output

There is no context output for this command.

Command example

!mattermost-add-group-member group_id=group_id user_ids=user_ids

Human Readable Output

The member username was added to the group successfully, with group ID: moi9ygz8qby1pr1xgkcfuqww9r

mattermost-remove-group-member


Remove group member(s). Groups are available on Enterprise and Professional plans. Minimum server version: 6.3

Required Permissions

Must have custom_group_manage_members permission for the given group.

Base Command

mattermost-remove-group-member

Input

Argument Name Description Required
group_id The group ID of the user group to list members for. Use the command ‘mattermost-list-groups’ to fetch the group ID. Required
user_ids A comma-separated list of IDs of the users to remove. Use the command ‘mattermost-list-users’ to fetch the user ID(s). Required

Context Output

There is no context output for this command.

Command example

!mattermost-remove-group-member group_id=group_id user_ids=user_ids

Human Readable Output

The member username was removed from group successfully, with group ID: moi9ygz8qby1pr1xgkcfuqww9r

mattermost-set-channel-role


Update a user’s roles for a channel. The user has to already be a member of the channel.

Required Permissions

Must have manage_channel_roles permission for the channel.

Base Command

mattermost-set-channel-role

Input

Argument Name Description Required
channel_id The channel ID to set the role in. Use the command ‘mattermost-list-channels’ to fetch the channel ID. Required
user_id The ID of the user to set role for. Use the command ‘mattermost-list-users’ to fetch the user ID. Required
role The role to set for the user. Possible values are: Admin, Member. Default is Member. Optional

Context Output

There is no context output for this command.

Breaking changes from the previous version of this integration - Mattermost v2

A new required configuration parameters was added: Bot Access Token.

Configuration parameters

  • url — Server URL (required)
  • bot_access_token — (required)
  • personal_access_token — (required)
  • team_name — Team Name (required)
  • notification_channel — Default Notifications Channel
  • mirroring — Enable Incident Mirroring
  • allow_incidents — Allow external users to create incidents via DM.
  • permitted_notifications — Types of Notifications to Send
  • longRunning — Long running instance. Required for investigation mirroring and direct messages.
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (18)

  • close-channel

    Closes a mirrored Mattermost channel. If not provided, the mirrored investigation channel is archived (if the channel exists).

  • mattermost-add-channel-member

    Adds a channel member.

  • mattermost-add-group-member

    Add group member(s). Groups are available on Enterprise and Professional plans. Minimum server version: 6.3

  • mattermost-close-channel

    Closes a channel.

  • mattermost-create-channel

    Creates a channel.

  • mattermost-get-team

    Gets a team's details.

  • mattermost-list-channels

    Lists channels.

  • mattermost-list-group-members

    Lists group members. Groups are available on Enterprise and Professional plans. Minimum server version: 5.11

  • mattermost-list-groups

    Lists groups. Groups are available on Enterprise and Professional plans. Minimum server version: 5.11

  • mattermost-list-private-channels-for-user

    Get all the private channels on a team for a user.

  • mattermost-list-users

    Lists users.

  • mattermost-mirror-investigation

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

  • mattermost-remove-channel-member

    Removes a channel member.

  • mattermost-remove-group-member

    Remove group member(s). Groups are available on Enterprise and Professional plans. Minimum server version: 6.3

  • mattermost-send-file

    Sends a file.

  • mattermost-set-channel-role

    Update a user's roles for a channel. The user has to already be a member of the channel.

  • mirror-investigation

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

  • send-notification

    Send a message using a chatbot app.

import asyncio
import concurrent
from typing import Any
from urllib.parse import urlparse

import aiohttp
import demistomock as demisto  # noqa: F401
import urllib3
from CommonServerPython import *  # noqa: F401

from CommonServerUserPython import *  # noqa

# Disable insecure warnings
urllib3.disable_warnings()


""" CONSTANTS """
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR
DEFAULT_PAGE_NUMBER = 0
DEFAULT_PAGE_SIZE = 50
DEFAULT_LIMIT = 50
PLAYGROUND_INVESTIGATION_TYPE = 9
SECRET_TOKEN: str
BASE_URL: str
PROXY = False
VERIFY = False
SSL_CONTEXT: Optional[ssl.SSLContext]
PROXIES = {}
PROXY_URL: str
DEMISTO_URL: str
WEBSOCKET_URL: str
MAX_SAMPLES = 10
INCIDENT_TYPE: str
ALLOW_INCIDENTS: bool
PORT: int
MIRRORING_ENABLED: bool
LONG_RUNNING: bool
CACHED_INTEGRATION_CONTEXT: dict
VERIFY_CERT: bool
CACHE_EXPIRY: float
MESSAGE_FOOTER = "\n**From Mattermost**"
MIRROR_TYPE = "mirrorEntry"
OBJECTS_TO_KEYS = {
    "mirrors": "investigation_id",
    "messages": "entitlement",
}
DEFAULT_OPTIONS: Dict[str, Any] = {
    "timeout": 100,
    "request_timeout": None,
    "mfa_token": None,
    "auth": None,
    "keepalive": False,
    "keepalive_delay": 5,
    "websocket_kw_args": {},
    "debug": False,
    "http2": False,
}
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"
PERMITTED_NOTIFICATION_TYPES: list[str]
INCIDENT_NOTIFICATION_CHANNEL = "incidentNotificationChannel"
""" CLIENT CLASS """


class WebSocketClient:  # pragma: no cover
    def __init__(self, base_url: str, token: str, verify: bool, proxy: bool):
        self.base_url = base_url
        self.token = token
        self.alive = False
        self.last_msg = 0.0
        self.verify = verify
        self.proxy = proxy
        self.options = DEFAULT_OPTIONS.copy()

    async def connect(self, event_handler):
        """
        Connect to the websocket and authenticate it.
        When the authentication has finished, start the loop listening for messages,
        sending a ping to the server to keep the connection alive.

        :param event_handler: Every websocket event will be passed there. Takes one argument.
        :type event_handler: Function(message)
        :return:
        """
        if "https://" in self.base_url:
            uri = self.base_url.replace("https://", "wss://", 1)
        else:
            uri = self.base_url.replace("http://", "ws://", 1)
        uri += "/api/v4/websocket"
        url = self.base_url + "/api/v4/websocket"
        demisto.debug(f"MM: The uri for the websocket is {uri}, the url is {url}")

        self.alive = True

        while True:
            try:
                async with (
                    aiohttp.ClientSession() as session,
                    session.ws_connect(
                        uri,
                        ssl=SSL_CONTEXT,  # type: ignore[arg-type]
                        proxy=PROXY_URL,
                    ) as websocket,
                ):
                    demisto.debug("MM: starting to authenticate")
                    await self.authenticate(websocket, event_handler)
                    while self.alive:
                        try:
                            await self.start_loop(websocket, event_handler)
                        except aiohttp.ClientError:
                            break
                    if (not self.options["keepalive"]) or (not self.alive):
                        break
            except Exception as e:
                demisto.info(f"MM: Failed to establish websocket connection: {type(e)} thrown - {e!s}")
                await asyncio.sleep(float("inf"))

    async def start_loop(self, websocket, event_handler):
        """
        We will listen for websockets events, sending a heartbeats on a timer.
        If we don't the webserver would close the idle connection,
        forcing us to reconnect.
        """
        demisto.debug("MM: Starting websocket loop")
        keep_alive = asyncio.ensure_future(self.heartbeat(websocket))
        demisto.debug("MM: Waiting for messages on websocket")
        while self.alive:
            message = await websocket.receive_str()
            self.last_msg = time.time()
            demisto.debug(f"MM: {message=}")
            await event_handler(self, message)
        demisto.debug("MM: Cancelling heartbeat task")
        keep_alive.cancel()
        try:
            await keep_alive
        except asyncio.CancelledError:
            pass

    async def heartbeat(self, websocket):
        """
        Pongs the server if did not get a message within the timeframe
        """
        timeout: float = self.options["timeout"]
        while True:
            since_last_msg: float = time.time() - self.last_msg
            next_timeout: float = timeout - since_last_msg if since_last_msg <= timeout else timeout
            await asyncio.sleep(next_timeout)
            if time.time() - self.last_msg >= timeout:
                await websocket.pong()
                self.last_msg = time.time()

    def disconnect(self):
        """Sets `self.alive` to False so the loop in `self.start_loop` will finish."""
        demisto.debug("Disconnecting websocket")
        self.alive = False

    async def authenticate(self, websocket, event_handler):
        """
        Sends a authentication challenge over a websocket.
        """
        demisto.debug("MM: Authenticating websocket")
        json_data = json.dumps({"seq": 1, "action": "authentication_challenge", "data": {"token": self.token}})
        await websocket.send_str(json_data)
        while True:
            message = await websocket.receive_str()
            status = json.loads(message)
            demisto.debug(f"MM: The status is: {status}")
            await event_handler(self, message)
            if ("event" in status and status["event"] == "hello") and ("seq" in status and status["seq"] == 0):
                demisto.debug("MM: Websocket authentification OK")
                return True
            demisto.error("MM: Websocket authentification failed")


class HTTPClient(BaseClient):
    """Client class to interact with the MatterMost API"""

    def __init__(
        self,
        base_url: str,
        headers: dict,
        personal_access_token: str,
        bot_access_token: str,
        team_name: str,
        notification_channel: str | None = None,
        verify: bool = True,
        proxy: bool = False,
    ):
        super().__init__(base_url, verify, proxy, headers=headers)
        self.bot_access_token = bot_access_token
        self.personal_access_token = personal_access_token
        self.team_name = team_name
        self.notification_channel = notification_channel

    def get_team_request(self, team_name: str) -> dict[str, str]:
        """Gets a team details based on its name"""
        response = self._http_request(method="GET", url_suffix=f"/api/v4/teams/name/{team_name}")

        return response

    def list_channel_request(self, team_id: str, params: dict, get_private: bool = False) -> list[dict[str, Any]]:
        """lists channels in a specific team"""
        if get_private:
            response = self._http_request(method="GET", url_suffix=f"/api/v4/teams/{team_id}/channels/private", params=params)
        else:
            response = self._http_request(method="GET", url_suffix=f"/api/v4/teams/{team_id}/channels", params=params)

        return response

    def list_channels_for_user_request(self, team_id: str, user: str) -> list[dict[str, Any]]:
        """lists channels by user in a specific team"""
        response = self._http_request(method="GET", url_suffix=f"/api/v4/users/{user}/teams/{team_id}/channels")
        return response

    def create_channel_request(self, params: dict) -> dict[str, str]:
        """Creates a channel"""
        response = self._http_request(method="POST", url_suffix="/api/v4/channels", json_data=params)

        return response

    def get_channel_by_name_and_team_name_request(self, team_name: str, channel_name: str) -> dict[str, Any]:
        """Gets a channel based on name and team name"""
        response = self._http_request(method="GET", url_suffix=f"/api/v4/teams/name/{team_name}/channels/name/{channel_name}")

        return response

    def set_channel_role_request(self, channel_id: str, user_id: str, roles: str) -> dict[str, str]:
        """Set a channel role for channel member"""
        data = {"roles": roles}
        response = self._http_request(
            method="PUT", url_suffix=f"/api/v4/channels/{channel_id}/members/{user_id}/roles", json_data=data
        )

        return response

    def add_channel_member_request(self, channel_id: str, data: dict) -> dict[str, str]:
        """Adds a channel member"""
        response = self._http_request(method="POST", url_suffix=f"/api/v4/channels/{channel_id}/members", json_data=data)

        return response

    def remove_channel_member_request(self, channel_id: str, user_id: str) -> dict[str, str]:
        """Removes a channel member"""
        response = self._http_request(method="DELETE", url_suffix=f"/api/v4/channels/{channel_id}/members/{user_id}")

        return response

    def list_users_request(self, params: dict) -> list[dict[str, Any]]:
        """lists users"""
        response = self._http_request(method="GET", url_suffix="/api/v4/users", params=params)

        return response

    def close_channel_request(self, channel_id: str) -> list[dict[str, Any]]:
        """Closes a channel"""
        response = self._http_request(method="DELETE", url_suffix=f"/api/v4/channels/{channel_id}")

        return response

    def send_file_request(self, file_info: dict, params: dict) -> dict[str, Any]:
        "Sends a file"
        files = {"file": (file_info["name"], open(file_info["path"], "rb"))}

        response = self._http_request(
            method="POST",
            url_suffix="/api/v4/files",
            files=files,
            params=params,
            json_data={"channel_id": params.get("channel_id")},
        )
        return response

    def create_post_with_file_request(self, data: dict) -> list[dict[str, Any]]:
        """Creates a post with a file request"""
        response = self._http_request(method="POST", url_suffix="/api/v4/posts", json_data=data)

        return response

    def update_channel_request(self, channel_id: str, params: dict) -> list[dict[str, Any]]:
        """Updates a channel"""
        response = self._http_request(method="PUT", url_suffix=f"/api/v4/channels/{channel_id}", json_data=params)

        return response

    def get_user_request(self, user_id: str = "", bot_user: bool = False) -> dict[str, Any]:
        """Gets a user"""
        if not user_id:
            user_id = "me"
        if bot_user:
            response = self._http_request(
                method="GET", url_suffix=f"/api/v4/users/{user_id}", headers={"authorization": f"Bearer {self.bot_access_token}"}
            )
        else:
            response = self._http_request(method="GET", url_suffix=f"/api/v4/users/{user_id}")

        return response

    def send_notification_request(
        self, channel_id: str, message: str, file_ids: list[str] = [], root_id: str = "", props: dict = {}
    ) -> dict[str, Any]:  # noqa: E501
        "Sends a notification"
        data = {
            "channel_id": channel_id,
            "message": message,
            "props": props,
            "root_id": root_id,
            "file_ids": file_ids,
        }
        remove_nulls_from_dictionary(data)
        response = self._http_request(
            method="POST",
            url_suffix="/api/v4/posts",
            json_data=data,
            headers={"authorization": f"Bearer {self.bot_access_token}"},
        )

        return response

    def update_post_request(self, message: str, root_id: str) -> dict[str, Any]:  # noqa: E501
        "Sends a notification"
        data = {
            "message": message,
            "id": root_id,
        }
        demisto.debug(f"MM: {data=}")
        remove_nulls_from_dictionary(data)
        response = self._http_request(
            method="PUT",
            url_suffix=f"/api/v4/posts/{root_id}",
            json_data=data,
            headers={"authorization": f"Bearer {self.bot_access_token}"},
        )

        demisto.debug(f"MM: response fom update message. {response=}")
        return response

    def get_user_by_email_request(self, user_email: str) -> dict[str, Any]:
        "Gets a user by email"
        response = self._http_request(method="GET", url_suffix=f"/api/v4/users/email/{user_email}")

        return response

    def get_user_by_username_request(self, username: str) -> dict[str, Any]:
        "Gets a user by username"
        response = self._http_request(method="GET", url_suffix=f"/api/v4/users/username/{username}")

        return response

    def create_direct_channel_request(self, user_id: list, bot_id: str) -> dict[str, Any]:
        "creates a direct channel"
        user_id.append(bot_id)
        url_suffix = "/api/v4/channels/direct" if len(user_id) == 2 else "/api/v4/channels/group"
        response = self._http_request(method="POST", url_suffix=url_suffix, json_data=user_id)

        return response

    def list_groups_request(self, params: dict) -> list[dict[str, Any]]:
        """lists groups in a specific team"""
        response = self._http_request(method="GET", url_suffix="/api/v4/groups", params=params)

        return response

    def list_group_members_request(self, group_id: str, params: dict[str, Any]) -> dict[str, Any]:
        """list group members based on group id"""
        response = self._http_request(method="GET", url_suffix=f"/api/v4/groups/{group_id}/members", params=params)

        return response

    def add_group_member_request(self, group_id: str, data: dict) -> dict[str, str]:
        """Adds a group member"""
        response = self._http_request(method="POST", url_suffix=f"/api/v4/groups/{group_id}/members", json_data=data)

        return response

    def remove_group_member_request(self, group_id: str, data: dict) -> dict[str, str]:
        """Removes a group member"""
        response = self._http_request(method="DELETE", url_suffix=f"/api/v4/groups/{group_id}/members", json_data=data)

        return response


CLIENT: HTTPClient

""" HELPER FUNCTIONS """


def get_war_room_url(url: str, incident_id: str = "") -> str:
    # a workaround until this bug is resolved: https://jira-dc.paloaltonetworks.com/browse/CRTX-107526
    if is_xsiam() or is_platform():
        if not incident_id:
            incident_id = demisto.callingContext.get("context", {}).get("Inv", {}).get("id")
        incident_url = urlparse(url)
        war_room_url = f"{incident_url.scheme}://{incident_url.netloc}/incidents"
        # executed from the incident War Room
        if incident_id and incident_id.startswith("INCIDENT-"):
            war_room_url += f"/war_room?caseId={incident_id.split('-')[-1]}"
        # executed from the alert War Room
        else:
            war_room_url += f"/alerts_and_insights?caseId={incident_id}&action:openAlertDetails={incident_id}-warRoom"

        return war_room_url

    return url


def next_expiry_time() -> float:
    """
    Returns:
        A float representation of a new expiry time with an offset of 5 seconds
    """
    return (datetime.now(timezone.utc) + timedelta(seconds=5)).timestamp()


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


async def check_and_handle_entitlement(answer_text: str, root_id: str, user_name: str) -> str:  # pragma: no cover
    """
    Handles an entitlement message (a reply to a question)
    Args:
    Returns:
        If the message contains entitlement, return a reply.
    """
    integration_context = fetch_context(force_refresh=True)
    messages = integration_context.get("messages", [])
    reply = ""
    if not messages:
        return reply
    messages = json.loads(messages)
    demisto.debug(f"MM: messages with entitlements. {messages=}")
    message_filter = list(filter(lambda q: q.get("root_id") == root_id, messages))
    if message_filter:
        demisto.debug("MM: Found correct message")
        message = message_filter[0]
        entitlement = message.get("entitlement")
        reply = message.get("reply")
        guid, incident_id, task_id = extract_entitlement(entitlement)
        demisto.handleEntitlementForUser(incident_id, guid, user_name, answer_text, task_id)
        demisto.debug(f"MM: Handled entitlement for {incident_id=}, {task_id=} with {answer_text=}")
        message["remove"] = True
        set_to_integration_context_with_retries({"messages": messages}, OBJECTS_TO_KEYS)
    return reply


def run_long_running():  # pragma: no cover
    """
    Starts the long running thread.
    """
    try:
        asyncio.run(start_listening())
    except Exception as e:
        demisto.error(f"MM: The Loop has failed to run {e!s}")
    finally:
        loop = asyncio.get_running_loop()
        try:
            loop.stop()
            loop.close()
        except Exception as e_:
            demisto.error(f"MM: Failed to gracefully close the loop - {e_}")


async def start_listening():  # pragma: no cover
    """
    Starts a Slack SocketMode client and checks for mirrored incidents.
    """
    try:
        demisto.debug("MM: Starting to listen")
        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 mattermost_loop()
    except Exception as e:
        demisto.error(f"An error has occurred while gathering the loop tasks. {e}")


async def mattermost_loop():  # pragma: no cover
    try:
        exception_await_seconds = 1
        while True:
            ws_client = WebSocketClient(BASE_URL, SECRET_TOKEN, VERIFY, PROXY)

            try:
                demisto.debug("MM: Trying to connect")
                await ws_client.connect(event_handler)
                # After successful connection, we reset the backoff time.
                exception_await_seconds = 1
                await asyncio.sleep(float("inf"))
            except Exception as e:
                demisto.debug(f"MM: Exception in long running loop, waiting {exception_await_seconds} - {e}")
                await asyncio.sleep(exception_await_seconds)
                exception_await_seconds *= 2
            finally:
                try:
                    ws_client.disconnect()
                except Exception as e:
                    demisto.debug(f"MM: Failed to close client. - {e}")
    except Exception as e:
        demisto.error(f"MM: An error has occurred while trying to create the socket client. {e}")


def long_running_loop():  # pragma: no cover
    global MIRRORING_ENABLED
    tts = 30 if MIRRORING_ENABLED else 60
    while True:
        error = ""
        try:
            check_for_unanswered_messages()
            time.sleep(tts)
        except requests.exceptions.ConnectionError as e:
            error = f"Could not connect to the MatterMost endpoint: {e!s}"
        except Exception as e:
            error = f"An error occurred: {e}"
        finally:
            demisto.updateModuleHealth("")
            if error:
                demisto.error(error)
                demisto.updateModuleHealth(error)


def check_for_unanswered_messages():
    integration_context = fetch_context()
    messages = integration_context.get("messages")
    if messages:
        messages = json.loads(messages)
        now = datetime.utcnow()
        updated_messages = []

        for message in messages:
            if message.get("expiry"):
                # Check if the question expired - if it did, answer it with the default response
                # and remove it
                expiry = datetime.strptime(message["expiry"], DATE_FORMAT)
                if expiry < now:
                    demisto.debug(f"MM: message expired: {message}, answering it with the default response")
                    answer_question(message.get("default_response"), message, email="")
                    message["remove"] = True
                    updated_messages.append(message)
                    continue
            updated_messages.append(message)

        if updated_messages:
            set_to_integration_context_with_retries({"messages": messages}, OBJECTS_TO_KEYS)


async def event_handler(client: WebSocketClient, req: str):
    """Handling the events coming from the websocket"""
    demisto.debug(f"MM: Got events: {req} - with type {type(req)}")
    payload = json.loads(req)

    if "error" in payload:
        error = payload.get("error", {})
        error_code = error.get("id")
        error_msg = error.get("message")
        await handle_listen_error(f"MatterMost API has thrown an error. Code: {error_code}, Message: {error_msg}.")
        return

    if payload.get("event") == "hello" or payload.get("seq_reply") == 1:
        # we handle hello and authentication events afterwards
        return

    if payload.get("event") == "posted":
        await handle_posts(payload)
        return


def is_bot_message(payload: dict) -> bool:
    """
    Determines if the message received was created by a bot or not.
    :param payload: dict: The payload sent with the message
    :return: bool: True indicates the message was from a Bot, False indicates it was from an individual
    """
    global CLIENT
    from_bot = payload.get("props", {}).get("from_bot", "")
    bot_id = get_user_id_from_token(CLIENT, bot_user=True)
    post = json.loads(payload.get("data", {}).get("post"))

    if bot_id and bot_id == post.get("user_id", ""):
        return True
    elif from_bot:
        return True
    return False


def is_dm(payload: dict):
    """
    Takes the channel type and will see if it's 'D'. If so, we know it's from a direct message.
    :param channel: str: The channel ID to check.
    :return: bool: Boolean indicating if the channel is a DM or not.
    """
    channel_type = payload.get("data", {}).get("channel_type")
    return channel_type == "D"


def is_thread(post: dict):
    """
    Takes the root ID and will see if its not empty. If so, we know it's from a direct message.
    :param post: str: The post to check.
    :return: bool: Boolean indicating if the post is part of a thread or not.
    """
    root_id = post.get("root_id", "")
    return root_id != ""


def get_user_id_from_token(client: HTTPClient, bot_user: bool = False) -> str:
    """
    Gets the user id from the token
    :return: str: The id of the user
    """
    result = client.get_user_request(bot_user=bot_user)

    return result.get("id", "")


def get_user_id_by_email(client: HTTPClient, email: str) -> str:
    """
    Gets a user ID from the email
    :param email: str: The email of the user
    :return: str: The id of the user
    """
    result = client.get_user_by_email_request(email)
    return result.get("id", "")


def get_user_id_by_username(client: HTTPClient, username: str) -> str:
    """
    Gets a user ID from the email
    :param email: str: The email of the user
    :return: str: The id of the user
    """
    result = client.get_user_by_username_request(username)
    return result.get("id", "")


def get_username_by_email(client: HTTPClient, email: str) -> str:
    """
    Gets a username from the email
    :param email: str: The email of the user
    :return: str: The username of the user
    """
    result = client.get_user_by_email_request(email)
    return result.get("username", "")


def get_username_by_id(client: HTTPClient, user_id: str) -> str:
    """
    Gets a username from the id
    :param email: str: The email of the user
    :return: str: The username of the user
    """
    result = client.get_user_request(user_id)
    return result.get("username", "")


def get_team_id_from_team_name(client: HTTPClient, team_name: str):
    """
    Gets a id from the team_name
    :param email: str: The email of the user
    :return: str: The username of the user
    """
    result = client.get_team_request(team_name)
    return result.get("id", "")


def fetch_context(force_refresh: bool = False) -> dict:
    """
    Fetches the integration instance context from the server if the CACHE_EXPIRY is smaller than the current epoch time
    In the event that the cache is not expired, we return a cached copy of the context which has been stored in memory.
    We can force the retrieval of the updated context by setting the force_refresh flag to True.
    :param force_refresh: bool: Indicates if the context should be refreshed regardless of the expiry time.
    :return: dict: Either a cached copy of the integration context, or the context itself.
    """
    global CACHED_INTEGRATION_CONTEXT, CACHE_EXPIRY
    now = int(datetime.now(timezone.utc).timestamp())
    if (now >= CACHE_EXPIRY) or force_refresh:
        demisto.debug(
            f"Cached context has expired or forced refresh. forced refresh value is {force_refresh}. Fetching new context"
        )
        CACHE_EXPIRY = next_expiry_time()
        CACHED_INTEGRATION_CONTEXT = get_integration_context()

    return CACHED_INTEGRATION_CONTEXT


def get_channel_id_from_context(channel_name: str = "", investigation_id=None):
    """
    Retrieves a MatterMost channel ID based on the provided criteria.

    :param channel_name: The name of the channel to get the ID for.
    :param investigation_id: The Demisto investigation ID to search for a mirrored channel.

    :return: The requested channel ID or None if not found.
    """
    if not channel_name and not investigation_id:
        return None
    integration_context = fetch_context()
    mirrors = json.loads(integration_context.get("mirrors", "[]"))

    # Filter mirrors based on the provided criteria.
    if investigation_id:
        mirrored_channel_filter = next((m for m in mirrors if m["investigation_id"] == investigation_id), None)
    else:
        mirrored_channel_filter = next((m for m in mirrors if m["channel_name"] == channel_name), None)
    if mirrored_channel_filter:
        return mirrored_channel_filter.get("channel_id")
    return None


def get_channel_id_to_send_notif(client: HTTPClient, to: list, channel_name: str | None, investigation_id: str) -> str:
    """
    Gets a channel ID for the correct channel to send the notification to
    :return: str: The channel id of the channel
    """
    channel_id = ""
    if to:
        # resolve users to list
        users = []
        for user in to:
            if re.match(emailRegex, user):
                uid = get_user_id_by_email(client, user)
            else:
                uid = get_user_id_by_username(client, user)
            users.append(uid)

        bot_id = get_user_id_from_token(client, bot_user=True)
        channel_object = client.create_direct_channel_request(users, bot_id)
        channel_id = channel_object.get("id", "")
        demisto.debug(f"MM: Created a new direct channel to: {users} with channel_id: {channel_id}")

    elif channel_name:  # if channel name provided and the channel was mirrored
        channel_id = get_channel_id_from_context(channel_name, investigation_id)

        if not channel_id:
            try:
                channel_details = client.get_channel_by_name_and_team_name_request(client.team_name, channel_name)
                channel_id = channel_details.get("id", "")
            except Exception as e:
                if channel_name == INCIDENT_NOTIFICATION_CHANNEL:
                    try:
                        demisto.debug(f"MM: Creating a {INCIDENT_NOTIFICATION_CHANNEL} channel to send notification to.")
                        params = {
                            "team_id": get_team_id_from_team_name(client, client.team_name),
                            "name": INCIDENT_NOTIFICATION_CHANNEL.lower(),
                            "display_name": INCIDENT_NOTIFICATION_CHANNEL,
                            "type": "O",
                        }
                        channel_details = client.create_channel_request(params)
                        channel_id = channel_details.get("id", "")
                    except Exception as sec_e:
                        if "Channel does not exist." in str(sec_e):
                            hr = (
                                "Could not create a new channel. An archived channel with the same name may exist"
                                "in the provided team, choose a different name."
                            )
                            raise DemistoException(hr)
                        else:
                            raise sec_e
                else:
                    raise DemistoException(
                        f"Did not find channel with name {channel_name}. Make sure it exists or choose a "
                        f"different one to send notifications to. Error: {e}"
                    )

    return channel_id


def save_entitlement(entitlement, message_id, reply, expiry, default_response, to_id):
    """
    Saves an entitlement

    Args:
        entitlement: The entitlement
        message_id: The message_id
        reply: The reply to send to the user.
        expiry: The question expiration date.
        default_response: The response to send if the question times out.
        to_id: the user id the message was sent to
    """
    integration_context = get_integration_context()
    messages = integration_context.get("messages", [])
    if messages:
        messages = json.loads(integration_context["messages"])
    messages.append(
        {
            "root_id": message_id,
            "entitlement": entitlement,
            "reply": reply,
            "expiry": expiry,
            "sent": datetime.strftime(datetime.utcnow(), DATE_FORMAT),
            "default_response": default_response,
            "to_id": to_id,
        }
    )
    set_to_integration_context_with_retries({"messages": messages}, OBJECTS_TO_KEYS)


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

    Returns:
        Entitlement components
    """
    parts = entitlement.split("@")
    if len(parts) < 2:
        raise DemistoException("Entitlement cannot be parsed")
    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]

    return guid, incident_id, task_id


def answer_question(text: str, message: dict, email: str = ""):
    """Answers a question from MattermostAskUser"""
    global CLIENT
    entitlement = message.get("entitlement", "")
    root_id = message.get("root_id", "")
    guid, incident_id, task_id = extract_entitlement(entitlement)
    try:
        demisto.handleEntitlementForUser(incident_id, guid, email, text, task_id)
        process_entitlement_reply(text, root_id)
        demisto.debug(f"MM: Handled question for {incident_id=}, {task_id=} with {text=}")
    except Exception as e:
        demisto.error(f"Failed handling entitlement {entitlement}: {e!s}")
    message["remove"] = True
    return incident_id


async def send_notification_async(client: HTTPClient, channel_id, message, root_id=""):
    client.send_notification_request(channel_id, message, root_id=root_id)


async def update_post_async(client: HTTPClient, message, root_id):
    client.update_post_request(message, root_id)


def process_entitlement_reply(  # pragma: no cover
    entitlement_reply: str,
    root_id: str = "",
    user_name: str | None = None,
    answer_text: str | None = None,
):
    """
    Triggered when an entitlement reply is found, this function will update the original message with the reply message.
    :param entitlement_reply: str: The text to update the asking question with.
    :param user_name: str: name of the user who answered the entitlement
    :param answer_text: str: The text attached to the button, used for string replacement.
    :return: None
    """
    global CLIENT
    if "{user}" in entitlement_reply:
        entitlement_reply = entitlement_reply.replace("{user}", str(user_name))
    if "{response}" in entitlement_reply and answer_text:
        entitlement_reply = entitlement_reply.replace("{response}", str(answer_text))
    demisto.debug(f"MM: process entitlement reply with {entitlement_reply} for {root_id}")
    CLIENT.update_post_request(entitlement_reply, root_id)


async def handle_text_received_from_mm(investigation_id: str, text: str, operator_email: str, operator_name: str):
    """
    Handles text received from MatterMost

    Args:
        investigation_id: The mirrored investigation ID
        text: The received text
        operator_email: The sender email
        operator_name: The sender name
    """
    if text:
        demisto.addEntry(id=investigation_id, entry=text, username=operator_name, email=operator_email, footer=MESSAGE_FOOTER)


async def handle_posts(payload):
    """
    handle posts from the Mattermost that have been identified as possible mirrored messages
    If we find one, we will update the mirror object and send
    the message to the corresponding investigation's war room as an entry.
    :param payload: str: The request payload from mattermost
    :return: None
    """
    global CLIENT
    post = json.loads(payload.get("data", {}).get("post"))
    message = post.get("message", {})
    channel_id = post.get("channel_id")
    user_id = post.get("user_id")
    if not channel_id:
        return

    if is_bot_message(payload):
        demisto.debug("MM: Got a bot message. Will not mirror.")
        return

    # If a thread, we will check if it is a reply to a MattermostAsk task.
    if is_thread(post):
        demisto.debug(f"MM: Got a thread message. {payload=}")
        username = get_username_by_id(CLIENT, user_id)
        answer_text = post.get("message", "")
        root_id = post.get("root_id", "")
        entitlement_reply = await check_and_handle_entitlement(answer_text, root_id, username)
        demisto.debug(f"MM: Got {entitlement_reply=}")
        if entitlement_reply:
            process_entitlement_reply(entitlement_reply, root_id, username, answer_text)

        reset_listener_health()
        return

    # Check if the message is being sent directly to our bot.
    if is_dm(payload):
        demisto.debug(f"MM: Got a dm message. {payload=}")
        await handle_dm(user_id, message, channel_id, CLIENT)
        reset_listener_health()
        return

    integration_context = fetch_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_direction"] and mirror["mirror_type"]:
                investigation_id = mirror["investigation_id"]
                mirror_type = mirror["mirror_type"]
                auto_close = mirror["auto_close"]
                direction = mirror["mirror_direction"]
                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)

        user_details = CLIENT.get_user_request(user_id)
        operator_name = user_details.get("username", "")
        operator_email = user_details.get("email", "")
        investigation_id = mirror["investigation_id"]
        await handle_text_received_from_mm(investigation_id, message, operator_email, operator_name)


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 handle_dm(user_id: str, text: str, channel_id: str, client: HTTPClient):
    """
    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
    """
    message: str = text.lower()
    user_details = client.get_user_request(user_id)
    user_name = user_details.get("username", "")
    user_email = user_details.get("email", "")
    if message.find("incident") != -1 and (
        message.find("create") != -1 or message.find("open") != -1 or message.find("new") != -1
    ):
        demisto_user = demisto.findUser(email=user_email) if user_email else demisto.findUser(username=user_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: {e!s}"
    else:
        try:
            data = demisto.directMessage(text, user_name, user_email, ALLOW_INCIDENTS)
        except Exception as e:
            data = str(e)

    if not data:
        data = "Sorry, I could not perform the selected operation."

    await send_notification_async(client, channel_id, data)


async def translate_create(message: str, user_name: str, user_email: str, demisto_user: dict) -> str:  # pragma: no cover
    """
    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 = ""
    request_fields = {"ReporterEmail": user_email, "Message": message}
    incidents = []
    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]
            add_req_data_to_incidents(incidents, request_fields)
            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
            incidents = add_req_data_to_incidents([incident], request_fields)
            created_incident = await create_incidents([incident], user_name, user_email, user_demisto_id)
            if not created_incident:
                data = "Failed creating incidents."

    if created_incident:
        demisto.debug(f"Created {len(incidents)} incidents")
        update_integration_context_samples(incidents)
        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"]
        incident_url = get_war_room_url(f"{server_link}#/WarRoom/{incident_id}", incident_id)
        data = f"Successfully created incident {incident_name}.\n View it on: {incident_url}"

    return data


def add_req_data_to_incidents(incidents: list, request_fields: dict) -> list:  # pragma: no cover
    """
    Adds the request_fields as a rawJSON to every created incident for further information on the incident
    """
    for incident in incidents:
        incident["rawJSON"] = json.dumps(request_fields)
    return incidents


async def create_incidents(incidents: list, user_name: str, user_email: str, user_demisto_id: str = ""):
    """
    Creates incidents according to a provided JSON object
    Args:
        incidents: The incidents JSON
        user_name: The name of the user in MatterMost
        user_email: The email of the user in MattermOST
        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

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

    return data


def update_integration_context_samples(incidents: list, max_samples: int = MAX_SAMPLES):  # pragma: no cover
    """
    Updates the integration context samples with the newly created incident.
    If the size of the samples has reached `MAX_SAMPLES`, will pop out the latest sample.
    Args:
        incidents (list): The list of the newly created incidents.
        max_samples (int): Max samples size.
    """
    ctx = get_integration_context()
    updated_samples_list: List[Dict] = incidents + ctx.get("samples", [])
    ctx["samples"] = updated_samples_list[:max_samples]
    set_integration_context(ctx)


def reset_listener_health():
    demisto.updateModuleHealth("MatterMost V2 - Event handled successfully.")
    demisto.info("MatterMost V2 - Event handled successfully.")


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()
        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


""" COMMAND FUNCTIONS """


def test_module(client: HTTPClient) -> str:  # pragma: no cover
    """Tests connectivity with the client."""
    try:
        client.get_user_request(user_id="me", bot_user=False)  # Validating the Personal Access Token
    except Exception as e:
        demisto.debug(str(e))
        if "Invalid or expired session, please login again." in str(e):
            raise DemistoException("Invalid or expired session. Make sure the Personal Access Token is configured properly.")
        else:
            raise e

    try:
        client.get_user_request(user_id="me", bot_user=True)  # Validating the Bot Access Token
    except Exception as e:
        demisto.debug(str(e))
        if "Invalid or expired session, please login again." in str(e):
            raise DemistoException("Invalid or expired session. Make sure the Bot Access Token is configured properly.")
        else:
            raise e

    try:
        if client.notification_channel and client.team_name:
            # Validating the default team and channel exists
            channel_details = client.get_channel_by_name_and_team_name_request(client.team_name, client.notification_channel)
            client.send_notification_request(channel_details.get("id", ""), "Hi there! This is a test message from XSOAR.")

    except Exception as e:
        demisto.debug(str(e))
        if "Unable to find the existing team" in str(e):
            raise DemistoException("Could not find the team, make sure it is valid and/or exists.")
        elif "Channel does not exist" in str(e):
            raise DemistoException("Channel does not exist or archived, choose a different channel to send notifications to")
        else:
            raise e

    return "ok"


def get_team_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """Gets a team"""
    team_name = args.get("team_name", client.team_name)

    team_details = client.get_team_request(team_name)

    hr = tableToMarkdown("Team details:", team_details, headers=["name", "display_name", "type", "id"])
    return CommandResults(
        outputs_prefix="Mattermost.Team",
        outputs_key_field="name",
        outputs=team_details,
        readable_output=hr,
    )


def list_channels_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """Lists channels"""
    team_name = args.get("team", client.team_name)
    include_private_channels = argToBoolean(args.get("include_private_channels", False))
    page = arg_to_number(args.get("page", DEFAULT_PAGE_NUMBER))
    page_size = arg_to_number(args.get("page_size", DEFAULT_PAGE_SIZE))
    limit = args.get("limit", "")
    channel_details = []
    if limit:
        page = DEFAULT_PAGE_NUMBER
        page_size = limit

    team_details = client.get_team_request(team_name)

    params = {"page": page, "per_page": page_size}
    channel_details = client.list_channel_request(team_details.get("id", ""), params)

    if include_private_channels:
        channel_details.extend(client.list_channel_request(team_details.get("id", ""), params, get_private=True))

    hr = tableToMarkdown("Channels:", channel_details, headers=["name", "display_name", "type", "id"])
    return CommandResults(
        outputs_prefix="Mattermost.Channel",
        outputs_key_field="name",
        outputs=channel_details,
        readable_output=hr,
    )


def list_private_channels_for_user_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """
    Lists all private channels which belong to team_name and user is member
    """
    team_name = args.get("team_name", client.team_name)
    user_id = args.get("user_id", "")

    team_details = client.get_team_request(team_name)

    channels = client.list_channels_for_user_request(team_details.get("id", ""), user_id)
    # filter for private channels only
    channels = list(filter(lambda x: x.get("type", "O") == "P", channels))

    user_details = client.get_user_request(user_id)

    hr = tableToMarkdown(
        f'Channels for {user_details.get("username", user_id)}:', channels, headers=["name", "display_name", "type", "id"]
    )
    return CommandResults(
        outputs_prefix="Mattermost.User",
        outputs_key_field="id",
        outputs={"id": user_id, "channels": channels},
        raw_response=channels,
        readable_output=hr,
    )


def create_channel_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """Creates a channel"""
    team_name = args.get("team", client.team_name)
    channel_name = args.get("name", "")
    channel_display_name = args.get("display_name")
    channel_type = "O" if args.get("type") == "public" else "P"
    purpose = args.get("purpose", "")
    header = args.get("header", "")

    team_details = client.get_team_request(team_name)

    params = {
        "team_id": team_details.get("id", ""),
        "name": channel_name,
        "display_name": channel_display_name,
        "type": channel_type,
        "purpose": purpose,
        "header": header,
    }

    remove_nulls_from_dictionary(params)

    try:
        channel_details = client.create_channel_request(params)
        hr = f'The channel {channel_display_name} was created successfully, with channel ID: {channel_details.get("id")}'
    except Exception as e:
        if "A channel with that name already exists" in str(e):
            try:
                channel_details = client.get_channel_by_name_and_team_name_request(team_name, channel_name)
                hr = f"Channel {channel_display_name} already exists."
            except Exception as sec_e:
                if "Channel does not exist." in str(sec_e):
                    hr = (
                        "Could not create a new channel. An archived channel with the same name may exist"
                        "in the provided team. Please choose a different name."
                    )
                    raise DemistoException(hr)
                else:
                    raise sec_e
        else:
            raise e

    return CommandResults(
        outputs_prefix="Mattermost.Channel", outputs_key_field="id", outputs=channel_details, readable_output=hr
    )


def add_channel_member_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """Adds a channel member"""
    team_name = args.get("team", client.team_name)
    channel_name = args.get("channel", "")
    user_id = args.get("user_id", "")

    channel_details = client.get_channel_by_name_and_team_name_request(team_name, channel_name)

    data = {"user_id": user_id}
    client.add_channel_member_request(channel_details.get("id", ""), data)

    user_details = client.get_user_request(user_id)

    hr = f'The member {user_details.get("username", user_id)} was added to the channel successfully, with channel ID: {channel_details.get("id")}'  # noqa: E501
    return CommandResults(readable_output=hr)


def remove_channel_member_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """Removes a channel member"""
    team_name = args.get("team", client.team_name)
    channel_name = args.get("channel", "")
    user_id = args.get("user_id", "")

    channel_details = client.get_channel_by_name_and_team_name_request(team_name, channel_name)

    client.remove_channel_member_request(channel_details.get("id", ""), user_id)

    user_details = client.get_user_request(user_id)

    hr = f'The member {user_details.get("username", user_id)} was removed from the channel successfully.'
    return CommandResults(readable_output=hr)


def close_channel_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """Closes a channels"""
    team_name = args.get("team_name", client.team_name)
    channel_name = args.get("channel", "")

    channel_details = {}
    channel_id = ""
    if channel_name:
        try:
            channel_details = client.get_channel_by_name_and_team_name_request(team_name, channel_name)
        except Exception as e:
            if "Channel does not exist." in str(e):
                hr = f"The channel {channel_name} was not found. It may have been already deleted, or not in the team provided."
                return CommandResults(readable_output=hr)
            else:
                raise e

    try:
        client.close_channel_request(channel_details.get("id", "") or channel_id)
        hr = f"The channel {channel_name} was delete successfully."
    except Exception as e:
        if "Channel does not exist." in str(e):
            hr = f"The channel {channel_name} was already deleted."
        else:
            raise e

    mirror = find_mirror_by_investigation()
    integration_context = get_integration_context()
    if mirror:
        demisto.debug("MM: Found mirrored channel to close.")
        channel_id = mirror.get("channel_id", "")
        mirrors = json.loads(integration_context["mirrors"])
        # 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)

    return CommandResults(readable_output=hr)


def list_users_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """Lists users"""
    team_name = args.get("team_name", "")
    channel_name = args.get("channel", "")
    page = arg_to_number(args.get("page", DEFAULT_PAGE_NUMBER))
    page_size = arg_to_number(args.get("page_size", DEFAULT_PAGE_SIZE))
    limit = arg_to_number(args.get("limit", ""))

    if limit:
        page = DEFAULT_PAGE_NUMBER
        page_size = limit

    team_id = ""
    if team_name:
        team_details = client.get_team_request(team_name)
        team_id = team_details.get("id", "")

    channel_id = ""
    if channel_name:
        if not team_name:
            raise DemistoException("Must provide a team name if a channel name was provided.")
        channel_details = client.get_channel_by_name_and_team_name_request(team_name, channel_name)
        channel_id = channel_details.get("id", "")
        team_id = ""  # The search in Mattermost is done with an OR operator

    params = {"page": page, "per_page": page_size, "in_team": team_id, "in_channel": channel_id}
    remove_nulls_from_dictionary(params)

    users = client.list_users_request(params)

    hr = tableToMarkdown("Users:", users, headers=["username", "email", "role", "id"])
    return CommandResults(
        outputs_prefix="Mattermost.User",
        outputs_key_field="id",
        outputs=users,
        readable_output=hr,
    )


def send_file_command(client: HTTPClient, args) -> CommandResults:
    """Sends a file"""
    channel_name = args.get("channel", "")
    team_name = args.get("team_name", client.team_name)
    message = args.get("message")
    entry_id = args.get("entry_id") or args.get("file")
    to = args.get("to", "")

    demisto.debug(f"{to=}, {channel_name=}")

    if to and channel_name:
        raise DemistoException("Cannot use both to and channel_name arguments")

    if not to and not channel_name:
        raise DemistoException("You must provide an to or channel_name arguments")

    if to:
        # create a new direct channel and send the message there
        if re.match(emailRegex, to):
            to = get_user_id_by_email(client, to)
        else:
            to = get_user_id_by_username(client, to)

        bot_id = get_user_id_from_token(client, bot_user=True)
        channel_details = client.create_direct_channel_request([to], bot_id)
        demisto.debug(f'MM: Created a new direct channel to: {to} with channel_id: {channel_details.get("id")}')
    else:
        channel_details = client.get_channel_by_name_and_team_name_request(team_name, channel_name)

    file_info = demisto.getFilePath(entry_id)
    params = {"channel_id": channel_details.get("id"), "filename": file_info["name"]}

    upload_response = client.send_file_request(file_info, params)
    demisto.debug("MM: Uploaded the file successfully to mattermost")

    data = {
        "channel_id": channel_details.get("id"),
        "message": message,
        "file_ids": [upload_response.get("file_infos", [])[0].get("id", "")],
    }  # always uploading a single file
    remove_nulls_from_dictionary(params)

    client.create_post_with_file_request(data)

    return CommandResults(readable_output=f'file {file_info["name"]} was successfully sent to channel {channel_name}')


def mirror_investigation(client: HTTPClient, **args) -> CommandResults:
    """
    Updates the integration context with a new or existing mirror.
    """
    if not MIRRORING_ENABLED:
        raise DemistoException("Couldn't mirror investigation, Mirroring is disabled")
    if not LONG_RUNNING:
        raise DemistoException(
            "Mirroring is enabled, however long running is disabled. For mirrors to work correctly,"
            " long running must be enabled."
        )
    client = client
    mirror_type = args.get("type", "all")
    direction = args.get("direction", "Both")
    channel_name = args.get("channel", "")
    team_name = args.get("team_name", client.team_name)
    mirror_to = args.get("mirrorTo", "group")

    autoclose = argToBoolean(args.get("autoclose", True))
    send_first_message = False
    kick_admin = argToBoolean(args.get("kickAdmin", False))

    investigation = demisto.investigation()
    investigation_id = str(investigation.get("id"))
    if investigation.get("type") == PLAYGROUND_INVESTIGATION_TYPE:
        raise DemistoException("This action cannot be performed in the playground.")

    integration_context = get_integration_context()
    if not integration_context or not integration_context.get("mirrors", []):
        mirrors: list = []
        current_mirror = []
    else:
        mirrors = json.loads(integration_context["mirrors"])
        current_mirror = list(filter(lambda m: m["investigation_id"] == investigation_id, mirrors))

    demisto.debug(f"MM: {mirrors=}")
    demisto.debug(f"MM: {current_mirror=}")
    # get admin user id from token
    admin_user_id = get_user_id_from_token(client)

    channel_filter: list = []
    channel_id = ""
    if channel_name:
        # check if channel already exists
        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:
            channel_details: dict = {}
            try:
                channel_details = client.get_channel_by_name_and_team_name_request(team_name, channel_name)
                send_first_message = False
            except Exception as e:
                if "404" in str(e):
                    # create new channel
                    demisto.debug(f"MM: Creating a new channel for mirroring with name: {channel_name}")
                    channel_type = "public" if mirror_to == "channel" else "private"
                    args = {
                        "team_name": team_name,
                        "name": channel_name.lower(),
                        "display_name": channel_name,
                        "type": channel_type,
                    }
                    result = create_channel_command(client=client, args=args)
                    channel_details = result.outputs  # type: ignore
                    send_first_message = True
                else:
                    raise e

            channel_id = channel_details.get("id", "")
            channel_team_id = channel_details.get("team_id")
        else:
            mirrored_channel = channel_filter[0]
            channel_team_id = mirrored_channel["channel_team_id"]
            channel_id = mirrored_channel["channel_id"]
            channel_name = mirrored_channel["channel_name"]

        mirror = {
            "channel_team_id": channel_team_id,
            "channel_id": channel_id,
            "channel_name": channel_name,
            "investigation_id": investigation.get("id"),
            "mirror_type": mirror_type,
            "mirror_direction": direction,
            "auto_close": bool(autoclose),
            "mirrored": True,
        }
    else:
        mirror = mirrors.pop(mirrors.index(current_mirror[0]))
        channel_id = mirror["channel_id"]
        if mirror_type:
            mirror["mirror_type"] = mirror_type
        if autoclose:
            mirror["auto_close"] = autoclose
        if direction:
            mirror["mirror_direction"] = direction
        if channel_name:
            # update channel name if needed
            demisto.debug(f"MM: Updating channel name to {channel_name}")
            params = {"name": channel_name, "display_name": channel_name, "id": channel_id}
            client.update_channel_request(channel_id=channel_id, params=params)
            mirror["channel_name"] = channel_name
        channel_name = mirror["channel_name"]
        mirror["mirrored"] = True
    demisto.mirrorInvestigation(investigation_id, f"{mirror_type}:{direction}", autoclose)

    mirrors.append(mirror)
    set_to_integration_context_with_retries({"mirrors": mirrors}, OBJECTS_TO_KEYS)

    if send_first_message:
        server_links = demisto.demistoUrls()
        server_link = server_links.get("server")
        incident_url = get_war_room_url(f"{server_link}#/WarRoom/{investigation_id}", investigation_id)
        message_to_send = f"This channel was created to mirror incident {investigation_id}. \n View it on: {incident_url}"

        client.send_notification_request(channel_id, message_to_send)
    if kick_admin:
        try:
            client.remove_channel_member_request(channel_id, admin_user_id)
        except Exception as e:
            demisto.debug(f"Could not kick admin from channel. Error: {e}")

    return CommandResults(
        readable_output=f"Investigation mirrored successfully with mirror type {mirror_type},\n channel name: {channel_name}"
    )


def send_notification(client: HTTPClient, **args):
    """
    Sends notification for a MatterMost channel
    """
    demisto.debug(f"MM: Sending notification with {args=}")
    to = argToList(args.get("to", ""))
    entry = args.get("entry")
    channel_name = args.get("channel", "")
    message_to_send = args.get("message", "")
    ignore_add_url = argToBoolean(args.get("ignoreAddURL", False))
    mattermost_ask = argToBoolean(args.get("mattermost_ask", False))
    entitlement = ""
    reply = ""
    expiry = ""
    default_response = ""

    if mattermost_ask:
        parsed_message = json.loads(args.get("message", ""))
        entitlement = parsed_message.get("entitlement", "")
        expiry = parsed_message.get("expiry", "")
        default_response = parsed_message.get("default_response", "")
        reply = parsed_message.get("reply", "")
        message_to_send = parsed_message.get("message", "")

    message_type = args.get("messageType", "")  # From server
    original_message = args.get("originalMessage", "")  # From server
    entry_object = args.get("entryObject")  # From server
    investigation_id = ""
    poll: dict = {}

    if to and channel_name:
        raise DemistoException("Cannot use both to and channel_name arguments")

    channel_name = channel_name or client.notification_channel

    if entry_object:
        investigation_id = entry_object.get("investigationId")  # From server, available from demisto v6.1 and above

    if message_type and (message_type not in PERMITTED_NOTIFICATION_TYPES) and message_type != MIRROR_TYPE:
        demisto.debug(f"MM: Will not mirror the message type: {message_type}")
        return f"Message type is not in permitted options. Received: {message_type}"

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

    if channel_name == INCIDENT_NOTIFICATION_CHANNEL:
        if client.notification_channel:
            # change the notification channel to the one in the configuration
            channel_name = client.notification_channel
        else:
            demisto.debug(
                f"MM: No notification channel was configured, will send notification to {INCIDENT_NOTIFICATION_CHANNEL}"
            )

    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:
                    link = get_war_room_url(link)
                    if entry:
                        link += "/" + entry
                    message_to_send += f"\nView it on: {link}"
            else:
                link = server_links.get("server", "")
                if link:
                    message_to_send += f"\nView it on: {link}#/home"
    channel_id = get_channel_id_to_send_notif(client, to, channel_name, investigation_id)

    raw_data = client.send_notification_request(channel_id, message_to_send, props=poll)
    message_id = raw_data.get("id")
    demisto.debug(f"MM: Got replay from post: {raw_data}")
    if entitlement:
        demisto.debug(f"MM: Found entitlement, saving message to context: {entitlement}")
        save_entitlement(entitlement, message_id, reply, expiry, default_response, to if to else channel_id)
    return CommandResults(
        readable_output=f"Message sent to MatterMost successfully. Message ID is: {message_id}", raw_response=raw_data
    )


def list_groups_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """Lists groups"""
    page = arg_to_number(args.get("page", DEFAULT_PAGE_NUMBER))
    page_size = arg_to_number(args.get("page_size", DEFAULT_PAGE_SIZE))
    limit = arg_to_number(args.get("limit", ""))
    q = args.get("group", "")
    group_details: list[Any] = []
    if limit:
        page = DEFAULT_PAGE_NUMBER
        page_size = limit

    params = {"page": page, "per_page": page_size}
    if q:
        params["q"] = q
    group_details = client.list_groups_request(params)

    hr = tableToMarkdown("Groups:", group_details, headers=["name", "display_name", "description", "id"])
    return CommandResults(
        outputs_prefix="Mattermost.Groups",
        outputs_key_field="id",
        outputs=group_details,
        raw_response=group_details,
        readable_output=hr,
    )


def list_group_members_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """List the members of a group"""
    group_id = args.get("group_id", "")
    page = arg_to_number(args.get("page", DEFAULT_PAGE_NUMBER))
    page_size = arg_to_number(args.get("page_size", DEFAULT_PAGE_SIZE))
    limit = arg_to_number(args.get("limit", ""))
    member_details = {}
    if limit:
        page = DEFAULT_PAGE_NUMBER
        page_size = limit

    params = {"page": page, "per_page": page_size}
    member_details = client.list_group_members_request(group_id, params)

    hr = tableToMarkdown(
        f'Group {group_id} members ({len(member_details.get("members",[]))}/'
        f'{member_details.get("total_member_count", "Unknown")}):',
        member_details.get("members", []),
        headers=["username", "email", "id"],
    )
    return CommandResults(
        outputs_prefix="Mattermost.Members",
        outputs_key_field="id",
        outputs=member_details.get("members", []),
        readable_output=hr,
        raw_response=member_details,
    )


def add_group_member_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """Adds member(s) to a group"""
    group_id = args.get("group_id", "")
    user_ids = argToList(args.get("user_ids", ""))

    data = {"user_ids": user_ids}
    try:
        response = client.add_group_member_request(group_id, data)
    except DemistoException as ex:
        if ex.res.status_code == 500:
            raise DemistoException("Got 500 from the server - this error could indicate that the user ID is invalid")
        raise ex

    hr = []
    for user in user_ids:
        user_details = client.get_user_request(user)
        hr.append(
            f'The member {user_details.get("username", user)} was added to the group successfully, with group ID: ' f'{group_id}'
        )

    return CommandResults(readable_output="\n".join(hr), raw_response=response)


def remove_group_member_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """Removes member(s) form a group"""
    group_id = args.get("group_id", "")
    user_ids = argToList(args.get("user_ids", ""))

    data = {"user_ids": user_ids}
    try:
        response = client.remove_group_member_request(group_id, data)
    except DemistoException as ex:
        if ex.res.status_code == 500:
            raise DemistoException(
                "Got 500 from the server - this error could indicate that the user does not exist in the group or is invalid"
            )
        raise ex

    hr = []
    for user in user_ids:
        user_details = client.get_user_request(user)
        hr.append(
            f'The member {user_details.get("username", user)} was removed from the group successfully, with group ID:'
            f' {group_id}'
        )

    return CommandResults(readable_output="\n".join(hr), raw_response=response)


def set_channel_role_command(client: HTTPClient, args: dict[str, Any]) -> CommandResults:
    """Set channel role for a channel member"""
    channel_id = args.get("channel_id", "")
    user_id = args.get("user_id", "")
    channel_role = "channel_user" + (" channel_admin" if args.get("role", "member").lower() == "admin" else "")

    raw_response = client.set_channel_role_request(channel_id, user_id, channel_role)

    user_details = client.get_user_request(user_id)
    hr = (
        f'Set channel role for {user_details.get("username", user_id)} successfully to '
        f'{"Admin" if args.get("role", "admin").lower() == "admin" else "Member"}.'
    )

    return CommandResults(readable_output=hr, raw_response=raw_response)


""" MAIN FUNCTION """


def handle_global_parameters(params: dict):  # pragma: no cover
    """Initializing the global parameters"""
    url = params.get("url", "")
    bot_access_token = params.get("bot_access_token", {}).get("password")
    personal_access_token = params.get("personal_access_token", {}).get("password")
    proxy = params.get("proxy", False)

    # Initializing global variables
    global SECRET_TOKEN, LONG_RUNNING, MIRRORING_ENABLED, CACHE_EXPIRY, CACHED_INTEGRATION_CONTEXT, DEMISTO_URL
    global BASE_URL, PROXY, SSL_CONTEXT, VERIFY_CERT, PROXIES, ALLOW_INCIDENTS, INCIDENT_TYPE, PROXY_URL
    global WEBSOCKET_URL, PORT, PERMITTED_NOTIFICATION_TYPES
    LONG_RUNNING = params.get("longRunning", False)
    MIRRORING_ENABLED = params.get("mirroring", False)
    SECRET_TOKEN = personal_access_token
    BASE_URL = url
    PROXY = proxy
    demisto_urls = demisto.demistoUrls()
    DEMISTO_URL = demisto_urls.get("server", "")
    PROXIES, _ = handle_proxy_for_long_running()
    PROXY_URL = PROXIES.get("http", "")  # aiohttp only supports http proxy
    ALLOW_INCIDENTS = params.get("allow_incidents", True)
    INCIDENT_TYPE = params.get("incidentType", "Unclassified")
    default_permitted_notification_types = ["externalAskSubmit", "externalFormSubmit"]
    custom_permitted_notification_types = demisto.params().get("permitted_notifications", [])
    PERMITTED_NOTIFICATION_TYPES = default_permitted_notification_types + custom_permitted_notification_types
    VERIFY_CERT = not params.get("insecure", 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 "https://" in url:
        uri = url.replace("https://", "wss://", 1)
    else:
        uri = url.replace("http://", "ws://", 1)
    uri += "/api/v4/websocket"
    WEBSOCKET_URL = uri

    # Pull initial Cached context and set the Expiry
    CACHE_EXPIRY = next_expiry_time()
    CACHED_INTEGRATION_CONTEXT = get_integration_context()

    if MIRRORING_ENABLED and (not LONG_RUNNING or not bot_access_token):
        raise DemistoException("""Mirroring is enabled, however long running is disabled
or the necessary bot authentication parameters are missing.
For mirrors to work correctly, long running must be enabled and you must provide all
the mattermost-bot following parameters:
Bot Access Token""")


def main():  # pragma: no cover
    params = demisto.params()
    args = demisto.args()
    url = params.get("url", "")
    bot_access_token = params.get("bot_access_token", {}).get("password")
    personal_access_token = params.get("personal_access_token", {}).get("password")
    team_name = params.get("team_name", "")
    notification_channel = params.get("notification_channel")
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)

    handle_global_parameters(params)

    command = demisto.command()
    try:
        global CLIENT

        headers = {"Authorization": f"Bearer {personal_access_token}"}
        client = HTTPClient(
            base_url=url,
            headers=headers,
            verify=verify_certificate,
            proxy=proxy,
            bot_access_token=bot_access_token,
            personal_access_token=personal_access_token,
            team_name=team_name,
            notification_channel=notification_channel,
        )
        CLIENT = client
        demisto.debug(f"Command being called is {command}")
        if command == "test-module":
            return_results(test_module(client))
        elif command == "mirror-investigation":
            return_results(mirror_investigation(client, **args))
        elif command == "mattermost-mirror-investigation":
            return_results(mirror_investigation(client, **args))
        elif command == "send-notification":
            return_results(send_notification(client, **args))
        elif command == "long-running-execution":
            run_long_running()
        elif command == "mattermost-get-team":
            return_results(get_team_command(client, args))
        elif command == "mattermost-list-channels":
            return_results(list_channels_command(client, args))
        elif command == "mattermost-list-private-channels-for-user":
            return_results(list_private_channels_for_user_command(client, args))
        elif command == "mattermost-create-channel":
            return_results(create_channel_command(client, args))
        elif command == "mattermost-add-channel-member":
            return_results(add_channel_member_command(client, args))
        elif command == "mattermost-remove-channel-member":
            return_results(remove_channel_member_command(client, args))
        elif command == "mattermost-list-users":
            return_results(list_users_command(client, args))
        elif command == "mattermost-close-channel":
            return_results(close_channel_command(client, args))
        elif command == "close-channel":
            return_results(close_channel_command(client, args))
        elif command == "mattermost-send-file":
            return_results(send_file_command(client, args))
        elif command == "mattermost-list-groups":
            return_results(list_groups_command(client, args))
        elif command == "mattermost-list-group-members":
            return_results(list_group_members_command(client, args))
        elif command == "mattermost-add-group-member":
            return_results(add_group_member_command(client, args))
        elif command == "mattermost-remove-group-member":
            return_results(remove_group_member_command(client, args))
        elif command == "mattermost-set-channel-role":
            return_results(set_channel_role_command(client, args))
        else:
            raise DemistoException("Unrecognized command: " + demisto.command())

    except Exception as e:
        # For any other integration command exception, return an error
        return_error(f"Failed to execute {command} command. Error: {e!s}.")


""" ENTRY POINT """


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