MicrosoftGraphTeamsApiModule

Provides common Microsoft Graph Teams code that is appended into Cortex integrations during deployment.

Type
python
Pack
ApiModules

Source

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

""" IMPORTS """

import urllib3
from MicrosoftApiModule import *  # noqa: E402

# Disable insecure warnings
urllib3.disable_warnings()

""" GLOBAL VARS """


""" CLIENT """


class MsGraphClient:
    def __init__(
        self,
        tenant_id,
        auth_id,
        enc_key,
        app_name,
        base_url,
        verify,
        proxy,
        self_deployed,
        redirect_uri,
        auth_code,
        handle_error,
        certificate_thumbprint: str | None = None,
        private_key: str | None = None,
        delegated_user: str | None = None,
    ):
        grant_type = AUTHORIZATION_CODE if auth_code and redirect_uri else CLIENT_CREDENTIALS
        resource = None if self_deployed else ""
        self.ms_client = MicrosoftClient(
            tenant_id=tenant_id,
            auth_id=auth_id,
            enc_key=enc_key,
            app_name=app_name,
            base_url=base_url,
            verify=verify,
            proxy=proxy,
            self_deployed=self_deployed,
            redirect_uri=redirect_uri,
            auth_code=auth_code,
            grant_type=grant_type,
            resource=resource,
            certificate_thumbprint=certificate_thumbprint,
            private_key=private_key,
            command_prefix="msgraph-teams",
        )
        self.handle_error = handle_error

        self.delegated_user = delegated_user

    @staticmethod
    def _build_members_input(user_id, members):
        """
        Builds valid members input for the chat.

        :type members: ``list``
        :param members: List of principle names (email)

        :return: List of valid member objects
        :rtype: ``list``
        """
        member_list = []
        member_list.append(
            {
                "@odata.type": "#microsoft.graph.aadUserConversationMember",
                "roles": ["owner"],
                "user@odata.bind": f"https://graph.microsoft.com/v1.0/users('{user_id}')",
            }
        )
        for member in members:
            member_list.append(
                {
                    "@odata.type": "#microsoft.graph.aadUserConversationMember",
                    "roles": ["owner"],
                    "user@odata.bind": f"https://graph.microsoft.com/v1.0/users('{member}')",
                }
            )

        return member_list

    @staticmethod
    def _get_user_name(user: dict):
        """
        Receives dict of form  "emailAddress":{"name":"_", "address":"_"} and return the address

        :type user: ``dict``
        :param user: user

        :return: The name of the user
        :rtype: ``str``
        """
        return user.get("user", {}).get("displayName", "")

    @staticmethod
    def _get_body_content(body: dict):
        """
        Receives dict of form  "emailAddress":{"name":"_", "address":"_"} and return the address

        :type body: ``dict``
        :param body: body

        :return: The content of the body
        :rtype: ``str``
        """
        return body.get("content", "")

    def pages_puller(self, response: dict, page_count: int = 100) -> list:
        """Gets first response from API and returns all pages

        Args:
            response (dict):
            page_count (int):

        Returns:
            list: list of all pages
        """
        responses = [response]
        for _i in range(page_count - 1):
            next_link = response.get("@odata.nextLink")
            if next_link:
                response = self.ms_client.http_request("GET", full_url=next_link, url_suffix=None)
                responses.append(response)
            else:
                return responses
        return responses

    def list_chats(self, user_id: str = None, odata: str = None, limit: str = "20") -> dict | list:
        """Returning all chats from given user

        Args:
            user_id (str):
            odata (str):
            limit (str):

        Returns:
            dict or list:
        """
        user_id = user_id if user_id else self.delegated_user
        suffix = f"users/{user_id}/chats"
        odata = f"{odata}&$top={limit}" if odata else f"$top={limit}"

        if odata:
            suffix += f"?{odata}"
        demisto.debug(f"URL suffix is {suffix}")
        response = self.ms_client.http_request(method="GET", url_suffix=suffix)
        return self.pages_puller(response, 1)

    def create_chat(self, subject: str, members: list, user_id: str = None, type="group") -> dict:
        """Create a new chat for a given user

        Args:
            subject (str):
            members (list):
            user_id (str):
            type (str):

        Returns:
            dict:
        """
        user_id = user_id if user_id else self.delegated_user
        suffix = "chats"

        if type == "group":
            json_data = {"chatType": type, "topic": subject, "members": self._build_members_input(user_id, members)}
        else:
            json_data = {"chatType": type, "members": self._build_members_input(user_id, members)}
        return self.ms_client.http_request(method="POST", url_suffix=suffix, json_data=json_data)

    def get_chat(self, chat_id: str, user_id: str = None) -> dict:
        """retrieves an existing chat for a given user

        Args:
            chat_id (str):
            user_id (str):

        Returns:
            dict:
        """
        user_id = user_id if user_id else self.delegated_user
        suffix = f"users/{user_id}/chats/{chat_id}"

        demisto.debug(f"URL suffix is {suffix}")
        response = self.ms_client.http_request(method="GET", url_suffix=suffix)
        return response

    def update_chat(self, chat_id: str, subject: str) -> dict:
        """Updates an existing chat for a given user

        Args:
            chat_id (str):
            subject (str):

        Returns:
            dict:
        """
        suffix = f"chats/{chat_id}"

        json_data = {"topic": subject}
        return self.ms_client.http_request(method="PATCH", url_suffix=suffix, json_data=json_data)

    def list_members(self, chat_id: str, user_id: str = None) -> dict | list:
        """Returning all members from given chat

        Args:
            chat_id (str):
            user_id (str):

        Returns:
            dict or list:
        """
        user_id = user_id if user_id else self.delegated_user
        suffix = f"users/{user_id}/chats/{chat_id}/members"

        demisto.debug(f"URL suffix is {suffix}")
        response = self.ms_client.http_request(method="GET", url_suffix=suffix)
        return response

    def add_member(self, chat_id: str, user_id: str, share_history: bool) -> bool:
        """Add a member to a given chat

        Args:
            chat_id (str):
            user_id (str):
            share_history (bool):

        Returns:
            bool:
        """
        suffix = f"chats/{chat_id}/members"

        json_data = {
            "@odata.type": "#microsoft.graph.aadUserConversationMember",
            "roles": ["owner"],
            "user@odata.bind": f"https://graph.microsoft.com/v1.0/users('{user_id}')",
            "visibleHistoryStartDateTime": "0001-01-01T00:00:00Z" if share_history else "",
        }
        self.ms_client.http_request(method="POST", url_suffix=suffix, json_data=json_data, resp_type="text")

        return True

    def list_messages(self, chat_id: str, user_id: str = None, limit: str = "50") -> dict | list:
        """Returning all mails from given user

        Args:
            chat_id (str):
            user_id (str):
            limit (str):

        Returns:
            dict or list:
        """
        user_id = user_id if user_id else self.delegated_user
        suffix = f"chats/{chat_id}/messages"
        suffix += f"?$top={limit}"

        demisto.debug(f"URL suffix is {suffix}")
        response = self.ms_client.http_request(method="GET", url_suffix=suffix)
        return self.pages_puller(response)

    def send_message(self, chat_id: str, body: str) -> dict:
        """Returning all mails from given user

        Args:
            chat_id (str):
            body (str):

        Returns:
            dict:
        """
        suffix = f"chats/{chat_id}/messages"

        json_data = {"body": {"contentType": "html", "content": body}}
        return self.ms_client.http_request(method="POST", url_suffix=suffix, json_data=json_data)

    def update_message_policy_violation(self, url_suffix: str, body: dict) -> requests.Response:
        """Apply a data loss prevention policy violation to a Teams message.

        Args:
            url_suffix: The message resource path (chat, channel, or channel reply).
            body: The request body containing the policyViolation object.

        Returns:
            The raw HTTP response object. `resp_type="response"` is required because a
            successful PATCH returns 204 No Content, which has no JSON body to decode.
        """
        return self.ms_client.http_request(
            method="PATCH",
            url_suffix=url_suffix,
            json_data=body,
            resp_type="response",
        )


