diff --git a/integrations/hubspot/integration.definition.ts b/integrations/hubspot/integration.definition.ts index 9286fab854d..55ed12a5643 100644 --- a/integrations/hubspot/integration.definition.ts +++ b/integrations/hubspot/integration.definition.ts @@ -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', @@ -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', + }, }, }) diff --git a/integrations/hubspot/src/auth.ts b/integrations/hubspot/src/auth.ts index bb19d9d282b..0989dbfe9e3 100644 --- a/integrations/hubspot/src/auth.ts +++ b/integrations/hubspot/src/auth.ts @@ -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' @@ -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 @@ -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 }) @@ -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 diff --git a/integrations/hubspot/src/setup.ts b/integrations/hubspot/src/setup.ts index 12f657d33a4..857a2142a65 100644 --- a/integrations/hubspot/src/setup.ts +++ b/integrations/hubspot/src/setup.ts @@ -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 () => {} diff --git a/integrations/sendgrid/integration.definition.ts b/integrations/sendgrid/integration.definition.ts index 30abf6b3f65..f9823c3eed0 100644 --- a/integrations/sendgrid/integration.definition.ts +++ b/integrations/sendgrid/integration.definition.ts @@ -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.', @@ -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( diff --git a/integrations/sendgrid/src/actions/send-mail.ts b/integrations/sendgrid/src/actions/send-mail.ts index 11b43aa78ef..be804751514 100644 --- a/integrations/sendgrid/src/actions/send-mail.ts +++ b/integrations/sendgrid/src/actions/send-mail.ts @@ -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, @@ -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.') } diff --git a/integrations/sendgrid/src/index.ts b/integrations/sendgrid/src/index.ts index 00433802f52..e0e18f5422b 100644 --- a/integrations/sendgrid/src/index.ts +++ b/integrations/sendgrid/src/index.ts @@ -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' @@ -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) { diff --git a/integrations/sendgrid/src/misc/sendgrid-api.ts b/integrations/sendgrid/src/misc/sendgrid-api.ts new file mode 100644 index 00000000000..e0ad15fa2df --- /dev/null +++ b/integrations/sendgrid/src/misc/sendgrid-api.ts @@ -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 + } +} diff --git a/integrations/webflow/integration.definition.ts b/integrations/webflow/integration.definition.ts index 48c923d99d4..679c476881a 100644 --- a/integrations/webflow/integration.definition.ts +++ b/integrations/webflow/integration.definition.ts @@ -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',