Zoom
Use the Zoom integration to manage your Zoom users and meetings.
Messaging and Conferencing · Zoom
Details
| ID | Zoom |
|---|---|
| Provider | Zoom |
| Category | Messaging and Conferencing |
| From Version | 5.0.0 |
| Docker Image | demisto/fastapi:0.125.0.10158186 |
| Supported Modules | Agentix XSIAM EDR Cortex Cloud Cloud Runtime Security |
README
Use the Zoom integration manage your Zoom users and meetings
This integration was integrated and tested with version 2.0.0 of Zoom
Configure Zoom in Cortex
| Parameter | Description | Required |
|---|---|---|
Server URL (e.g., ‘https://api.zoom.us/v2/’) |
True | |
Account ID (OAuth) |
True | |
Client ID (OAuth) |
True | |
Client Secret (OAuth) |
True | |
Use system proxy settings |
False | |
Trust any certificate (not secure) |
False | |
Long running instance |
Enable in order to use zoom-ask and for mirroring. | False |
Listen Port |
Listener port number. Required when choosing long running instance with an external engine for using the Zoom Bot. | False |
Bot JID |
Zoom Bot app JID. | False |
Bot Client ID (OAuth) |
Zoom Bot app client ID. | False |
Bot Client Secret (OAuth) |
Zoom Bot app secret ID. | False |
Secret Token |
For mirroring, see Configuring Secret Token. | False |
Verification Token |
For verify the mirror in. | False |
Mirroring |
Enable Incident Mirroring. See how to configure the app. | False |
Certificate (Required for HTTPS) |
(For Cortex XSOAR 6.x) For use with HTTPS - the certificate that the service should use. (For Cortex XSOAR 8 and Cortex XSIAM) Custom certificates are supported only using engine. | False |
Private Key (Required for HTTPS) |
(For Cortex XSOAR 6.x) For use with HTTPS - the private key that the service should use. (For Cortex XSOAR 8 and Cortex XSIAM) When using an engine, configure a private API key | False |
Server configuration (XSOAR 6.x)
In the Server Configuration section, verify that the value for the instance.execute.external.INTEGRATION-INSTANCE-NAME key is set to true. If this key does not exist:
- Click + Add Server Configuration.
- Add instance.execute.external.
INTEGRATION-INSTANCE-NAMEand set the value to true.
XSOAR endpoint URL-
- For Cortex XSOAR 6.x:
<CORTEX-XSOAR-URL>/instance/execute/<INTEGRATION-INSTANCE-NAME>. For example,https://my.demisto.live/instance/execute/zoom. Note that the stringinstancedoes not refer to the name of your XSOAR instance, but rather is part of the URL. - For Cortex XSOAR 8.x / XSIAM: you need to run using external engine:
https://<Engine URL>:<port>. For example, https://my-engine-url:7001.
Create Zoom ChatBOT app
- Log onto the Zoom App Marketplace: https://marketplace.zoom.us/.
- Click Develop > Build App.
- Choose General App.
- Click Create.

Configure App Settings
- Update your app’s name. By default, the build flow generates a generic name for your app. To change your app’s name, select the edit icon (pencil) and update the name. To save your changes, click anywhere outside of the app name field.
- Under the section “Select how the app is managed”, select “Admin-managed”, and click “Save”.
- Enter your Cortex XSOAR endpoint URL in the OAuth Redirect URL.
- Click Features > Surface > Team Chat.
Toggle on the “Team Chat Subscription” section. Under “Bot Endpoint URL”, add:- For Cortex XSOAR 6.x:
<CORTEX-XSOAR-URL>/instance/execute/<INTEGRATION-INSTANCE-NAME>. For example,https://my.demisto.live/instance/execute/zoom. Note that the stringinstancedoes not refer to the name of your Cortex XSOAR instance, but rather is part of the URL. - For Cortex XSOAR 8.x / XSIAM you need to run using external engine:
https://<Engine Url>:<port>. For example, https://my-engine-url:7001.
- For Cortex XSOAR 6.x:

-
Click Scopes > + Add Scopes to add the following scope permissions.
Scope Type Scope Name Team Chat /imchat:bot Team Chat /team_chat:read:user_message:admin Team Chat /team_chat:read:list_user_sessions:admin Team Chat /team_chat:read:list_user_messages:admin Team Chat /team_chat:write:user_channel:admin Team Chat /team_chat:write:members:admin Team Chat /team_chat:update:user_channel:admin User /user:read:user:admin

-
Click Local Test > Add App Now to test your app and authorize your Cortex XSOAR app.
-
If mirroring is enabled in the integration configuration or using
ZoomAsk:- Endpoint URL Requirements:
According to the Zoom requirements in order to receive webhooks, the Event notification endpoint URL that you specify for each event subscription must:- Be a publicly accessible https endpoint url that supports TLSv1.2+ with a valid certificate chain issued by a Certificate Authority (CA).
- Employ the
https://scheme. - Be able to accept HTTP POST requests containing JSON payloads.
- Be a fully qualified domain name (FQDN).
- be able to respond with an HTTP Status Code.
-
Zoom Configurations:
- Copy the Secret Token from the “Features” > “Access” page under the “Token” section and add it to the instance configuration.

- Configure Event Subscriptions.
- In the “Features” section, under “Access”, in the “General Features” section, enable “Event Subscription”.
- Click +Add New Event Subscription.
- Enter the following information:
- Subscription name: Enter a name for this Event Subscription (e.g., “Send Message Sent”)
- Add Events: Click +Add Events. Under Event Types, select Chat Message, and then select Chat Message Sent.
- Event notification endpoint URL: Enter the Cortex XSOAR URL of your server (
CORTEX-XSOAR-URL/instance/execute/INTEGRATION-INSTANCE-NAME) where you want to receive event notifications. This URL should handle incoming event data from Zoom. Make sure it’s publicly accessible. - Authentication Header Option -
-
Default Header Provided by Zoom option - This option allows you to use a verification token provided by Zoom. Copy the Verification Token under the “Token” section and add it to the instance configuration.

-
Basic Authentication option - You can use Basic Authentication by providing your Zoom Client ID (OAuth) and Secret ID (OAuth) as configured in the instance configuration.

-

- Copy the Secret Token from the “Features” > “Access” page under the “Token” section and add it to the instance configuration.
- Endpoint URL Requirements:
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.
zoom-create-user
Create a new user in zoom account
Base Command
zoom-create-user
Input
| Argument Name | Description | Required |
|---|---|---|
| first_name | First name of the new user. | Required |
| last_name | Last name of the new user. | Required |
| The email of the new user. | Required | |
| user_type | The type of the newly created user. Note: the old type “pro” in now called “Licensed”, and the type “Corporate” is not sopprted in Zoom v2 and above.. Possible values are: Basic, Licensed, pro, Corporate. Default is Basic. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Zoom.User.id | string | The ID of the created user |
| Zoom.User.first_name | string | First name of the created user |
| Zoom.User.last_name | string | Last name for the created user |
| Zoom.User.email | string | Email of the created user |
| Zoom.User.created_at | date | Created date of the user. Note that this field does not appear in zoom v2 and above. |
| Zoom.User.type | number | The type of the user |
Command example
!zoom-create-user email=example@example.com first_name=john last_name=smith user_type=Basic
Context Example
{
"Zoom": {
"User": {
"email": "example@example.com",
"first_name": "john",
"id": "wSQafNLNSJWq_oBzmT7XOw",
"last_name": "smith",
"type": 1
}
}
}
Human Readable Output
User created successfully with ID: wSQafNLNSJWq_oBzmT7XOw
zoom-create-meeting
Create a new zoom meeting (scheduled ,instant, or recurring)
Base Command
zoom-create-meeting
Input
| Argument Name | Description | Required |
|---|---|---|
| type | The type of the meeting. Possible values are: Instant, Scheduled, Recurring meeting with fixed time. Default is Instant. | Required |
| end_date_time | For recurring meetings only. Select the final date on which the meeting will recur before it is canceled. For example: 2017-11-25T12:00:00Z. | Optional |
| end_times | For recurring meetings only.Select how many times the meeting should recur before it is canceled. max = 365. Default = 1.. | Optional |
| monthly_day | For recurring meetings with Monthly recurrence_type only. State the day in a month the meeting should recur. The value range is from 1 to 31. Default = 1.. | Optional |
| monthly_week | For recurring meetings with Monthly recurrence_type only.State the week of the month when the meeting should recur. If you use this field, you must also use the monthly_week_day field to state the day of the week when the meeting should recur. Allowed: -1 (for last week of the month) ┃1┃2┃3┃4. | Optional |
| monthly_week_day | For recurring meetings with Monthly recurrence_type only. State a specific day in a week when the monthly meeting should recur. Allowed: 1┃2┃3┃4┃5┃6┃7To use this field, you must also use the monthly_week field.. | Optional |
| repeat_interval | For recurring meeting with fixed time only. Define the interval at which the meeting should recur. For instance, if you would like to schedule a meeting that recurs every two months, you must set the value of this field as 2 and the value of the type parameter as Monthly. For a daily meeting, the maximum is 90 days. For a weekly meeting the maximum is of 12 weeks. For a monthly meeting, there is a maximum of 3 months. | Optional |
| recurrence_type | For recurring meetings only. Set the recurrence meeting types.. Possible values are: Daily, Weekly, Monthly. | Optional |
| weekly_days | For recurring meetings with a Weekly recurrence_type only. State a specific day in a week when the weekly meeting should recur. Allowed: 1┃2┃3┃4┃5┃6┃7 Default = 1.. | Optional |
| auto-record-meeting | The automatic recording settings. Note that the Cloud option is available for Zoom paid customers only. Possible values are: local, cloud, none. Default is none. | Optional |
| encryption_type | The type of end-to-end (E2EE) encryption, enhanced_encryption or e2ee. Possible values are: enhanced_encryption, e2ee. Default is enhanced_encryption. | Optional |
| host_video | start meetings with the host video on. Possible values are: true, false. Default is True. | Optional |
| join_before_host_time | If the value of the join_before_host field is true, this field sets the time that a participant can join before the meeting’s host. You can choose: 5 or 10 (minutes), or 0 for any time. Possible values are: 0, 5, 10. | Optional |
| join_before_host | Whether participants can join the meeting before its host. For scheduled or recurring meetings only. The default value is False. Possible values are: false, true. | Optional |
| meeting_authentication | If true, only authenticated users can join the meeting. Possible values are: false, true. Default is false. | Optional |
| user | email address or id of user for meeting. | Required |
| topic | The topic of the meeting. | Required |
| waiting_room | This allows the host to control when a participant joins the meeting. The default is False. Possible values are: false, true. | Optional |
| start-time | Meeting start time. When using a format like “yyyy-MM-ddTHH:mm:ssZ”, always use GMT time. When using a format like “yyyy-MM-ddTHH:mm:ss”, you should use local time and you will need to specify the time zone. Only used for scheduled meetings and recurring meetings with fixed time. | Optional |
| timezone | Timezone to format start_time. For example, “America/Los_Angeles”. For scheduled meetings only. . | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Zoom.Meeting.join_url | string | Join url for the meeting |
| Zoom.Meeting.id | string | Meeting id of the new meeting that is created |
| Zoom.Meeting.start_url | string | The URL to start the meeting |
| Zoom.Meeting.uuid | string | Unique meeting ID of the new meeting that is created |
| Zoom.Meeting.status | string | The status of the meeting |
| Zoom.Meeting.start_time | Date | The time that the meeting will start at |
| Zoom.Meeting.host email | string | The email of the host of this meeting |
| Zoom.Meeting.topic | string | The topic of the meeting |
| Zoom.Meeting.duration | number | The duratian of the meeting |
| Zoom.Meeting.created_at | Date | The time that this meeting was created |
| Zoom.Meeting.type | number | The type of the new meeting, Instant = 1, Scheduled =2,Recurring with fixed time = 8 |
Command example
!zoom-create-meeting topic=test type=scheduled user=example@example.com start-time=2023-01-29T12:12:12Z
Context Example
{
"Zoom": {
"Meeting": {
"created_at": "2023-01-15T12:44:30Z",
"duration": 60,
"host_email": "example@example.com",
"host_id": "uJiZN-O7Rp6Jp_995FpZGg",
"id": 88454393857,
"join_url": "https://us06web.zoom.us/j/8845?pwd=WkI1WUdvbHhnMlJuaXU1WDNPdz09",
"pre_schedule": false,
"settings": {
"allow_multiple_devices": false,
"alternative_host_update_polls": false,
"alternative_hosts": "",
"alternative_hosts_email_notification": true,
"approval_type": 2,
"approved_or_denied_countries_or_regions": {
"enable": false
},
"audio": "both",
"auto_recording": "none",
"breakout_room": {
"enable": false
},
"close_registration": false,
"cn_meeting": false,
"device_testing": false,
"email_notification": true,
"enable_dedicated_group_chat": false,
"encryption_type": "enhanced_encryption",
"enforce_login": false,
"enforce_login_domains": "",
"focus_mode": false,
"global_dial_in_countries": [
"US"
],
"global_dial_in_numbers": [
{
"city": "Houston",
"country": "US",
"country_name": "US",
"number": "+1 3462487799",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 3602095623",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 3863475053",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 5074734847",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 5642172000",
"type": "toll"
},
{
"city": "New York",
"country": "US",
"country_name": "US",
"number": "+1 6465588656",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 6469313860",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 6694449171",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 6892781000",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 7193594580",
"type": "toll"
},
{
"city": "Denver",
"country": "US",
"country_name": "US",
"number": "+1 7207072699",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 2532050468",
"type": "toll"
},
{
"city": "Tacoma",
"country": "US",
"country_name": "US",
"number": "+1 2532158782",
"type": "toll"
},
{
"city": "Washington DC",
"country": "US",
"country_name": "US",
"number": "+1 3017158592",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 3052241968",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 3092053325",
"type": "toll"
},
{
"city": "US",
"country": "US",
"country_name": "US",
"number": "+1 3126266799",
"type": "toll"
}
],
"host_save_video_order": false,
"host_video": true,
"in_meeting": false,
"jbh_time": 0,
"join_before_host": false,
"meeting_authentication": false,
"mute_upon_entry": false,
"participant_video": false,
"private_meeting": false,
"registrants_confirmation_email": true,
"registrants_email_notification": true,
"request_permission_to_unmute_participants": false,
"show_share_button": false,
"use_pmi": false,
"waiting_room": false,
"watermark": false
},
"start_time": "2023-01-29T12:12:12Z",
"start_url": "https://us06web.zoom.us/s/883857?zIjAwMDAwMSIsInptX3NrbSI6InptX28ybSIsImFsZyI6IkhTMjU2In0.eyJhdWQiOiJjbGllbnRzbSIsInVpZCI6InVKaVpOLU83UnA2SnBfOTk1RnBaR2ciLCJpc3MiOiJ3ZWIiLCJzayI6IjczMjU5NTExMTgxNDYyODc0NjciLCJzdHkiOjEwMCwid2NkIjoidXMwNiIsImNsdCI6MCwibW51bSI6Ijg4NDU0MzkzODU3IiwiZXhwIjoxNjczNzkzODcwLCJpYXQiOjE2NzM3ODY2NzAsImFpZCI6ImFlS0QyQkZKUkFTdDFRVlVSV285Q0EiLCJjaWQiOiIifQ.5vRJBkMbmODUD_7H3bkS7OjR-MuuLUzNMJ_KeCzWc_U",
"status": "waiting",
"timezone": "Asia/Jerusalem",
"topic": "test",
"type": 2,
"uuid": "4gbib+fjTFmz1wH1LoE7EQ=="
}
}
}
Human Readable Output
Meeting details
uuid id host_id host_email topic type status start_time duration timezone created_at start_url join_url 4gbib+fjTFmz1wH1LoE7EQ== 88454393857 uJiZN-O7Rp6Jp_995FpZGg example@example.com test 2 waiting 2023-01-29T12:12:12Z 60 Asia/Jerusalem 2023-01-15T12:44:30Z https://us06web.zoom.us/s/88454393857?zak=eyJ0eXAiOiJKV1QiLCJzdiI6IjAwMDAwMSIsInptX3NrbSI6InptX28ybSIsImFsZyI6IkhTMjU2In0.eyJhdWQiOiJjbGllbnRzbSIsInVpZCI6InVKaVpOLU83UnA2SnBfOTk1RnBaR2ciLCJpc3MiOiJ3ZWIiLCJzayI6IjczMjU5NTExMTgxNDYyODc0NjciLCJzdHkiOjEwMCwid2NkIjoidXMwNiIsImNsdCI6MCwibW51bSI6Ijg4NDU0MzkzODU3IiwiZXhwIjoxNjczNzkzODcwLCJpYXQiOjE2NzM3ODY2NzAsImFpZCI6ImFlS0QyQkZKUkFTdDFRVlVSV285Q0EiLCJjaWQiOiIifQ.5vRJBkMbmODUD_7H3bkS7OjR-MuuLUzNMJ_KeCzWc_U https://us06web.zoom.us/j/88454393857?pwd=WkI1WnVEWUdvbHhnMlJuaXU1WDNPdz09
zoom-fetch-recording
Get meeting record and save as file in the warroom
Base Command
zoom-fetch-recording
Input
| Argument Name | Description | Required |
|---|---|---|
| meeting_id | Meeting ID of the recorded meeting. | Required |
| delete_after | Whether to delete the recording from the cloud after downloading. Possible values are: false, true. Default is true. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| File.SHA256 | Unknown | Attachment’s SHA256 |
| File.SHA1 | Unknown | Attachment’s SHA1 |
| File.MD5 | Unknown | Attachment’s MD5 |
| File.Name | Unknown | Attachment’s Name |
| File.Info | Unknown | Attachment’s Info |
| File.Size | Unknown | Attachment’s Size (In Bytes) |
| File.Extension | Unknown | Attachment’s Extension |
| File.Type | Unknown | Attachment’s Type |
| File.EntryID | Unknown | Attachment’s EntryID |
| File.SSDeep | Unknown | Attachment’s SSDeep hash |
Command example
!zoom-fetch-recording meeting_id=83622325727 delete_after=false
Context Example
{
"File": [
{
"EntryID": "446@37e93103-1bd6-4776-8021-0f7023b1bb79",
"Extension": "MP4",
"Info": "video/mp4",
"MD5": "0de01f8f6d037e9ebecde2f91ad9b7a3",
"Name": "recording_83622325727_d831fbd5-3938-44d0-b30b-962bf76e2916.MP4",
"SHA1": "c81f9abb6dcf9be42dae99bc5e1501c43496d66e",
"SHA256": "8b252b01c8e6af62b88de64abf80d360467da1598dcd6b50ac9a252ffafb2eb5",
"SHA512": "b273e90a9cd0589eaba1c5e647fedab7b46e198193a54bec70a18def9ada57575833ea89b6eabf7f5852c3dc98b862780793d3e87e32a238ec2a7acffdba2495",
"SSDeep": "1536:TDWnSNbM8oEWzjSn7anYBcoGdee32hfUunvSHx8:T6t8JWzjSWnYBcoG9325UunvSHq",
"Size": 320333,
"Type": "ISO Media, MP4 v2 [ISO 14496-14]"
},
{
"EntryID": "447@37e93103-1bd6-4776-8021-0f7023b1bb79",
"Extension": "M4A",
"Info": "video/mp4",
"MD5": "e826111564499ca8021d0ddfcfde064b",
"Name": "recording_83622325727_19bf5f8a-e77c-4b75-b09e-13983521703c.M4A",
"SHA1": "50d3386dc5b74935b8f8d541f319df34a49f90a0",
"SHA256": "2719528cf61358ccbee861fb8e42d9d5fb37390baa22d0dc7dad1aa7e3935146",
"SHA512": "74bfb40ade824e6e36329b3e2335b2028fac648d3ecef1a02e9be93d4a455e3acf4c1f9c844d1c2ebcc661d4c84d66c1da4f0ca0462eabc12bf2dff84006d460",
"SSDeep": "24:fctSXvr4S+08n11/TlllZltk2B0tDilduxEJvCXyxrApd4dEcpXuFxnZQ4r:uYr41Xnf/T/vX/8OvqyrjiCuFhZQK",
"Size": 243652,
"Type": "ISO Media, MP4 v2 [ISO 14496-14]"
}
]
}
Human Readable Output
The Audio file recording_83622325727_19bf5f8a-e77c-4b75-b09e-13983521703c.M4A was downloaded successfully
zoom-list-users
List the existing users
Base Command
zoom-list-users
Input
| Argument Name | Description | Required |
|---|---|---|
| status | Which status of users to list. Possible values are: active, inactive, pending. Default is active. | Optional |
| page-size | Number of users to return. Max 300. The default is 30. | Optional |
| next_page_token | The next page token is used to get the next page. IMPORTANT: You must pass the same page size that you passed in the first call. | Optional |
| page-number | Which page of results to return. The default = 1. Note: This argument is in a deprecate process by the API. As an alternative use “next_page_token” or “limit”.. | Optional |
| limit | The total amunt of results to show. | Optional |
| user_id | A user ID. this is for a singel user. | Optional |
| role_id | Filter the response by a specific role. For example: role_id=0 (Owner), role_id=2 (Member). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Zoom.Metadata.Count | number | Total page count available |
| Zoom.Metadata.Number | number | Current page number |
| Zoom.Metadata.Size | number | Number of results in current page |
| Zoom.Metadata.Total | number | Total number of records |
| Zoom.User.id | string | ID of the user |
| Zoom.User.first_name | string | First name of user |
| Zoom.User.last_name | string | Last name of user |
| Zoom.User.email | string | Email of user |
| Zoom.User.type | number | Type of user |
| Zoom.User.created_at | date | Date when user was created |
| Zoom.User.dept | string | Department for user |
| Zoom.User.verified | number | Is the user verified |
| Zoom.User.last_login_time | date | Last login time of the user |
| Zoom.User.timezone | string | Default timezone for the user |
| Zoom.User.pmi | string | PMI of user |
| Zoom.User.group_ids | string | Groups user belongs to |
Command example
!zoom-list-users status=pending limit=10
Context Example
{
"Zoom": {
"Metadata": {
"Count": 6,
"Number": 1,
"Size": 10,
"Total": 59
},
"User": [
{
"created_at": "2023-01-15T12:44:34Z",
"email": "example@example.com",
"id": "",
"pmi": 0,
"role_id": "0",
"status": "pending",
"type": 1,
"user_created_at": "2023-01-02T12:55:33Z",
"verified": 0
},
{
"created_at": "2023-01-15T12:44:34Z",
"email": "example@example.com",
"id": "",
"pmi": 0,
"role_id": "0",
"status": "pending",
"type": 1,
"user_created_at": "2023-01-02T13:09:28Z",
"verified": 0
},
{
"created_at": "2023-01-15T12:44:34Z",
"email": "example@example.com",
"id": "",
"pmi": 0,
"role_id": "0",
"status": "pending",
"type": 1,
"user_created_at": "2022-12-22T10:19:47Z",
"verified": 0
},
{
"created_at": "2023-01-15T12:44:34Z",
"email": "example@example.com",
"id": "",
"pmi": 0,
"role_id": "0",
"status": "pending",
"type": 1,
"user_created_at": "2023-01-02T13:12:46Z",
"verified": 0
},
{
"created_at": "2023-01-15T12:44:34Z",
"email": "example@example.com",
"id": "",
"pmi": 0,
"role_id": "0",
"status": "pending",
"type": 1,
"user_created_at": "2023-01-02T12:04:08Z",
"verified": 0
},
{
"created_at": "2023-01-15T12:44:34Z",
"email": "example@example.com",
"id": "",
"pmi": 0,
"role_id": "0",
"status": "pending",
"type": 1,
"user_created_at": "2023-01-02T12:56:08Z",
"verified": 0
},
{
"created_at": "2023-01-15T12:44:34Z",
"email": "example@example.com",
"id": "",
"pmi": 0,
"role_id": "0",
"status": "pending",
"type": 1,
"user_created_at": "2023-01-02T12:58:16Z",
"verified": 0
},
{
"created_at": "2023-01-15T12:44:34Z",
"email": "example@example.com",
"id": "",
"pmi": 0,
"role_id": "0",
"status": "pending",
"type": 1,
"user_created_at": "2023-01-02T13:15:04Z",
"verified": 0
},
{
"created_at": "2023-01-15T12:44:34Z",
"email": "example@example.com",
"id": "",
"pmi": 0,
"role_id": "0",
"status": "pending",
"type": 1,
"user_created_at": "2023-01-02T11:58:04Z",
"verified": 0
},
{
"created_at": "2023-01-15T12:44:34Z",
"email": "example@example.com",
"id": "",
"pmi": 0,
"role_id": "0",
"status": "pending",
"type": 1,
"user_created_at": "2023-01-02T13:09:52Z",
"verified": 0
}
]
}
}
Human Readable Output
Users
id type pmi verified created_at status role_id example@example.com 1 0 0 2023-01-15T12:44:34Z pending 0 example@example.com 1 0 0 2023-01-15T12:44:34Z pending 0 example@example.com 1 0 0 2023-01-15T12:44:34Z pending 0 example@example.com 1 0 0 2023-01-15T12:44:34Z pending 0 example@example.com 1 0 0 2023-01-15T12:44:34Z pending 0 example@example.com 1 0 0 2023-01-15T12:44:34Z pending 0 example@example.com 1 0 0 2023-01-15T12:44:34Z pending 0 example@example.com 1 0 0 2023-01-15T12:44:34Z pending 0 example@example.com 1 0 0 2023-01-15T12:44:34Z pending 0 example@example.com 1 0 0 2023-01-15T12:44:34Z pending 0 Metadata
total_records 59
zoom-delete-user
Delete a user from Zoom
Base Command
zoom-delete-user
Input
| Argument Name | Description | Required |
|---|---|---|
| user | The user ID or email to delete. | Required |
| action | The action to take. Possible values are: disassociate, delete. Default is disassociate. | Optional |
Context Output
There is no context output for this command.
zoom-meeting-get
Get the information of an existing zoom meeting
Base Command
zoom-meeting-get
Input
| Argument Name | Description | Required |
|---|---|---|
| meeting_id | The id of the existing meeting. | Required |
| occurrence_id | Provide this field to view meeting details of a particular occurrence of the recurring meeting. | Optional |
| show_previous_occurrences | Set the value of this field to true if you would like to view meeting details of all previous occurrences of a recurring meeting. Possible values are: false, true. Default is True. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Zoom.Meeting.join_url | string | Join url for the meeting |
| Zoom.Meeting.id | string | Meeting id of the new meeting that is created |
| Zoom.Meeting.start_url | string | The URL to start the meeting |
| Zoom.Meeting.uuid | string | Unique meeting ID of the new meeting that is created |
| Zoom.Meeting.status | string | The status of the meeting |
| Zoom.Meeting.start_time | Date | The time that the meeting will start at |
| Zoom.Meeting.host email | string | The email of the host of this meeting |
| Zoom.Meeting.topic | string | The topic of the meeting |
| Zoom.Meeting.duration | number | The duratian of the meeting |
| Zoom.Meeting.created_at | Date | The time that this meeting was created |
| Zoom.Meeting.type | number | The type of the new meeting, Instant = 1, Scheduled =2,Recurring with fixed time = 8 |
Command example
!zoom-meeting-get meeting_id=88949894296
Context Example
{
"Zoom": {
"Meeting": {
"agenda": "",
"assistant_id": "",
"created_at": "2022-12-29T08:10:13Z",
"duration": 60,
"host_email": "example@example.com",
"host_id": "uJiZN-O7Rp6Jp_995FpZGg",
"id": 88949894296,
"join_url": "https://us06web.zoom.us/94296?pwd=b3GdrSDBBNU1FYTVRVmdadz09",
"pre_schedule": false,
"settings": {
"allow_multiple_devices": false,
"alternative_host_update_polls": false,
"alternative_hosts": "",
"alternative_hosts_email_notification": true,
"approval_type": 2,
"approved_or_denied_countries_or_regions": {
"enable": false
},
"audio": "both",
"auto_recording": "none",
"breakout_room": {
"enable": false
},
"close_registration": false,
"cn_meeting": false,
"device_testing": false,
"email_notification": true,
"enable_dedicated_group_chat": false,
"encryption_type": "enhanced_encryption",
"enforce_login": false,
"enforce_login_domains": "",
"focus_mode": false,
"global_dial_in_countries": [
"US"
],
"global_dial_in_numbers": [
{
"country": "US",
"country_name": "US",
"number": "+1 6469313860",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 6694449171",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 6892781000",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 7193594580",
"type": "toll"
},
{
"city": "Denver",
"country": "US",
"country_name": "US",
"number": "+1 7207072699",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 2532050468",
"type": "toll"
},
{
"city": "Tacoma",
"country": "US",
"country_name": "US",
"number": "+1 2532158782",
"type": "toll"
},
{
"city": "Washington DC",
"country": "US",
"country_name": "US",
"number": "+1 3017158592",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 3052241968",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 3092053325",
"type": "toll"
},
{
"city": "US",
"country": "US",
"country_name": "US",
"number": "+1 3126266799",
"type": "toll"
},
{
"city": "Houston",
"country": "US",
"country_name": "US",
"number": "+1 3462487799",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 3602095623",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 3863475053",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 5074734847",
"type": "toll"
},
{
"country": "US",
"country_name": "US",
"number": "+1 5642172000",
"type": "toll"
},
{
"city": "New York",
"country": "US",
"country_name": "US",
"number": "+1 6465588656",
"type": "toll"
}
],
"host_save_video_order": false,
"host_video": true,
"in_meeting": false,
"jbh_time": 0,
"join_before_host": false,
"meeting_authentication": false,
"mute_upon_entry": false,
"participant_video": false,
"private_meeting": false,
"registrants_confirmation_email": true,
"registrants_email_notification": true,
"request_permission_to_unmute_participants": false,
"show_share_button": false,
"use_pmi": false,
"waiting_room": false,
"watermark": false
},
"start_time": "2022-12-29T12:12:12Z",
"start_url": "https://us06web.zoom.us/s/88949?zak=eyJ0eXAiOiJKV1QiLCJzdiI6IjAwMDAwMSIsInptX3NrbSI6InVKaVpOLU83UnA2SnBfOTk1RnBaR2ciLCJpc3MiOiJ3ZWIiLCJzayI6IjczMjU5NTExMTgxNDYyODc0NjciLCJzdHkiOjEwMCwid2NkIjoidXMwNiIsImNsdCI6MCwibW51bSI6Ijg4OTQ5ODk0Mjk2IiwiZXhwIjoxNjczNzkzODc2LCJpYXQiOjE2NzM3ODY2NzYsImFpZCI6ImFlS0QyQkZKUkFTdDFRVlVSV285Q0EiLCJjaWQiOiIifQ.BTOeH_-MZRm7A5sACnDJrP_zKbzaDCWZ5orvtH4rVb0",
"status": "waiting",
"timezone": "Asia/Jerusalem",
"topic": "test",
"type": 2,
"uuid": "anhEx2x6QWG7TREn71MmoA=="
}
}
}
Human Readable Output
Meeting details
uuid id host_id host_email topic type status start_time duration timezone agenda created_at start_url join_url anhEx2x6QWG7TREn71MmoA== 88949894296 uJiZN-O7Rp6Jp_995FpZGg example@example.com test 2 waiting 2022-12-29T12:12:12Z 60 Asia/Jerusalem 2022-12-29T08:10:13Z https://us06web.zoom.us/s/88949894296?zak=eyJ0eXAiOiJKV1QiLCJzdiI6IjAwMDAwMSIsInptX3NrbSI6InptX28ybSIsImFsZyI6IkhTMjU2In0.eyJhdWQiOiJjbGllbnRzbSIsInVpZCI6InVKaVpOLU83UnA2SnBfOTk1RnBaR2ciLCJpc3MiOiJ3ZWIiLCJzayI6IjczMjU5NTExMTgxNDYyODc0NjciLCJzdHkiOjEwMCwid2NkIjoidXMwNiIsImNsdCI6MCwibW51bSI6Ijg4OTQ5ODk0Mjk2IiwiZXhwIjoxNjczNzkzODc2LCJpYXQiOjE2NzM3ODY2NzYsImFpZCI6ImFlS0QyQkZKUkFTdDFRVlVSV285Q0EiLCJjaWQiOiIifQ.BTOeH_-MZRm7A5sACnDJrP_zKbzaDCWZ5orvtH4rVb0 https://us06web.zoom.us/j/88949894296?pwd=b3dzT1pzWGdrSDBBNU1FYTVRVmdadz09
zoom-meeting-list
Show all the meetings of a given user. Note: only scheduled and unexpired meetings will appear.
Base Command
zoom-meeting-list
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | The user ID of the meetings owner. | Required |
| page_size | Number of users to return. Default = 30. Max = 300. | Optional |
| page_number | Which page of results to return. The default = 1. Note: This argument is in a deprecate process by the API. As an alternative use “next_page_token” or “limit”. | Optional |
| next_page_token | The next page token is used to paginate te the next page. IMPORTENT: You must pass the same page size that you pased at the first call.. | Optional |
| limit | The total amunt of results to show. | Optional |
| type | Filter the results by searching specific types. Possible values are: all, scheduled, live, upcoming, upcoming_meetings, previous_meetings. Default is “scheduled”. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Zoom.Metadata.Total | number | Total number of records |
| Zoom.Meeting.page_size | number | The size of the page returned from the API |
| Zoom.Meeting.total_records | number | The total records in the API for this request. |
| Zoom.Meeting.join_url | string | Join url for the meeting |
| Zoom.Meeting.id | string | Meeting id of the new meeting that is created |
| Zoom.Meeting.start_url | string | The URL to start the meeting |
| Zoom.Meeting.uuid | string | Unique meeting ID of the new meeting that is created |
| Zoom.Meeting.status | string | The status of the meeting |
| Zoom.Meeting.start_time | Date | The time that the meeting will start at |
| Zoom.Meeting.host email | string | The email of the host of this meeting |
| Zoom.Meeting.topic | string | The topic of the meeting |
| Zoom.Meeting.duration | number | The duratian of the meeting |
| Zoom.Meeting.created_at | Date | The time that this meeting was created |
| Zoom.Meeting.type | unknown | The ty pe of this meeting |
Command example
!zoom-meeting-list user_id=example@example.com type=scheduled limit=7
Context Example
{
"Zoom": {
"Meeting": {
"meetings": [
{
"created_at": "2022-12-04T10:41:27Z",
"duration": 60,
"host_id": "uJiZN-O7Rp6Jp_995FpZGg",
"id": 83810397585,
"join_url": "https://us06web.zoom.us/j/83810397585?pwd=OGFiZjVvMWZzdDBoaXlLdz09",
"start_time": "2022-12-10T11:00:00Z",
"timezone": "Asia/Jerusalem",
"topic": "2",
"type": 8,
"uuid": "z93Dc6Kj3jx+zaYg=="
},
{
"created_at": "2022-12-06T07:59:02Z",
"duration": 60,
"host_id": "uJiZN-O7Rp6Jp_995FpZGg",
"id": 84540166459,
"join_url": "https://us06web.zoom.us/j/84540166459?pwd=ZzZaaVpFYlBVYnFoUT09",
"start_time": "2022-12-12T08:00:00Z",
"timezone": "Asia/Jerusalem",
"topic": "My recurring Meeting -Dima",
"type": 8,
"uuid": "4kZQ+HlFOAz0VBPHoCYg=="
},
{
"created_at": "2022-12-27T21:05:41Z",
"duration": 60,
"host_id": "uJiZN-O7Rp6Jp_995FpZGg",
"id": 89824497327,
"join_url": "https://us06web.zoom.us/j/89824497327?pwd=SWp3b3VDRVhmQT09",
"start_time": "2022-12-27T21:05:00Z",
"timezone": "Asia/Jerusalem",
"topic": "efe",
"type": 8,
"uuid": "RABXy2D4hA1rgpDgQ=="
},
{
"created_at": "2022-12-27T21:11:01Z",
"duration": 60,
"host_id": "uJiZN-O7Rp6Jp_995FpZGg",
"id": 83245658341,
"join_url": "https://us06web.zoom.us/j/83245658341?pwd=SmNGMHI0RDNkZ2Yms3Zz09",
"start_time": "2022-12-27T21:11:00Z",
"timezone": "Asia/Jerusalem",
"topic": "efe",
"type": 8,
"uuid": "qOXvloSamJjjFaFgniTA=="
},
{
"created_at": "2022-12-27T21:25:54Z",
"duration": 60,
"host_id": "uJiZN-O7Rp6Jp_995FpZGg",
"id": 88468901206,
"join_url": "https://us06web.zoom.us/j/88468901206?pwd=RXZYQlWCtpS3l1MHUxZz09",
"start_time": "2022-12-27T21:25:00Z",
"timezone": "Asia/Jerusalem",
"topic": "efe",
"type": 8,
"uuid": "yHzCvl4Sry+C9LnCwdnwQ=="
},
{
"created_at": "2022-12-28T06:39:48Z",
"duration": 60,
"host_id": "uJiZN-O7Rp6Jp_995FpZGg",
"id": 87525048161,
"join_url": "https://us06web.zoom.us/j/87525048161?pwd=ZXlV3U0OUxxSlBkdz09",
"start_time": "2022-12-28T06:39:00Z",
"timezone": "Asia/Jerusalem",
"topic": "efe",
"type": 8,
"uuid": "Sblh/S1W+rTKUcojJjw=="
},
{
"created_at": "2022-12-28T06:42:25Z",
"duration": 60,
"host_id": "uJiZN-O7Rp6Jp_995FpZGg",
"id": 83877839723,
"join_url": "https://us06web.zoom.us/j/83877839723?pwd=WySkY4Zkc4Zz09",
"start_time": "2022-12-28T06:42:00Z",
"timezone": "Asia/Jerusalem",
"topic": "efe",
"type": 8,
"uuid": "iLXDe4HsR6uMb+x8GyybTA=="
}
],
"next_page_token": "N58N4bhqiBFapzndJocx6cc8NKr2",
"page_size": 7,
"total_records": 60
},
"Metadata": {
"Size": 7,
"Total": 60
}
}
}
Human Readable Output
Meeting list
uuid id host_id topic type start time duration timezone created_at join_url z93Dc6KjSo20Wr3jx+zaYg== 83810397585 uJiZN-O7Rp6Jp_995FpZGg 2 8 60 Asia/Jerusalem 2022-12-04T10:41:27Z https://us06web.zoom.us/j/83810397585?pwd=OGFiZjRYNGhwWkVvMWZzdDBoaXlLdz09 4kZQ+Hl2RFOAz0VBPHoCYg== 84540166459 uJiZN-O7Rp6Jp_995FpZGg My recurring Meeting -Dima 8 60 Asia/Jerusalem 2022-12-06T07:59:02Z https://us06web.zoom.us/j/84540166459?pwd=ZzdmUEJ5QkZaaUZaaVpFYlBVYnFoUT09 RABXyk81T02D4hA1rgpDgQ== 89824497327 uJiZN-O7Rp6Jp_995FpZGg efe 8 60 Asia/Jerusalem 2022-12-27T21:05:41Z https://us06web.zoom.us/j/89824497327?pwd=SWpvK0I0L3pQcTNnWlF3b3VDRVhmQT09 qOXvlLOoSamJjjFaFgniTA== 83245658341 uJiZN-O7Rp6Jp_995FpZGg efe 8 60 Asia/Jerusalem 2022-12-27T21:11:01Z https://us06web.zoom.us/j/83245658341?pwd=SmNGMHI0R1hndnNlRXRDNkZ2Yms3Zz09 yHzCvl4USry+C9LnCwdnwQ== 88468901206 uJiZN-O7Rp6Jp_995FpZGg efe 8 60 Asia/Jerusalem 2022-12-27T21:25:54Z https://us06web.zoom.us/j/88468901206?pwd=RXZYQlhVbWJKZ1pLWCtpS3l1MHUxZz09 Sblh/I34S1W+rTKUcojJjw== 87525048161 uJiZN-O7Rp6Jp_995FpZGg efe 8 60 Asia/Jerusalem 2022-12-28T06:39:48Z https://us06web.zoom.us/j/87525048161?pwd=ZXlXdXl0QWlLOFVyV3U0OUxxSlBkdz09 iLXDe4HsR6uMb+x8GyybTA== 83877839723 uJiZN-O7Rp6Jp_995FpZGg efe 8 60 Asia/Jerusalem 2022-12-28T06:42:25Z https://us06web.zoom.us/j/83877839723?pwd=WU9xNmp5RW5KRDhsZ1RySkY4Zkc4Zz09 Metadata
total_records 60
Command example
#### Context Example
```json
{
"Zoom": {
"Channel": {
"channels": [
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"id": "f7ce414a24634240ae0a7894823d8707",
"jid": "f7ce414a24634240ae0a7894823d8707@conference.xmpp.zoom.us",
"name": "Channel1",
"type": 3
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"id": "e82e99ac9b0c40b08449c76766cf398d",
"jid": "e82e99ac9b0c40b08449c76766cf398d@conference.xmpp.zoom.us",
"name": "Channel2",
"type": 3
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": false,
"posting_permissions": 1
},
"id": "e7e2c5e1ba854056906af1c9dc36148f",
"jid": "e7e2c5e1ba854056906af1c9dc36148f@conference.xmpp.zoom.us",
"name": "Channel3",
"type": 3
},
{
"channel_settings": {
"add_member_permissions": 2,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"id": "e6388c9667c245c3844009ae995664d5",
"jid": "e6388c9667c245c3844009ae995664d5@conference.xmpp.zoom.us",
"name": "Channel4",
"type": 3
}
],
"next_page_token": "",
"page_size": 20,
"total_records": 4
},
"ChannelsNextToken": ""
}
}
Human Readable Output
Channels
Channel ID Channel JID Channel name Channel type f7ce414a24634240ae0a7894823d8707 f7ce414a24634240ae0a7894823d8707@conference.xmpp.zoom.us Channel1 3 e82e99ac9b0c40b08449c76766cf398d e82e99ac9b0c40b08449c76766cf398d@conference.xmpp.zoom.us Channel2 3 e7e2c5e1ba854056906af1c9dc36148f e7e2c5e1ba854056906af1c9dc36148f@conference.xmpp.zoom.us channel3 3 e6388c9667c245c3844009ae995664d5 e6388c9667c245c3844009ae995664d5@conference.xmpp.zoom.us Channel4 3
zoom-list-user-channels
List the existing channels
Base Command
zoom-list-user-channels
Input
| Argument Name | Description | Required |
|---|---|---|
| page-size | Number of channels to return. Max 300. The default is 50. | Optional |
| next_page_token | The next page token is used to get the next page. IMPORTANT: You must pass the same page size that you passed in the first call. | Optional |
| page-number | Which page of results to return. The default = 1. Note: This argument is in a deprecate process by the API. As an alternative use “next_page_token” or “limit”. | Optional |
| limit | The total amunt of results to show. | Optional |
| channel_id | A channel ID. This is for a single channel. | Optional |
| user_id | A user ID. This is for a single user. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Zoom.Channel.id | string | The ID of the created channel. |
| Zoom.Channel.name | string | The name of the created channel. |
| Zoom.Channel.type | string | The created channel type. |
| Zoom.Channel.url | string | The created channel URL. |
Command example
!zoom-list-user-channels user_id=uJiZN-O7Rp6Jp_995FpZGg channel_id=88f2f20f4abd415cba2429538be58025
Context Example
{
"Zoom": {
"Channel": {
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiI4OGYyZjIwZjRhYmQ0MTVjYmEyNDI5NTM4YmU1ODAyNUBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "88f2f20f4abd415cba2429538be58025",
"jid": "88f2f20f4abd415cba2429538be58025@conference.xmpp.zoom.us",
"name": "Channel-Updated5",
"type": 3
},
"UserChannelsNextToken": null
}
}
Human Readable Output
Channels
Channel ID Channel name Channel type Channel url User id channel JID 88f2f20f4abd415cba2429538be58025 Channel-Updated5 3 https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiI4OGYyZjIwZjRhYmQ0MTVjYmEyNDI5NTM4YmU1ODAyNUBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9 uJiZN-O7Rp6Jp_995FpZGg 88f2f20f4abd415cba2429538be58025@conference.xmpp.zoom.us
Command example
!zoom-list-user-channels user_id=uJiZN-O7Rp6Jp_995FpZGg
Context Example
{
"Zoom": {
"Channel": {
"channels": [
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": false,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiJlN2UyYzVlMWJhODU0MDU2OTA2YWYxYzlkYzM2MTQ4ZkBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "e7e2c5e1ba854056906af1c9dc36148f",
"jid": "e7e2c5e1ba854056906af1c9dc36148f@conference.xmpp.zoom.us",
"name": "TestChannel1",
"type": 3
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiI3ODI4YTFiMjgzYmQ0MjYxOTkwOTRlNWVhMGFhOTM1M0Bjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "7828a1b283bd426199094e5ea0aa9353",
"jid": "7828a1b283bd426199094e5ea0aa9353@conference.xmpp.zoom.us",
"name": "Channel",
"type": 3
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiIwOGE0OWVmY2I5YTA0N2MzYmU5NWMwNDc4NzI1NjJjZkBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "08a49efcb9a047c3be95c047872562cf",
"jid": "08a49efcb9a047c3be95c047872562cf@conference.xmpp.zoom.us",
"name": "DemoChannel-Updated",
"type": 3
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": false,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiI5MmIwMjY4YWFmOGQ0MDM5OTQ4YjE3ZDc4M2IwODE5OEBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "92b0268aaf8d4039948b17d783b08198",
"jid": "92b0268aaf8d4039948b17d783b08198@conference.xmpp.zoom.us",
"name": "TestGroupChannel1",
"type": 3
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiIzZDRjZjFiNDkzNDk0NGFkODM5YzEwOWJhYWM5MzEyNkBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "3d4cf1b4934944ad839c109baac93126",
"jid": "3d4cf1b4934944ad839c109baac93126@conference.xmpp.zoom.us",
"name": "Admins1",
"type": 2
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiJlODJlOTlhYzliMGM0MGIwODQ0OWM3Njc2NmNmMzk4ZEBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "e82e99ac9b0c40b08449c76766cf398d",
"jid": "e82e99ac9b0c40b08449c76766cf398d@conference.xmpp.zoom.us",
"name": "Admins_to_delete_2",
"type": 3
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiIwZWJlNzEwMDNlYjI0ZjVmODU3N2U1YTEyNGYwNjQyOEBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "0ebe71003eb24f5f8577e5a124f06428",
"jid": "0ebe71003eb24f5f8577e5a124f06428@conference.xmpp.zoom.us",
"name": "Admins_to_delete_2_updated",
"type": 3
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 1,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiIyZTk5YTdlMDdkZTA0ZjUyYmYyYzQ0OGNkMmIxNTFmY0Bjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "2e99a7e07de04f52bf2c448cd2b151fc",
"jid": "2e99a7e07de04f52bf2c448cd2b151fc@conference.xmpp.zoom.us",
"name": "BotChannel",
"type": 5
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiJiMThkMTUxNDBjMjU0NTQ5YTgwYWY2NTcyYjNiNTUyZUBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "b18d15140c254549a80af6572b3b552e",
"jid": "b18d15140c254549a80af6572b3b552e@conference.xmpp.zoom.us",
"name": "ChannelName",
"type": 3
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiIzNjQxYzNmZGMxZGM0Zjc4OGU4ZWIzNDA3YTQwYzQ3MkBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "3641c3fdc1dc4f788e8eb3407a40c472",
"jid": "3641c3fdc1dc4f788e8eb3407a40c472@conference.xmpp.zoom.us",
"name": "Admins",
"type": 2
},
{
"channel_settings": {
"add_member_permissions": 1,
"allow_to_add_external_users": 0,
"mention_all_permissions": 1,
"new_members_can_see_previous_messages_files": true,
"posting_permissions": 1
},
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiJmN2NlNDE0YTI0NjM0MjQwYWUwYTc4OTQ4MjNkODcwN0Bjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "f7ce414a24634240ae0a7894823d8707",
"jid": "f7ce414a24634240ae0a7894823d8707@conference.xmpp.zoom.us",
"name": "TestChannel3",
"type": 3
}
],
"next_page_token": "",
"page_size": 50,
"total_records": 11
},
"UserChannelsNextToken": ""
}
}
Human Readable Output
Channels
zoom-create-channel
Creates a channel for a user
Base Command
zoom-create-channel
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | The user’s unique identifier. | Required |
| member_emails | The member(s) to include in the channel. A maximum of 20 members can be added to the channel at once with this API. | Required |
| add_member_permissions | Who can add new channel members: 1 - All channel members can add new members. 2 - Only channel owner and administrators can add new members. Note: This setting can only be modified by the channel owner. Default: 1. | |
| posting_permissions | The channel members’ posting permissions: 1 — All chat channel members can post to the channel. 2 — Only the channel owner and administrators can post to the channel. * 3 — Only the channel owner, administrators and certain members can post to the channel. Default: 1. | Optional |
| new_members_can_see_prev_msgs | Whether new channel members can view messages and files previously posted in the channel. Possible values are: true, false. Default is True. | Optional |
| channel_name | The name of the channel. Maximum of 128 characters. | Required |
| channel_type | The type of the channel. The value can be one of the following: 1: Private channel. In this type of channel, members must be invited to join a channel. 2: Private channel with members that belong to one Zoom account. Members in this channel should be invited and the members should be from the same organization. 3: Public channel. Anyone can search for this channel and join the channel. 4: New chat. This is an instant channel which can be created by adding members to a new chat. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Zoom.Channel.id | string | the channel’s unique identifier. |
| Zoom.Channel.jid | string | The channel JID. |
| Zoom.Channel.name | string | The channel name. |
| Zoom.Channel.type | string | The type of the channel. The value can be one of the following: 1: Private channel. In this type of channel, members must be invited to join a channel. 2: Private channel with members that belong to one Zoom account. Members in this channel should be invited and the members should be from the same organization. 3: Public channel. Anyone can search for this channel and join the channel. 4: Group chat. This is an instant channel which can be created by adding members to a new chat. |
| Zoom.Channel.url | string | The URL of the Zoom chat channel. |
Command example
!zoom-create-channel user_id=uJiZN-O7Rp6Jp_995FpZGg member_emails=example@example.com channel_name=ChannelName channel_type="Public channel"
Context Example
{
"Zoom": {
"Channel": {
"channel_url": "https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiJiMThkMTUxNDBjMjU0NTQ5YTgwYWY2NTcyYjNiNTUyZUBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9",
"id": "b18d15140c254549a80af6572b3b552e",
"jid": "b18d15140c254549a80af6572b3b552e@conference.xmpp.zoom.us",
"name": "ChannelName",
"type": 3
}
}
}
Human Readable Output
Channel details
Channel ID Channel name Channel type Channel url User id b18d15140c254549a80af6572b3b552e ChannelName 3 https://us06web.zoom.us/launch/chat/v2/eyJzaWQiOiJiMThkMTUxNDBjMjU0NTQ5YTgwYWY2NTcyYjNiNTUyZUBjb25mZXJlbmNlLnhtcHAuem9vbS51cyJ9 uJiZN-O7Rp6Jp_995FpZGg
zoom-delete-channel
Deletes a specific Zoom channel
Base Command
zoom-delete-channel
Input
| Argument Name | Description | Required |
|---|---|---|
| channel_id | Unique identifier of a channel. | Required |
| user_id | User unique identifier. | Required |
Context Output
There is no context output for this command.
Command example
!zoom-delete-channel user_id=uJiZN-O7Rp6Jp_995FpZGg channel_id=88f2f20f4abd415cba2429538be58025
Human Readable Output
Channel 88f2f20f4abd415cba2429538be58025 was deleted successfully
zoom-update-channel
Update an existing channel in a Zoom account.
Base Command
zoom-update-channel
Input
| Argument Name | Description | Required |
|---|---|---|
| channel_id | The channel ID. | Required |
| user_id | User unique identifier. | Required |
| add_member_permissions | Who can add new channel members: 1 - All channel members can add new members. 2 - Only channel owner and administrators can add new members. Note: This setting can only be modified by the channel owner. | Optional |
| posting_permissions | The channel members’ posting permissions: 1 — All chat channel members can post to the channel. 2 — Only the channel owner and administrators can post to the channel. * 3 — Only the channel owner, administrators and certain members can post to the channel. | Optional |
| new_members_can_see_prev_msgs | Whether new channel members can view messages and files previously posted in the channel. Possible values are: true, false. Default is True. | Optional |
| channel_name | A new name for the channel. | Optional |
Context Output
There is no context output for this command.
Command example
!zoom-update-channel user_id=uJiZN-O7Rp6Jp_995FpZGg channel_name=Channel-Updated5 channel_id=88f2f20f4abd415cba2429538be58025
Human Readable Output
Channel 88f2f20f4abd415cba2429538be58025 was updated successfully
zoom-invite-to-channel
Invites members that are in a user’s contact list to a channel. A channel can have one or multiple members. The maximum number of members that can be added at once with this API is 5.
Base Command
zoom-invite-to-channel
Input
| Argument Name | Description | Required |
|---|---|---|
| channel_id | The channel’s unique identifier. | Required |
| user_id | User unique identifier. | Required |
| members | The member’s email address. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Zoom.Channel.ids | string | A comma-separated list of user IDs of the members added to the channel. User IDs of those who are not from the same account will be omitted from the list. |
| Zoom.Channel.added_at | string | The date and time when the members are added to the channel. |
| Zoom.Channel.member_ids | string | A comma-separated list of member IDs of the members added to the channel. |
Command example
!zoom-invite-to-channel user_id=uJiZN-O7Rp6Jp_995FpZGg members=example@example.com channel_id=88f2f20f4abd415cba2429538be58025
Context Example
{
"Zoom": {
"Channel": {
"added_at": "2023-05-22T08:36:57Z",
"ids": "pq0t1uvlrqmi_p_-cg2ugq",
"member_ids": "1vqMC-BQdfok1tpAnSw8OT6FtcMiXBaUPNoRhTIzcvJjtbxgPXQRLmDm-10C2Ggg"
}
}
}
Human Readable Output
Channel details
Added at date and time Channel ID User id 2023-05-22T08:36:57Z 88f2f20f4abd415cba2429538be58025 pq0t1uvlrqmi_p_-cg2ugq
zoom-remove-from-channel
Removes a member from a chat channel. A channel can have one or more members.
Base Command
zoom-remove-from-channel
Input
| Argument Name | Description | Required |
|---|---|---|
| channel_id | The unique identifier of the channel from where you would like to remove a member. | Required |
| user_id | User unique identifier. | Required |
| member_id | The email address or user ID or member ID of the member to remove from the channel. | Required |
Context Output
There is no context output for this command.
Command example
!zoom-remove-from-channel user_id=uJiZN-O7Rp6Jp_995FpZGg member_id=example@example.com channel_id=88f2f20f4abd415cba2429538be58025
Human Readable Output
Member example@example.com was successfully removed from channel 88f2f20f4abd415cba2429538be58025
zoom-send-file
Sends a file on Zoom to either an individual user in your contact list or a channel of which you are a member.
Base Command
zoom-send-file
Input
| Argument Name | Description | Required |
|---|---|---|
| to_channel | The channel ID of the channel to which to send the chat file. | Optional |
| user_id | The user’s ID. | Required |
| to_contact | The user ID or member ID of the contact to send the chat file. | Optional |
| entry_id | Cortex XSOAR internal file ID. | Required |
Context Output
There is no context output for this command.
zoom-send-message
Sends chat messages on Zoom to either an individual user who is in your contact list or to a channel of which you are a member.
Base Command
zoom-send-message
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | Unique identifier of the user. | Required |
| at_contact | Email address of the mention contact. | Optional |
| is_markdown | if a markdown message provide in the message argument | Optional |
| message | The message to be sent. Maximum of 1024 characters. | Required |
| entry_ids | A list of the file IDs to send. This field only accepts a maximum of six file IDs. | Optional |
| reply_main_message_id | The reply message’s ID. This field only returns if the message is a reply message. | Optional |
| to_channel | The channel ID of the channel to send a message. | Optional |
| to_contact | The email address or user ID or member ID of the person to send a message. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Zoom.ChatMessage.id | number | Unique identifier of the message. |
Command example
!zoom-send-message user_id=uJiZN-O7Rp6Jp_995FpZGg message="message" to_contact=example@example.com
Context Example
{
"Zoom": {
"ChatMessage": {
"id": "7df8cc5b-efc4-4e08-9083-9823b8871c82"
}
}
}
Human Readable Output
Message
Contact Message ID example@example.com 7df8cc5b-efc4-4e08-9083-9823b8871c82
zoom-delete-message
Deletes a chat message previously sent to a contact or a channel.
Base Command
zoom-delete-message
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | Unique identifier of the user. | Required |
| message_id | Unique identifier of the message. | Required |
| to_channel | The channel ID where the message was sent. You must provide this parameter or the to_contact parameter. | Optional |
| to_contact | The member ID or user ID or email address of a chat contact to whom the message was sent. You must provide this parameter or the to_channel parameter. | Optional |
Context Output
There is no context output for this command.
Command example
!zoom-delete-message user_id=uJiZN-O7Rp6Jp_995FpZGg to_contact=example@example.com message_id=4a59df4a-9668-46bd-bff2-3e1f3462ecc3
Human Readable Output
Message 4a59df4a-9668-46bd-bff2-3e1f3462ecc3 was deleted successfully
zoom-update-message
Edits a chat message that you previously sent to either a contact or a channel in Zoom by providing the ID of the message as the value of the messageId parameter. As a query parameter, you must provide either the contact’s email address or the Channel ID of the channel where the message was sent.
Base Command
zoom-update-message
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | Unique identifier of the user. | Required |
| message_id | Unique identifier of the message. | Required |
| message | The edited message. | Required |
| to_channel | The channel ID of the channel where you sent the message. You must provide either to_contact or to_channel parameter in the API request. Channel ID can be retrieved from List User’s Channels API. | Optional |
| to_contact | The email address or user ID or member ID of the contact to whom the message was sent. You must provide either this parameter or the to_channel parameter in the API request. | Optional |
| entry_ids | A list of Cortex XSOAR file entry IDs to send. | Optional |
Context Output
There is no context output for this command.
Command example
!zoom-update-message user_id=uJiZN-O7Rp6Jp_995FpZGg message="message2" to_contact=example@example.com message_id=4a59df4a-9668-46bd-bff2-3e1f3462ecc3
Human Readable Output
Message 4a59df4a-9668-46bd-bff2-3e1f3462ecc3 was successfully updated
zoom-list-messages
Searches chat messages or shared files between a user and an individual contact or a chat channel.
Base Command
zoom-list-messages
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | Unique identifier of the user. | Required |
| to_contact | This field allows you to query by the email address or user ID or member ID of a chat contact with whom the user communicated. The API only returns messages sent and/or received between the user and the queried contact. | Optional |
| to_channel | This field queries by the channel ID of a channel in which the user had chat conversations. The API only returns messages sent and/or received by the user in the queried channel. | Optional |
| date | The query date from which to retrieve the chat messages. This value defaults to the current date. Format %Y-%m-%dT%H:%M:%S.. | Optional |
| from | Start date. Format %Y-%m-%dT%H:%M:%S. | Optional |
| to | End date. Format %Y-%m-%dT%H:%M:%S. | Optional |
| include_deleted_and_edited_message | Whether to include deleted and edited messages. Default false. | Optional |
| search_type | The type of search: message — Search messages. file — Search files.. Possible values are: message, file. | Optional |
| search_key | Key for search in messages. | Optional |
| exclude_child_message | Whether to exclude returning all child messages in a chat, leaving only the parent messages. Default: false. | Optional |
| next_page_token | The next page token paginates through a large set of results. A next page token is returned whenever the set of available results exceeds the current page size. This token’s expiration period is 15 minutes. | Optional |
| page_size | Number of records returned from a single API call. Default 50. | Optional |
| page_number | The page of results to return. Default 1. | Optional |
| limit | Total amount of results to show. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Zoom.ChatMessage.date | date | The queried date value. |
| Zoom.ChatMessage.id | string | The channel’s unique identifier. |
| Zoom.Channel.jid | string | The channel JID. |
| Zoom.Channel.name | string | The channel name. |
| Zoom.Channel.type | string | The type of the channel. The value can be one of the following: 1: Private channel. In this type of channel, members must be invited to join a channel. 2: Private channel with members that belong to one Zoom account. Members in this channel should be invited and the members should be from the same organization. 3: Public channel. Anyone can search for this channel and join the channel. 4: Group chat. This is an instant channel which can be created by adding members to a new chat. |
| Zoom.ChatMessage.date | date | The queried date value. |
| Zoom.ChatMessage.from | date-time | The queried from value. (Returned only if the from query parameter is used) |
| Zoom.ChatMessage.messages | array of object | Information about received messages and files. |
| Zoom.ChatMessage.messages.bot_message | object | The chatbot’s message in JSON format. |
| Zoom.ChatMessage.messages.bot_message.date_time | date-time | The date and time when the message was sent. |
| Zoom.ChatMessage.messages.files | array of object | Information about chat files (if multiple files exist). |
| Zoom.ChatMessage.messages.files.download_url | string | The download URL of the chat file. |
| Zoom.ChatMessage.messages.files.file_id | string | The unique ID of the chat file. |
| Zoom.ChatMessage.messages.files.file_name | string | The name and file extension of the chat file. |
| Zoom.ChatMessage.messages.files.file_size | integer | The size of the chat file in bytes. |
| Zoom.ChatMessage.messages.rich_text | array of object | Rich text formatting information. |
| Zoom.ChatMessage.messages.rich_text.start_position | integer | The start position of the rich text in the message string. |
| Zoom.ChatMessage.messages.rich_text.end_position | integer | The end position of the rich text. |
| Zoom.ChatMessage.messages.rich_text.format_type | enum | The type of rich text format. |
| Zoom.ChatMessage.messages.rich_text.format_attr | string | The attribute values of a specific format_type. |
| Zoom.ChatMessage.messages.rich_text.download_url | string | The download URL of the first file. |
| Zoom.ChatMessage.messages.rich_text.file_id | string | The unique ID of the first file. |
| Zoom.ChatMessage.messages.rich_text.file_name | string | The name of the first file. |
| Zoom.ChatMessage.messages.rich_text.file_size | integer | The size of the first file in bytes. |
| Zoom.ChatMessage.messages.id | string | Unique identifier of the message. |
| Zoom.ChatMessage.messages.message | string | The message content. |
| Zoom.ChatMessage.messages.reply_main_message_id | string | The unique identifier of a reply message. (Returned only for reply messages) |
| Zoom.ChatMessage.messages.reply_main_message_timestamp | integer | The timestamp of when the reply message was sent. (Returned only for reply messages) |
| Zoom.ChatMessage.messages.sender | string | The email address of the message sender. Empty if the sender does not belong to the same account as the current user or is not a contact. |
| Zoom.ChatMessage.messages.sender_member_id | string | The member ID of the message sender. |
| Zoom.ChatMessage.messages.sender_display_name | string | The display name of the message sender. |
| Zoom.ChatMessage.messages.status | enum | Indicates the status of the message. Allowed values Deleted, Edited, Normal. (Returned only when include_deleted_and_edited_message query parameter is set to true) |
| Zoom.ChatMessage.messages.timestamp | integer | The timestamp of the message in microseconds. |
| Zoom.ChatMessage.at_items | array of object | Information about mentions. |
| Zoom.ChatMessage.at_items.at_contact | string | The user ID of the mentioned contact. Empty if the contact does not belong to the same account as the current user. |
| Zoom.ChatMessage.at_items.at_contact_member_id | string | The member ID of the mentioned contact. |
| Zoom.ChatMessage.at_items.at_type | integer | The type of mention. Allowed values 1 (Contact), 2 (Channel). |
| Zoom.ChatMessage.at_items.end_position | integer | The end position of the mention. |
| Zoom.ChatMessage.at_items.start_position | integer | The start position of the mention. |
| Zoom.ChatMessageNextToken.user_id | string | The user ID associated with the next page token. |
| Zoom.ChatMessageNextToken.to_channel | string | The channel ID associated with the next page token. |
| Zoom.ChatMessageNextToken.include_deleted_and_edited_message | boolean | Indicates whether deleted and edited messages are included in the next page token request. |
| Zoom.ChatMessageNextToken.search_type | string | The search type associated with the next page token. |
| Zoom.ChatMessageNextToken.to | string | The recipient associated with the next page token. |
| Zoom.ChatMessageNextToken.date | string | The date associated with the next page token. |
| Zoom.ChatMessageNextToken.page_size | integer | The page size associated with the next page token. |
| Zoom.ChatMessageNextToken.next_page_token | string | The next page token for pagination. |
| Zoom.ChatMessageNextToken.from | string | The sender associated with the next page token. |
| Zoom.ChatMessageNextToken.search_key | string | The search key associated with the next page token. |
| Zoom.ChatMessageNextToken.exclude_child_message | boolean | Indicates whether child messages are excluded in the next page token request. |
| Zoom.ChatMessageNextToken.to_contact | string | The contact ID associated with the next page token. |
Command example
!zoom-list-messages user_id=uJiZN-O7Rp6Jp_995FpZGg to_contact=example@example.com date=today
Context Example
{
"Zoom": {
"ChatMessage": {
"ChatMessage": [
{
"date_time": "2023-05-22T08:24:14Z",
"id": "a62636c8-b6c1-4135-9352-88ac61eafc31",
"message": "message",
"send_member_id": "1vrdvnC1gHuxZtpVLR7Oz1_A1dcCrBf0PNoRhTIzcvJjtbxgPXQRLmDm-10C2Ggg",
"sender": "example@example.com",
"sender_display_name": "admin zoom",
"timestamp": 1684743854543
},
{
"date_time": "2023-05-22T08:20:22Z",
"id": "4a59df4a-9668-46bd-bff2-3e1f3462ecc3",
"message": "my message",
"send_member_id": "1vrdvnC1gHuxZtpVLR7Oz1_A1dcCrBf0PNoRhTIzcvJjtbxgPXQRLmDm-10C2Ggg",
"sender": "example@example.com",
"sender_display_name": "admin zoom",
"timestamp": 1684743622899
}
],
"ChatMessageNextToken": ""
}
}
}
Human Readable Output
Messages
Date Time From Message Id Message sender Message text Sender display name To User id 2023-05-22T08:24:14Z None a62636c8-b6c1-4135-9352-88ac61eafc31 example@example.com message admin zoom None uJiZN-O7Rp6Jp_995FpZGg 2023-05-22T08:20:22Z None 4a59df4a-9668-46bd-bff2-3e1f3462ecc3 example@example.com my message admin zoom None uJiZN-O7Rp6Jp_995FpZGg
zoom-delete-user-token
Revoke a user’s Zoom SSO session.
To list all available users use the zoom-list-users command.
Base Command
zoom-delete-user-token
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | The user ID or email to be revoked. | Required |
Command example
!zoom-delete-user-token user_id=useremail@myself.com
Context Output
There is no context output for this command.
Human Readable Output
Message
User SSO token for user useremail@myself.com is deleted
send-notification
Sends messages from your Marketplace Chatbot app on Zoom to either an individual user or to a channel.
Base Command
send-notification
Input
| Argument Name | Description | Required |
|---|---|---|
| to | The email address or user ID or member ID of the person to send a message. | Required |
| channel_id | The channel ID of the channel to send a message. | Optional |
| message | The message to be sent. Maximum of 1024 characters. | Required |
| visible_to_user | The UserID that allows a Chatbot to send a message to a group channel when it has only one designated person in that group channel to see the message. | Optional |
Context Output
There is no context output for this command.
Command example
!send-notification message=hi to=example@example.com
Context Output
There is no context output for this command.
Human Readable Output
Message
Message sent to Zoom successfully. Message ID is: 20230815153245201_BPK3S3S_aw1
mirror-investigation
Mirrors the investigation between Zoom and the Cortex XSOAR War Room.
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. Can be “true” or “false”. Default is “true”. | Optional |
| direction | The mirroring direction. Can be “FromDemisto”, “ToDemisto”, or “Both”. Default value is “Both”. | Optional |
| channelName | The name of the channel. The default is “incident-<incidentID>”. | Optional |
Context Output
There is no context output for this command.
Command Example
!mirror-investigation direction="FromDemisto" channelName="example"
Human Readable Output
Investigation mirrored successfully, channel:example
close-channel
Delete a mirrored Zoom channel.
Base Command
close-channel
Input
| Argument Name | Description | Required |
|---|---|---|
| channel | The name of the channel to delete. If not provided, the mirrored investigation channel is deleted (if the channel exists). | Optional |
| channel_id | The ID of the channel to delete. If not provided, the mirrored investigation channel is deleted (if the channel exists). | Optional |
Context Output
There is no context output for this command.
Command Example
!close-channel channel=new-zoom-channel
Human Readable Output
Channel successfully deleted.
Configuration parameters
url— Server URL (e.g. 'https://api.zoom.us/v2/') (required)account_id— Account ID (OAuth) (required)credentials— Client ID (OAuth) (required)botJID— Bot JIDbot_credentials— Bot Client ID ((Team Chat App))secret_token—verification_token—longRunning— Long running instancelongRunningPort— Listen Portmirroring— Enable Incident Mirroringproxy— Use system proxy settingsinsecure— Trust any certificate (not secure)certificate— Certificate (Required for HTTPS)key— Private Key (Required for HTTPS)
Commands (23)
-
close-channelDelete a mirrored Zoom channel.
-
mirror-investigationMirrors the investigation between Zoom and the Cortex XSOAR War Room.
-
send-notificationSend a message using a chatbot app.
-
zoom-create-channelCreates a channel for a user. Use the existing command zoom-list-users to get the user ID by email (only if the user has an email address as part of the user parameters).
-
zoom-create-meetingCreate a new Zoom meeting (scheduled, instant, or recurring).
-
zoom-create-userCreate a new user in zoom account.
-
zoom-delete-channelDeletes a specific Zoom channel. Use the command zoom-list-account-public-channels in order to fetch the channel ID by the channel name.
-
zoom-delete-messageDeletes a chat message previously sent to a contact or a channel. Use the command zoom-list-user-channels to get the channel ID by the channel name (for to_channel). See the existing command zoom-list-users to get the user ID by email (for to_contact). In order to update messages (or a single message using messageId), you need to provide to_contact (contact to send) or to_channel (channel to send to), but not both of them.
-
zoom-delete-userDelete a user from Zoom.
-
zoom-delete-user-tokenRevoke a user's Zoom SSO session. Use the zoom-list-users command to get the user ID or user email.
-
zoom-fetch-recordingGet the meeting record and save it as a file in the War Room.
-
zoom-invite-to-channelInvites members that are in a user's contact list to a channel. A channel can have one or more members. The maximum number of members that can be added at once with this API is 5. Use the command zoom-list-account-public-channels in order to fetch the channel ID by the channel name.
-
zoom-list-account-public-channelsList the existing channels You can only retrieve the channel information for the authorized user. In order to get a public channel by its name (for example Admins_to_delete_4) you need to run the command zoom-list-account-public-channels without the channel_id param and find which channel_id fits the channel name.
-
zoom-list-messagesSearches chat messages or shared files between a user and an individual contact or a chat channel.
-
zoom-list-user-channelsList the existing channels. In order get the user ID by user email - use the existing command zoom-list-users. In order to get the user channel by its name, run the command zoom-list-user-channels without the channel_id param and find which channel_id fits the channel name.
-
zoom-list-usersList the existing users.
-
zoom-meeting-getGet the information of an existing Zoom meeting.
-
zoom-meeting-listShow all the meetings of a given user. Note: only scheduled and unexpired meetings will appear.
-
zoom-remove-from-channelRemoves a member from a chat channel. A channel can have one or more members. Use the command zoom-list-account-public-channels in order to fetch the channel ID by the channel name. In order get the user ID by the user email - use the existing command zoom-list-users.
-
zoom-send-fileSends a file on Zoom to either an individual user in your contact list or a channel of which you are a member. Use existing command zoom-list-users to get the user ID by the user email (for to_contact). Use the command zoom-list-user-channels in order to fetch the channel ID by the channel name (for to_channel).
-
zoom-send-messageSends chat messages on Zoom to either an individual user who is in your contact list or to a channel of which you are a member. Use the command zoom-list-users to get the user id by the user email (for to_contact). Use the command zoom-list-user-channels in order to fetch the channel ID by the channel name (for to_channel). In order to send a message, you need to provide at least a message (message text) and to_contact (contact to send) or to_channel (channel to send to), but not both of them. In order to send a style message you need to provide is-markdown=true and the message can contain a markdown format. If you are using mention markdown in a message, provide the email address in the at_contact argument.
-
zoom-update-channelUpdate an existing channel in a Zoom account. Use the command zoom-list-account-public-channels in order to fetch the channel ID by the channel name. "Need to provide at least one of the following arguments: posting_permissions or channnel_name.".
-
zoom-update-messageEdits a chat message that you previously sent to either a contact or a channel in Zoom by providing the ID of the message as the value of the messageId parameter. As a query parameter, you must provide either the contact's email address of the contact or the Channel ID of the channel where the message was sent. Use the command zoom-list-user-channels to get the channel ID by the channel name (for to_channel). Use the existing command zoom-list-users to get the user ID by email (for to_contact). In order to update messages (or a single message using messageId), you need to provide to_contact (contact to send) or to_channel (channel to send to), but not both of them.
import hashlib import hmac import shutil from copy import copy from datetime import datetime from secrets import compare_digest from tempfile import NamedTemporaryFile from traceback import format_exc import demistomock as demisto # noqa: F401 import uvicorn from CommonServerPython import * # noqa: F401 from fastapi import Depends, FastAPI, Request, Response, status from fastapi.responses import JSONResponse from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.security.api_key import APIKey, APIKeyHeader from fastapi_utils.tasks import repeat_every from uvicorn.logging import AccessFormatter from ZoomApiModule import * app = FastAPI() basic_auth = HTTPBasic(auto_error=False) token_auth = APIKeyHeader(auto_error=False, name="Authorization") SYNC_CONTEXT = True OBJECTS_TO_KEYS = { "messages": "entitlement", } DATE_FORMAT = "%Y-%m-%d %H:%M:%S" PLAYGROUND_INVESTIGATION_TYPE = 9 # Note#1: type "Pro" is the old version, and "Licensed" is the new one, and i want to support both. # Note#2: type "Corporate" is officially not supported any more, but i did not remove it just in case it still works. USER_TYPE_MAPPING = {"Basic": 1, "Pro": 2, "Licensed": 2, "Corporate": 3} MONTHLY_RECURRING_TYPE_MAPPING = {"Daily": 1, "Weekly": 2, "Monthly": 3} INSTANT = "Instant" SCHEDULED = "Scheduled" RECURRING_WITH_TIME = "Recurring meeting with fixed time" MEETING_TYPE_NUM_MAPPING = {"Instant": 1, "Scheduled": 2, "Recurring meeting with fixed time": 8} FILE_TYPE_MAPPING = {"MP4": "Video", "M4A": "Audio"} POSTING_PERMISSIONS_MAPPING = { "All members can post": 1, "Only the owner and admins can post": 2, "Only the owner, admins and certain members can post": 3, } MEMBER_PERMISSIONS_MAPPING = {"All channel members can add": 1, "Only channel owner and admins can add": 2} CHANNEL_TYPE_MAPPING = { "Private channel": 1, "Private channel with members that belong to one account": 2, "Public channel": 3, "New chat": 4, } AT_TYPE = {"Mention a contact": 1, 'Mention "all" to notify everyone in the channel.': 2} WRONG_TIME_FORMAT = "Wrong time format. Use this format: 'yyyy-MM-ddTHH:mm:ssZ' or 'yyyy-MM-ddTHH:mm:ss' " LIMIT_AND_EXTRA_ARGUMENTS = """Too many arguments. If you choose a limit, don't enter a user_id or page_size or next_page_token or page_number.""" LIMIT_AND_EXTRA_ARGUMENTS_MEETING_LIST = """Too many arguments. If you choose a limit, don't enter a page_size or next_page_token or page_number.""" INSTANT_AND_TIME = "Too many arguments.Use start_time and timezone for scheduled meetings only." JBH_TIME_AND_NO_JBH = """Collision arguments. join_before_host_time argument can be used only if join_before_host is 'True'.""" WAITING_ROOM_AND_JBH = "Collision arguments. join_before_host argument can be used only if waiting_room is 'False'." END_TIMES_AND_END_DATE_TIME = "Collision arguments. Choose only one of these two arguments, end_time or end_date_time." NOT_RECURRING_WITH_RECURRING_ARGUMENTS = """One or more arguments that were filed are used for a recurring meeting with a fixed time only.""" NOT_MONTHLY_AND_MONTHLY_ARGUMENTS = """One or more arguments that were filed are for a recurring meeting with a fixed time and monthly recurrence_type only.""" MONTHLY_RECURRING_MISIING_ARGUMENTS = """Missing arguments. A recurring meeting with a fixed time and monthly recurrence_type must have the following arguments: monthly_week and monthly_week_day.""" NOT_WEEKLY_WITH_WEEKLY_ARGUMENTS = "Weekly_days is for weekly recurrence_type only." EXTRA_PARAMS = """Too many fields were filled. You should fill the Account ID, Client ID, and Client Secret fields (OAuth), OR the API Key and API Secret fields (JWT - Deprecated).""" RECURRING_MISSING_ARGUMENTS = """Missing arguments. A recurring meeting with a fixed time is missing this argument: recurrence_type.""" MISSING_ARGUMENT = """Missing either a contact info or a channel id""" USER_NOT_FOUND = """ This user email can't be found """ MARKDOWN_AND_EXTRA_ARGUMENTS = """Too many arguments. If you choose is_markdown, don't provide one of the following arguments: start_position, end_position, format_type, at_type, rt_start_position, rt_end_position or format_attr""" MARKDOWN_EXTRA_FORMATS = """Too many style in text. you can provide only one style type""" MARKDOWN_EXTRA_MENTIONS = """Too many mentions in text. you can provide only one mention in each message""" WRONG_CHANNEL = """Couldn't find channel id base on provided channel_name. channel_name can use only for mirrored channel. Otherwise, please use the channel ID instead.""" MISSING_ARGUMENT_JID = """Missing argument: You must provide either a user ID or a channel ID. If you're using a mirrored channel, you have the option to specify the channel name as well.""" TOO_MANY_JID = """Too many argument you must provide either a user JID or a channel id and not both """ BOT_PARAM_CHECK = """If you're using the Zoom chatbot, it's essential to provide all the necessary bot parameters, including the botJID, bot_client_id, and bot_client_secret. you can find these values in the Zoom Chatbot app configuration""" OAUTH_PARAM_CHECK = """if you are using zoom APIs you must provide Oauth client_id and client_secret you can find this values in the Zoom Oauth Server-To-Server app configuration""" CLIENT: Zoom_Client SECRET_TOKEN: str MESSAGE_FOOTER = "\n**From Zoom**" MIRRORING_ENABLED: bool = False LONG_RUNNING: bool = False MIRROR_TYPE = "mirrorEntry" CACHED_INTEGRATION_CONTEXT: dict CACHE_EXPIRY: float """CLIENT CLASS""" class UserAgentFormatter(AccessFormatter): """This formatter extracts and includes the 'User-Agent' header information in the log messages.""" def get_user_agent(self, scope: Dict) -> str: headers = scope.get("headers", []) user_agent_header = list(filter(lambda header: header[0].decode().lower() == "user-agent", headers)) user_agent = "" if len(user_agent_header) == 1: user_agent = user_agent_header[0][1].decode() return user_agent def format_message(self, record): """Include the 'User-Agent' header information in the log message. Args: record: The log record to be formatted. Returns: str: The formatted log message.""" record_copy = copy(record) scope = record_copy.__dict__["scope"] user_agent = self.get_user_agent(scope) record_copy.__dict__.update({"user_agent": user_agent}) return super().formatMessage(record_copy) class Client(Zoom_Client): """A client class that implements logic to authenticate with Zoom application.""" def zoom_create_user(self, user_type_num: int, email: str, first_name: str, last_name: str): return self.error_handled_http_request( method="POST", url_suffix="users", headers={"authorization": f"Bearer {self.access_token}"}, json_data={ "action": "create", "user_info": {"email": email, "type": user_type_num, "first_name": first_name, "last_name": last_name}, }, ) def zoom_list_users( self, page_size: int, status: str = "active", next_page_token: str = None, role_id: str = None, url_suffix: str = None, page_number: int = None, ): return self.error_handled_http_request( method="GET", url_suffix=url_suffix, headers={"authorization": f"Bearer {self.access_token}"}, params={ "status": status, "page_size": page_size, "page_number": page_number, "next_page_token": next_page_token, "role_id": role_id, }, ) def zoom_delete_user(self, user: str, action: str): return self.error_handled_http_request( method="DELETE", url_suffix="users/" + user, headers={"authorization": f"Bearer {self.access_token}"}, json_data={"action": action}, resp_type="response", return_empty_response=True, ) def zoom_create_meeting(self, url_suffix: str, json_data: dict): return self.error_handled_http_request( method="POST", url_suffix=url_suffix, headers={"authorization": f"Bearer {self.access_token}"}, json_data=json_data ) def zoom_meeting_get(self, meeting_id: str, occurrence_id: str | None = None, show_previous_occurrences: bool | str = False): return self.error_handled_http_request( method="GET", url_suffix=f"/meetings/{meeting_id}", headers={"authorization": f"Bearer {self.access_token}"}, params={"occurrence_id": occurrence_id, "show_previous_occurrences": show_previous_occurrences}, ) def zoom_meeting_list( self, user_id: str, next_page_token: str | None = None, page_size: int | str = 30, type: str | int | None = None, page_number: int = None, ): return self.error_handled_http_request( method="GET", url_suffix=f"users/{user_id}/meetings", headers={"authorization": f"Bearer {self.access_token}"}, params={"type": type, "next_page_token": next_page_token, "page_size": page_size, "page_number": page_number}, ) def zoom_fetch_recording( self, method: str, url_suffix: str = "", full_url: str = "", stream: bool = False, resp_type: str = "json" ): return self.error_handled_http_request( method=method, full_url=full_url, url_suffix=url_suffix, resp_type=resp_type, stream=stream, headers={"authorization": f"Bearer {self.access_token}"}, ) def zoom_list_channels(self, page_size: int, next_page_token: str = None, url_suffix: str = "", page_number: int = 1): return self.error_handled_http_request( method="GET", url_suffix=f"chat/{url_suffix}", headers={"authorization": f"Bearer {self.access_token}"}, params={"page_size": page_size, "page_number": page_number, "next_page_token": next_page_token}, ) def zoom_create_channel(self, url_suffix: str, json_data: dict): return self.error_handled_http_request( method="POST", url_suffix=url_suffix, headers={"authorization": f"Bearer {self.access_token}"}, json_data=json_data ) def zoom_update_channel(self, url_suffix: str, json_data: dict): return self.error_handled_http_request( method="PATCH", url_suffix=url_suffix, headers={"authorization": f"Bearer {self.access_token}"}, json_data=json_data, resp_type="response", return_empty_response=True, ) def zoom_list_user_channels( self, user_id: str, page_size: int, next_page_token: str = None, url_suffix: str = "", page_number: int = None ): return self.error_handled_http_request( method="GET", url_suffix=f"chat/{url_suffix}", headers={"authorization": f"Bearer {self.access_token}"}, params={"user_id": user_id, "page_size": page_size, "page_number": page_number, "next_page_token": next_page_token}, ) def zoom_delete_channel(self, url_suffix: str): return self.error_handled_http_request( method="DELETE", url_suffix=url_suffix, headers={"authorization": f"Bearer {self.access_token}"}, resp_type="response", return_empty_response=True, ) def zoom_invite_to_channel(self, members_json: str, url: str = None): return self.error_handled_http_request( method="POST", url_suffix=url, headers={"authorization": f"Bearer {self.access_token}"}, json_data=members_json ) def zoom_remove_from_channel(self, url_suffix: str = None): return self.error_handled_http_request( method="DELETE", url_suffix=url_suffix, headers={"authorization": f"Bearer {self.access_token}"}, resp_type="response", return_empty_response=True, ) def zoom_send_file(self, url_suffix: str, file_info: dict, json_data: dict): files = {"file": (file_info["name"], open(file_info["path"], "rb"))} response = self.error_handled_http_request( method="POST", full_url=url_suffix, headers={"Authorization": f"Bearer {self.access_token}"}, files=files, data=json_data, ) return response def zoom_upload_file(self, url_suffix: str, file_info: dict): files = {"file": (file_info["name"], open(file_info["path"], "rb"))} return self._http_request( "POST", headers={"Authorization": f"Bearer {self.access_token}"}, files=files, full_url=url_suffix ) def zoom_send_message(self, url_suffix: str, json_data: dict): return self.error_handled_http_request( method="POST", url_suffix=url_suffix, json_data=json_data, headers={"authorization": f"Bearer {self.access_token}"} ) def zoom_delete_message(self, url_suffix: str = None): return self.error_handled_http_request( method="DELETE", url_suffix=url_suffix, headers={"authorization": f"Bearer {self.access_token}"}, resp_type="response", return_empty_response=True, ) def zoom_update_message(self, url_suffix: str, json_data: dict): return self.error_handled_http_request( method="PUT", url_suffix=url_suffix, json_data=json_data, headers={"authorization": f"Bearer {self.access_token}"}, return_empty_response=True, ) def zoom_list_user_messages( self, user_id: str, date_arg: datetime, from_arg: datetime, to_arg: datetime, page_size: int, next_page_token: str = None, url_suffix: str = None, page_number: int = None, to_contact: str = None, to_channel: str = None, include_deleted_and_edited_message: bool = False, search_type: str = None, search_key: str = None, exclude_child_message: bool = False, ): return self.error_handled_http_request( method="GET", url_suffix=f"chat/{url_suffix}", headers={"authorization": f"Bearer {self.access_token}"}, params={ "user_id": user_id, "page_size": page_size, "page_number": page_number, "next_page_token": next_page_token, "to_contact": to_contact, "to_channel": to_channel, "date": date_arg, "from": from_arg, "to": to_arg, "include_deleted_and_edited_message": include_deleted_and_edited_message, "search_key": search_key, "search_type": search_type, "exclude_child_message": exclude_child_message, }, ) def zoom_send_notification(self, url_suffix: str, json_data: dict): return self.error_handled_http_request( method="POST", url_suffix=url_suffix, json_data=json_data, headers={"authorization": f"Bearer {self.bot_access_token}"}, ) def zoom_get_admin_user_id_from_token(self): return self.error_handled_http_request( method="get", url_suffix="users/me", headers={"authorization": f"Bearer {self.access_token}"} ) def zoom_delete_user_token(self, url_suffix: str): return self.error_handled_http_request( method="DELETE", url_suffix=url_suffix, resp_type="response", headers={"authorization": f"Bearer {self.access_token}"} ) """HELPER FUNCTIONS""" 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() async def check_and_handle_entitlement(text: str, message_id: str, user_name: str, user_email: str) -> str: """ 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 or not message_id: return reply messages = json.loads(messages) message_filter = list(filter(lambda q: q.get("message_id") == message_id, messages)) if message_filter: message = message_filter[0] entitlement = message.get("entitlement") reply = message.get("reply", f"Thank you {user_name} for your response {text}.") guid, incident_id, task_id = extract_entitlement(entitlement) demisto.handleEntitlementForUser(incident_id, guid, user_email, text, task_id) message["remove"] = True set_to_integration_context_with_retries({"messages": messages}, OBJECTS_TO_KEYS, SYNC_CONTEXT) return reply def save_entitlement(entitlement, message_id, reply, expiry, default_response, to_jid): """ 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_jid: the user jid the message was sent to """ integration_context = get_integration_context(SYNC_CONTEXT) messages = integration_context.get("messages", []) if messages: messages = json.loads(integration_context["messages"]) messages.append( { "message_id": message_id, "entitlement": entitlement, "reply": reply, "expiry": expiry, "sent": datetime.strftime(datetime.utcnow(), DATE_FORMAT), "default_response": default_response, "to_jid": to_jid, } ) set_to_integration_context_with_retries({"messages": messages}, OBJECTS_TO_KEYS, SYNC_CONTEXT) def extract_entitlement(entitlement: str) -> tuple[str, str, str]: """ Extracts entitlement components from an entitlement string Args: entitlement: The entitlement itself text: The actual reply text Returns: Entitlement components """ parts = entitlement.split("@") 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 @app.on_event("startup") @repeat_every(seconds=60, wait_first=True) async def check_for_unanswered_messages(): # demisto.debug('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"message expired: {message}") _ = await answer_question(message.get("default_response"), message, email="") updated_messages.append(message) continue updated_messages.append(message) if updated_messages: set_to_integration_context_with_retries({"messages": messages}, OBJECTS_TO_KEYS, SYNC_CONTEXT) def run_long_running(port: int, is_test: bool = False): while True: certificate = demisto.params().get("certificate", "") private_key = demisto.params().get("key", "") certificate_path = "" private_key_path = "" try: ssl_args = {} if certificate and private_key: certificate_file = NamedTemporaryFile(delete=False) certificate_path = certificate_file.name certificate_file.write(bytes(certificate, "utf-8")) certificate_file.close() ssl_args["ssl_certfile"] = certificate_path private_key_file = NamedTemporaryFile(delete=False) private_key_path = private_key_file.name private_key_file.write(bytes(private_key, "utf-8")) private_key_file.close() ssl_args["ssl_keyfile"] = private_key_path demisto.debug("Starting HTTPS Server") else: demisto.debug("Starting HTTP Server") integration_logger = IntegrationLogger() integration_logger.buffering = False log_config = dict(uvicorn.config.LOGGING_CONFIG) log_config["handlers"]["default"]["stream"] = integration_logger log_config["handlers"]["access"]["stream"] = integration_logger log_config["formatters"]["access"] = { "()": UserAgentFormatter, "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s "%(user_agent)s"', } uvicorn.run(app, host="0.0.0.0", port=port, log_config=log_config, **ssl_args) # type: ignore[arg-type] except Exception as e: demisto.error(f"An error occurred in the long running loop: {e!s} - {format_exc()}") demisto.updateModuleHealth(f"An error occurred: {e!s}") finally: if certificate_path: os.unlink(certificate_path) if private_key_path: os.unlink(private_key_path) time.sleep(5) def run_log_running(port: int, is_test: bool = False): while True: try: demisto.debug("Starting Server") integration_logger = IntegrationLogger() integration_logger.buffering = False log_config = dict(uvicorn.config.LOGGING_CONFIG) log_config["handlers"]["default"]["stream"] = integration_logger log_config["handlers"]["access"]["stream"] = integration_logger log_config["formatters"]["access"] = { "()": UserAgentFormatter, "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s "%(user_agent)s"', } uvicorn.run(app, host="0.0.0.0", port=port, log_config=log_config) if is_test: time.sleep(5) return "ok" except Exception as e: demisto.error(f"An error occurred in the long running loop: {e!s} - {format_exc()}") demisto.updateModuleHealth(f"An error occurred: {e!s}") finally: time.sleep(5) async def zoom_send_notification_async(client, url_suffix, json_data_all): client.zoom_send_notification(url_suffix, json_data_all) async def process_entitlement_reply( entitlement_reply: str, account_id: str, robot_jid: str, to_jid: str | None = None, user_name: str | None = None, action_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 action_text: str: The text attached to the button, used for string replacement. :param toJid: str: The Jid of where the question exists. :param accountId: str: Zoom account ID :param robotJid: str: Zoom BOT JID :return: None """ if "{user}" in entitlement_reply: entitlement_reply = entitlement_reply.replace("{user}", str(user_name)) if "{response}" in entitlement_reply and action_text: entitlement_reply = entitlement_reply.replace("{response}", str(action_text)) url_suffix = "/im/chat/messages" content_json = { "content": {"body": [{"type": "message", "text": entitlement_reply}]}, "to_jid": to_jid, "robot_jid": robot_jid, "account_id": account_id, } await zoom_send_notification_async(CLIENT, url_suffix, content_json) async def answer_question(text: str, question: dict, email: str = ""): entitlement = question.get("entitlement", "") to_jid = question.get("to_jid") guid, incident_id, task_id = extract_entitlement(entitlement) try: demisto.handleEntitlementForUser(incident_id, guid, email, text, task_id) account_id = CLIENT.account_id bot_jid = CLIENT.bot_jid if account_id and bot_jid: _ = await process_entitlement_reply(text, account_id, bot_jid, to_jid) except Exception as e: demisto.error(f"Failed handling entitlement {entitlement}: {e!s}") question["remove"] = True return incident_id async def handle_text_received_from_zoom(investigation_id: str, text: str, operator_email: str, operator_name: str): """ Handles text received from Zoom 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_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_mirroring(payload): """ handle messages from the Zoom webhook 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 zoom :return: None """ channel_id = payload.get("object", {}).get("channel_id") if not channel_id: 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, SYNC_CONTEXT) message = payload["object"]["message"] demisto.info(f"payload:{payload}") operator_email = payload["operator"] operator_name = zoom_get_user_name_by_email(CLIENT, operator_email) investigation_id = mirror["investigation_id"] await handle_text_received_from_zoom(investigation_id, message, operator_email, operator_name) async def event_url_validation(payload): """Verify the authenticity of a token Args: payload : request from Zoom Returns: json_res: A dictionary containing the 'plainToken' and its corresponding 'encryptedToken' (HMAC-SHA256 signature). """ plaintoken = payload.get("plainToken") hash_object = hmac.new(SECRET_TOKEN.encode("utf-8"), msg=plaintoken.encode("utf-8"), digestmod=hashlib.sha256) expected_signature = hash_object.hexdigest() json_res = {"plainToken": plaintoken, "encryptedToken": expected_signature} return json_res @app.post("/") async def handle_zoom_response( request: Request, credentials: HTTPBasicCredentials = Depends(basic_auth), token: APIKey = Depends(token_auth) ): """handle any response that came from Zoom app Args: request : zoom request Returns: JSONResponse:response to zoom """ request = await request.json() demisto.debug(request) credentials_param = demisto.params().get("credentials") auth_failed = False v_token = demisto.params().get("verification_token", {}).get("password") if not str(token).startswith("Basic") and v_token: if token != v_token: auth_failed = True elif credentials and credentials_param and (username := credentials_param.get("identifier")): password = credentials_param.get("password", "") if not compare_digest(credentials.username, username) or not compare_digest(credentials.password, password): auth_failed = True if auth_failed: demisto.debug("Authorization failed") return Response(status_code=status.HTTP_401_UNAUTHORIZED, content="Authorization failed.") event_type = request["event"] payload = request["payload"] try: if event_type == "endpoint.url_validation" and SECRET_TOKEN: res = await event_url_validation(payload) return JSONResponse(content=res) elif event_type == "interactive_message_actions": if "actionItem" in payload: action = payload["actionItem"]["value"] elif "selectedItems" in payload: action = payload["selectedItems"][0]["value"] else: return Response(status_code=status.HTTP_400_BAD_REQUEST) message_id = payload["messageId"] account_id = payload["accountId"] robot_jid = payload["robotJid"] to_jid = payload["toJid"] user_name = payload["userName"] user_id = payload["userId"] user_email = zoom_get_user_email_by_id(CLIENT, user_id) entitlement_reply = await check_and_handle_entitlement(action, message_id, user_name, user_email) if entitlement_reply: await process_entitlement_reply(entitlement_reply, account_id, robot_jid, to_jid, user_name, action) demisto.updateModuleHealth("") demisto.debug(f"Action {action} was clicked on message id {message_id}") return Response(status_code=status.HTTP_200_OK) elif event_type == "chat_message.sent" and MIRRORING_ENABLED: await handle_mirroring(payload) return Response(status_code=status.HTTP_200_OK) else: return Response(status_code=status.HTTP_400_BAD_REQUEST) except Exception as e: await handle_listen_error(f"An error occurred while handling a response from Zoom: {e}") def test_module(client: Client): """Tests connectivity with the client. Takes as an argument all client arguments to create a new client """ try: if MIRRORING_ENABLED and ( not LONG_RUNNING or not SECRET_TOKEN or not client.bot_client_id or not client.bot_client_secret or not client.bot_jid ): 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 zoom-bot following parameters: secret token, Bot JID, bot client id and secret id""") client.zoom_list_users(page_size=1, url_suffix="users") if client.bot_access_token: json_data = { "robot_jid": client.bot_jid, "to_jid": "foo@conference.xmpp.zoom.us", "account_id": client.account_id, "content": {"head": {"text": "hi"}}, } client.zoom_send_notification(url_suffix="/im/chat/messages", json_data=json_data) except DemistoException as e: error_message = e.message if "Invalid access token" in error_message: error_message = INVALID_CREDENTIALS elif "The Token's Signature resulted invalid" in error_message: error_message = INVALID_API_SECRET elif "Invalid client_id or client_secret" in error_message: error_message = INVALID_ID_OR_SECRET elif "No channel or user can be found with the given to_jid." in error_message: return "ok" elif "Invalid authorization token." in error_message: error_message = INVALID_TOKEN elif "No Chatbot can be found with the given robot_jid value." in error_message: error_message = INVALID_BOT_ID elif "Invalid robot_jid value specified in the Request Body." in error_message: error_message = INVALID_BOT_ID else: error_message = f"Problem reaching Zoom API, check your credentials. Error message: {error_message}" return error_message return "ok" def remove_None_values_from_dict(dict_to_reduce: Dict[str, Any]): """ Removes None values (but not False values) from a given dict and from the nested dicts in it. """ reduced_dict = {} for key, value in dict_to_reduce.items(): if value is not None: if isinstance(value, dict): reduced_nested_dict = remove_None_values_from_dict(value) if reduced_nested_dict: reduced_dict[key] = reduced_nested_dict else: reduced_dict[key] = value return reduced_dict def check_start_time_format(start_time): """checking if the time format is a full time format""" expected_format = "%Y-%m-%dT%H:%M:%S" if start_time.endswith("Z"): expected_format += "%z" try: datetime.strptime(start_time, expected_format) except ValueError as e: raise DemistoException(WRONG_TIME_FORMAT) from e def manual_list_user_pagination(client: Client, next_page_token: str | None, limit: int, status: str, role_id: str | None): res = [] page_size = min(limit, MAX_RECORDS_PER_PAGE) while limit > 0 and next_page_token != "": response = client.zoom_list_users( page_size=page_size, status=status, next_page_token=next_page_token, role_id=role_id, url_suffix="users" ) next_page_token = response.get("next_page_token") res.append(response) limit -= MAX_RECORDS_PER_PAGE return res def manual_list_channel_pagination(client: Client, next_page_token: str | None, limit: int, url_suffix: str): res = {} page_size = min(limit, MAX_RECORDS_PER_PAGE) while limit > 0: response = client.zoom_list_channels(page_size=page_size, next_page_token=next_page_token, url_suffix=url_suffix) next_page_token = response.get("next_page_token") res.update(response) limit -= MAX_RECORDS_PER_PAGE return res def manual_list_user_channel_pagination(client: Client, user_id: str, next_page_token: str | None, limit: int, url_suffix: str): res = {} page_size = min(limit, MAX_RECORDS_PER_PAGE) while limit > 0: response = client.zoom_list_user_channels( user_id=user_id, page_size=page_size, next_page_token=next_page_token, url_suffix=url_suffix ) next_page_token = response.get("next_page_token") res.update(response) limit -= MAX_RECORDS_PER_PAGE return res def manual_meeting_list_pagination(client: Client, user_id: str, next_page_token: str | None, limit: int, type: str | int | None): res = [] page_size = min(limit, MAX_RECORDS_PER_PAGE) while limit > 0 and next_page_token != "": response = client.zoom_meeting_list(user_id=user_id, next_page_token=next_page_token, page_size=page_size, type=type) next_page_token = response.get("next_page_token") res.append(response) # subtract what i already got limit -= MAX_RECORDS_PER_PAGE return res def remove_extra_info_list_users(limit, raw_data): """_summary_ Due to the fact that page_size must be const, Extra information may be provided Args: limit (int): the number of records the user asked for raw_data (dict):the entire response from the pagination function """ all_info = [] for page in raw_data: users_info = page.get("users", []) for user in users_info: all_info.append(user) if len(all_info) >= limit: return all_info return all_info def remove_extra_info_list(name, limit, raw_data): """_summary_ Due to the fact that page_size must be const, Extra information may be provided Args: limit (int): the number of records the user asked for raw_data (dict):the entire response from the pagination function """ all_info = [] channel_info = raw_data.get(name, [raw_data]) for channel in channel_info: all_info.append(channel) if len(all_info) >= limit: return all_info return all_info def remove_extra_info_meeting_list(limit, raw_data): """ Due to the fact that page_size must be const, Extra information may be provided to me, such as: In the case of limit = 301, manual_meeting_list_pagination will return 600 meetings (MAX_RECORDS * 2), The last 299 must be removed. Args: limit (int): the number of records the user asked for raw_data (dict):the entire response from the pagination function """ all_info = [] for page in raw_data: meetings = page.get("meetings") for meeting in meetings: all_info.append(meeting) if len(all_info) >= limit: return all_info return all_info """FORMATTING FUNCTIONS""" def zoom_list_users_command(client, **args) -> CommandResults: # PREPROCESSING client = client page_size = arg_to_number(args.get("page_size", 30)) user_id = args.get("user_id") status = args.get("status", "active") next_page_token = args.get("next_page_token") role_id = args.get("role_id") limit = arg_to_number(args.get("limit")) page_number = arg_to_number(args.get("page_number", 1)) url_suffix = f"users/{user_id}" if user_id else "users" if limit: if "page_size" in args or "page_number" in args or next_page_token or user_id: # arguments collision raise DemistoException(LIMIT_AND_EXTRA_ARGUMENTS) else: # multiple requests are needed raw_data = manual_list_user_pagination( client=client, next_page_token=next_page_token, limit=limit, status=status, role_id=role_id ) minimal_needed_info = remove_extra_info_list_users(limit, raw_data) md = tableToMarkdown( "Users", minimal_needed_info, ["id", "email", "type", "pmi", "verified", "created_at", "status", "role_id"] ) md += "\n" + tableToMarkdown("Metadata", [raw_data][0][0], ["total_records"]) raw_data = raw_data[0] else: # only one request is needed raw_data = client.zoom_list_users( page_size=page_size, status=status, # type: ignore[arg-type] next_page_token=next_page_token, role_id=role_id, url_suffix=url_suffix, page_number=page_number, ) # parsing the data according to the different given arguments if user_id: md = tableToMarkdown( "User", [raw_data], ["id", "email", "type", "pmi", "verified", "created_at", "status", "role_id"] ) else: md = tableToMarkdown( "Users", raw_data.get("users"), ["id", "email", "type", "pmi", "verified", "created_at", "status", "role_id"] ) md += "\n" + tableToMarkdown( "Metadata", [raw_data], ["page_count", "page_number", "page_size", "total_records", "next_page_token"] ) return CommandResults( outputs_prefix="Zoom", readable_output=md, outputs={ "User": raw_data.get("users"), "Metadata": { "Count": raw_data.get("page_count"), "Number": raw_data.get("page_number"), "Size": raw_data.get("page_size"), "Total": raw_data.get("total_records"), }, }, raw_response=raw_data, ) def zoom_create_user_command(client, **args) -> CommandResults: client = client user_type = args.get("user_type", "") email = args.get("email") first_name = args.get("first_name") last_name = args.get("last_name") user_type_num = USER_TYPE_MAPPING.get(user_type) raw_data = client.zoom_create_user(user_type_num, email, first_name, last_name) return CommandResults( outputs_prefix="Zoom.User", readable_output=f"User created successfully with ID: {raw_data.get('id')}", outputs=raw_data, raw_response=raw_data, ) def zoom_delete_user_command(client, **args) -> CommandResults: client = client user = args.get("user") action = args.get("action") client.zoom_delete_user(user, action) return CommandResults( readable_output=f"User {user} was deleted successfully", ) def zoom_create_meeting_command(client, **args) -> CommandResults: client = client user_id = args.get("user") topic = args.get("topic", "") host_video = argToBoolean(args.get("host_video", True)) join_before_host_time = args.get("join_before_host_time") start_time = args.get("start_time") timezone = args.get("timezone", "") type = args.get("type", "Instant") auto_record_meeting = args.get("auto_record_meeting") encryption_type = args.get("encryption_type") join_before_host = argToBoolean(args.get("join_before_host", False)) meeting_authentication = argToBoolean(args.get("meeting_authentication", False)) waiting_room = argToBoolean(args.get("waiting_room", False)) end_date_time = args.get("end_date_time") end_times = arg_to_number(args.get("end_times", 1)) monthly_day = arg_to_number(args.get("monthly_day", 1)) monthly_week = arg_to_number(args.get("monthly_week")) monthly_week_day = arg_to_number(args.get("monthly_week_day")) repeat_interval = arg_to_number(args.get("repeat_interval")) recurrence_type = args.get("recurrence_type", "") weekly_days = arg_to_number(args.get("weekly_days", 1)) num_type: int | None = MEETING_TYPE_NUM_MAPPING.get(type) # argument checking if type == INSTANT and (timezone or start_time): raise DemistoException(INSTANT_AND_TIME) if join_before_host_time and not join_before_host: raise DemistoException(JBH_TIME_AND_NO_JBH) if waiting_room and join_before_host: raise DemistoException(WAITING_ROOM_AND_JBH) if args.get("end_times") and end_date_time: raise DemistoException(END_TIMES_AND_END_DATE_TIME) if type != RECURRING_WITH_TIME and any( ( end_date_time, args.get("end_times"), args.get("monthly_day"), monthly_week, monthly_week_day, repeat_interval, args.get("weekly_days"), ) ): raise DemistoException(NOT_RECURRING_WITH_RECURRING_ARGUMENTS) if ( type == RECURRING_WITH_TIME and recurrence_type != "Monthly" and any((args.get("monthly_day"), monthly_week, monthly_week_day)) ): raise DemistoException(NOT_MONTHLY_AND_MONTHLY_ARGUMENTS) if ( type == RECURRING_WITH_TIME and recurrence_type == "Monthly" and not (monthly_week and monthly_week_day) and not args.get("monthly_day") ): raise DemistoException(MONTHLY_RECURRING_MISIING_ARGUMENTS) if type == RECURRING_WITH_TIME and recurrence_type != "Weekly" and args.get("weekly_days"): raise DemistoException(NOT_WEEKLY_WITH_WEEKLY_ARGUMENTS) if type == RECURRING_WITH_TIME and not recurrence_type: raise DemistoException(RECURRING_MISSING_ARGUMENTS) # converting separately after the argument checking, because 0 as an int is equaled to false join_before_host_time = arg_to_number(join_before_host_time) if start_time: check_start_time_format(start_time) json_all_data: Dict[str, Union[Any, None, int]] = {} # special section for recurring meeting with fixed time if type == RECURRING_WITH_TIME: recurrence_type_num = MONTHLY_RECURRING_TYPE_MAPPING.get(recurrence_type) json_all_data.update( { "recurrence": { "end_date_time": end_date_time, "end_times": end_times, "monthly_day": monthly_day, "monthly_week": monthly_week, "monthly_week_day": monthly_week_day, "repeat_interval": repeat_interval, "type": recurrence_type_num, "weekly_days": weekly_days, } } ) json_all_data.update( { "settings": { "auto_recording": auto_record_meeting, "encryption_type": encryption_type, "host_video": host_video, "jbh_time": join_before_host_time, "join_before_host": join_before_host, "meeting_authentication": meeting_authentication, "waiting_room": waiting_room, }, "start_time": start_time, "timezone": timezone, "type": num_type, "topic": topic, } ) # remove all keys with val of None json_data = remove_None_values_from_dict(json_all_data) url_suffix = f"users/{user_id}/meetings" # call the API raw_data = client.zoom_create_meeting(url_suffix=url_suffix, json_data=json_data) # parsing the response if type == "Recurring meeting with fixed time": raw_data.update({"start_time": raw_data.get("occurrences")[0].get("start_time")}) raw_data.update({"duration": raw_data.get("occurrences")[0].get("duration")}) md = tableToMarkdown( "Meeting details", [raw_data], [ "uuid", "id", "host_id", "host_email", "topic", "type", "status", "start_time", "duration", "timezone", "created_at", "start_url", "join_url", ], ) # removing passwords from the response# safe_raw_data = raw_data for sensitive_info in ["password", "pstn_password", "encrypted_password", "h323_password"]: safe_raw_data.pop(sensitive_info, None) return CommandResults(outputs_prefix="Zoom.Meeting", readable_output=md, outputs=safe_raw_data, raw_response=raw_data) def zoom_fetch_recording_command(client: Client, **args): # preprocessing results = [] meeting_id = args.get("meeting_id") delete_after = argToBoolean(args.get("delete_after")) client = client data = client.zoom_fetch_recording(method="GET", url_suffix=f"meetings/{meeting_id}/recordings") recording_files = data.get("recording_files") # Getting the audio and video files which are contained in every recording. for file in recording_files: download_url = file.get("download_url") try: # download the file demisto.debug(f"Trying to download the files of meeting {meeting_id}") record = client.zoom_fetch_recording(method="GET", full_url=download_url, resp_type="response", stream=True) file_type = file.get("file_type") file_type_as_literal = FILE_TYPE_MAPPING.get(file_type) # save the file filename = f'recording_{meeting_id}_{file.get("id")}.{file_type}' with open(filename, "wb") as f: # Saving the content of the file locally. record.raw.decode_content = True shutil.copyfileobj(record.raw, f) results.append(file_result_existing_file(filename)) results.append( CommandResults(readable_output=f"The {file_type_as_literal} file {filename} was downloaded successfully") ) if delete_after: try: # delete the file from the cloud demisto.debug(f"Trying to delete the file {filename}") client.zoom_fetch_recording( method="DELETE", url_suffix=f'meetings/{meeting_id}/recordings/{file["id"]}', resp_type="response" ) results.append( CommandResults( readable_output=f"The {file_type_as_literal} file {filename} was successfully removed from the cloud." ) ) except DemistoException as exp: results.append(CommandResults(readable_output=f"Failed to delete file {filename}. {exp}")) except DemistoException as exp: raise DemistoException(f"Unable to download recording for meeting {meeting_id}: {exp}") return results def zoom_meeting_get_command(client, **args) -> CommandResults: client = client meeting_id = args.get("meeting_id") occurrence_id = args.get("occurrence_id") show_previous_occurrences = argToBoolean(args.get("show_previous_occurrences")) raw_data = client.zoom_meeting_get(meeting_id, occurrence_id, show_previous_occurrences) # parsing the response md = tableToMarkdown( "Meeting details", raw_data, [ "uuid", "id", "host_id", "host_email", "topic", "type", "status", "start_time", "duration", "timezone", "agenda", "created_at", "start_url", "join_url", ], ) # removing passwords from the response# safe_raw_data = raw_data for sensitive_info in ["password", "pstn_password", "encrypted_password", "h323_password"]: safe_raw_data.pop(sensitive_info, None) return CommandResults( outputs_prefix="Zoom.Meeting", readable_output=md, outputs_key_field="id", outputs=safe_raw_data, raw_response=raw_data ) def zoom_meeting_list_command(client, **args) -> CommandResults: client = client user_id = args.get("user_id", "") next_page_token = args.get("next_page_token") page_size = arg_to_number(args.get("page_size", 30)) limit = arg_to_number(args.get("limit")) type = args.get("type") page_number = arg_to_number(args.get("page_number", 1)) if limit: if "page_size" in args or next_page_token or "page_number" in args: # arguments collision raise DemistoException(LIMIT_AND_EXTRA_ARGUMENTS_MEETING_LIST) else: # multiple request are needed raw_data = manual_meeting_list_pagination( client=client, user_id=user_id, next_page_token=next_page_token, limit=limit, type=type ) minimal_needed_info = remove_extra_info_meeting_list(limit=limit, raw_data=raw_data) md = tableToMarkdown( "Meeting list", minimal_needed_info, ["uuid", "id", "host_id", "topic", "type", "start time", "duration", "timezone", "created_at", "join_url"], ) md += "\n" + tableToMarkdown("Metadata", [raw_data][0][0], ["total_records"]) raw_data = raw_data[0] else: # one request in needed raw_data = client.zoom_meeting_list( user_id=user_id, next_page_token=next_page_token, page_size=page_size, type=type, page_number=page_number ) # parsing the data md = tableToMarkdown( "Meeting list", raw_data.get("meetings"), ["uuid", "id", "host_id", "topic", "type", "start_time", "duration", "timezone", "created_at", "join_url"], ) md += "\n" + tableToMarkdown("Metadata", [raw_data], ["next_page_token", "page_size", "page_number", "total_records"]) return CommandResults( outputs_prefix="Zoom", readable_output=md, # keeping the syntax of the output of the previous version outputs={"Meeting": raw_data, "Metadata": {"Size": raw_data.get("page_size"), "Total": raw_data.get("total_records")}}, raw_response=raw_data, ) def check_authentication_type_parameters( api_key: str, api_secret: str, # checking if the user entered extra parameters # at the configuration level account_id: str, client_id: str, client_secret: str, ): if any((api_key, api_secret)) and any((account_id, client_id, client_secret)): raise DemistoException(EXTRA_PARAMS) def check_authentication_bot_parameters(bot_Jid: str, client_id: str, client_secret: str): """check authentication parameters that both client_id, secret_id, bot_Jid are provided or none of them""" if (bot_Jid and client_id and client_secret) or (not bot_Jid and not client_id and not client_secret): return else: raise DemistoException(BOT_PARAM_CHECK) def check_authentication_parameters(client_id: str, client_secret: str): """check authentication parameters that both client_id and secret are provided""" if (client_id and client_secret) or (not client_id and not client_secret): return else: raise DemistoException(OAUTH_PARAM_CHECK) def zoom_list_account_public_channels_command(client, **args) -> CommandResults: """ Lists public channels associated with a Zoom account. """ # PREPROCESSING client = client page_size = arg_to_number(args.get("page_size", 50)) channel_id = args.get("channel_id") next_page_token = args.get("next_page_token") limit = arg_to_number(args.get("limit")) page_number = arg_to_number(args.get("page_number", 1)) url_suffix = f"channels/{channel_id}" if channel_id else "channels" if limit: if "page_size" in args or "page_number" in args or next_page_token or channel_id: # arguments collision raise DemistoException(LIMIT_AND_EXTRA_ARGUMENTS) else: # multiple requests are needed raw_data = manual_list_channel_pagination( client=client, next_page_token=next_page_token, limit=limit, url_suffix=url_suffix ) data = remove_extra_info_list("channels", limit, raw_data) token = raw_data.get("next_page_token", None) else: # only one request is needed raw_data = client.zoom_list_channels( page_size=page_size, next_page_token=next_page_token, url_suffix=url_suffix, page_number=page_number ) # parsing the data according to the different given arguments data = [raw_data] if channel_id else raw_data.get("channels") token = raw_data.get("next_page_token", None) outputs = [] for i in data: outputs.append( { "Channel JID": i.get("jid"), "Channel ID": i.get("id"), "Channel name": i.get("name"), "Channel type": i.get("type"), "Channel url": i.get("channel_url"), } ) md = tableToMarkdown("Channels", outputs, removeNull=True) md += "\n" + f"Channels Next Token: {token}" return CommandResults( outputs_prefix="Zoom.Channel", readable_output=md, outputs={**raw_data, "ChannelsNextToken": token}, raw_response=raw_data ) def zoom_list_user_channels_command(client, **args) -> CommandResults: """ Lists channels associated with a specific Zoom user. """ # PREPROCESSING client = client page_size = arg_to_number(args.get("page_size", 50)) channel_id = args.get("channel_id") user_id = args.get("user_id", "") next_page_token = args.get("next_page_token") limit = arg_to_number(args.get("limit")) page_number = arg_to_number(args.get("page_number", 1)) if user_id and re.match(emailRegex, user_id): user_id = zoom_get_user_id_by_email(client, user_id) data = [] url_suffix = f"users/{user_id}/channels/{channel_id}" if channel_id else f"users/{user_id}/channels" if limit: if "page_size" in args or "page_number" in args or next_page_token: # arguments collision raise DemistoException(LIMIT_AND_EXTRA_ARGUMENTS) else: # multiple requests are needed raw_data = manual_list_user_channel_pagination( client=client, user_id=user_id, next_page_token=next_page_token, limit=limit, url_suffix=url_suffix ) data = remove_extra_info_list("channels", limit, raw_data) token = raw_data.get("next_page_token", None) else: # only one request is needed raw_data = client.zoom_list_user_channels( user_id=user_id, page_size=page_size, next_page_token=next_page_token, url_suffix=url_suffix, page_number=page_number ) # parsing the data according to the different given arguments data = [raw_data] if channel_id else raw_data.get("channels") token = raw_data.get("next_page_token", None) outputs = [] for i in data: outputs.append( { "User id": user_id, "Channel ID": i.get("id"), "channel JID": i.get("jid"), "Channel name": i.get("name"), "Channel type": i.get("type"), "Channel url": i.get("channel_url"), } ) md = tableToMarkdown("Channels", outputs) return CommandResults( outputs_prefix="Zoom.Channel", outputs_key_field="id", readable_output=md, outputs={**raw_data, "UserChannelsNextToken": token}, raw_response=raw_data, ) def zoom_create_channel_command(client, **args) -> CommandResults: """ Create a new zoom channel """ client = client user_id = args.get("user_id") member_emails = argToList(args.get("member_emails")) add_member_permissions = args.get("add_member_permissions", None) add_member_permissions_num = MEMBER_PERMISSIONS_MAPPING.get(add_member_permissions) posting_permissions = args.get("posting_permissions", None) posting_permissions_num = POSTING_PERMISSIONS_MAPPING.get(posting_permissions) new_members_can_see_prev_msgs = args.get("new_members_can_see_prev_msgs", True) channel_name = args.get("channel_name") channel_type = args.get("channel_type", None) channel_type_num = CHANNEL_TYPE_MAPPING.get(channel_type) json_all_data = {} email_json = [{"email": email} for email in member_emails] if user_id and re.match(emailRegex, user_id): user_id = zoom_get_user_id_by_email(client, user_id) # special section for recurring meeting with fixed time json_all_data.update( { "channel_settings": { "add_member_permissions": add_member_permissions_num, "new_members_can_see_previous_messages_files": new_members_can_see_prev_msgs, "posting_permissions": posting_permissions_num, }, "name": channel_name, "type": channel_type_num, "members": email_json, } ) json_data = remove_None_values_from_dict(json_all_data) url_suffix = f"/chat/users/{user_id}/channels" raw_data = client.zoom_create_channel(url_suffix, json_data) # md = tableToMarkdown('Channel details', [raw_data], ['id', 'name', 'type', 'channel_url']) outputs = [] outputs.append( { "User id": user_id, "Channel ID": raw_data.get("id"), "Channel name": raw_data.get("name"), "Channel type": raw_data.get("type"), "Channel url": raw_data.get("channel_url"), } ) human_readable = tableToMarkdown("Channel details", outputs, removeNull=True) return CommandResults( outputs_prefix="Zoom.Channel", outputs_key_field="id", readable_output=human_readable, outputs=raw_data, raw_response=raw_data, ) def zoom_delete_channel_command(client, **args) -> CommandResults: """ Delete a Zoom channel """ client = client channel_id = args.get("channel_id") user_id = args.get("user_id") url_suffix = f"/chat/users/{user_id}/channels/{channel_id}" if user_id and re.match(emailRegex, user_id): user_id = zoom_get_user_id_by_email(client, user_id) client.zoom_delete_channel(url_suffix) return CommandResults( readable_output=f"Channel {channel_id} was deleted successfully", ) def zoom_update_channel_command(client, **args) -> CommandResults: """ Update a Zoom channel """ client = client add_member_permissions = args.get("add_member_permissions", None) add_member_permissions_num = MEMBER_PERMISSIONS_MAPPING.get(add_member_permissions) posting_permissions = args.get("posting_permissions", None) posting_permissions_num = POSTING_PERMISSIONS_MAPPING.get(posting_permissions) new_members_can_see_prev_msgs = args.get("new_members_can_see_prev_msgs", True) channel_name = args.get("channel_name") channel_id = args.get("channel_id") user_id = args.get("user_id") json_all_data = {} if user_id and re.match(emailRegex, user_id): user_id = zoom_get_user_id_by_email(client, user_id) # special section for recurring meeting with fixed time json_all_data.update( { "name": channel_name, "channel_settings": { "add_member_permissions": add_member_permissions_num, "new_members_can_see_previous_messages_files": new_members_can_see_prev_msgs, "posting_permissions": posting_permissions_num, }, } ) json_data = remove_None_values_from_dict(json_all_data) url_suffix = f"/chat/users/{user_id}/channels/{channel_id}" client.zoom_update_channel(url_suffix, json_data) return CommandResults( readable_output=f"Channel {channel_id} was updated successfully", ) def zoom_invite_to_channel_command(client, **args) -> CommandResults: """ invite users to Zoom channel """ channel_id = args.get("channel_id") user_id = args.get("user_id") members = argToList(args.get("members")) url_suffix = f"/chat/users/{user_id}/channels/{channel_id}/members" json_members = [{"email": email} for email in members] if user_id and re.match(emailRegex, user_id): user_id = zoom_get_user_id_by_email(client, user_id) members_json = {"members": json_members} raw_data = client.zoom_invite_to_channel(members_json, url_suffix) outputs = [{"User id": raw_data.get("ids"), "Channel ID": channel_id, "Added at date and time": raw_data.get("added_at")}] human_readable = tableToMarkdown("Channel details", outputs, removeNull=True) return CommandResults( outputs_prefix="Zoom.Channel", outputs_key_field="id", readable_output=human_readable, outputs=raw_data, raw_response=raw_data, ) def zoom_remove_from_channel_command(client, **args) -> CommandResults: """ Remove a user from Zoom channel """ client = client channel_id = args.get("channel_id") member_id = args.get("member_id") user_id = args.get("user_id") if user_id and re.match(emailRegex, user_id): user_id = zoom_get_user_id_by_email(client, user_id) url_suffix = f"/chat/users/{user_id}/channels/{channel_id}/members/{member_id}" client.zoom_remove_from_channel(url_suffix) return CommandResults( readable_output=f"Member {member_id} was successfully removed from channel {channel_id}", ) def zoom_send_file_command(client, **args) -> CommandResults: """ Send file in Zoom """ client = client user_id = args.get("user_id") to_channel = args.get("to_channel") to_contact = args.get("to_contact") entry_id = args.get("entry_id") if user_id and re.match(emailRegex, user_id): user_id = zoom_get_user_id_by_email(client, user_id) if "to_contact" not in args and "to_channel" not in args: raise DemistoException(MISSING_ARGUMENT) file_info = demisto.getFilePath(entry_id) upload_url = f"https://file.zoom.us/v2/chat/users/{user_id}/messages/files" json_data = remove_None_values_from_dict({"to_contact": to_contact, "to_channel": to_channel}) upload_response = client.zoom_send_file(upload_url, file_info, json_data) message_id = upload_response.get("id") return CommandResults(readable_output=f"Message with id {message_id} was successfully sent") def process_links(formatted_message, formats): """ Processes markdown links in the formatted message. for example- [my link](https://****.com) Args: formatted_message (str): The formatted message to process. Returns: str: The formatted message with links processed. """ add_link_regex = r"\[([^[\]]*?)\]\((http[s]?://.*?)\)" matches = re.findall(add_link_regex, formatted_message) for match in matches: link_text, link_url = match add_link_range = {"text": link_text, "format_type": "AddLink", "format_attr": link_url} formats.append(add_link_range) # Replace Markdown syntax with plain text formatted_message = formatted_message.replace("[" + link_text + "](" + link_url + ")", link_text) return formatted_message, formats def process_mentions(formatted_message, formats, at_contact=None): """ Processes mentions in the markdown message you can provide just one mention on each message. for example-'@all' Args: formatted_message (str): The markdown message to process. at_contact (str): The contact to mention. Returns: str: The formatted message without the mention markdown. """ mention_regex = r"(@\w+)" matches = re.findall(mention_regex, formatted_message)[:1] for match in matches: if match == "@all": mention_range = { "text": match, "at_type": 2, "at_contact": at_contact, } formats.append(mention_range) elif at_contact: mention_range = { "text": match, "at_contact": at_contact, "at_type": 1, } formats.append(mention_range) return formatted_message, formats def process_background_colors(formatted_message, formats): """ Processes background colors in the markdown message. for example- [#<rgb>bg](message that will be with background) Args: formatted_message (str): The markdown message to process. Returns: str: The formatted message without background color markdown. """ bg_color_regex = r"\[#([A-Fa-f0-9]{6})bg\]\((.*?)\)" matches = re.findall(bg_color_regex, formatted_message) for match in matches: bg_color, text = match bg_color_range = { "text": text, "format_type": "BackgroundColor", "format_attr": bg_color, } formats.append(bg_color_range) # Replace Markdown syntax with plain text formatted_message = formatted_message.replace("[#" + bg_color + "bg](" + text + ")", text) return formatted_message, formats def process_font_sizes(formatted_message, formats): """ Processes font sizes in the markdown message. use s|m|l for text size for example- [s](message) Args: formatted_message (str): The markdown message to process. Returns: str: The formatted message without fontsize markdown. """ font_size_regex = "\\[(s|m|l)\\]\\((.*?)\\)" matches = re.findall(font_size_regex, formatted_message) for match in matches: size, text = match font_size_range = {"text": text, "format_type": "FontSize", "format_attr": size} formats.append(font_size_range) # Replace Markdown syntax with plain text formatted_message = formatted_message.replace("[" + size + "](" + text + ")", text) return formatted_message, formats def process_font_colors(formatted_message, formats): """ Processes font colors in the formatted message. Args: formatted_message (str): The formatted message to process. Returns: str: The formatted message with font colors processed. """ font_color_regex = "\\[#([A-Fa-f0-9]{6})\\]\\((.*?)\\)" matches = re.findall(font_color_regex, formatted_message) for match in matches: color, text = match font_color_range = { "text": text, "format_type": "FontColor", "format_attr": color, } formats.append(font_color_range) # Replace Markdown syntax with plain text formatted_message = formatted_message.replace("[#" + color + "](" + text + ")", text) return formatted_message, formats def process_left_indent_and_paragraphs(formatted_message, formats): """ Processes left indents and paragraphs in the markdown message. for left indents use >,>>,>>> for paragraph usr #,##,### (up to 3) this type of markdowns effect the whole line. Args: formatted_message (str): The formatted message to process. Returns: tuple: A tuple containing the formatted message, rich text formatting data, and @mention items. """ left_indent_regex = r"(>{1,4})\s(.*?)$" paragraph_regex = r"(#{1,4})\s(.*?)$" lines = formatted_message.split("\n") for i, line in enumerate(lines): match = re.findall(left_indent_regex, line) for matches in match: indent_level, indent_text = matches left_indent_range = { "text": indent_text, "format_type": "LeftIndent", "format_attr": len(indent_level) * 20, } formats.append(left_indent_range) # Replace Markdown syntax with plain text lines[i] = indent_text pp_matches = re.findall(paragraph_regex, line) for p_matches in pp_matches: heading_level, heading_text = p_matches paragraph_range = { "text": heading_text, "format_type": "paragraph", "format_attr": f"h{len(heading_level)}", } formats.append(paragraph_range) lines[i] = heading_text formatted_message = "\n".join(lines) return formatted_message, formats def parse_markdown_message(markdown_message: str, at_contact: str = None): """ Parses a Markdown message and extracts formatting information. Args: markdown_message (str): The Markdown message to parse. at_contact (str, optional): The contact to mention. Defaults to None. Returns: tuple: A tuple containing the formatted message, rich text formatting data, and @mention items. """ formats: List[dict] = [] at_items = [] rich_text = [] formatted_message, formats = process_links(markdown_message, formats) formatted_message, formats = process_mentions(formatted_message, formats, at_contact) formatted_message, formats = process_background_colors(formatted_message, formats) formatted_message, formats = process_font_sizes(formatted_message, formats) formatted_message, formats = process_font_colors(formatted_message, formats) formatted_message, formats = process_left_indent_and_paragraphs(formatted_message, formats) # extract the start,end positions for i in range(len(formats)): t = formats[i]["text"] start_position = formatted_message.find(t) end_position = start_position + len(t) - 1 formats[i]["start_position"] = start_position formats[i]["end_position"] = end_position if formats[i].get("at_type"): at_items.append(formats[i]) else: rich_text.append(formats[i]) return formatted_message, {"message": formatted_message, "rich_text": rich_text, "at_items": at_items} def zoom_send_message_command(client, **args) -> CommandResults: """ Send Zoom chat message """ client = client at_contact = args.get("at_contact") user_id = args.get("user_id") message = args.get("message", "") entry_ids = argToList(args.get("entry_ids", [])) reply_main_message_id = args.get("reply_main_message_id") to_channel = args.get("to_channel") to_contact = args.get("to_contact") is_markdown = args.get("is_markdown", False) if user_id and re.match(emailRegex, user_id): user_id = zoom_get_user_id_by_email(client, user_id) url_suffix = f"/chat/users/{user_id}/messages" upload_file_url = f"https://file.zoom.us/v2/chat/users/{user_id}/files" zoom_file_id: List = [] for id in entry_ids: file_info = demisto.getFilePath(id) res = client.zoom_upload_file(upload_file_url, file_info) zoom_file_id.append(res.get("id")) # check if the text contain markdown to parse and also provide text style arguments if is_markdown: # check if text have more then 1 mention if message.count("@") > 1 and at_contact: raise DemistoException(MARKDOWN_EXTRA_MENTIONS) message, json_data_all = parse_markdown_message(message, at_contact) json_data_all.update( { "file_ids": zoom_file_id, "reply_main_message_id": reply_main_message_id, "to_channel": to_channel, "to_contact": to_contact, } ) else: json_data_all = { "message": message, "file_ids": zoom_file_id, "reply_main_message_id": reply_main_message_id, "to_channel": to_channel, "to_contact": to_contact, } json_data = remove_None_values_from_dict(json_data_all) raw_data = client.zoom_send_message(url_suffix, json_data) data = {"Mentioned user": at_contact, "Channel ID ": to_channel, "Message ID": raw_data.get("id"), "Contact": to_contact} md = tableToMarkdown("Message", data, removeNull=True) return CommandResults( outputs_prefix="Zoom.ChatMessage", readable_output=md, outputs_key_field="id", outputs=raw_data, raw_response=raw_data ) def zoom_delete_message_command(client, **args) -> CommandResults: """ Delete Zoom chat message """ client = client message_id = args.get("message_id") to_contact = args.get( "to_contact", ) user_id = args.get("user_id") to_channel = args.get("to_channel") if user_id and re.match(emailRegex, user_id): user_id = zoom_get_user_id_by_email(client, user_id) if to_channel: url_suffix = f"/chat/users/{user_id}/messages/{message_id}?to_channel={to_channel}" elif to_contact: url_suffix = f"/chat/users/{user_id}/messages/{message_id}?to_contact={to_contact}" else: raise DemistoException(MISSING_ARGUMENT) client.zoom_delete_message(url_suffix) return CommandResults( readable_output=f"Message {message_id} was deleted successfully", ) def zoom_update_message_command(client, **args) -> CommandResults: """ Update Zoom chat message """ client = client message_id = args.get("message_id") to_contact = args.get("to_contact") user_id = args.get("user_id") to_channel = args.get("to_channel") message = args.get("message") entry_ids = argToList(args.get("entry_ids", [])) if user_id and re.match(emailRegex, user_id): user_id = zoom_get_user_id_by_email(client, user_id) upload_file_url = f"https://file.zoom.us/v2/chat/users/{user_id}/files" zoom_file_id: List = [] demisto.debug(f"file id args {entry_ids}") for id in entry_ids: file_info = demisto.getFilePath(id) res = client.zoom_upload_file(upload_file_url, file_info) zoom_file_id.append(res.get("id")) demisto.debug("upload the file without error") url_suffix = f"/chat/users/{user_id}/messages/{message_id}" json_data = remove_None_values_from_dict( {"message": message, "file_ids": zoom_file_id, "to_channel": to_channel, "to_contact": to_contact} ) client.zoom_update_message(url_suffix, json_data) return CommandResults( readable_output=f"Message {message_id} was successfully updated", ) def zoom_delete_user_token_command(client, **args) -> CommandResults: """ Revoke a user's Zoom SSO session """ client = client user_id = args.get("user_id") url_suffix = f"/users/{user_id}/token" client.zoom_delete_user_token(url_suffix) return CommandResults( readable_output=f"User SSO token for user {user_id} is deleted", ) def zoom_get_user_id_by_email(client, email): """ Retrieves the user ID associated with the given email address. :param client: The Zoom client object., email: The email address of the user. :return: The user ID associated with the email address. :rtype: str """ user_url_suffix = f"users/{email}" user_id = client.zoom_list_users(page_size=50, url_suffix=user_url_suffix).get("id") if not user_id: raise DemistoException(USER_NOT_FOUND) return user_id def zoom_get_user_name_by_email(client, user_email): """ Retrieves the user name associated with the given email address. :param client: The Zoom client object. user_email: The email address of the user. :return: The user name associated with the email address. :rtype: str """ user_url_suffix = f"users/{user_email}" user_name = client.zoom_list_users(page_size=1, url_suffix=user_url_suffix) demisto.info(f"user_name: {user_name}") if not user_name: raise DemistoException(USER_NOT_FOUND) user_name = user_name.get("display_name") return user_name def zoom_get_user_email_by_id(client, user_id): """ Retrieves the user email address associated with the given user ID. :param client: The Zoom client object. user_id: The user ID of the user. :return: The email address associated with the user ID. :rtype: str """ user_url_suffix = f"users/{user_id}" user_email = client.zoom_list_users(page_size=1, url_suffix=user_url_suffix).get("email") if not user_email: raise DemistoException(USER_NOT_FOUND) return user_email def zoom_list_messages_command(client, **args) -> CommandResults: """ Lists messages from Zoom chat. :raises DemistoException: If a required argument is missing. """ client = client user_id = args.get("user_id") to_contact = args.get("to_contact") to_channel = args.get("to_channel") date_arg = arg_to_datetime(args.get("date")) from_arg = arg_to_datetime(args.get("from")) to_arg = arg_to_datetime(args.get("to")) date_arg = date_arg.strftime("%Y-%m-%dT%H:%M:%SZ") if date_arg else None from_arg = from_arg.strftime("%Y-%m-%dT%H:%M:%SZ") if from_arg else None to_arg = to_arg.strftime("%Y-%m-%dT%H:%M:%SZ") if to_arg else None include_deleted_and_edited_message = args.get("include_deleted_and_edited_message") search_type = args.get("search_type") search_key = args.get("search_key") exclude_child_message = args.get("exclude_child_message", False) limit = arg_to_number(args.get("limit", 50)) page_size = arg_to_number(args.get("page_size")) if limit and page_size and limit != 50: raise DemistoException(LIMIT_AND_EXTRA_ARGUMENTS) else: limit = page_size if page_size else limit if not to_contact and not to_channel: raise DemistoException(MISSING_ARGUMENT) if user_id and re.match(emailRegex, user_id): user_id = zoom_get_user_id_by_email(client, user_id) json_data = { "user_id": user_id, "to_contact": to_contact, "to_channel": to_channel, "date": date_arg, "from": from_arg, "to": to_arg, "include_deleted_and_edited_message": include_deleted_and_edited_message, "search_type": search_type, "search_key": search_key, "exclude_child_message": exclude_child_message, "page_size": limit, } # remove all keys with val of None request_data = remove_None_values_from_dict(json_data) url_suffix = f"users/{user_id}/messages" all_messages: List = [] next_page_token = args.get("next_page_token", None) while True: try: raw_data = client.zoom_list_user_messages( url_suffix=url_suffix, user_id=user_id, to_contact=to_contact, to_channel=to_channel, date_arg=date_arg, from_arg=from_arg, to_arg=to_arg, include_deleted_and_edited_message=include_deleted_and_edited_message, search_type=search_type, search_key=search_key, exclude_child_message=exclude_child_message, next_page_token=next_page_token, page_size=limit, ) data = raw_data.get("messages", []) if limit and len(all_messages) + len(data) > limit: remaining_limit = limit - len(all_messages) data = data[:remaining_limit] all_messages.extend(data) if limit and len(all_messages) >= limit: all_messages = all_messages[:limit] break next_page_token = raw_data.get("next_page_token", None) if next_page_token and next_page_token != "": next_page_token = raw_data["next_page_token"] else: break except DemistoException as e: error_message = e.message if "The next page token is invalid or expired." in error_message and next_page_token: raise DemistoException(f"Please ensure that the correct argument values are used when attempting to use \ the next_page_toke.\n Note that when using next_page_token it is mandatory to specify date time and not relative time.\n \ To find the appropriate values, refer to the ChatMessageNextToken located in the context. \n {error_message}") outputs = [] for i in all_messages: outputs.append( { "User id": user_id, "Message Id": i.get("id"), "Message text": i.get("message"), "Message sender": i.get("sender"), "Sender display name": i.get("sender_display_name"), "Date Time": i.get("date_time"), "From": str(from_arg), "To": str(to_arg), } ) md = tableToMarkdown("Messages", outputs) md += "\n" + "Messages next token:" + raw_data.get("next_page_token", "") if raw_data.get("next_page_token"): request_data.update({"next_page_token": raw_data.get("next_page_token")}) else: request_data = None return CommandResults( outputs_prefix="Zoom", readable_output=md, outputs={"ChatMessage": {"messages": all_messages}, "ChatMessageNextToken": request_data}, raw_response=raw_data, ) def get_channel_jid_from_context(channel_name: str = None, investigation_id=None): """ Retrieves a Zoom channel JID based on the provided criteria. :param channel_name: The name of the channel to get the JID for. :param investigation_id: The Demisto investigation ID to search for a mirrored channel. :return: The requested channel JID 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_jid") return None def send_notification(client, **args): client = client bot_jid = client.bot_jid account_id = client.account_id to = args.get("to") channel_id = args.get("channel_id") visible_to_user = args.get("visible_to_user") zoom_ask = argToBoolean(args.get("zoom_ask", False)) entitlement = None message_type = args.get("messageType", "") # From server original_message = args.get("originalMessage", "") # From server entry_object = args.get("entryObject") # From server channel = args.get("channel") # From server investigation_id = None if entry_object: investigation_id = entry_object.get("investigationId") # From server, available from demisto v6.1 and above if message_type and message_type != MIRROR_TYPE: return f"Message type is not in permitted options. Received: {message_type}" if 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 to and "@xmpp.zoom.us" not in to: if re.match(emailRegex, to): to = zoom_get_user_id_by_email(client, to).lower() + "@xmpp.zoom.us" else: to = to.lower() + "@xmpp.zoom.us" if channel_id and "@conference.xmpp.zoom.us" not in channel_id: channel_id = channel_id.lower() + "@conference.xmpp.zoom.us" if zoom_ask: parsed_message = json.loads(args.get("message", "")) entitlement = parsed_message.get("entitlement") message = parsed_message.get("blocks") reply = parsed_message.get("reply") expiry = parsed_message.get("expiry") default_response = parsed_message.get("default_response") else: message = {"head": {"type": "message", "text": args.get("message", "")}} reply = None expiry = None default_response = None if channel: # if channel name provided channel_id = get_channel_jid_from_context(channel, investigation_id) if not channel_id: raise DemistoException(WRONG_CHANNEL) if to and channel_id: raise DemistoException(TOO_MANY_JID) if not to and not channel_id: raise DemistoException(MISSING_ARGUMENT_JID) url_suffix = "/im/chat/messages" json_data_all = { "robot_jid": bot_jid, "to_jid": to if to else channel_id, "account_id": account_id, "visible_to_user": visible_to_user, "content": message, } json_data = remove_None_values_from_dict(json_data_all) raw_data = client.zoom_send_notification(url_suffix, json_data) if raw_data: message_id = raw_data.get("message_id") if entitlement: save_entitlement(entitlement, message_id, reply, expiry, default_response, to if to else channel_id) return CommandResults(readable_output=f'Message sent to Zoom successfully. Message ID is: {raw_data.get("message_id")}') def get_admin_user_id_from_token(client): """get the user admin id from the token""" admin_user_result = client.zoom_get_admin_user_id_from_token() return admin_user_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(SYNC_CONTEXT) return CACHED_INTEGRATION_CONTEXT def mirror_investigation(client, **args) -> CommandResults: if not MIRRORING_ENABLED: demisto.error(" couldn't mirror investigation, Mirroring is disabled") if MIRRORING_ENABLED and not LONG_RUNNING: demisto.error( "Mirroring is enabled, however long running is disabled. For mirrors to work correctly," " long running must be enabled." ) client = client type = args.get("type", "all") direction = args.get("direction", "Both") channel_name = args.get("channelName") autoclose = argToBoolean(args.get("autoclose", True)) send_first_message = False # kick_admin = argToBoolean(args.get('kickAdmin', False)) # members = argToList(args.get('members')) investigation = demisto.investigation() investigation_id = str(investigation.get("id")) if investigation.get("type") == PLAYGROUND_INVESTIGATION_TYPE: return_error("Sorry, but this action cannot be performed in the playground ") integration_context = get_integration_context(SYNC_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)) # get admin user id from token admin_user_id = get_admin_user_id_from_token(client) channel_filter: list = [] 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: # create new channel result = zoom_create_channel_command( client=client, user_id=admin_user_id, channel_type="Public channel", channel_name=channel_name, member_emails=admin_user_id, ) if result and isinstance(result.outputs, dict) and len(result.outputs) > 0: channel_jid = result.outputs.get("jid") channel_id = result.outputs.get("id") else: raise DemistoException("error in create new channel") send_first_message = True else: mirrored_channel = channel_filter[0] channel_jid = mirrored_channel["channel_jid"] channel_id = mirrored_channel["channel_id"] channel_name = mirrored_channel["channel_name"] mirror = { "channel_jid": channel_jid, "channel_id": channel_id, "channel_name": channel_name, "investigation_id": investigation.get("id"), "mirror_type": type, "mirror_direction": direction, "auto_close": bool(autoclose), "mirrored": True, } else: mirror = mirrors.pop(mirrors.index(current_mirror[0])) channel_jid = mirror["channel_jid"] channel_id = mirror["channel_id"] if type: mirror["mirror_type"] = type if autoclose: mirror["auto_close"] = autoclose if direction: mirror["mirror_direction"] = direction if channel_name and not channel_filter: # update channel name result = zoom_update_channel_command( client=client, user_id=admin_user_id, channel_name=channel_name, channel_id=channel_id ) mirror["channel_name"] = channel_name channel_name = mirror["channel_name"] mirror["mirrored"] = True demisto.mirrorInvestigation(investigation_id, f"{type}:{direction}", autoclose) mirrors.append(mirror) set_to_integration_context_with_retries({"mirrors": mirrors}, OBJECTS_TO_KEYS, SYNC_CONTEXT) if send_first_message: server_links = demisto.demistoUrls() server_link = server_links.get("server") message = ( f"This channel was created to mirror incident {investigation_id}." f" \n View it on: {server_link}#/WarRoom/{investigation_id}" ) url_suffix = "/im/chat/messages" json_data_all = { "content": {"body": [{"type": "message", "text": message}]}, "to_jid": channel_jid, "robot_jid": client.bot_jid, "account_id": client.account_id, } client.zoom_send_notification(url_suffix, json_data_all) # if kick_admin: # demisto.debug("kick-admin:") # url_suffix = f'/chat/users/{admin_user_id}/channels/{channel_id}/members/{admin_user_id}' # res = client.zoom_remove_from_channel(url_suffix) # demisto.debug(f"res: {res}") return CommandResults( readable_output=f"Investigation mirrored successfully,\n channel name:{channel_name} \n channel JID: {channel_jid}" ) def find_mirror_by_investigation() -> dict: """ Finds a mirrored channel by the mirrored investigation Returns: The mirror object """ mirror: dict = {} investigation = demisto.investigation() if investigation: integration_context = get_integration_context(SYNC_CONTEXT) if "mirrors" in integration_context: mirrors = json.loads(integration_context["mirrors"]) investigation_filter = list(filter(lambda m: investigation.get("id") == m["investigation_id"], mirrors)) if investigation_filter: mirror = investigation_filter[0] return mirror def close_channel(client, **args): """ if AutoClose is true in mirroring delete the mirrored zoom channel from zoom and from the context """ client = client admin_user_id = get_admin_user_id_from_token(client) mirror = find_mirror_by_investigation() channel_id = None integration_context = get_integration_context(SYNC_CONTEXT) if mirror: mirrors = json.loads(integration_context["mirrors"]) channel_id = mirror.get("channel_id", "") 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"]}', bool(mirror["auto_close"]) ) set_to_integration_context_with_retries({"mirrors": mirrors}, OBJECTS_TO_KEYS, SYNC_CONTEXT) if channel_id: zoom_delete_channel_command(client=client, channel_id=channel_id, user_id=admin_user_id) return "Channel successfully deleted." return return_error("Channel {channel_id} not found") def main(): # pragma: no cover params = demisto.params() args = demisto.args() base_url = params.get("url") account_id = params.get("account_id") client_id = params.get("credentials", {}).get("identifier") client_secret = params.get("credentials", {}).get("password") bot_client_id = params.get("bot_credentials", {}).get("identifier") bot_client_secret = params.get("bot_credentials", {}).get("password") verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) bot_jid = params.get("botJID", None) secret_token = params.get("secret_token", {}).get("password") global SECRET_TOKEN, LONG_RUNNING, MIRRORING_ENABLED, CACHE_EXPIRY, CACHED_INTEGRATION_CONTEXT SECRET_TOKEN = secret_token LONG_RUNNING = params.get("longRunning", False) MIRRORING_ENABLED = params.get("mirroring", False) # Pull initial Cached context and set the Expiry CACHE_EXPIRY = next_expiry_time() CACHED_INTEGRATION_CONTEXT = get_integration_context(SYNC_CONTEXT) if MIRRORING_ENABLED and (not LONG_RUNNING or not SECRET_TOKEN or not bot_client_id or not bot_client_secret or not bot_jid): 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 zoom-bot following parameters: secret token, Bot JID, bot client id and secret id""") if LONG_RUNNING: try: port = int(params.get("longRunningPort")) except ValueError as e: raise ValueError(f"Invalid listen port - {e}") else: port = 0 demisto.debug(f"Not a longrunning, setting {port=}") command = demisto.command() # this is to avoid BC. because some of the arguments given as <a-b>, i.e "user-list" args = {key.replace("-", "_"): val for key, val in args.items()} try: check_authentication_bot_parameters(bot_jid, bot_client_id, bot_client_secret) check_authentication_parameters(client_id, client_secret) global CLIENT client = Client( base_url=base_url, verify=verify_certificate, proxy=proxy, bot_jid=bot_jid, account_id=account_id, client_id=client_id, client_secret=client_secret, bot_client_id=bot_client_id, bot_client_secret=bot_client_secret, ) CLIENT = client results = CommandResults() if command == "test-module": return_results(test_module(client=client)) demisto.debug(f"Command being called is {command}") """CRUD commands""" if command == "long-running-execution": run_long_running(port) elif command == "mirror-investigation": results = mirror_investigation(client, **args) elif command == "close-channel": results = close_channel(client, **args) elif command == "zoom-create-user": results = zoom_create_user_command(client, **args) elif command == "zoom-create-meeting": results = zoom_create_meeting_command(client, **args) elif command == "zoom-meeting-get": results = zoom_meeting_get_command(client, **args) elif command == "zoom-meeting-list": results = zoom_meeting_list_command(client, **args) elif command == "zoom-delete-user": results = zoom_delete_user_command(client, **args) elif command == "zoom-fetch-recording": results = zoom_fetch_recording_command(client, **args) elif command == "zoom-list-users": results = zoom_list_users_command(client, **args) elif command == "zoom-list-account-public-channels": results = zoom_list_account_public_channels_command(client, **args) elif command == "zoom-list-user-channels": results = zoom_list_user_channels_command(client, **args) elif command == "zoom-create-channel": results = zoom_create_channel_command(client, **args) elif command == "zoom-delete-channel": results = zoom_delete_channel_command(client, **args) elif command == "zoom-update-channel": results = zoom_update_channel_command(client, **args) elif command == "zoom-invite-to-channel": results = zoom_invite_to_channel_command(client, **args) elif command == "zoom-remove-from-channel": results = zoom_remove_from_channel_command(client, **args) elif command == "zoom-send-file": results = zoom_send_file_command(client, **args) elif command == "zoom-list-messages": results = zoom_list_messages_command(client, **args) elif command == "zoom-send-message": results = zoom_send_message_command(client, **args) elif command == "zoom-delete-message": results = zoom_delete_message_command(client, **args) elif command == "zoom-update-message": results = zoom_update_message_command(client, **args) elif command == "zoom-delete-user-token": results = zoom_delete_user_token_command(client, **args) elif command == "send-notification": results = send_notification(client, **args) else: return_error("Unrecognized command: " + demisto.command()) return_results(results) except DemistoException as e: # For any other integration command exception, return an error demisto.error(format_exc()) return_error(f"Failed to execute {command} command. Error: {e!s}.") if __name__ in ("__main__", "__builtin__", "builtins"): main()