""" HELPER FUNCTIONS """


def build_chat_object(raw_response: dict | list, user_id: str = None):
    """Building chat entry context
    Getting a list from build_chat_object

    Args:
        user_id (str): user id
        raw_response (dict or list): list of pages

    Returns:
        dict or list: output context
    """

    def build_chat(given_chat: dict) -> dict:
        """

        Args:
            given_chat (dict):

        Returns:
            dict:
        """
        # Dicts
        chat_properties = {
            "ID": "id",
            "Subject": "topic",
            "Created": "createdDateTime",
            "LastUpdatedTime": "lastUpdatedDateTime",
            "Type": "chatType",
        }

        # Create entry properties
        entry = {k: given_chat.get(v) for k, v in chat_properties.items()}

        if user_id:
            entry["UserID"] = user_id
        return entry

    chat_list = []
    if isinstance(raw_response, list):  # response from list_emails_command
        for page in raw_response:
            # raw_response is a list containing multiple pages or one page
            # if value is not empty, there are emails in the page
            value = page.get("value")
            if value:
                for chat in value:
                    chat_list.append(build_chat(chat))
    elif isinstance(raw_response, dict):  # response from get_message_command
        value = raw_response.get("value")
        if value:
            for chat in value:
                chat_list.append(build_chat(chat))
        else:
            return build_chat(raw_response)
    return chat_list


