chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import rootConfig from '../../eslint.config.mjs'
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,767 @@
|
||||
# Botpress Integration: Zoho CRM
|
||||
|
||||
## **Zoho CRM Integration Guide**
|
||||
|
||||
For further details, refer to the [**Zoho CRM API documentation**](https://www.zoho.com/crm/developer/docs/api/v7/).
|
||||
|
||||
## **Overview**
|
||||
|
||||
This Botpress integration allows seamless interaction with **Zoho CRM**. It enables users to manage contacts, deals, appointments, and files directly through their chatbot.
|
||||
|
||||
## **Features**
|
||||
|
||||
- **Record Management:** Create, retrieve, update, delete, and search records.
|
||||
- **Appointments Management:** Create, update, retrieve, and delete appointments.
|
||||
- **File Management:** Upload and retrieve files.
|
||||
- **Emails:** Send emails and associate them with records.
|
||||
- **Organization & User Management:** Retrieve organization details and user information.
|
||||
|
||||
## Connect Zoho CRM
|
||||
|
||||
Use the **Connect with OAuth** setup flow in Botpress. The integration will ask for your Zoho data center, redirect you to Zoho for consent, request the required CRM scopes, and securely store the returned OAuth tokens.
|
||||
|
||||
The Zoho OAuth app must allow the Botpress callback URL shown by your integration setup. It should use the stable callback path `/oauth/wizard/oauth-callback`; the OAuth `state` value is sent separately and should not be included in the registered redirect URI.
|
||||
|
||||
The scopes are requested during the authorization flow. They are not configured directly on the Zoho application:
|
||||
|
||||
```text
|
||||
ZohoCRM.modules.ALL,ZohoCRM.org.ALL,ZohoCRM.users.ALL,ZohoCRM.settings.ALL,ZohoCRM.send_mail.all.CREATE,ZohoCRM.files.CREATE,ZohoCRM.files.READ
|
||||
```
|
||||
|
||||
### Zoho Accounts Domains
|
||||
|
||||
| Region | Accounts URL |
|
||||
| ----------- | ------------------------------- |
|
||||
| US | `https://accounts.zoho.com` |
|
||||
| AU | `https://accounts.zoho.com.au` |
|
||||
| EU | `https://accounts.zoho.eu` |
|
||||
| IN | `https://accounts.zoho.in` |
|
||||
| CN | `https://accounts.zoho.com.cn` |
|
||||
| JP | `https://accounts.zoho.jp` |
|
||||
| CA (Canada) | `https://accounts.zohocloud.ca` |
|
||||
|
||||
## Manual Configuration
|
||||
|
||||
Manual configuration is available as an advanced fallback. Use it only if you need to provide your own Zoho OAuth client credentials and refresh token.
|
||||
|
||||
### Register Your Application
|
||||
|
||||
1. Go to the [Zoho Developer Console](https://accounts.zoho.com/developerconsole).
|
||||
2. Click **Add Client**.
|
||||
3. Choose a server-based client type.
|
||||
4. Add the Botpress OAuth callback URL as an authorized redirect URI.
|
||||
5. Save the client ID and client secret.
|
||||
|
||||
### Generate Refresh Token
|
||||
|
||||
Create an authorization URL with the scopes above, `access_type=offline`, and `prompt=consent`. After granting consent, exchange the returned authorization code for OAuth tokens.
|
||||
|
||||
Replace the placeholders (`CLIENT_ID`, `CLIENT_SECRET`, and `AUTHORIZATION_CODE`) with your actual values before executing the request.
|
||||
|
||||
```sh
|
||||
curl --request POST \
|
||||
--url 'https://YOUR_REGION_ACCOUNT_URL/oauth/v2/token' \
|
||||
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data 'grant_type=authorization_code' \
|
||||
--data 'client_id=YOUR_CLIENT_ID' \
|
||||
--data 'client_secret=YOUR_CLIENT_SECRET' \
|
||||
--data 'redirect_uri=YOUR_REDIRECT_URI' \
|
||||
--data 'code=AUTHORIZATION_CODE'
|
||||
```
|
||||
|
||||
If successful, Zoho returns a response similar to:
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "{access_token}",
|
||||
"refresh_token": "{refresh_token}",
|
||||
"api_domain": "https://www.zohoapis.com",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600
|
||||
}
|
||||
```
|
||||
|
||||
Enter these values in the **Manual configuration** setup:
|
||||
|
||||
- **Client ID**
|
||||
- **Client Secret**
|
||||
- **Refresh Token**
|
||||
- **Region**
|
||||
|
||||
---
|
||||
|
||||
## API Functions & Usage
|
||||
|
||||
Below are the available actions in this integration:
|
||||
|
||||
### 1️⃣ **Record Management**
|
||||
|
||||
#### **Insert Record**
|
||||
|
||||
- **Method:** `POST /crm/v7/{module}`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"module": "Leads",
|
||||
"data": [{ "Last_Name": "Daly", "First_name": "Paul", "Email": "p.daly@zylker.com" }]
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"code": "SUCCESS",
|
||||
"message": "Record added",
|
||||
"status": "success",
|
||||
"details": {
|
||||
"id": "27234000000176001",
|
||||
"Created_By": "Matea Vasileski",
|
||||
"Modified_By": "Matea Vasileski",
|
||||
"Created_Time": "2025-02-26T18:39:39-05:00",
|
||||
"Modified_Time": "2025-02-26T18:39:39-05:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Get Records**
|
||||
|
||||
- **Method:** `GET /crm/v7/{module}`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"module": "Leads",
|
||||
"params": { "fields": "Email" }
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"id": "27234000000157008",
|
||||
"Full_Name": "Jim Mulani",
|
||||
"First_Name": "Jim",
|
||||
"Last_Name": "Mulani",
|
||||
"Email": "updated@email.com",
|
||||
"Company": "envy",
|
||||
"Owner": {
|
||||
"name": "Matea Vasileski",
|
||||
"id": "27234000000095001",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"Created_By": {
|
||||
"name": "Matea Vasileski",
|
||||
"id": "27234000000095001",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"Modified_By": {
|
||||
"name": "Matea Vasileski",
|
||||
"id": "27234000000095001",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"Created_Time": "2025-02-23T21:51:21-05:00",
|
||||
"Modified_Time": "2025-02-26T18:19:10-05:00",
|
||||
"Lead_Status": null,
|
||||
"Lead_Source": null,
|
||||
"Record_Status": "Available"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Get Record By ID**
|
||||
|
||||
- **Method:** `GET /crm/v7/{module}/{recordId}`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"module": "Leads",
|
||||
"recordId": "27234000000157008"
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"id": "27234000000157008",
|
||||
"Full_Name": "Jim Mulani",
|
||||
"First_Name": "Jim",
|
||||
"Last_Name": "Mulani",
|
||||
"Email": "updated@email.com",
|
||||
"Company": "envy",
|
||||
"Owner": {
|
||||
"name": "Matea Vasileski",
|
||||
"id": "27234000000095001",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"Created_By": {
|
||||
"name": "Matea Vasileski",
|
||||
"id": "27234000000095001",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"Modified_By": {
|
||||
"name": "Matea Vasileski",
|
||||
"id": "27234000000095001",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"Created_Time": "2025-02-23T21:51:21-05:00",
|
||||
"Modified_Time": "2025-02-26T18:19:10-05:00",
|
||||
"Lead_Status": null,
|
||||
"Lead_Source": null,
|
||||
"Record_Status": "Available"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Update Record**
|
||||
|
||||
- **Method:** `PUT /crm/v7/{module}/{recordId}`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"module": "Leads",
|
||||
"recordId": "27234000000162001",
|
||||
"data": [{ "Email": "updated@email.com" }]
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"code": "SUCCESS",
|
||||
"message": "Record updated",
|
||||
"status": "success",
|
||||
"details": {
|
||||
"id": "27234000000157008",
|
||||
"Created_By": "Matea Vasileski",
|
||||
"Modified_By": "Matea Vasileski",
|
||||
"Created_Time": "2025-02-23T21:51:21-05:00",
|
||||
"Modified_Time": "2025-02-26T18:55:37-05:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Delete Record**
|
||||
|
||||
- **Method:** `DELETE /crm/v7/{module}/{recordId}`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"module": "Leads",
|
||||
"recordId": "27234000000162008"
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"code": "SUCCESS",
|
||||
"message": "Record deleted",
|
||||
"status": "success",
|
||||
"details": {
|
||||
"id": "27234000000157008"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Search Records**
|
||||
|
||||
- **Method:** `GET /crm/v7/{module}/search`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"module": "Leads",
|
||||
"criteria": "(First_Name:equals:John)"
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"id": "27234000000176001",
|
||||
"Full_Name": "Daly",
|
||||
"Email": "p.daly@zylker.com",
|
||||
"Created_Time": "2025-02-26T18:39:39-05:00",
|
||||
"Modified_Time": "2025-02-26T18:39:39-05:00",
|
||||
"Owner": {
|
||||
"name": "Matea Vasileski",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"Record_Status": "Available"
|
||||
},
|
||||
{
|
||||
"id": "27234000000174002",
|
||||
"Full_Name": "Daly",
|
||||
"Email": "p.daly@zylker.com",
|
||||
"Created_Time": "2025-02-26T18:17:10-05:00",
|
||||
"Modified_Time": "2025-02-26T18:17:10-05:00",
|
||||
"Owner": {
|
||||
"name": "Matea Vasileski",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"Record_Status": "Available"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"per_page": 200,
|
||||
"count": 2,
|
||||
"page": 1,
|
||||
"sort_by": "id",
|
||||
"sort_order": "desc",
|
||||
"more_records": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ **Appointments Management**
|
||||
|
||||
#### **Create Appointment**
|
||||
|
||||
- **Method:** `POST /crm/v7/Appointments__s`
|
||||
- **Input:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"Appointment_Name": "Matea - Mowing Service",
|
||||
"Appointment_For": {
|
||||
"module": {
|
||||
"api_name": "Contacts"
|
||||
},
|
||||
"name": "k m",
|
||||
"id": "27234000000163029"
|
||||
},
|
||||
"Service_Name": {
|
||||
"name": "mow",
|
||||
"id": "27234000000168178"
|
||||
},
|
||||
"Appointment_Start_Time": "2025-02-24T19:33:00Z",
|
||||
"Owner": "27234000000095001",
|
||||
"Location": "Business Address",
|
||||
"Address": "Business Address",
|
||||
"Additional_Information": "",
|
||||
"Remind_At": [
|
||||
{
|
||||
"unit": 30,
|
||||
"period": "minutes"
|
||||
}
|
||||
],
|
||||
"Price": "$1.00"
|
||||
}
|
||||
]
|
||||
```
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"code": "SUCCESS",
|
||||
"message": "Record added",
|
||||
"status": "success",
|
||||
"details": {
|
||||
"id": "27234000000175008",
|
||||
"Created_By": "Matea Vasileski",
|
||||
"Modified_By": "Matea Vasileski",
|
||||
"Created_Time": "2025-02-26T19:21:59-05:00",
|
||||
"Modified_Time": "2025-02-26T19:21:59-05:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Get Appointments**
|
||||
|
||||
- **Method:** `GET /crm/v7/Appointments__s`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"params": { "fields": "Service_Name" }
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"id": "27234000000159008",
|
||||
"Service_Name": {
|
||||
"name": "mow",
|
||||
"id": "27234000000168178"
|
||||
}
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"per_page": 200,
|
||||
"count": 1,
|
||||
"page": 1,
|
||||
"sort_by": "id",
|
||||
"sort_order": "desc",
|
||||
"more_records": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Get Appointment By ID**
|
||||
|
||||
- **Method:** `GET /crm/v7/Appointments__s/{appointmentId}`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"appointmentId": "123456"
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"appointment": {
|
||||
"id": "27234000000175008",
|
||||
"Appointment_Name": "Matea - Mowing Service",
|
||||
"Service_Name": {
|
||||
"name": "mow",
|
||||
"id": "27234000000168178"
|
||||
},
|
||||
"Appointment_For": {
|
||||
"name": "k m",
|
||||
"id": "27234000000163029",
|
||||
"module": "Contacts"
|
||||
},
|
||||
"Owner": {
|
||||
"name": "Matea Vasileski",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"Created_By": {
|
||||
"name": "Matea Vasileski",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"Modified_By": {
|
||||
"name": "Matea Vasileski",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"Appointment_Start_Time": "2025-02-24T14:33:00-05:00",
|
||||
"Appointment_End_Time": "2025-02-24T15:03:00-05:00",
|
||||
"Duration": 30,
|
||||
"Status": "Overdue",
|
||||
"Location": "Business Address",
|
||||
"Remind_At": [
|
||||
{
|
||||
"unit": 30,
|
||||
"period": "minutes"
|
||||
}
|
||||
],
|
||||
"Created_Time": "2025-02-26T19:21:59-05:00",
|
||||
"Modified_Time": "2025-02-26T19:21:59-05:00",
|
||||
"Record_Status": "Available"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Update Appointment**
|
||||
|
||||
- **Method:** `PUT /crm/v7/Appointments__s/{appointmentId}`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"appointmentId": "27234000000159008",
|
||||
"data": {
|
||||
"appointments": [
|
||||
{
|
||||
"Appointment_Name": "Update appt",
|
||||
"Appointment_For": {
|
||||
"module": "Contacts",
|
||||
"name": "k m",
|
||||
"id": "27234000000163029"
|
||||
},
|
||||
"Service_Name": {
|
||||
"name": "mow",
|
||||
"id": "27234000000168178"
|
||||
},
|
||||
"Appointment_Start_Time": "2025-02-24T19:33:00Z",
|
||||
"Owner": "27234000000095001",
|
||||
"Location": "Business Address",
|
||||
"Address": "Business Address",
|
||||
"Additional_Information": "",
|
||||
"Remind_At": [
|
||||
{
|
||||
"unit": 30,
|
||||
"period": "minutes"
|
||||
}
|
||||
],
|
||||
"Price": "$1.00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"code": "SUCCESS",
|
||||
"message": "Record updated",
|
||||
"status": "success",
|
||||
"details": {
|
||||
"id": "27234000000159008",
|
||||
"Created_By": "Matea Vasileski",
|
||||
"Modified_By": "Matea Vasileski",
|
||||
"Created_Time": "2025-02-24T19:44:23-05:00",
|
||||
"Modified_Time": "2025-02-26T19:48:20-05:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Delete Appointment**
|
||||
|
||||
- **Method:** `DELETE /crm/v7/Appointments__s/{appointmentId}`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"appointmentId": "123456"
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"code": "SUCCESS",
|
||||
"message": "Record deleted",
|
||||
"status": "success",
|
||||
"details": {
|
||||
"id": "27234000000159008"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ **File Management**
|
||||
|
||||
#### **Upload File**
|
||||
|
||||
- **Method:** `POST /crm/v7/files`
|
||||
- **Input:**
|
||||
|
||||
```json
|
||||
{
|
||||
"fileUrl": "https://example.com/file.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "File uploaded successfully",
|
||||
"data": {
|
||||
"code": "SUCCESS",
|
||||
"message": "File uploaded successfully",
|
||||
"status": "success",
|
||||
"details": {
|
||||
"name": "20250226050635-LO9N1PT0.webp",
|
||||
"id": "36c38a1979b316686084c58303b1b6cb654eb04f0f1038ed0a8fdf8a6ff28598dceae7f8711509bfd80b56bf8cd4dbba"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Get File**
|
||||
|
||||
- **Method:** `GET /crm/v7/files/{fileId}`
|
||||
- **Input:**
|
||||
|
||||
```json
|
||||
{
|
||||
"fileId": "dcc53e79cfef0810414e8335b0e11d8882a51116f390194f400828673ca4a59492a22be84db32aa8425d0859862491f9"
|
||||
}
|
||||
```
|
||||
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success":true,
|
||||
"message":"Request successful",
|
||||
"data": BinaryFileData
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4️⃣ **Organization & User Management**
|
||||
|
||||
#### **Get Organization Details**
|
||||
|
||||
- **Method:** `GET /crm/v7/org`
|
||||
- **Input:**
|
||||
No input is required.
|
||||
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"organization": {
|
||||
"id": "27234000000000684",
|
||||
"company_name": "Envyro",
|
||||
"domain_name": "org110000680144",
|
||||
"primary_email": "matea@envyro.io",
|
||||
"currency": "Canadian Dollar - CAD",
|
||||
"currency_symbol": "$",
|
||||
"time_zone": "America/Toronto",
|
||||
"country_code": "US",
|
||||
"license_details": {
|
||||
"paid": true,
|
||||
"paid_type": "professional",
|
||||
"paid_expiry": "2025-03-23T20:00:00-04:00",
|
||||
"users_license_purchased": 1
|
||||
},
|
||||
"created_time": "2024-09-11T11:04:19-04:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Get Users**
|
||||
|
||||
- **Method:** `GET /crm/v7/users`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"params": { "status": "active" }
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"users": [
|
||||
{
|
||||
"id": "27234000000095001",
|
||||
"full_name": "Matea Vasileski",
|
||||
"email": "matea@envyro.io",
|
||||
"role": "CEO",
|
||||
"profile": "Administrator",
|
||||
"status": "active",
|
||||
"country": "Canada",
|
||||
"state": "Ontario",
|
||||
"time_zone": "America/Toronto",
|
||||
"created_time": "2024-09-11T11:04:19-04:00",
|
||||
"modified_time": "2024-09-11T11:04:19-04:00"
|
||||
},
|
||||
{
|
||||
"id": "27234000000103062",
|
||||
"full_name": "Milos Arsik",
|
||||
"email": "milos@envyro.io",
|
||||
"role": "CEO",
|
||||
"profile": "Administrator",
|
||||
"status": "closed",
|
||||
"time_zone": "America/Toronto",
|
||||
"created_time": "2024-09-16T00:36:14-04:00",
|
||||
"modified_time": "2024-11-01T12:18:54-04:00"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"per_page": 200,
|
||||
"count": 2,
|
||||
"page": 1,
|
||||
"more_records": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5️⃣ **Emails**
|
||||
|
||||
#### **Send Email**
|
||||
|
||||
- **Method:** `POST /crm/v7/emails`
|
||||
- **Input:**
|
||||
```json
|
||||
{
|
||||
"module": "Leads",
|
||||
"recordId": "123456",
|
||||
"data": [
|
||||
{
|
||||
"from": {
|
||||
"user_name": "Matea Vasileski",
|
||||
"email": "matea@envyro.io"
|
||||
},
|
||||
"to": [
|
||||
{
|
||||
"user_name": "user1",
|
||||
"email": "milos@envyro.io"
|
||||
}
|
||||
],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": "Important Update",
|
||||
"content": "Here is an important update for you.",
|
||||
"mail_format": "html"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- **Output:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Request successful",
|
||||
"data": {
|
||||
"code": "SUCCESS",
|
||||
"message": "Your mail has been sent successfully.",
|
||||
"status": "success",
|
||||
"details": {
|
||||
"message_id": "2e660cab6382a85766b68e77778eadf168f923354a69b362ace2e52ce0b934ba"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,202 @@
|
||||
import { IntegrationDefinition, z } from '@botpress/sdk'
|
||||
import {
|
||||
makeApiCallInputSchema,
|
||||
makeApiCallOutputSchema,
|
||||
getRecordByIdInputSchema,
|
||||
getRecordByIdOutputSchema,
|
||||
insertRecordInputSchema,
|
||||
insertRecordOutputSchema,
|
||||
updateRecordInputSchema,
|
||||
updateRecordOutputSchema,
|
||||
deleteRecordInputSchema,
|
||||
deleteRecordOutputSchema,
|
||||
searchRecordsInputSchema,
|
||||
searchRecordsOutputSchema,
|
||||
getRecordsInputSchema,
|
||||
getRecordsOutputSchema,
|
||||
getOrganizationDetailsOutputSchema,
|
||||
getUsersInputSchema,
|
||||
getUsersOutputSchema,
|
||||
emptyInputSchema,
|
||||
getAppointmentsInputSchema,
|
||||
getAppointmentsOutputSchema,
|
||||
getAppointmentByIdInputSchema,
|
||||
getAppointmentByIdOutputSchema,
|
||||
createAppointmentInputSchema,
|
||||
createAppointmentOutputSchema,
|
||||
updateAppointmentInputSchema,
|
||||
updateAppointmentOutputSchema,
|
||||
deleteAppointmentInputSchema,
|
||||
deleteAppointmentOutputSchema,
|
||||
sendMailInputSchema,
|
||||
sendMailOutputSchema,
|
||||
uploadFileInputSchema,
|
||||
uploadFileOutputSchema,
|
||||
getFileInputSchema,
|
||||
getFileOutputSchema,
|
||||
} from './src/misc/custom-schemas'
|
||||
import { DATA_CENTERS } from './src/misc/data-centers'
|
||||
import { zohoCredentialsStateSchema, zohoOAuthWizardStateSchema } from './src/misc/oauth-schemas'
|
||||
|
||||
export default new IntegrationDefinition({
|
||||
name: 'zoho',
|
||||
version: '4.0.1',
|
||||
title: 'Zoho',
|
||||
readme: 'hub.md',
|
||||
icon: 'icon.svg',
|
||||
description:
|
||||
'Integrate your Botpress chatbot with Zoho CRM to manage customer interactions. Add, update, and retrieve contacts, deals, orders, and appointments directly through your chatbot.',
|
||||
configuration: {
|
||||
identifier: {
|
||||
linkTemplateScript: 'linkTemplate.vrl',
|
||||
required: true,
|
||||
},
|
||||
schema: z.object({}),
|
||||
},
|
||||
configurations: {
|
||||
manual: {
|
||||
title: 'Manual configuration',
|
||||
description: 'Configure manually with Zoho OAuth client credentials and a refresh token',
|
||||
schema: z.object({
|
||||
clientId: z.string().title('Client ID').describe('Your Zoho Client ID'),
|
||||
clientSecret: z.string().title('Client Secret').describe('Your Zoho Client Secret'),
|
||||
refreshToken: z.string().title('Refresh Token').describe('Your Zoho Refresh Token'),
|
||||
dataCenter: z.enum(DATA_CENTERS).title('Data Center Region').describe('Zoho Data Center Region'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
CLIENT_ID: {
|
||||
description: 'The OAuth Client ID provided by Zoho.',
|
||||
},
|
||||
CLIENT_SECRET: {
|
||||
description: 'The OAuth Client Secret provided by Zoho.',
|
||||
},
|
||||
},
|
||||
user: {
|
||||
tags: {
|
||||
id: {
|
||||
title: 'Zoho Tokens',
|
||||
description: 'Zoho CRM access and refresh tokens',
|
||||
},
|
||||
},
|
||||
},
|
||||
states: {
|
||||
credentials: {
|
||||
type: 'integration',
|
||||
schema: zohoCredentialsStateSchema,
|
||||
},
|
||||
oauthWizard: {
|
||||
type: 'integration',
|
||||
schema: zohoOAuthWizardStateSchema,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
makeApiCall: {
|
||||
title: 'Make API Call',
|
||||
description: 'Make a custom API call to the Zoho CRM API',
|
||||
input: { schema: makeApiCallInputSchema },
|
||||
output: { schema: makeApiCallOutputSchema },
|
||||
},
|
||||
insertRecord: {
|
||||
title: 'Insert Record',
|
||||
description: 'Insert a new record into a Zoho CRM module',
|
||||
input: { schema: insertRecordInputSchema },
|
||||
output: { schema: insertRecordOutputSchema },
|
||||
},
|
||||
updateRecord: {
|
||||
title: 'Update Record',
|
||||
description: 'Update an existing record in a Zoho CRM module',
|
||||
input: { schema: updateRecordInputSchema },
|
||||
output: { schema: updateRecordOutputSchema },
|
||||
},
|
||||
deleteRecord: {
|
||||
title: 'Delete Record',
|
||||
description: 'Delete a record from a Zoho CRM module',
|
||||
input: { schema: deleteRecordInputSchema },
|
||||
output: { schema: deleteRecordOutputSchema },
|
||||
},
|
||||
searchRecords: {
|
||||
title: 'Search Records',
|
||||
description: 'Search for records in a Zoho CRM module',
|
||||
input: { schema: searchRecordsInputSchema },
|
||||
output: { schema: searchRecordsOutputSchema },
|
||||
},
|
||||
getRecordById: {
|
||||
title: 'Get Record By ID',
|
||||
description: 'Retrieve a record from a Zoho CRM module by its unique ID',
|
||||
input: { schema: getRecordByIdInputSchema },
|
||||
output: { schema: getRecordByIdOutputSchema },
|
||||
},
|
||||
getRecords: {
|
||||
title: 'Get Records',
|
||||
description: 'Retrieve records from a Zoho CRM module',
|
||||
input: { schema: getRecordsInputSchema },
|
||||
output: { schema: getRecordsOutputSchema },
|
||||
},
|
||||
getOrganizationDetails: {
|
||||
title: 'Get Organization Details',
|
||||
description: 'Retrieve details about the Zoho CRM organization',
|
||||
input: { schema: emptyInputSchema },
|
||||
output: { schema: getOrganizationDetailsOutputSchema },
|
||||
},
|
||||
getUsers: {
|
||||
title: 'Get Users',
|
||||
description: 'Retrieve a list of users from the Zoho CRM organization',
|
||||
input: { schema: getUsersInputSchema },
|
||||
output: { schema: getUsersOutputSchema },
|
||||
},
|
||||
getAppointments: {
|
||||
title: 'Get Appointments',
|
||||
description: 'Retrieve a list of appointments from the Zoho CRM organization',
|
||||
input: { schema: getAppointmentsInputSchema },
|
||||
output: { schema: getAppointmentsOutputSchema },
|
||||
},
|
||||
getAppointmentById: {
|
||||
title: 'Get Appointment By ID',
|
||||
description: 'Retrieve an appointment from the Zoho CRM organization by its unique ID',
|
||||
input: { schema: getAppointmentByIdInputSchema },
|
||||
output: { schema: getAppointmentByIdOutputSchema },
|
||||
},
|
||||
createAppointment: {
|
||||
title: 'Create Appointment',
|
||||
description: 'Create a new appointment in the Zoho CRM organization',
|
||||
input: { schema: createAppointmentInputSchema },
|
||||
output: { schema: createAppointmentOutputSchema },
|
||||
},
|
||||
updateAppointment: {
|
||||
title: 'Update Appointment',
|
||||
description: 'Update an existing appointment in the Zoho CRM organization',
|
||||
input: { schema: updateAppointmentInputSchema },
|
||||
output: { schema: updateAppointmentOutputSchema },
|
||||
},
|
||||
deleteAppointment: {
|
||||
title: 'Delete Appointment',
|
||||
description: 'Delete an appointment from the Zoho CRM organization',
|
||||
input: { schema: deleteAppointmentInputSchema },
|
||||
output: { schema: deleteAppointmentOutputSchema },
|
||||
},
|
||||
sendMail: {
|
||||
title: 'Send Mail',
|
||||
description: 'Send an email using the Zoho CRM Mail API',
|
||||
input: { schema: sendMailInputSchema },
|
||||
output: { schema: sendMailOutputSchema },
|
||||
},
|
||||
uploadFile: {
|
||||
title: 'Upload File',
|
||||
description: 'Upload a file to the Zoho CRM organization',
|
||||
input: { schema: uploadFileInputSchema },
|
||||
output: { schema: uploadFileOutputSchema },
|
||||
},
|
||||
getFile: {
|
||||
title: 'Get File',
|
||||
description: 'Retrieve a file from the Zoho CRM organization by its unique ID',
|
||||
input: { schema: getFileInputSchema },
|
||||
output: { schema: getFileOutputSchema },
|
||||
},
|
||||
},
|
||||
attributes: {
|
||||
category: 'Business Operations',
|
||||
repo: 'botpress',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
webhookId = to_string!(.webhookId)
|
||||
webhookUrl = to_string!(.webhookUrl)
|
||||
|
||||
"{{ webhookUrl }}/oauth/wizard/start?state={{ webhookId }}"
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@botpresshub/zoho",
|
||||
"scripts": {
|
||||
"check:type": "tsc --noEmit",
|
||||
"check:bplint": "bp lint",
|
||||
"build": "bp add -y && bp build",
|
||||
"test": "vitest --run"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/client": "workspace:*",
|
||||
"@botpress/common": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"axios": "^1.4.0",
|
||||
"form-data": "^4.0.0",
|
||||
"preact": "^10.26.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@botpress/cli": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getClient } from '../client'
|
||||
import { createAppointmentInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const createAppointment: IntegrationProps['actions']['createAppointment'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
input,
|
||||
}) => {
|
||||
const validatedInput = createAppointmentInputSchema.parse(input)
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.createAppointment(validatedInput.data)
|
||||
|
||||
logger.forBot().info(`Successful - Create Appointment - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Create Appointment' exception: ${JSON.stringify(errorMessage)}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getClient } from '../client'
|
||||
import { deleteAppointmentInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const deleteAppointment: IntegrationProps['actions']['deleteAppointment'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
input,
|
||||
}) => {
|
||||
const validatedInput = deleteAppointmentInputSchema.parse(input)
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.deleteAppointment(validatedInput.appointmentId)
|
||||
|
||||
logger.forBot().info(`Successful - Delete Appointment - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Delete Appointment' exception: ${JSON.stringify(errorMessage)}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getClient } from '../client'
|
||||
import { deleteRecordInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const deleteRecord: IntegrationProps['actions']['deleteRecord'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = deleteRecordInputSchema.parse(input)
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.deleteRecord(validatedInput.module, validatedInput.recordId)
|
||||
|
||||
logger.forBot().info(`Successful - Delete Record - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Delete Record' exception: ${JSON.stringify(errorMessage)}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getClient } from '../client'
|
||||
import { getAppointmentByIdInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const getAppointmentById: IntegrationProps['actions']['getAppointmentById'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
input,
|
||||
}) => {
|
||||
const validatedInput = getAppointmentByIdInputSchema.parse(input)
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.getAppointmentById(validatedInput.appointmentId)
|
||||
|
||||
logger.forBot().info(`Successful - Get Appointment By ID - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Get Appointment By ID' exception: ${JSON.stringify(errorMessage)}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { getClient } from '../client'
|
||||
import { getAppointmentsInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const getAppointments: IntegrationProps['actions']['getAppointments'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
input,
|
||||
}) => {
|
||||
const validatedInput = getAppointmentsInputSchema.parse(input)
|
||||
const params = validatedInput.params ?? '{}'
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.getAppointments(params)
|
||||
|
||||
logger.forBot().info(`Successful - Get Appointments - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Get Appointments' exception: ${JSON.stringify(errorMessage)}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getClient } from '../client'
|
||||
import { getFileInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const getFile: IntegrationProps['actions']['getFile'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = getFileInputSchema.parse(input)
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().debug(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.getFile(validatedInput.fileId)
|
||||
|
||||
logger.forBot().info(`Successful - Get File - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Get File' exception: ${JSON.stringify(errorMessage)}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { getClient } from '../client'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const getOrganizationDetails: IntegrationProps['actions']['getOrganizationDetails'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
}) => {
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.getOrganizationDetails()
|
||||
|
||||
logger.forBot().info(`Successful - Get Organization Details - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Get Organization Details' exception: ${JSON.stringify(errorMessage)}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { getClient } from '../client'
|
||||
import { getRecordByIdInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const getRecordById: IntegrationProps['actions']['getRecordById'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = getRecordByIdInputSchema.parse(input)
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.getRecordById(validatedInput.module, validatedInput.recordId)
|
||||
|
||||
logger
|
||||
.forBot()
|
||||
.info(`Successful - Get Record By ID - Module: ${validatedInput.module}, Record ID: ${validatedInput.recordId}`)
|
||||
logger.forBot().debug(`Result - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Get Record By ID' exception: ${JSON.stringify(errorMessage)}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getClient } from '../client'
|
||||
import { getRecordsInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const getRecords: IntegrationProps['actions']['getRecords'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = getRecordsInputSchema.parse(input)
|
||||
const params = validatedInput.params ?? '{}' // Default to empty JSON if no params provided
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().info(`Validated Input - Module: ${validatedInput.module}, Params: ${params}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.getRecords(validatedInput.module, params)
|
||||
|
||||
logger.forBot().info(`Successful - Get Records - Module: ${validatedInput.module}`)
|
||||
logger.forBot().debug(`Result Data - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Get Records' exception: ${errorMessage}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getClient } from '../client'
|
||||
import { getUsersInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const getUsers: IntegrationProps['actions']['getUsers'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = getUsersInputSchema.parse(input)
|
||||
const params = validatedInput.params ?? '{}' // Ensure params is always a valid JSON string
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().info(`Validated Input - Params: ${params}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.getUsers(params)
|
||||
|
||||
logger.forBot().info('Successful - Get Users')
|
||||
logger.forBot().debug(`Result Data - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Get Users' exception: ${errorMessage}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { createAppointment } from './create-appointment'
|
||||
import { deleteAppointment } from './delete-appointment'
|
||||
import { deleteRecord } from './delete-record'
|
||||
import { getAppointmentById } from './get-appointment-by-id'
|
||||
import { getAppointments } from './get-appointments'
|
||||
import { getFile } from './get-file'
|
||||
import { getOrganizationDetails } from './get-organization-details'
|
||||
import { getRecordById } from './get-record-by-id'
|
||||
import { getRecords } from './get-records'
|
||||
import { getUsers } from './get-users'
|
||||
import { insertRecord } from './insert-record'
|
||||
import { makeApiCall } from './make-api-call'
|
||||
import { searchRecords } from './search-records'
|
||||
import { sendMail } from './send-email'
|
||||
import { updateAppointment } from './update-appointment'
|
||||
import { updateRecord } from './update-record'
|
||||
import { uploadFile } from './upload-file'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export default {
|
||||
makeApiCall,
|
||||
deleteRecord,
|
||||
getRecordById,
|
||||
getRecords,
|
||||
insertRecord,
|
||||
searchRecords,
|
||||
updateRecord,
|
||||
getOrganizationDetails,
|
||||
getUsers,
|
||||
getAppointments,
|
||||
getAppointmentById,
|
||||
createAppointment,
|
||||
updateAppointment,
|
||||
deleteAppointment,
|
||||
sendMail,
|
||||
getFile,
|
||||
uploadFile,
|
||||
} satisfies bp.IntegrationProps['actions']
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getClient } from '../client'
|
||||
import { insertRecordInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const insertRecord: IntegrationProps['actions']['insertRecord'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = insertRecordInputSchema.parse(input)
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.insertRecord(validatedInput.module, validatedInput.data)
|
||||
|
||||
logger.forBot().info('Successful - Insert Record')
|
||||
logger.forBot().debug(`Result Data - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Insert Record' exception: ${errorMessage}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { getClient } from '../client'
|
||||
import { makeApiCallInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const makeApiCall: IntegrationProps['actions']['makeApiCall'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = makeApiCallInputSchema.parse(input)
|
||||
const params = validatedInput.params ?? '{}' // Default to empty JSON if no params provided
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.makeApiCall(
|
||||
validatedInput.endpoint,
|
||||
validatedInput.method,
|
||||
validatedInput.data,
|
||||
params
|
||||
)
|
||||
|
||||
logger.forBot().debug(`Successful - Make API Call - ${JSON.stringify(validatedInput)}`)
|
||||
logger.forBot().debug(`Result - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Make API Call' exception ${JSON.stringify(error)}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { getClient } from '../client'
|
||||
import { searchRecordsInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const searchRecords: IntegrationProps['actions']['searchRecords'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = searchRecordsInputSchema.parse(input)
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.searchRecords(validatedInput.module, validatedInput.criteria)
|
||||
|
||||
logger.forBot().info('Successful - Search Records')
|
||||
logger.forBot().debug(`Result Data - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Search Records' exception: ${errorMessage}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getClient } from '../client'
|
||||
import { sendMailInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const sendMail: IntegrationProps['actions']['sendMail'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = sendMailInputSchema.parse(input)
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.sendMail(validatedInput.module, validatedInput.recordId, validatedInput.data)
|
||||
logger.forBot().info('Successful - Send Mail')
|
||||
logger.forBot().debug(`Result Data - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Send Mail' exception: ${errorMessage}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { getClient } from '../client'
|
||||
import { updateAppointmentInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const updateAppointment: IntegrationProps['actions']['updateAppointment'] = async ({
|
||||
ctx,
|
||||
client,
|
||||
logger,
|
||||
input,
|
||||
}) => {
|
||||
const validatedInput = updateAppointmentInputSchema.parse(input)
|
||||
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
// Call Zoho API to update the appointment
|
||||
const result = await zohoClient.updateAppointment(validatedInput.appointmentId, validatedInput.data)
|
||||
|
||||
logger.forBot().info('Successful - Update Appointment')
|
||||
logger.forBot().debug(`Result Data - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Update Appointment' exception: ${errorMessage}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getClient } from '../client'
|
||||
import { updateRecordInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const updateRecord: IntegrationProps['actions']['updateRecord'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = updateRecordInputSchema.parse(input)
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
const result = await zohoClient.updateRecord(validatedInput.module, validatedInput.recordId, validatedInput.data)
|
||||
|
||||
logger.forBot().info('Successful - Update Record')
|
||||
logger.forBot().debug(`Result Data - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Update Record' exception: ${errorMessage}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { getClient } from '../client'
|
||||
import { uploadFileInputSchema } from '../misc/custom-schemas'
|
||||
import type { IntegrationProps } from '../misc/types'
|
||||
|
||||
export const uploadFile: IntegrationProps['actions']['uploadFile'] = async ({ ctx, client, logger, input }) => {
|
||||
const validatedInput = uploadFileInputSchema.parse(input)
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
logger.forBot().info(`Validated Input - ${JSON.stringify(validatedInput)}`)
|
||||
|
||||
try {
|
||||
// Call Zoho API to upload file
|
||||
const result = await zohoClient.uploadFile(validatedInput.fileUrl)
|
||||
|
||||
logger.forBot().info('Successful - Upload File')
|
||||
logger.forBot().debug(`Upload File Result - ${JSON.stringify(result.data)}`)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: result.data,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
logger.forBot().error(`'Upload File' exception: ${errorMessage}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
import { IntegrationLogger } from '@botpress/sdk'
|
||||
import axios from 'axios'
|
||||
import FormData from 'form-data'
|
||||
import { DataCenter, getZohoApiBaseUrl, getZohoAuthUrl, isDataCenter } from './misc/data-centers'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const logger = new IntegrationLogger()
|
||||
const OAUTH_CLIENT_ID = bp.secrets.CLIENT_ID
|
||||
const OAUTH_CLIENT_SECRET = bp.secrets.CLIENT_SECRET
|
||||
|
||||
// Retry once after refreshing the access token. If Zoho still returns 401,
|
||||
// the credentials are likely revoked, mis-scoped, or tied to the wrong data center.
|
||||
const MAX_AUTH_RETRIES = 1
|
||||
|
||||
type AuthMode = 'oauth' | 'manual'
|
||||
type StoredCredentials = {
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
dataCenter?: DataCenter
|
||||
apiDomain?: string
|
||||
expiresAt?: number
|
||||
}
|
||||
type LegacyConfiguration = {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
refreshToken: string
|
||||
dataCenter: DataCenter
|
||||
}
|
||||
|
||||
const _getErrorMessage = (error: unknown): string => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
return error.response?.data?.message ?? error.message
|
||||
}
|
||||
|
||||
return error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
|
||||
const _getErrorLogData = (error: unknown): unknown => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
return error.response?.data ?? error.message
|
||||
}
|
||||
|
||||
return error instanceof Error ? error.message : error
|
||||
}
|
||||
|
||||
const _getLegacyConfiguration = (configuration: unknown): LegacyConfiguration | null => {
|
||||
if (!configuration || typeof configuration !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const maybeConfiguration = configuration as Record<string, unknown>
|
||||
const { clientId, clientSecret, refreshToken, dataCenter } = maybeConfiguration
|
||||
if (
|
||||
typeof clientId !== 'string' ||
|
||||
typeof clientSecret !== 'string' ||
|
||||
typeof refreshToken !== 'string' ||
|
||||
typeof dataCenter !== 'string' ||
|
||||
!isDataCenter(dataCenter)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { clientId, clientSecret, refreshToken, dataCenter }
|
||||
}
|
||||
|
||||
export class ZohoApi {
|
||||
private _refreshToken: string
|
||||
private _clientId: string
|
||||
private _clientSecret: string
|
||||
private _dataCenter: DataCenter
|
||||
private _baseUrl: string
|
||||
private _ctx: bp.Context
|
||||
private _bpClient: bp.Client
|
||||
private _authMode: AuthMode
|
||||
|
||||
public constructor(
|
||||
refreshToken: string,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
dataCenter: DataCenter,
|
||||
ctx: bp.Context,
|
||||
bpClient: bp.Client,
|
||||
authMode: AuthMode = 'manual',
|
||||
apiDomain?: string
|
||||
) {
|
||||
this._refreshToken = refreshToken
|
||||
this._clientId = clientId
|
||||
this._clientSecret = clientSecret
|
||||
this._dataCenter = dataCenter
|
||||
this._ctx = ctx
|
||||
this._bpClient = bpClient
|
||||
this._authMode = authMode
|
||||
this._baseUrl = apiDomain ?? getZohoApiBaseUrl(dataCenter)
|
||||
}
|
||||
|
||||
/** Retrieves stored credentials from Botpress state */
|
||||
private async _getStoredCredentials(): Promise<StoredCredentials | null> {
|
||||
try {
|
||||
const { state } = await this._bpClient.getState({
|
||||
id: this._ctx.integrationId,
|
||||
name: 'credentials',
|
||||
type: 'integration',
|
||||
})
|
||||
|
||||
if (!state?.payload?.accessToken) {
|
||||
logger.forBot().error('No credentials found in state')
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: state.payload.accessToken,
|
||||
refreshToken: state.payload.refreshToken,
|
||||
dataCenter: state.payload.dataCenter,
|
||||
apiDomain: state.payload.apiDomain,
|
||||
expiresAt: state.payload.expiresAt,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.forBot().error('Error retrieving credentials from state:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private async _makeRequest(
|
||||
endpoint: string,
|
||||
method: string = 'GET',
|
||||
data: any = null,
|
||||
params: any = {},
|
||||
retryCount: number = 0
|
||||
): Promise<any> {
|
||||
try {
|
||||
const creds = await this._getStoredCredentials()
|
||||
if (!creds) {
|
||||
logger.forBot().error('Error retrieving credentials.')
|
||||
throw new Error('Error grabbing credentials.')
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${creds.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}
|
||||
if (method !== 'GET' && method !== 'DELETE') {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
logger.forBot().info(`Making request to ${method} ${this._baseUrl}${endpoint}`)
|
||||
logger.forBot().info('Params:', params)
|
||||
|
||||
const response = await axios({
|
||||
method,
|
||||
url: `${this._baseUrl}${endpoint}`,
|
||||
headers,
|
||||
data,
|
||||
params,
|
||||
})
|
||||
|
||||
return { success: true, message: 'Request successful', data: response.data }
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401 && retryCount < MAX_AUTH_RETRIES) {
|
||||
logger.forBot().warn('Access token expired. Refreshing...', error)
|
||||
await this.refreshAccessToken()
|
||||
return this._makeRequest(endpoint, method, data, params, retryCount + 1)
|
||||
}
|
||||
logger.forBot().error(`Error in ${method} ${endpoint}:`, _getErrorLogData(error))
|
||||
return { success: false, message: _getErrorMessage(error), data: null }
|
||||
}
|
||||
}
|
||||
|
||||
private async _makeFileUploadRequest(endpoint: string, formData: FormData, retryCount: number = 0): Promise<any> {
|
||||
try {
|
||||
const creds = await this._getStoredCredentials()
|
||||
if (!creds) {
|
||||
logger.forBot().error('Error retrieving credentials.')
|
||||
throw new Error('Error grabbing credentials.')
|
||||
}
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${creds.accessToken}`,
|
||||
...formData.getHeaders(),
|
||||
}
|
||||
|
||||
logger.forBot().info(`Uploading file to ${this._baseUrl}${endpoint}`)
|
||||
|
||||
const response = await axios.post(`${this._baseUrl}${endpoint}`, formData, { headers })
|
||||
|
||||
return { success: true, message: 'File uploaded successfully', data: response.data }
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401 && retryCount < MAX_AUTH_RETRIES) {
|
||||
logger.forBot().warn('Access token expired. Refreshing...', error)
|
||||
await this.refreshAccessToken()
|
||||
return this._makeFileUploadRequest(endpoint, formData, retryCount + 1)
|
||||
}
|
||||
logger.forBot().error(`Error in file upload ${endpoint}:`, _getErrorLogData(error))
|
||||
return { success: false, message: _getErrorMessage(error), data: null }
|
||||
}
|
||||
}
|
||||
|
||||
public async refreshAccessToken() {
|
||||
try {
|
||||
const requestData = new URLSearchParams()
|
||||
requestData.append('client_id', this._clientId)
|
||||
requestData.append('client_secret', this._clientSecret)
|
||||
requestData.append('refresh_token', this._refreshToken)
|
||||
requestData.append('grant_type', 'refresh_token')
|
||||
|
||||
const response = await axios.post(`${getZohoAuthUrl(this._dataCenter)}/oauth/v2/token`, requestData.toString(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
|
||||
const currentCredentials = await this._getStoredCredentials()
|
||||
|
||||
await this._bpClient.setState({
|
||||
id: this._ctx.integrationId,
|
||||
type: 'integration',
|
||||
name: 'credentials',
|
||||
payload: {
|
||||
...currentCredentials,
|
||||
accessToken: response.data.access_token,
|
||||
refreshToken: this._authMode === 'oauth' ? this._refreshToken : currentCredentials?.refreshToken,
|
||||
dataCenter: this._authMode === 'oauth' ? this._dataCenter : currentCredentials?.dataCenter,
|
||||
apiDomain: response.data.api_domain ?? currentCredentials?.apiDomain,
|
||||
expiresAt: response.data.expires_in
|
||||
? Date.now() + response.data.expires_in * 1000
|
||||
: currentCredentials?.expiresAt,
|
||||
},
|
||||
})
|
||||
|
||||
logger.forBot().info('Access token refreshed successfully.')
|
||||
} catch (error: unknown) {
|
||||
logger.forBot().error(_getErrorLogData(error))
|
||||
logger.forBot().error('Error refreshing access token:', _getErrorLogData(error))
|
||||
throw new Error('Authentication error. Please reauthorize the integration.')
|
||||
}
|
||||
}
|
||||
|
||||
public async makeApiCall(endpoint: string, method: string = 'GET', data: any = null, rawParams: any = {}) {
|
||||
const params = JSON.parse(rawParams)
|
||||
return this._makeRequest(endpoint, method, data, params)
|
||||
}
|
||||
|
||||
public async insertRecord(module: string, rawData: string) {
|
||||
const data = JSON.parse(rawData)
|
||||
return this._makeRequest(`/crm/v7/${module}`, 'POST', { data })
|
||||
}
|
||||
|
||||
public async getRecords(module: string, rawParams: string = '{}') {
|
||||
const params = JSON.parse(rawParams)
|
||||
return this._makeRequest(`/crm/v7/${module}`, 'GET', null, params)
|
||||
}
|
||||
|
||||
public async getRecordById(module: string, recordId: string, params: any = {}) {
|
||||
return this._makeRequest(`/crm/v7/${module}/${recordId}`, 'GET', null, params)
|
||||
}
|
||||
|
||||
public async updateRecord(module: string, recordId: string, rawData: string) {
|
||||
const data = JSON.parse(rawData)
|
||||
return this._makeRequest(`/crm/v7/${module}/${recordId}`, 'PUT', { data })
|
||||
}
|
||||
|
||||
public async deleteRecord(module: string, recordId: string) {
|
||||
return this._makeRequest(`/crm/v7/${module}/${recordId}`, 'DELETE')
|
||||
}
|
||||
|
||||
public async searchRecords(module: string, criteria: string) {
|
||||
return this._makeRequest(`/crm/v7/${module}/search`, 'GET', null, { criteria })
|
||||
}
|
||||
|
||||
public async getOrganizationDetails() {
|
||||
return this._makeRequest('/crm/v7/org', 'GET')
|
||||
}
|
||||
|
||||
public async getUsers(rawParams?: string) {
|
||||
const params = rawParams ? JSON.parse(rawParams) : {}
|
||||
return this._makeRequest('/crm/v7/users', 'GET', null, params)
|
||||
}
|
||||
|
||||
public async downloadFileBuffer(fileUrl: string): Promise<Blob> {
|
||||
try {
|
||||
const response = await axios.get(fileUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
|
||||
const contentType = response.headers['content-type'] || 'application/octet-stream'
|
||||
|
||||
return new Blob([response.data], { type: contentType })
|
||||
} catch (error) {
|
||||
logger.forBot().error('Error downloading the file:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadFile(fileUrl: string) {
|
||||
try {
|
||||
const file = await this.downloadFileBuffer(fileUrl)
|
||||
|
||||
const fileName = fileUrl.split('/').pop() || 'uploaded_file'
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', buffer, fileName)
|
||||
|
||||
return this._makeFileUploadRequest('/crm/v7/files', formData)
|
||||
} catch (error) {
|
||||
logger.forBot().error('Error uploading file:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async getFile(fileId: string) {
|
||||
return this._makeRequest('/crm/v7/files', 'GET', null, { id: fileId })
|
||||
}
|
||||
|
||||
public async getAppointments(rawParams: string = '{}') {
|
||||
const params = JSON.parse(rawParams)
|
||||
return this._makeRequest('/crm/v7/Appointments__s', 'GET', null, params)
|
||||
}
|
||||
|
||||
public async getAppointmentById(appointmentId: string) {
|
||||
return this._makeRequest(`/crm/v7/Appointments__s/${appointmentId}`)
|
||||
}
|
||||
|
||||
public async createAppointment(rawData: string) {
|
||||
const data = JSON.parse(rawData)
|
||||
return this._makeRequest('/crm/v7/Appointments__s', 'POST', { data })
|
||||
}
|
||||
|
||||
public async updateAppointment(appointmentId: string, rawData: string) {
|
||||
const data = JSON.parse(rawData)
|
||||
return this._makeRequest(`/crm/v7/Appointments__s/${appointmentId}`, 'PUT', { data })
|
||||
}
|
||||
|
||||
public async deleteAppointment(appointmentId: string) {
|
||||
return this._makeRequest(`/crm/v7/Appointments__s/${appointmentId}`, 'DELETE')
|
||||
}
|
||||
|
||||
public async sendMail(module: string, recordId: string, rawData: string) {
|
||||
const data = JSON.parse(rawData)
|
||||
return this._makeRequest(`/crm/v7/${module}/${recordId}/actions/send_mail`, 'POST', { data })
|
||||
}
|
||||
}
|
||||
|
||||
export const getClient = async (ctx: bp.Context, bpClient: bp.Client): Promise<ZohoApi> => {
|
||||
const manualConfiguration =
|
||||
ctx.configurationType === 'manual' ? ctx.configuration : _getLegacyConfiguration(ctx.configuration)
|
||||
|
||||
if (manualConfiguration) {
|
||||
return new ZohoApi(
|
||||
manualConfiguration.refreshToken,
|
||||
manualConfiguration.clientId,
|
||||
manualConfiguration.clientSecret,
|
||||
manualConfiguration.dataCenter,
|
||||
ctx,
|
||||
bpClient,
|
||||
'manual'
|
||||
)
|
||||
}
|
||||
|
||||
const { state } = await bpClient.getState({
|
||||
id: ctx.integrationId,
|
||||
name: 'credentials',
|
||||
type: 'integration',
|
||||
})
|
||||
const credentials = state.payload
|
||||
if (!credentials.refreshToken || !credentials.dataCenter) {
|
||||
throw new Error('Zoho OAuth credentials not found. Please reconnect the integration.')
|
||||
}
|
||||
|
||||
return new ZohoApi(
|
||||
credentials.refreshToken,
|
||||
OAUTH_CLIENT_ID,
|
||||
OAUTH_CLIENT_SECRET,
|
||||
credentials.dataCenter,
|
||||
ctx,
|
||||
bpClient,
|
||||
'oauth',
|
||||
credentials.apiDomain
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import actions from './actions'
|
||||
import { register, unregister, handler } from './setup'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export default new bp.Integration({
|
||||
register,
|
||||
unregister,
|
||||
actions,
|
||||
handler,
|
||||
channels: {},
|
||||
})
|
||||
@@ -0,0 +1,210 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
|
||||
export const emptyInputSchema = z.object({})
|
||||
|
||||
export const makeApiCallInputSchema = z.object({
|
||||
endpoint: z.string().title('Endpoint').describe('The API endpoint to call'),
|
||||
method: z.string().title('Method').describe("The HTTP method to use ['GET', 'POST', 'PUT', 'DELETE']"),
|
||||
data: z.string().optional().title('Date').describe('The data to send with the request as a string JSON object.'),
|
||||
params: z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Params')
|
||||
.describe('The params to send with the request as a string JSON object if required.'),
|
||||
})
|
||||
|
||||
export const makeApiCallOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the API call was successful'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('The data returned from the API call'),
|
||||
})
|
||||
|
||||
export const getRecordsInputSchema = z.object({
|
||||
module: z
|
||||
.string()
|
||||
.title('Module')
|
||||
.describe('The Zoho CRM module to retrieve records from (e.g., Leads, Contacts, Deals)'),
|
||||
params: z
|
||||
.string()
|
||||
.title('Params')
|
||||
.optional()
|
||||
.describe('Optional query parameters as a JSON string (e.g., pagination, sorting)'),
|
||||
})
|
||||
|
||||
export const getRecordsOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the records were retrieved successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('List of retrieved records'),
|
||||
})
|
||||
|
||||
export const getRecordByIdInputSchema = z.object({
|
||||
module: z
|
||||
.string()
|
||||
.title('Module')
|
||||
.describe('The Zoho CRM module to retrieve the record from (e.g., Leads, Contacts, Deals)'),
|
||||
recordId: z.string().title('Record ID').describe('The unique ID of the record to retrieve'),
|
||||
})
|
||||
|
||||
export const getRecordByIdOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the record was retrieved successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('The retrieved record data from Zoho CRM'),
|
||||
})
|
||||
|
||||
export const insertRecordInputSchema = z.object({
|
||||
module: z
|
||||
.string()
|
||||
.title('Module')
|
||||
.describe('The Zoho CRM module to insert the records into (e.g., Leads, Contacts, Deals)'),
|
||||
data: z.string().title('Data').describe('The raw JSON string containing the records to insert'),
|
||||
})
|
||||
|
||||
export const insertRecordOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the records were inserted successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('Details of the inserted records'),
|
||||
})
|
||||
|
||||
export const updateRecordInputSchema = z.object({
|
||||
module: z.string().title('Module').describe('The Zoho CRM module where the record exists'),
|
||||
recordId: z.string().title('Record ID').describe('The unique ID of the record to update'),
|
||||
data: z.string().title('Data').describe('The raw JSON string containing the updated data'),
|
||||
})
|
||||
|
||||
export const updateRecordOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the record was updated successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('Details of the updated record'),
|
||||
})
|
||||
|
||||
export const deleteRecordInputSchema = z.object({
|
||||
module: z.string().title('Module').describe('The Zoho CRM module where the record exists'),
|
||||
recordId: z.string().title('Record ID').describe('The unique ID of the record to delete'),
|
||||
})
|
||||
|
||||
export const deleteRecordOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the record was deleted successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('Response from Zoho confirming the record deletion'),
|
||||
})
|
||||
|
||||
export const searchRecordsInputSchema = z.object({
|
||||
module: z.string().title('Module').describe('The Zoho CRM module to search within'),
|
||||
criteria: z.string().title('Criteria').describe('The search criteria using Zoho CRM query syntax'),
|
||||
})
|
||||
|
||||
export const searchRecordsOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the search was successful'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('List of records matching the search criteria'),
|
||||
})
|
||||
|
||||
export const getOrganizationDetailsOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the organization details were retrieved successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('Details of the organization retrieved from Zoho CRM'),
|
||||
})
|
||||
|
||||
export const getUsersInputSchema = z.object({
|
||||
params: z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Params')
|
||||
.describe('Optional query parameters as a JSON string for filtering users (e.g., role, status)'),
|
||||
})
|
||||
|
||||
export const getUsersOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the users were retrieved successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('List of users retrieved from Zoho CRM'),
|
||||
})
|
||||
|
||||
export const getAppointmentByIdInputSchema = z.object({
|
||||
appointmentId: z.string().title('Appointment ID').describe('The unique ID of the appointment to retrieve'),
|
||||
})
|
||||
|
||||
export const getAppointmentByIdOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the appointment was retrieved successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('The retrieved appointment data from Zoho CRM'),
|
||||
})
|
||||
|
||||
export const getAppointmentsInputSchema = z.object({
|
||||
params: z
|
||||
.string()
|
||||
.optional()
|
||||
.title('Params')
|
||||
.describe('Optional query parameters as a JSON string (e.g., date range, filters)'),
|
||||
})
|
||||
|
||||
export const getAppointmentsOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the appointments were retrieved successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('List of retrieved appointments'),
|
||||
})
|
||||
|
||||
export const createAppointmentInputSchema = z.object({
|
||||
data: z.string().title('Data').describe('The raw JSON string containing the appointment details'),
|
||||
})
|
||||
|
||||
export const createAppointmentOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the appointment was created successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('Details of the created appointment'),
|
||||
})
|
||||
|
||||
export const updateAppointmentInputSchema = z.object({
|
||||
appointmentId: z.string().title('Appointment ID').describe('The unique ID of the appointment to update'),
|
||||
data: z.string().title('Data').describe('The raw JSON string containing the updated appointment details'),
|
||||
})
|
||||
|
||||
export const updateAppointmentOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the appointment was updated successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('Details of the updated appointment'),
|
||||
})
|
||||
|
||||
export const deleteAppointmentInputSchema = z.object({
|
||||
appointmentId: z.string().title('Appointment ID').describe('The unique ID of the appointment to delete'),
|
||||
})
|
||||
|
||||
export const deleteAppointmentOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the appointment was deleted successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('Response from Zoho confirming the appointment deletion'),
|
||||
})
|
||||
|
||||
export const sendMailInputSchema = z.object({
|
||||
module: z.string().title('Module').describe('The Zoho CRM module to send mail to'),
|
||||
recordId: z.string().title('Record ID').describe('The unique ID of the record to attach the file to'),
|
||||
data: z
|
||||
.string()
|
||||
.title('Data')
|
||||
.describe('The raw JSON string containing email details including recipient, subject, and body'),
|
||||
})
|
||||
|
||||
export const sendMailOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the email was sent successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('Response from Zoho after sending the email'),
|
||||
})
|
||||
|
||||
export const uploadFileInputSchema = z.object({
|
||||
fileUrl: z.string().title('File URL').describe('The url of the file being uploaded.'),
|
||||
})
|
||||
|
||||
export const uploadFileOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the file was uploaded successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('Details of the uploaded file, including its Zoho file ID.'),
|
||||
})
|
||||
|
||||
export const getFileInputSchema = z.object({
|
||||
fileId: z.string().title('File ID').describe('The encrypted ID of the file to retrieve'),
|
||||
})
|
||||
|
||||
export const getFileOutputSchema = z.object({
|
||||
success: z.boolean().title('Success').describe('Whether the file was retrieved successfully'),
|
||||
message: z.string().title('Message').describe('The message from the API call'),
|
||||
data: z.any().title('Data').describe('The retrieved file data from Zoho'),
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
export const DATA_CENTERS = ['us', 'eu', 'in', 'au', 'cn', 'jp', 'ca'] as const
|
||||
|
||||
export type DataCenter = (typeof DATA_CENTERS)[number]
|
||||
|
||||
export const DATA_CENTER_LABELS: Record<DataCenter, string> = {
|
||||
us: 'US - accounts.zoho.com',
|
||||
eu: 'EU - accounts.zoho.eu',
|
||||
in: 'IN - accounts.zoho.in',
|
||||
au: 'AU - accounts.zoho.com.au',
|
||||
cn: 'CN - accounts.zoho.com.cn',
|
||||
jp: 'JP - accounts.zoho.jp',
|
||||
ca: 'CA - accounts.zohocloud.ca',
|
||||
}
|
||||
|
||||
const ZOHO_AUTH_URLS: Record<DataCenter, string> = {
|
||||
us: 'https://accounts.zoho.com',
|
||||
eu: 'https://accounts.zoho.eu',
|
||||
in: 'https://accounts.zoho.in',
|
||||
au: 'https://accounts.zoho.com.au',
|
||||
cn: 'https://accounts.zoho.com.cn',
|
||||
jp: 'https://accounts.zoho.jp',
|
||||
ca: 'https://accounts.zohocloud.ca',
|
||||
}
|
||||
|
||||
const ZOHO_DATA_CENTER_TLDS: Record<DataCenter, string> = {
|
||||
us: 'com',
|
||||
eu: 'eu',
|
||||
in: 'in',
|
||||
au: 'com.au',
|
||||
cn: 'com.cn',
|
||||
jp: 'jp',
|
||||
ca: 'ca',
|
||||
}
|
||||
|
||||
export const DATA_CENTER_CHOICES: { label: string; value: DataCenter }[] = DATA_CENTERS.map((dataCenter) => ({
|
||||
label: DATA_CENTER_LABELS[dataCenter],
|
||||
value: dataCenter,
|
||||
}))
|
||||
|
||||
export const isDataCenter = (value: string | undefined): value is DataCenter =>
|
||||
DATA_CENTERS.includes(value as DataCenter)
|
||||
|
||||
export const getZohoAuthUrl = (dataCenter: DataCenter): string => ZOHO_AUTH_URLS[dataCenter]
|
||||
|
||||
export const getZohoApiBaseUrl = (dataCenter: DataCenter): string =>
|
||||
`https://www.zohoapis.${ZOHO_DATA_CENTER_TLDS[dataCenter]}`
|
||||
@@ -0,0 +1,24 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
import { DATA_CENTERS } from './data-centers'
|
||||
|
||||
export const zohoTokenResponseSchema = z.object({
|
||||
access_token: z.string().min(1),
|
||||
refresh_token: z.string().min(1),
|
||||
api_domain: z.string().optional(),
|
||||
expires_in: z.number().optional(),
|
||||
})
|
||||
|
||||
export const zohoCredentialsStateSchema = z.object({
|
||||
accessToken: z.string().title('Access Token').describe('Your Zoho Access Token'),
|
||||
refreshToken: z.string().optional().title('Refresh Token').describe('Your Zoho Refresh Token'),
|
||||
dataCenter: z.enum(DATA_CENTERS).optional().title('Data Center Region').describe('Zoho Data Center Region'),
|
||||
apiDomain: z.string().optional().title('API Domain').describe('Zoho API domain returned by OAuth'),
|
||||
expiresAt: z.number().optional().title('Expiration Timestamp').describe('Access token expiration timestamp'),
|
||||
})
|
||||
|
||||
export const zohoOAuthWizardStateSchema = z.object({
|
||||
dataCenter: z
|
||||
.enum(DATA_CENTERS)
|
||||
.title('Data Center Region')
|
||||
.describe('Zoho Data Center Region selected during OAuth setup'),
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import type * as bp from '.botpress'
|
||||
|
||||
export type Config = bp.configuration.Configuration
|
||||
export type IntegrationProps = bp.IntegrationProps
|
||||
|
||||
export type RegisterFunction = IntegrationProps['register']
|
||||
export type UnregisterFunction = IntegrationProps['unregister']
|
||||
export type Channels = IntegrationProps['channels']
|
||||
export type Handler = IntegrationProps['handler']
|
||||
export type Client = bp.Client
|
||||
@@ -0,0 +1,16 @@
|
||||
import { getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard'
|
||||
import * as wizard from './wizard'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export const oauthWizardHandler: bp.IntegrationProps['handler'] = async (props) => {
|
||||
const { logger } = props
|
||||
|
||||
try {
|
||||
return await wizard.handler(props)
|
||||
} catch (thrown: unknown) {
|
||||
const error = thrown instanceof Error ? thrown : Error(String(thrown))
|
||||
const message = `OAuth wizard error: ${error.message}`
|
||||
logger.forBot().error(message)
|
||||
return generateRedirection(getInterstitialUrl(false, message))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import * as oauthWizard from '@botpress/common/src/oauth-wizard'
|
||||
import axios from 'axios'
|
||||
import { DATA_CENTER_CHOICES, getZohoAuthUrl, isDataCenter } from '../misc/data-centers'
|
||||
import { zohoTokenResponseSchema } from '../misc/oauth-schemas'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
type WizardHandler = oauthWizard.WizardStepHandler<bp.HandlerProps>
|
||||
|
||||
const ZOHO_SCOPES = [
|
||||
'ZohoCRM.modules.ALL',
|
||||
'ZohoCRM.org.ALL',
|
||||
'ZohoCRM.users.ALL',
|
||||
'ZohoCRM.settings.ALL',
|
||||
'ZohoCRM.send_mail.all.CREATE',
|
||||
'ZohoCRM.files.CREATE',
|
||||
'ZohoCRM.files.READ',
|
||||
]
|
||||
|
||||
const _getOAuthRedirectUri = () => oauthWizard.getWizardStepUrl('oauth-callback').toString()
|
||||
|
||||
export const handler = async (props: bp.HandlerProps) => {
|
||||
return await new oauthWizard.OAuthWizardBuilder(props)
|
||||
.addStep({ id: 'start', handler: _startStep })
|
||||
.addStep({ id: 'oauth-redirect', handler: _oauthRedirectStep })
|
||||
.addStep({ id: 'oauth-callback', handler: _oauthCallbackStep })
|
||||
.build()
|
||||
.handleRequest()
|
||||
}
|
||||
|
||||
const _startStep: WizardHandler = ({ responses }) => {
|
||||
return responses.displayChoices({
|
||||
pageTitle: 'Connect Zoho CRM',
|
||||
htmlOrMarkdownPageContents: 'Select the Zoho data center for the account you want to connect.',
|
||||
choices: DATA_CENTER_CHOICES,
|
||||
nextStepId: 'oauth-redirect',
|
||||
defaultValues: ['us'],
|
||||
})
|
||||
}
|
||||
|
||||
const _oauthRedirectStep: WizardHandler = async ({ selectedChoice, responses, ctx, client }) => {
|
||||
if (!isDataCenter(selectedChoice)) {
|
||||
return responses.endWizard({
|
||||
success: false,
|
||||
errorMessage: 'Please select a valid Zoho data center.',
|
||||
})
|
||||
}
|
||||
|
||||
await client.setState({
|
||||
id: ctx.integrationId,
|
||||
type: 'integration',
|
||||
name: 'oauthWizard',
|
||||
payload: {
|
||||
dataCenter: selectedChoice,
|
||||
},
|
||||
})
|
||||
|
||||
const params = new URLSearchParams({
|
||||
scope: ZOHO_SCOPES.join(','),
|
||||
client_id: bp.secrets.CLIENT_ID,
|
||||
response_type: 'code',
|
||||
access_type: 'offline',
|
||||
prompt: 'consent',
|
||||
redirect_uri: _getOAuthRedirectUri(),
|
||||
state: ctx.webhookId,
|
||||
})
|
||||
|
||||
return responses.redirectToExternalUrl(`${getZohoAuthUrl(selectedChoice)}/oauth/v2/auth?${params.toString()}`)
|
||||
}
|
||||
|
||||
const _oauthCallbackStep: WizardHandler = async ({ query, responses, client, ctx, logger }) => {
|
||||
const error = query.get('error')
|
||||
if (error) {
|
||||
const description = query.get('error_description') ?? ''
|
||||
return responses.endWizard({ success: false, errorMessage: `OAuth error: ${error} - ${description}` })
|
||||
}
|
||||
|
||||
const code = query.get('code')
|
||||
if (!code) {
|
||||
return responses.endWizard({ success: false, errorMessage: 'Authorization code not present in OAuth callback.' })
|
||||
}
|
||||
|
||||
const returnedState = query.get('state')
|
||||
if (returnedState !== ctx.webhookId) {
|
||||
logger.forBot().warn('Invalid Zoho OAuth state parameter received.')
|
||||
return responses.endWizard({ success: false, errorMessage: 'Invalid OAuth state parameter.' })
|
||||
}
|
||||
|
||||
const { state } = await client.getState({
|
||||
id: ctx.integrationId,
|
||||
type: 'integration',
|
||||
name: 'oauthWizard',
|
||||
})
|
||||
const dataCenter = state.payload.dataCenter
|
||||
if (!isDataCenter(dataCenter)) {
|
||||
return responses.endWizard({ success: false, errorMessage: 'Zoho data center not found. Please reconnect.' })
|
||||
}
|
||||
|
||||
const requestData = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: bp.secrets.CLIENT_ID,
|
||||
client_secret: bp.secrets.CLIENT_SECRET,
|
||||
redirect_uri: _getOAuthRedirectUri(),
|
||||
code,
|
||||
})
|
||||
|
||||
let response
|
||||
try {
|
||||
response = await axios.post(`${getZohoAuthUrl(dataCenter)}/oauth/v2/token`, requestData.toString(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
logger.forBot().error('Zoho OAuth token exchange failed.', axios.isAxiosError(error) ? error.response?.data : error)
|
||||
return responses.endWizard({
|
||||
success: false,
|
||||
errorMessage: 'Zoho could not exchange the authorization code. Please reconnect and try again.',
|
||||
})
|
||||
}
|
||||
|
||||
const tokenResponse = zohoTokenResponseSchema.safeParse(response.data)
|
||||
if (!tokenResponse.success) {
|
||||
logger.forBot().warn('Zoho OAuth token response failed validation.', tokenResponse.error.message)
|
||||
return responses.endWizard({
|
||||
success: false,
|
||||
errorMessage: 'Zoho returned an invalid OAuth token response. Please reconnect and grant consent.',
|
||||
})
|
||||
}
|
||||
|
||||
await client.setState({
|
||||
id: ctx.integrationId,
|
||||
type: 'integration',
|
||||
name: 'credentials',
|
||||
payload: {
|
||||
accessToken: tokenResponse.data.access_token,
|
||||
refreshToken: tokenResponse.data.refresh_token,
|
||||
dataCenter,
|
||||
apiDomain: tokenResponse.data.api_domain,
|
||||
expiresAt: tokenResponse.data.expires_in ? Date.now() + tokenResponse.data.expires_in * 1000 : undefined,
|
||||
},
|
||||
})
|
||||
|
||||
await client.configureIntegration({ identifier: ctx.webhookId })
|
||||
|
||||
return responses.endWizard({ success: true })
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { isOAuthWizardUrl } from '@botpress/common/src/oauth-wizard'
|
||||
import type { Handler } from '../misc/types'
|
||||
import { oauthWizardHandler } from '../oauth-wizard'
|
||||
|
||||
export const handler: Handler = async (props) => {
|
||||
if (isOAuthWizardUrl(props.req.path)) {
|
||||
return await oauthWizardHandler(props)
|
||||
}
|
||||
|
||||
props.logger.forBot().warn('Received request for an invalid OAuth wizard endpoint.', {
|
||||
path: props.req.path,
|
||||
})
|
||||
|
||||
return {
|
||||
status: 404,
|
||||
body: 'Invalid OAuth wizard endpoint',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { register } from './register'
|
||||
export { unregister } from './unregister'
|
||||
export { handler } from './handler'
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as bpclient from '@botpress/client'
|
||||
import { getClient } from 'src/client'
|
||||
import type { RegisterFunction } from '../misc/types'
|
||||
|
||||
export const register: RegisterFunction = async ({ ctx, client, logger }) => {
|
||||
try {
|
||||
const zohoClient = await getClient(ctx, client)
|
||||
|
||||
await zohoClient.refreshAccessToken()
|
||||
|
||||
const orgResult = await zohoClient.getOrganizationDetails()
|
||||
|
||||
if (!orgResult.success || !orgResult.data || orgResult.data.length === 0) {
|
||||
throw new Error('Failed to retrieve organization details.')
|
||||
}
|
||||
|
||||
logger.forBot().info('Successfully accessed Zoho CRM: Integration can proceed')
|
||||
logger.forBot().debug(`Organization Details: ${JSON.stringify(orgResult.data)}`)
|
||||
} catch (error) {
|
||||
logger.forBot().error('Failed to access Zoho CRM: Check configuration', error)
|
||||
|
||||
throw new bpclient.RuntimeError('Configuration Error! Unable to retrieve organization details.')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { UnregisterFunction } from '../misc/types'
|
||||
|
||||
export const unregister: UnregisterFunction = async ({ logger }) => {
|
||||
logger.forBot().info('Unregister process for Zoho integration invoked. No resources to clean up.')
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist",
|
||||
"types": ["preact"]
|
||||
},
|
||||
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import config from '../../vitest.config'
|
||||
export default config
|
||||
Reference in New Issue
Block a user