chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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.')
|
||||
}
|
||||
Reference in New Issue
Block a user