def build_member_object(raw_response: dict | list, chat_id: str) -> dict | list:
    """Building member entry context
    Getting a list from build_member_object

    Args:
        raw_response (dict or list): list of members
        chat_id (str): chat id

    Returns:
        dict or list: output context
    """

    def build_member(given_member: dict) -> dict:
        """

        Args:
            given_member (dict):

        Returns:
            dict:
        """
        # Dicts
        member_properties = {
            "ID": "id",
            "Name": "displayName",
            "HistoryStartTime": "visibleHistoryStartDateTime",
        }

        # Create entry properties
        entry = {k: given_member.get(v) for k, v in member_properties.items()}

        if chat_id:
            entry["ChatID"] = chat_id
        return entry

    member_list = []
    if isinstance(raw_response, list):  # response from list_emails_command
        for page in raw_response:
            # raw_response is a list containing multiple pages or one page
            # if value is not empty, there are emails in the page
            value = page.get("value")
            if value:
                for member in value:
                    member_list.append(build_member(member))
    elif isinstance(raw_response, dict):  # response from get_message_command
        value = raw_response.get("value")
        if value:
            for member in value:
                member_list.append(build_member(member))
        else:
            return build_member(raw_response)
    return member_list


def build_message_object(raw_response: dict | list, chat_id: str) -> dict | list:
    """Building message entry context
    Getting a list from build_message_object

    Args:
        raw_response (dict or list): list of messages
        chat_id (str): chat id

    Returns:
        dict or list: output context
    """

    def build_message(given_message: dict) -> dict:
        """

        Args:
            given_message (dict):

        Returns:
            dict:
        """
        # Dicts
        message_properties = {
            "ID": "id",
            "Created": "createdDateTime",
            "LastModifiedTime": "lastModifiedDateTime",
        }

        # Create entry properties
        entry = {k: given_message.get(v) for k, v in message_properties.items()}

        entry["From"] = given_message.get("from", {}).get("user", {}).get("displayName", "")
        entry["Body"] = given_message.get("body", {}).get("content", "")

        if chat_id:
            entry["ChatID"] = chat_id
        return entry

    message_list = []
    if isinstance(raw_response, list):  # response from list_emails_command
        for page in raw_response:
            # raw_response is a list containing multiple pages or one page
            # if value is not empty, there are emails in the page
            value = page.get("value")
            if value:
                for message in value:
                    if message.get("messageType") == "message" and message.get("from", {}).get("user"):
                        message_list.append(build_message(message))
    elif isinstance(raw_response, dict):  # response from get_message_command
        value = raw_response.get("value")
        if value:
            for message in value:
                if message.get("messageType") == "message":
                    message_list.append(build_message(message))
        else:
            return build_message(raw_response)
    return message_list


def build_teams_message_url_suffix(
    chat_id: str,
    team_id: str,
    channel_id: str,
    message_id: str,
    parent_message_id: str,
    payment_model: str,
) -> str:
    """Build the Microsoft Graph message resource path for a chat, channel, or channel reply.

    Args:
        chat_id: The chat ID hosting the message.
        team_id: The team ID hosting the channel message.
        channel_id: The channel ID hosting the message.
        message_id: The ID of the message to update.
        parent_message_id: The parent message ID when targeting a channel reply.
        payment_model: The billing model appended as the model query parameter when provided.

    Returns:
        The resource path suffix relative to the Graph v1.0 base URL.
    """
    if chat_id:
        suffix = f"chats/{chat_id}/messages/{message_id}"
    elif parent_message_id:
        suffix = f"teams/{team_id}/channels/{channel_id}/messages/{parent_message_id}/replies/{message_id}"
    else:
        suffix = f"teams/{team_id}/channels/{channel_id}/messages/{message_id}"
    if payment_model:
        suffix = f"{suffix}?model={payment_model}"
    return suffix


