Skip to content

Commit acbbe9f

Browse files
valentin0hopencode
andauthored
HubSpot conversations (#932)
* Add HubSpot Conversations integration - Initial implementation of HubSpot Conversations integration - Support for webhook handling and conversation management - Configuration UI for HubSpot API integration - Client-side conversation display components - TypeScript types and API client setup * Improve HubSpot Conversations integration production readiness - Add webhook signature verification for security using SHA256 HMAC - Add production environment configuration to manifest - Implement rate limiting and retry logic with exponential backoff - Improve error handling for message fetching failures - Add proper logging for debugging and monitoring 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode <[email protected]> * remove exponential backoff * logo * fix signature validation * format * fix check * clean up --------- Co-authored-by: opencode <[email protected]>
1 parent 44a134e commit acbbe9f

File tree

13 files changed

+793
-3
lines changed

13 files changed

+793
-3
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@gitbook/integration-hubspot-conversations": minor
3+
---
4+
5+
Add HubSpot Conversations integration for GitBook

bun.lock

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
},
9191
"integrations/figma": {
9292
"name": "@gitbook/integration-figma",
93-
"version": "0.2.3",
93+
"version": "0.3.0",
9494
"dependencies": {
9595
"@gitbook/runtime": "*",
9696
},
@@ -237,6 +237,20 @@
237237
"@gitbook/tsconfig": "workspace:*",
238238
},
239239
},
240+
"integrations/hubspot-conversations": {
241+
"name": "@gitbook/integration-hubspot-conversations",
242+
"version": "0.0.1",
243+
"dependencies": {
244+
"@gitbook/api": "*",
245+
"@gitbook/runtime": "*",
246+
"itty-router": "^2.6.1",
247+
"p-map": "^7.0.3",
248+
},
249+
"devDependencies": {
250+
"@gitbook/cli": "workspace:*",
251+
"@gitbook/tsconfig": "workspace:*",
252+
},
253+
},
240254
"integrations/intercom": {
241255
"name": "@gitbook/integration-intercom",
242256
"version": "0.5.2",
@@ -493,7 +507,7 @@
493507
},
494508
"integrations/slack": {
495509
"name": "@gitbook/integration-slack",
496-
"version": "2.1.0",
510+
"version": "2.2.0",
497511
"dependencies": {
498512
"@gitbook/api": "*",
499513
"@gitbook/runtime": "*",
@@ -614,7 +628,7 @@
614628
},
615629
"packages/api": {
616630
"name": "@gitbook/api",
617-
"version": "0.131.0",
631+
"version": "0.133.0",
618632
"dependencies": {
619633
"event-iterator": "^2.0.0",
620634
"eventsource-parser": "^3.0.0",
@@ -975,6 +989,8 @@
975989

976990
"@gitbook/integration-hotjar": ["@gitbook/integration-hotjar@workspace:integrations/hotjar"],
977991

992+
"@gitbook/integration-hubspot-conversations": ["@gitbook/integration-hubspot-conversations@workspace:integrations/hubspot-conversations"],
993+
978994
"@gitbook/integration-intercom": ["@gitbook/integration-intercom@workspace:integrations/intercom"],
979995

980996
"@gitbook/integration-intercom-conversations": ["@gitbook/integration-intercom-conversations@workspace:integrations/intercom-conversations"],
@@ -2513,6 +2529,8 @@
25132529

25142530
"@gitbook/integration-gitlab/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
25152531

2532+
"@gitbook/integration-hubspot-conversations/itty-router": ["[email protected]", "", {}, "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g=="],
2533+
25162534
"@gitbook/integration-jira/itty-router": ["[email protected]", "", {}, "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g=="],
25172535

25182536
"@gitbook/integration-lucid/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# HubSpot Conversations Integration
2+
3+
This integration allows GitBook to ingest support conversations from HubSpot to provide AI-suggested improvements to your documentation.
4+
5+
## Features
6+
7+
- **Bulk Conversation Import**: When first installed, the integration fetches recent closed conversations from HubSpot
8+
- **Real-time Updates**: Receives webhook notifications when conversations are closed and ingests them automatically
9+
10+
## Setup
11+
12+
1. Install the integration in your GitBook organization
13+
2. Click "Authorize" to connect your HubSpot account
14+
3. The integration will automatically start ingesting closed conversations
15+
16+
17+
## Development
18+
19+
To develop and test this integration, you'll need to create a HubSpot public app with the following configuration:
20+
21+
### 1. Create a HubSpot Public App
22+
23+
1. Go to the [HubSpot Developer Portal](https://developers.hubspot.com/) - create a developer account if you don't have one
24+
2. Navigate to "Apps" and click "Create app"
25+
3. Fill in your app details (name, description, etc.), e.g "GitBook Conversations Integration - <your-env>"
26+
27+
### 2. Configure OAuth Scopes
28+
29+
In your HubSpot app settings, add the following OAuth scopes:
30+
- `oauth` - Required for OAuth authentication flow
31+
- `conversations.read` - Required to read conversation data from HubSpot
32+
33+
### 3. Set Up Webhook Subscription
34+
35+
Configure a webhook subscription for conversation property changes:
36+
37+
1. In your HubSpot app, go to the "Webhooks" section
38+
2. Add a new webhook subscription with:
39+
- **Subscription Type**: `conversation.propertyChange`
40+
- **Webhook URL**: `https://<your-integration-env>/v1/integrations/hubspot-conversations/integration/webhook`
41+
- **Property**: `status` (the integration listens for status changes to "CLOSED")
42+
43+
### 4. Environment Variables
44+
45+
Set the following environment variables in your development environment:
46+
- `HUBSPOT_CLIENT_ID` - Your HubSpot app's client ID
47+
- `HUBSPOT_CLIENT_SECRET` - Your HubSpot app's client secret
48+
62.4 KB
Loading
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: hubspot-conversations
2+
title: HubSpot Connector
3+
icon: ./assets/icon.png
4+
description: Ingest HubSpot support conversations into GitBook for auto-improvements.
5+
visibility: public
6+
script: ./src/index.ts
7+
summary: |
8+
# Overview
9+
10+
Automatically get AI-suggested change requests for your docs based on HubSpot support conversations.
11+
scopes:
12+
- conversations:ingest
13+
organization: gitbook
14+
configurations:
15+
account:
16+
componentId: config
17+
secrets:
18+
CLIENT_ID: ${{ env.HUBSPOT_CLIENT_ID }}
19+
CLIENT_SECRET: ${{ env.HUBSPOT_CLIENT_SECRET }}
20+
target: organization
21+
envs:
22+
test:
23+
secrets:
24+
CLIENT_ID: ${{ "op://gitbook-integrations/hubspotConversationsStaging/CLIENT_ID" }}
25+
CLIENT_SECRET: ${{ "op://gitbook-integrations/hubspotConversationsStaging/CLIENT_SECRET" }}
26+
staging:
27+
secrets:
28+
CLIENT_ID: ${{ "op://gitbook-integrations/hubspotConversationsStaging/CLIENT_ID" }}
29+
CLIENT_SECRET: ${{ "op://gitbook-integrations/hubspotConversationsStaging/CLIENT_SECRET" }}
30+
production:
31+
secrets:
32+
CLIENT_ID: ${{ "op://gitbook-integrations/hubspotConversationsProduction/CLIENT_ID" }}
33+
CLIENT_SECRET: ${{ "op://gitbook-integrations/hubspotConversationsProduction/CLIENT_SECRET" }}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@gitbook/integration-hubspot-conversations",
3+
"version": "0.0.1",
4+
"private": true,
5+
"dependencies": {
6+
"@gitbook/api": "*",
7+
"@gitbook/runtime": "*",
8+
"itty-router": "^2.6.1",
9+
"p-map": "^7.0.3"
10+
},
11+
"devDependencies": {
12+
"@gitbook/cli": "workspace:*",
13+
"@gitbook/tsconfig": "workspace:*"
14+
},
15+
"scripts": {
16+
"typecheck": "tsc --noEmit",
17+
"check": "gitbook check",
18+
"publish-integrations-staging": "gitbook publish . --env staging"
19+
}
20+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { ExposableError, getOAuthToken, Logger, OAuthConfig } from '@gitbook/runtime';
2+
import { HubSpotRuntimeContext, HubSpotAccountInfo } from './types';
3+
4+
const logger = Logger('hubspot-conversations:client');
5+
6+
/**
7+
* Get the OAuth configuration for the HubSpot integration.
8+
*/
9+
export function getHubSpotOAuthConfig(context: HubSpotRuntimeContext) {
10+
const config: OAuthConfig = {
11+
redirectURL: `${context.environment.integration.urls.publicEndpoint}/oauth`,
12+
clientId: context.environment.secrets.CLIENT_ID,
13+
clientSecret: context.environment.secrets.CLIENT_SECRET,
14+
scopes: ['oauth', 'conversations.read'],
15+
authorizeURL: () => 'https://app.hubspot.com/oauth/authorize',
16+
accessTokenURL: () => 'https://api.hubapi.com/oauth/v1/token',
17+
extractCredentials: async (response) => {
18+
if (!response.access_token) {
19+
throw new Error(
20+
`Failed to exchange code for access token: ${JSON.stringify(response)}`,
21+
);
22+
}
23+
24+
logger.debug('HubSpot OAuth response received');
25+
26+
// Get account information using the access token
27+
try {
28+
const accountResponse = await fetch(
29+
'https://api.hubapi.com/account-info/v3/details',
30+
{
31+
headers: {
32+
Authorization: `Bearer ${response.access_token}`,
33+
'Content-Type': 'application/json',
34+
},
35+
},
36+
);
37+
38+
if (!accountResponse.ok) {
39+
throw new Error(
40+
`Failed to fetch account info: ${accountResponse.status} ${accountResponse.statusText}`,
41+
);
42+
}
43+
44+
const accountData = (await accountResponse.json()) as HubSpotAccountInfo;
45+
logger.info('Retrieved HubSpot account info', { portalId: accountData.portalId });
46+
47+
const portalId = accountData.portalId?.toString();
48+
if (!portalId) {
49+
throw new ExposableError('No portalId found in account info response');
50+
}
51+
52+
return {
53+
externalIds: [portalId],
54+
configuration: {
55+
oauth_credentials: {
56+
access_token: response.access_token,
57+
refresh_token: response.refresh_token || '',
58+
},
59+
},
60+
};
61+
} catch (error) {
62+
logger.error('Failed to fetch HubSpot account info', {
63+
error: error instanceof Error ? error.message : String(error),
64+
});
65+
throw new ExposableError(`Failed to get HubSpot account ID: ${error}`);
66+
}
67+
},
68+
};
69+
70+
return config;
71+
}
72+
73+
/**
74+
* Get the access token for HubSpot API calls.
75+
*/
76+
export async function getHubSpotAccessToken(context: HubSpotRuntimeContext) {
77+
const { installation } = context.environment;
78+
79+
if (!installation) {
80+
throw new Error('Installation not found');
81+
}
82+
83+
const { oauth_credentials } = installation.configuration;
84+
if (!oauth_credentials) {
85+
throw new Error('HubSpot OAuth credentials not found');
86+
}
87+
88+
return await getOAuthToken(oauth_credentials, getHubSpotOAuthConfig(context), context);
89+
}
90+
91+
/**
92+
* Make a request to the HubSpot API.
93+
*/
94+
export async function hubspotApiRequest<T = unknown>(
95+
context: HubSpotRuntimeContext,
96+
path: string,
97+
options: {
98+
method?: string;
99+
body?: unknown;
100+
params?: Record<string, string>;
101+
} = {},
102+
): Promise<T> {
103+
const token = await getHubSpotAccessToken(context);
104+
const { method = 'GET', body, params } = options;
105+
106+
const url = new URL(`https://api.hubapi.com${path}`);
107+
if (params) {
108+
Object.entries(params).forEach(([key, value]) => {
109+
url.searchParams.append(key, value);
110+
});
111+
}
112+
113+
const response = await fetch(url.toString(), {
114+
method,
115+
headers: {
116+
Authorization: `Bearer ${token}`,
117+
'Content-Type': 'application/json',
118+
},
119+
body: body ? JSON.stringify(body) : undefined,
120+
});
121+
122+
if (!response.ok) {
123+
throw new Error(`HubSpot API request failed: ${response.status} ${response.statusText}`);
124+
}
125+
126+
return (await response.json()) as T;
127+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { createComponent, InstallationConfigurationProps } from '@gitbook/runtime';
2+
import { HubSpotRuntimeContext, HubSpotRuntimeEnvironment } from './types';
3+
4+
/**
5+
* Configuration component for the HubSpot integration.
6+
*/
7+
export const configComponent = createComponent<
8+
InstallationConfigurationProps<HubSpotRuntimeEnvironment>,
9+
{},
10+
undefined,
11+
HubSpotRuntimeContext
12+
>({
13+
componentId: 'config',
14+
render: async (element, context) => {
15+
const { installation } = context.environment;
16+
if (!installation) {
17+
return null;
18+
}
19+
20+
return (
21+
<configuration>
22+
<input
23+
label="Authenticate"
24+
hint={'Authorize GitBook to access your HubSpot account.'}
25+
element={
26+
<button
27+
style="secondary"
28+
disabled={false}
29+
label={
30+
element.props.installation.configuration.oauth_credentials
31+
? 'Re-authorize'
32+
: 'Authorize'
33+
}
34+
onPress={{
35+
action: '@ui.url.open',
36+
url: `${installation?.urls.publicEndpoint}/oauth`,
37+
}}
38+
/>
39+
}
40+
/>
41+
42+
{element.props.installation.configuration.oauth_credentials ? (
43+
<hint>
44+
<text>
45+
The integration is configured and conversations are being ingested.
46+
</text>
47+
</hint>
48+
) : null}
49+
</configuration>
50+
);
51+
},
52+
});

0 commit comments

Comments
 (0)