Skip to content
34 changes: 24 additions & 10 deletions integrations/hubspot/integration.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,29 @@ export default new IntegrationDefinition({
name: 'hubspot',
title: 'HubSpot',
description: 'Manage contacts, tickets and more from your chatbot.',
version: '1.0.0',
version: '2.0.1',
readme: 'hub.md',
icon: 'icon.svg',
configuration: {
schema: z.object({
accessToken: z.string().min(1).secret().title('Access Token').describe('Your Hubspot Access Token'),
clientSecret: z
.string()
.secret()
.optional()
.title('Client Secret')
.describe('Hubspot Client Secret (used for webhook signature check)'),
}),
schema: z.object({}),
identifier: {
linkTemplateScript: 'linkTemplate.vrl',
},
},
configurations: {
manual: {
title: 'Manual Configuration',
description: 'Manual configuration, use your own Hubspot app',
schema: z.object({
accessToken: z.string().min(1).secret().title('Access Token').describe('Your Hubspot Access Token'),
clientSecret: z
.string()
.secret()
.optional()
.title('Client Secret')
.describe('Hubspot Client Secret (used for webhook signature check)'),
}),
},
},
identifier: {
extractScript: 'extract.vrl',
Expand Down Expand Up @@ -262,5 +272,9 @@ export default new IntegrationDefinition({
CLIENT_SECRET: {
description: 'The client secret of the Hubspot app',
},
DISABLE_OAUTH: {
// TODO: Remove once the OAuth app allows for unlimited installs
description: 'Whether to disable OAuth',
},
},
})
17 changes: 11 additions & 6 deletions integrations/hubspot/src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RuntimeError } from '@botpress/sdk'
import { RuntimeError, isApiError } from '@botpress/sdk'
import { Client as OfficialHubspotClient } from '@hubspot/api-client'
import * as bp from '.botpress'

Expand Down Expand Up @@ -54,7 +54,14 @@ const _getOrRefreshOAuthAccessToken = async ({ client, ctx }: { client: bp.Clien
state: {
payload: { accessToken, refreshToken, expiresAtSeconds },
},
} = await client.getState({ type: 'integration', name: 'oauthCredentials', id: ctx.integrationId })
} = await client
.getState({ type: 'integration', name: 'oauthCredentials', id: ctx.integrationId })
.catch((e: unknown) => {
if (isApiError(e) && e.code === 404) {
throw new RuntimeError('OAuth credentials not found, please reauthorize')
}
throw e
})
const nowSeconds = Date.now() / 1000
if (nowSeconds <= expiresAtSeconds - FIVE_MINUTES_IN_SECONDS) {
return accessToken
Expand Down Expand Up @@ -84,8 +91,7 @@ const _getOrRefreshOAuthAccessToken = async ({ client, ctx }: { client: bp.Clien

export const getAccessToken = async ({ client, ctx }: { client: bp.Client; ctx: bp.Context }) => {
let accessToken: string | undefined
// TODO: re-add oauth support and change this condition to === 'manual':
if (ctx.configurationType === null) {
if (ctx.configurationType === 'manual') {
accessToken = ctx.configuration.accessToken
} else {
accessToken = await _getOrRefreshOAuthAccessToken({ client, ctx })
Expand All @@ -100,8 +106,7 @@ export const getAccessToken = async ({ client, ctx }: { client: bp.Client; ctx:

export const getClientSecret = (ctx: bp.Context) => {
let clientSecret: string | undefined
// TODO: re-add oauth support and change this condition to === 'manual':
if (ctx.configurationType === null) {
if (ctx.configurationType === 'manual') {
clientSecret = ctx.configuration.clientSecret
} else {
clientSecret = bp.secrets.CLIENT_SECRET
Expand Down
10 changes: 9 additions & 1 deletion integrations/hubspot/src/setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { RuntimeError } from '@botpress/sdk'
import * as bp from '.botpress'

export const register: bp.IntegrationProps['register'] = async () => {}
export const register: bp.IntegrationProps['register'] = async ({ client, ctx }) => {
if (ctx.configurationType === null && bp.secrets.DISABLE_OAUTH === 'true') {
await client.configureIntegration({
identifier: null,
})
throw new RuntimeError('OAuth currently unavailable, please use manual configuration instead')
}
}
export const unregister: bp.IntegrationProps['unregister'] = async () => {}
4 changes: 2 additions & 2 deletions integrations/sendgrid/integration.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
export default new IntegrationDefinition({
name: 'sendgrid',
title: 'SendGrid',
version: '0.1.4',
version: '0.1.5',
readme: 'hub.md',
icon: 'icon.svg',
description: 'Send markdown rich-text emails using the SendGrid email service.',
Expand All @@ -21,7 +21,7 @@ export default new IntegrationDefinition({
apiKey: z.string().secret().min(1).describe('Your SendGrid API Key').title('SendGrid API Key'),
publicSignatureKey: z
.string()
// .secret() // Uncomment secret once the ZUI bug has been fixed (Linear Issue: DEV-3073)
.secret()
.min(1)
.optional()
.describe(
Expand Down
8 changes: 4 additions & 4 deletions integrations/sendgrid/src/actions/send-mail.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { RuntimeError } from '@botpress/sdk'
import sgMail from '@sendgrid/mail'
import { markdownToHtml } from '../misc/markdown-utils'
import { SendGridClient } from '../misc/sendgrid-api'
import { parseError } from '../misc/utils'
import * as bp from '.botpress'

export const sendMail: bp.IntegrationProps['actions']['sendMail'] = async ({ ctx, input, logger }) => {
try {
const [response] = await sgMail.send({
const httpClient = new SendGridClient(ctx.configuration.apiKey)
const response = await httpClient.sendMail({
personalizations: [
{
to: input.to,
Expand All @@ -20,8 +21,7 @@ export const sendMail: bp.IntegrationProps['actions']['sendMail'] = async ({ ctx
html: markdownToHtml(input.body),
})

if (response.statusCode < 200 && response.statusCode >= 300) {
// noinspection ExceptionCaughtLocallyJS
if (response.statusCode < 200 || response.statusCode >= 300) {
throw new RuntimeError('Failed to send email.')
}

Expand Down
15 changes: 4 additions & 11 deletions integrations/sendgrid/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import sgClient from '@sendgrid/client'
import sgMail from '@sendgrid/mail'
import actions from './actions'
import { SendGridClient } from './misc/sendgrid-api'
import { parseError } from './misc/utils'
import { parseWebhookData, verifyWebhookSignature } from './misc/webhook-utils'
import { dispatchIntegrationEvent } from './webhook-events/event-dispatcher'
Expand All @@ -9,17 +8,11 @@ import * as bp from '.botpress'

export default new bp.Integration({
register: async ({ ctx }) => {
sgClient.setApiKey(ctx.configuration.apiKey)
sgMail.setClient(sgClient)

try {
const [response] = await sgClient.request({
method: 'GET',
url: '/v3/scopes',
})
const httpClient = new SendGridClient(ctx.configuration.apiKey)
const response = await httpClient.getPermissionScopes()

if (response && response.statusCode < 200 && response.statusCode >= 300) {
// noinspection ExceptionCaughtLocallyJS
if (response && (response.statusCode < 200 || response.statusCode >= 300)) {
throw new Error(`The status code '${response.statusCode}' is not within the accepted bounds.`)
}
} catch (thrown: unknown) {
Expand Down
37 changes: 37 additions & 0 deletions integrations/sendgrid/src/misc/sendgrid-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import sgClient from '@sendgrid/client'
import sgMail, { MailDataRequired } from '@sendgrid/mail'

/** A class for making http requests to the SendGrid API
*
* @remark Always use this class over importing the client from either "@sendgrid/client" or "@sendgrid/mail".
* Otherwise, intermittent API key failures will occur. */
export class SendGridClient {
private _apiKey: string

public constructor(apiKey: string) {
this._apiKey = apiKey
}

private get _requestClient() {
sgClient.setApiKey(this._apiKey)
return sgClient
}

private get _mailClient() {
sgMail.setClient(this._requestClient)
return sgMail
}

public async getPermissionScopes() {
const [response] = await this._requestClient.request({
method: 'GET',
url: '/v3/scopes',
})
return response
}

public async sendMail(data: MailDataRequired) {
const [response] = await this._mailClient.send(data)
return response
}
}
4 changes: 2 additions & 2 deletions integrations/webflow/integration.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { actions } from './definitions/actions'

export default new IntegrationDefinition({
name: 'webflow',
version: '0.1.0',
title: 'Webflow CMS',
version: '3.0.0',
title: 'Webflow',
description: 'CRUD operations for Webflow CMS',
readme: 'hub.md',
icon: 'icon.svg',
Expand Down
Loading