def build_policy_violation_body(dlp_action: str, policy_tip_general_text: str, verdict_details: str) -> dict:
    """Build the request body containing the policyViolation object.

    Args:
        dlp_action: The action taken on the message. One of NoAction, BlockAccess, BlockAccessExternal.
        policy_tip_general_text: The general explanatory text shown to the user in the policy tip.
        verdict_details: The reviewer actions available on the flagged message. Omitted when empty
            so Microsoft Graph applies its default (permanent) behavior.

    Returns:
        The request body dictionary for the message update.
    """
    policy_violation: dict = {
        "dlpAction": dlp_action,
        "policyTip": {"generalText": policy_tip_general_text},
    }
    if verdict_details:
        policy_violation["verdictDetails"] = verdict_details
    return {"policyViolation": policy_violation}


def validate_teams_message_target_args(
    chat_id: str,
    team_id: str,
    channel_id: str,
    parent_message_id: str,
) -> None:
    """Validate the mutually exclusive message-target argument combinations.

    Args:
        chat_id: The chat ID hosting the message.
        team_id: The team ID hosting the channel message.
        channel_id: The channel ID hosting the message.
        parent_message_id: The parent message ID when targeting a channel reply.

    Raises:
        DemistoException: When the target argument combination is invalid.
    """
    is_channel = bool(team_id or channel_id)
    if chat_id and parent_message_id:
        raise DemistoException("'parent_message_id' is only supported for channel messages, not chat messages.")
    if chat_id and is_channel:
        raise DemistoException("Provide either 'chat_id' or 'team_id' and 'channel_id', not both.")
    if parent_message_id and not is_channel:
        raise DemistoException("A channel reply requires both 'team_id' and 'channel_id'.")
    if not chat_id and not is_channel:
        raise DemistoException("A message target is required: either 'chat_id' or 'team_id' and 'channel_id'.")
    if is_channel and not (team_id and channel_id):
        raise DemistoException("A channel message requires both 'team_id' and 'channel_id'.")


""" COMMANDS """


def test_function(client, _):
    """
    Performs a basic GET request to check if the API is reachable and authentication is successful.
    Returns ok if successful.
    """
    response = "ok"
    if demisto.params().get("self_deployed", False):
        response = "```✅ Success!```"
        if demisto.command() == "test-module":
            # cannot use test module due to the lack of ability to set refresh token to integration context
            # for self deployed app
            raise Exception(
                "When using a self-deployed configuration, "
                "Please enable the integration and run the !msgraph-teams-test command in order to test it"
            )

    # `chats` resolves the chats of the *signed-in user*, so it is delegated-only: Microsoft Graph
    # rejects it with 400 "Requested API is not supported in application-only context" whenever no
    # user is present. `delegated_user` is precisely the configured user to act as - it is required
    # by the community pack and absent from the UCP-driven satellite pack, which is served an
    # app-only (client credentials) token. So its presence is what decides whether a user-scoped
    # probe can resolve at all. Without one, fall back to `teams`, the app-only addressable resource
    # closest to this integration's Teams scope.
    url_suffix = "chats" if demisto.params().get("delegated_user") else "teams"
    client.ms_client.http_request(method="GET", url_suffix=url_suffix)
    # `test-module` is a platform contract: the entry content must be exactly "ok" for the
    # connection test to be reported as successful. Any other payload - including a decorated
    # CommandResults - is surfaced to the user as a failure even when the API call succeeded.
    # `run_command` discards this function's return value, so the result has to be emitted here.
    # The richer message is kept for the explicit `!msgraph-teams-test` command, which is a
    # regular command and carries no such constraint.
    if demisto.command() == "test-module":
        return_results("ok")
    else:
        return_results(CommandResults(readable_output="✅ Success!"))
    return response, None, None


