ZoomApiModule

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

python · 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.