ZoomApiModule

Common Zoom code that provides generic infrastructure, and will be appended to each Zoom integration when it is deployed.

Type
python
Pack
ApiModules

Source

from datetime import timedelta

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

""" CONSTANTS """

OAUTH_TOKEN_GENERATOR_URL = "https://zoom.us/oauth/token"
OAUTH_OGV_TOKEN_GENERATOR_URL = "https://zoomgov.com/oauth/token"
# The token's time to live is 1 hour,
# two minutes were subtract for extra safety.
TOKEN_LIFE_TIME = timedelta(minutes=58)
# the lifetime for an JWT token is 90 minutes == 5400 seconds
# 400 seconds were subtract for extra safety.
JWT_LIFETIME = 5000
# maximun records that the api can return in one request
MAX_RECORDS_PER_PAGE = 300

# ERRORS
INVALID_CREDENTIALS = "Invalid credentials. Please verify that your credentials are valid."
INVALID_API_SECRET = "Invalid API Secret. Please verify that your API Secret is valid."
INVALID_ID_OR_SECRET = "Invalid Client ID or Client Secret. Please verify that your ID and Secret is valid."
INVALID_TOKEN = "Invalid Authorization token. Please verify that your Bot ID and Bot Secret is valid."
INVALID_BOT_ID = "No Chatbot can be found with the given robot_jid value. Please verify that your Bot JID is correct"
"""CLIENT CLASS"""


class Zoom_Client(BaseClient):
    """A client class that implements logic to authenticate with Zoom application."""

    def __init__(
        self,
        base_url: str,
        api_key: str | None = None,
        api_secret: str | None = None,
        account_id: str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        bot_client_id: str | None = None,
        bot_client_secret: str | None = None,
        verify=True,
        proxy=False,
        bot_jid: str | None = None,
    ):
        super().__init__(base_url, verify, proxy)
        self.api_key = api_key
        self.api_secret = api_secret
        self.account_id = account_id
        self.client_id = client_id
        self.client_secret = client_secret
        self.bot_client_id = bot_client_id
        self.bot_client_secret = bot_client_secret
        self.bot_jid = bot_jid
        try:
            self.access_token, self.bot_access_token = self.get_oauth_token()
        except Exception as e:
            demisto.info(f"Cannot get access token. Error: {e}")
            self.access_token = None
            self.bot_access_token = None

    def generate_oauth_token(self):
        """
        Generate an OAuth Access token using the app credentials (AKA: client id and client secret) and the account id

        :return: valid token
        """
        full_url = OAUTH_OGV_TOKEN_GENERATOR_URL if "gov" in self._base_url else OAUTH_TOKEN_GENERATOR_URL
        token_res = self._http_request(
            method="POST",
            full_url=full_url,
            params={"account_id": self.account_id, "grant_type": "account_credentials"},
            auth=(self.client_id, self.client_secret),
        )
        return token_res.get("access_token")

    def generate_oauth_client_token(self):
        """
        Generate an OAuth Access token using the app credentials (AKA: client id and client secret) and the account id

        :return: valid token
        """
        full_url = OAUTH_OGV_TOKEN_GENERATOR_URL if "gov" in self._base_url else OAUTH_TOKEN_GENERATOR_URL
        token_res = self._http_request(
            method="POST",
            full_url=full_url,
            params={"account_id": self.account_id, "grant_type": "client_credentials"},
            auth=(self.bot_client_id, self.bot_client_secret),
        )
        return token_res.get("access_token")

    def get_oauth_token(self, force_gen_new_token=False):
        """
        Retrieves the token from the server if it's expired and updates the global HEADERS to include it

        :param force_gen_new_token: If set to True will generate a new token regardless of time passed

        :rtype: ``str``
        :return: Token
        """
        now = datetime.now()
        ctx = get_integration_context()
        client_oauth_token = None
        oauth_token = None

        if not ctx or not ctx.get("token_info").get("generation_time", force_gen_new_token):
            # new token is needed
            if self.client_id and self.client_secret:
                oauth_token = self.generate_oauth_token()
            if self.bot_client_id and self.bot_client_secret:
                client_oauth_token = self.generate_oauth_client_token()
            ctx = {}
        else:
            if generation_time := dateparser.parse(ctx.get("token_info").get("generation_time")):
                time_passed = now - generation_time
            else:
                time_passed = TOKEN_LIFE_TIME
            if time_passed < TOKEN_LIFE_TIME:
                # token hasn't expired
                return ctx.get("token_info", {}).get("oauth_token"), ctx.get("token_info", {}).get("client_oauth_token")
            else:
                # token expired
                # new token is needed
                if self.client_id and self.client_secret:
                    oauth_token = self.generate_oauth_token()
                if self.bot_client_id and self.bot_client_secret:
                    client_oauth_token = self.generate_oauth_client_token()

        ctx.update(
            {
                "token_info": {
                    "oauth_token": oauth_token,
                    "client_oauth_token": client_oauth_token,
                    "generation_time": now.strftime("%Y-%m-%dT%H:%M:%S"),
                }
            }
        )
        set_integration_context(ctx)
        return oauth_token, client_oauth_token

    def error_handled_http_request(
        self,
        method,
        url_suffix="",
        full_url=None,
        headers=None,
        auth=None,
        json_data=None,
        params=None,
        files=None,
        data=None,
        return_empty_response: bool = False,
        resp_type: str = "json",
        stream: bool = False,
    ):
        # all future functions should call this function instead of the original _http_request.
        # This is needed because the OAuth token may not behave consistently,
        # First the func will make an http request with a token,
        # and if it turns out to be invalid, the func will retry again with a new token.
        try:
            return super()._http_request(
                method=method,
                url_suffix=url_suffix,
                full_url=full_url,
                headers=headers,
                auth=auth,
                json_data=json_data,
                params=params,
                files=files,
                data=data,
                return_empty_response=return_empty_response,
                resp_type=resp_type,
                stream=stream,
            )
        except DemistoException as e:
            if any(
                message in e.message
                for message in ["Invalid access token", "Access token is expired.", "Invalid authorization token"]
            ):
                if url_suffix == "/im/chat/messages":
                    demisto.debug("generate new bot client token")
                    self.bot_access_token = self.generate_oauth_client_token()
                    headers = {"authorization": f"Bearer {self.bot_access_token}"}
                else:
                    self.access_token = self.generate_oauth_token()
                    headers = {"authorization": f"Bearer {self.access_token}"}
                return super()._http_request(
                    method=method,
                    url_suffix=url_suffix,
                    full_url=full_url,
                    headers=headers,
                    auth=auth,
                    json_data=json_data,
                    params=params,
                    files=files,
                    data=data,
                    return_empty_response=return_empty_response,
                    resp_type=resp_type,
                    stream=stream,
                )
            else:
                raise DemistoException(e.message, url_suffix)


""" HELPER FUNCTIONS """

README

To use the common Zoom Server logic (authentication and API requests), run the following command to import the ZoomApiModule.

def main():
    ...


from ZoomApiModule import *  # noqa: E402

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

Then, the ZoomApiModule will be available for usage. For examples, see the Zoom integration.