def list_chats_command(client: MsGraphClient, args):
    user_id = args.get("user_id")
    odata = args.get("odata")
    limit = args.get("limit")

    raw_response = client.list_chats(user_id=user_id, odata=odata, limit=limit)
    last_page_response = raw_response[len(raw_response) - 1]
    metadata = ""
    next_page = last_page_response.get("@odata.nextLink")
    if next_page:
        metadata = "\nPay attention there are more results than shown"

    chat_context = build_chat_object(raw_response, user_id)
    entry_context = {}
    if chat_context:
        entry_context = {"MSGraphTeamsChat(val.ID === obj.ID)": chat_context}
        if next_page:
            # .NextPage.indexOf(\'http\')>=0 : will make sure the NextPage token will always be updated because it's a url
            entry_context["MSGraphTeamsChat(val.NextPage.indexOf('http')>=0)"] = {"NextPage": next_page}

        # human_readable builder
        human_readable_header = (
            f"{len(chat_context)} chats found {metadata}" if metadata else f"Total of {len(chat_context)} chats found"
        )
        human_readable = tableToMarkdown(
            human_readable_header, chat_context, headers=["Subject", "Created", "LastUpdatedTime", "Type", "ID"]
        )
    else:
        human_readable = "### No chats were found"
    return_outputs(human_readable, entry_context, raw_response)


def create_chat_command(client: MsGraphClient, args):
    user_id = args.get("user_id")
    subject = args.get("subject")
    type = args.get("type")
    members = argToList(args.get("members"))

    raw_response = client.create_chat(subject, members, user_id=user_id, type=type)
    chat_context = build_chat_object(raw_response, user_id)
    entry_context = {}
    if chat_context:
        entry_context = {"MSGraphTeamsChat(val.ID === obj.ID)": chat_context}  # human_readable builder
        human_readable_header = f"The chat was created with subject: {subject}"
        human_readable = tableToMarkdown(
            human_readable_header, chat_context, headers=["Subject", "Created", "LastUpdatedTime", "Type", "ID"]
        )
    else:
        human_readable = "### No chats were created"
    return_outputs(human_readable, entry_context, raw_response)


def get_chat_command(client: MsGraphClient, args):
    user_id = args.get("user_id")
    chat_id = args.get("chat_id")

    raw_response = client.get_chat(chat_id, user_id=user_id)
    chat_context: dict = build_chat_object(raw_response, user_id)
    entry_context = {}
    if chat_context:
        entry_context = {"MSGraphTeamsChat(val.ID === obj.ID)": chat_context}  # human_readable builder
        subject = chat_context.get("Subject")
        human_readable_header = f"The chat was found, with subject: {subject}"
        human_readable = tableToMarkdown(
            human_readable_header, chat_context, headers=["Subject", "Created", "LastUpdatedTime", "Type", "ID"]
        )
    else:
        human_readable = "### No chats were found"
    return_outputs(human_readable, entry_context, raw_response)


def update_chat_command(client: MsGraphClient, args):
    chat_id = args.get("chat_id")
    subject = args.get("subject")

    raw_response = client.update_chat(chat_id, subject)
    chat_context = build_chat_object(raw_response)
    entry_context = {}
    if chat_context:
        entry_context = {"MSGraphTeamsChat(val.ID === obj.ID)": chat_context}  # human_readable builder
        human_readable_header = f"The chat was updated with subject: {subject}"
        human_readable = tableToMarkdown(
            human_readable_header, chat_context, headers=["Subject", "Created", "LastUpdatedTime", "Type", "ID"]
        )
    else:
        human_readable = "### No chats were updated"
    return_outputs(human_readable, entry_context, raw_response)


def list_members_command(client: MsGraphClient, args):
    user_id = args.get("user_id")
    chat_id = args.get("chat_id")

    raw_response = client.list_members(chat_id, user_id=user_id)
    member_context = build_member_object(raw_response, chat_id)
    entry_context = {}
    if member_context:
        entry_context = {"MSGraphTeamsChatMember(val.ID === obj.ID)": member_context}

        # human_readable builder
        human_readable_header = f"Total of {len(member_context)} members found"
        human_readable = tableToMarkdown(
            human_readable_header, member_context, headers=["Name", "HistoryStartTime", "ID", "ChatID"]
        )
    else:
        human_readable = "### No members were found"
    return_outputs(human_readable, entry_context, raw_response)


def add_member_command(client: MsGraphClient, args):
    chat_id = args.get("chat_id")
    user_id = args.get("user_id")
    share_history = args.get("share_history") == "true"

    client.add_member(chat_id, user_id, share_history)

    human_readable = tableToMarkdown(
        "Member has been added successfully",
        {"Chat ID": chat_id, "User ID": user_id, "Share history": share_history},
        headers=["Chat ID", "User ID", "Share history"],
        removeNull=True,
    )
    return_outputs(human_readable)


def list_messages_command(client: MsGraphClient, args):
    user_id = args.get("user_id")
    chat_id = args.get("chat_id")
    limit = args.get("limit")

    raw_response = client.list_messages(chat_id, user_id=user_id, limit=limit)
    # return_outputs(raw_response)
    last_page_response = raw_response[len(raw_response) - 1]
    metadata = ""
    next_page = last_page_response.get("@odata.nextLink")
    if next_page:
        metadata = "\nPay attention there are more results than shown"

    message_context = build_message_object(raw_response, chat_id)
    entry_context = {}
    if message_context:
        entry_context = {"MSGraphTeamsChatMessage(val.ID === obj.ID)": message_context}
        if next_page:
            # .NextPage.indexOf(\'http\')>=0 : will make sure the NextPage token will always be updated because it's a url
            entry_context["MSGraphTeamsChatMessage(val.NextPage.indexOf('http')>=0)"] = {"NextPage": next_page}

        # human_readable builder
        human_readable_header = (
            f"{len(message_context)} messages found {metadata}" if metadata else f"Total of {len(message_context)} messages found"
        )
        human_readable = tableToMarkdown(
            human_readable_header, message_context, headers=["Body", "Created", "From", "ID", "ChatID"]
        )
    else:
        human_readable = "### No messages were found"
    return_outputs(human_readable, entry_context, raw_response)


def send_message_command(client: MsGraphClient, args):
    chat_id = args.get("chat_id")
    body = args.get("body")

    raw_response = client.send_message(chat_id, body)
    message_context = build_message_object(raw_response, chat_id)
    entry_context = {}
    if message_context:
        entry_context = {"MSGraphTeamsChatMessage(val.ID === obj.ID)": message_context}  # human_readable builder
        human_readable_header = "The message was send"
        human_readable = tableToMarkdown(
            human_readable_header, message_context, headers=["Body", "Created", "From", "ID", "ChatID"]
        )
    else:
        human_readable = "### No messages were send"
    return_outputs(human_readable, entry_context, raw_response)


def update_teams_message_policy_violation_command(client: MsGraphClient, args: dict) -> None:
    """Apply a data loss prevention policy violation to a Microsoft Teams message.

    Targets a chat message, a channel message, or a channel reply. HTTP error responses
    from Microsoft Graph are surfaced verbatim as a command error by the outer main()
    try/except.

    Args:
        client: The Microsoft Graph client.
        args: The command arguments.
            chat_id (Optional): The chat ID hosting the message.
            team_id (Optional): The team ID hosting the channel message.
            channel_id (Optional): The channel ID hosting the message.
            parent_message_id (Optional): The parent message ID when targeting a channel reply.
            message_id (Required): The ID of the message to update.
            dlp_action (Optional): The action taken on the message.
            policy_tip_general_text (Optional): The explanatory text shown to the user.
            verdict_details (Optional): The reviewer actions available on the flagged message.
            payment_model (Optional): The billing model appended as the model query parameter.
    """
    chat_id = args.get("chat_id", "")
    team_id = args.get("team_id", "")
    channel_id = args.get("channel_id", "")
    parent_message_id = args.get("parent_message_id", "")
    message_id = args.get("message_id", "")
    dlp_action = args.get("dlp_action", "")
    policy_tip_general_text = args.get("policy_tip_general_text", "")
    verdict_details = args.get("verdict_details", "")
    payment_model = args.get("payment_model", "")

    validate_teams_message_target_args(chat_id, team_id, channel_id, parent_message_id)

    url_suffix = build_teams_message_url_suffix(
        chat_id=chat_id,
        team_id=team_id,
        channel_id=channel_id,
        message_id=message_id,
        parent_message_id=parent_message_id,
        payment_model=payment_model,
    )
    body = build_policy_violation_body(dlp_action, policy_tip_general_text, verdict_details)

    client.update_message_policy_violation(url_suffix=url_suffix, body=body)

    outputs = {
        "messageId": message_id,
        "chatId": chat_id,
        "teamId": team_id,
        "channelId": channel_id,
        "parentMessageId": parent_message_id,
        "dlpAction": dlp_action,
        "verdictDetails": verdict_details,
    }
    outputs = {key: value for key, value in outputs.items() if value}

    readable_output = tableToMarkdown(
        name="Teams Message Policy Violation",
        t=outputs,
        headerTransform=pascalToSpace,
    )

    return_results(
        CommandResults(
            outputs_prefix="MSGraphTeams.TeamsMessagePolicyViolation",
            outputs_key_field="messageId",
            outputs=outputs,
            readable_output=readable_output,
        )
    )


def run_command(commands, command, client: MsGraphClient, args, tries):
    while tries > 0:
        try:
            commands[command](client, demisto.args())  # type: ignore
        except NotFoundError as err:
            tries = tries - 1
            if tries == 0:
                return_error(err)
        else:
            tries = 0


def run_microsoft_graph_teams_integration():
    """COMMANDS MANAGER / SWITCH PANEL

    Shared entrypoint consumed by every pack that fronts the Microsoft Graph Teams
    integration (community `MicrosoftGraphTeams` pack + xsoar-supported
    `MicrosoftGraphTeamsStandardConnector` satellite pack). Each pack's YML declares its
    own command surface, and the platform routes only YML-declared commands here — so
    a satellite pack listing a subset of commands cannot invoke the others even though
    they are dispatched below.
    """
    params: dict = demisto.params()
    url = params.get("url", "").rstrip("/") + "/v1.0/"
    tenant: str = params.get("tenant_id", "")
    auth_and_token_url: str = params.get("client_id", "")
    enc_key: str = params.get("secret", "")
    verify: bool = not params.get("insecure", False)
    self_deployed: bool = params.get("self_deployed", False)
    redirect_uri: str = params.get("redirect_uri", "")
    auth_code: str = params.get("auth_code", "")
    app_name: str = "ms-graph-teams"
    proxy: bool = params.get("proxy", False)
    handle_error: bool = argToBoolean(params.get("handle_error", "true"))
    certificate_thumbprint: str = params.get("certificate_thumbprint", "")
    private_key: str = params.get("private_key", "")
    # Under UCP the credential envelope is injected per-request by the platform,
    # so config-time validation of `enc_key` / certificate is skipped.
    if not should_use_ucp_auth():
        if not self_deployed and not enc_key:
            raise DemistoException(
                "Key must be provided. For further information see "
                "https://xsoar.pan.dev/docs/reference/articles/microsoft-integrations---authentication"
            )
        elif not enc_key and not (certificate_thumbprint and private_key):
            raise DemistoException("Key or Certificate Thumbprint and Private Key must be provided.")

    # params related to teams to fetch incidents
    delegated_user = params.get("delegated_user", "")
    tries = 3

    commands = {
        "msgraph-teams-test": test_function,
        "test-module": test_function,
        "msgraph-teams-list-chats": list_chats_command,
        "msgraph-teams-create-chat": create_chat_command,
        "msgraph-teams-get-chat": get_chat_command,
        "msgraph-teams-update-chat": update_chat_command,
        "msgraph-teams-list-members": list_members_command,
        "msgraph-teams-add-member": add_member_command,
        "msgraph-teams-list-messages": list_messages_command,
        "msgraph-teams-send-message": send_message_command,
    }
    command = demisto.command()
    demisto.info(f"Command being called is {command}")

    try:
        client: MsGraphClient = MsGraphClient(
            tenant_id=tenant,
            auth_id=auth_and_token_url,
            enc_key=enc_key,
            app_name=app_name,
            base_url=url,
            verify=verify,
            proxy=proxy,
            self_deployed=self_deployed,
            redirect_uri=redirect_uri,
            auth_code=auth_code,
            handle_error=handle_error,
            certificate_thumbprint=certificate_thumbprint,
            private_key=private_key,
            delegated_user=delegated_user,
        )
        if command == "msgraph-teams-generate-login-url":
            return_results(generate_login_url(client.ms_client))
        elif command == "msgraph-teams-auth-reset":
            return_results(reset_auth())
        elif command == "msgraph-teams-message-update-policy-violation":
            update_teams_message_policy_violation_command(client, demisto.args())
        else:
            run_command(commands, command, client, demisto.args(), tries)

    except Exception as e:
        demisto.debug(traceback.format_exc())
        return_error(str(e))

README

To use the common Microsoft Graph Teams integration logic, add the following code to import the MicrosoftGraphTeamsApiModule.

def main():
    run_microsoft_graph_teams_integration()


from MicrosoftGraphTeamsApiModule import *  # noqa: E402

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

Then, the run_microsoft_graph_teams_integration entry point and MsGraphClient class will be available for usage. For the canonical consumer, see the O365 Teams (Using Graph API) integration in the MicrosoftGraphTeams pack.