Skip to content

Zendesk and Intercom integrations for Docs Agents #836

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
263dd81
Start
SamyPesse May 30, 2025
2e0aa36
Merge branch 'main' into zendesk
SamyPesse May 30, 2025
5122d4d
Merge branch 'main' into zendesk
SamyPesse May 30, 2025
bb3a7e3
Auth and configuration
SamyPesse Jun 1, 2025
cd52dba
Implement auth
SamyPesse Jun 1, 2025
0368320
Create or update webhook
SamyPesse Jun 1, 2025
0aff3ae
Start ingestion of tickets
SamyPesse Jun 1, 2025
a4abdf9
Dont push empty conversations
SamyPesse Jun 1, 2025
57c281a
Set url for conversations
SamyPesse Jun 1, 2025
0a6879a
Handle webhooks
SamyPesse Jun 1, 2025
4512a0a
Migrate to latest version of CF runtime
SamyPesse Jun 1, 2025
07eebd9
Start intercom
SamyPesse Jun 2, 2025
5e5829f
Cleanup
SamyPesse Jun 2, 2025
80171f6
Format
SamyPesse Jun 2, 2025
ae7395b
Fix TS error
SamyPesse Jun 2, 2025
6a8bed3
Format
SamyPesse Jun 2, 2025
9e423be
Fix check command
SamyPesse Jun 2, 2025
2ed0480
Improve check to use test env
SamyPesse Jun 2, 2025
d8cb0ee
Fix check not running in env
SamyPesse Jun 2, 2025
24e4432
Publish as v2
SamyPesse Jun 3, 2025
6a2177f
Fix webhook and team listing
SamyPesse Jun 3, 2025
4877adc
Fix request
SamyPesse Jun 3, 2025
eb4f404
Format
SamyPesse Jun 3, 2025
60b6aca
Add config for staging
SamyPesse Jun 3, 2025
a7853e3
Update configs
SamyPesse Jun 4, 2025
f964eaf
Install op
SamyPesse Jun 4, 2025
a502574
Auth 1password
SamyPesse Jun 4, 2025
d9eaff7
Update descriptions
SamyPesse Jun 4, 2025
a0ed3c4
Test it with credentials
SamyPesse Jun 4, 2025
b76a28b
Try running signin
SamyPesse Jun 5, 2025
d7282ea
Try again
SamyPesse Jun 5, 2025
15785ed
Fix env
SamyPesse Jun 5, 2025
a46dae3
Merge branch 'main' into zendesk
SamyPesse Jun 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/composite/setup-1password-op/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Setup 1Password CLI
description: Install 1Password CLI
inputs:
OP_SERVICE_ACCOUNT_TOKEN:
description: Service account token for 1Password
required: true
runs:
using: 'composite'
steps:
- name: Install 1Password CLI
shell: bash
run: curl -sSfo op.zip
https://cache.agilebits.com/dist/1P/op2/pkg/v2.31.1/op_linux_amd64_v2.31.1.zip
&& unzip -od /usr/local/bin/ op.zip
&& rm op.zip
- name: Setup 1Password CLI
shell: bash
run: op user get --me
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.OP_SERVICE_ACCOUNT_TOKEN }}
5 changes: 5 additions & 0 deletions .github/workflows/production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
uses: oven-sh/setup-bun@v2
with:
bun-version-file: 'package.json'
- name: Setup 1Password CLI
uses: ./.github/composite/setup-1password-op
with:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- name: Install dependencies
run: bun install --frozen-lockfile

Expand All @@ -42,6 +46,7 @@ jobs:
GITBOOK_TOKEN: ${{ secrets.GITBOOK_PROD_API_TOKEN }}
GITBOOK_ENDPOINT: https://api.gitbook.com
GITBOOK_ORGANIZATION: gitbook
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
# GitHub Files
UNFURL_GITHUB_CLIENT_ID: ${{ secrets.UNFURL_GITHUB_CLIENT_ID }}
UNFURL_GITHUB_CLIENT_SECRET: ${{ secrets.UNFURL_GITHUB_CLIENT_SECRET }}
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ jobs:
uses: oven-sh/setup-bun@v2
with:
bun-version-file: 'package.json'
- name: Setup 1Password CLI
uses: ./.github/composite/setup-1password-op
with:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Publish all integrations assets to staging
Expand All @@ -60,6 +64,7 @@ jobs:
GITBOOK_TOKEN: ${{ secrets.GITBOOK_STAGING_API_TOKEN }}
GITBOOK_ENDPOINT: https://api.gitbook-staging.com
GITBOOK_ORGANIZATION: gitbookio
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
# GitHub Files
UNFURL_GITHUB_CLIENT_ID: ${{ secrets.UNFURL_GITHUB_CLIENT_ID }}
UNFURL_GITHUB_CLIENT_SECRET: ${{ secrets.UNFURL_GITHUB_CLIENT_SECRET }}
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,16 @@ jobs:
uses: oven-sh/setup-bun@v2
with:
bun-version-file: 'package.json'
- name: Setup 1Password CLI
uses: ./.github/composite/setup-1password-op
with:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Check
run: bun run check
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
# GitHub Files
UNFURL_GITHUB_CLIENT_ID: ${{ secrets.UNFURL_GITHUB_CLIENT_ID }}
UNFURL_GITHUB_CLIENT_SECRET: ${{ secrets.UNFURL_GITHUB_CLIENT_SECRET }}
Expand Down
125 changes: 112 additions & 13 deletions bun.lock

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions integrations/intercom-conversations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Intercom Conversations Integration

This integration allows the users to ingest closed support conversations from Intercom into GitBook.

## Setup Instructions for development

1. Create an Intercom OAuth application at https://app.intercom.com/a/apps/_/developer-hub
2. Set `https://<integration-domain>/v1/integrations/intercom-conversations/integration/oauth` as the redirect URL
3. Get the client ID and secret
It is recommended to store them in 1Password
4. Set `https://<integration-domain>/v1/integrations/intercom-conversations/integration/webhook` as the webhook URL
1. Go to https://app.intercom.com/a/apps/_/developer-hub/webhooks
2. Create a new webhook
3. Set the webhook URL to `https://<integration-domain>/v1/integrations/intercom-conversations/integration/webhook`
4. Select the "Conversation closed" event type
5. Save the webhook
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions integrations/intercom-conversations/gitbook-manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: intercom-conversations
title: Intercom Connector
icon: ./assets/icon.png
description: Ingest Intercom support conversations into GitBook for auto-improvements.
visibility: public
script: ./src/index.ts
summary: |
# Overview

Automatically get AI-suggested change requests for your docs based on Intercom support conversations.
scopes:
- conversations:ingest
organization: gitbook
configurations:
account:
componentId: config
secrets:
CLIENT_ID: ${{ env.INTERCOM_CLIENT_ID }}
CLIENT_SECRET: ${{ env.INTERCOM_CLIENT_SECRET }}
target: organization
envs:
test:
secrets:
CLIENT_ID: ${{ "op://gitbook-integrations/intercomConversationsStaging/CLIENT_ID" }}
CLIENT_SECRET: ${{ "op://gitbook-integrations/intercomConversationsStaging/CLIENT_SECRET" }}
staging:
secrets:
CLIENT_ID: ${{ "op://gitbook-integrations/intercomConversationsStaging/CLIENT_ID" }}
CLIENT_SECRET: ${{ "op://gitbook-integrations/intercomConversationsStaging/CLIENT_SECRET" }}
dev-samy:
organization: 27dLEczo4lLcYIQhRuxg
secrets:
CLIENT_ID: ${{ "op://gitbook-integrations/IntercomDevSamyConversations/CLIENT_ID" }}
CLIENT_SECRET: ${{ "op://gitbook-integrations/IntercomDevSamyConversations/CLIENT_SECRET" }}
20 changes: 20 additions & 0 deletions integrations/intercom-conversations/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@gitbook/integration-intercom-conversations",
"version": "0.0.1",
"private": true,
"dependencies": {
"@gitbook/runtime": "*",
"@gitbook/api": "*",
"p-map": "^7.0.3",
"intercom-client": "^6.3.0"
},
"devDependencies": {
"@gitbook/cli": "workspace:*",
"@gitbook/tsconfig": "workspace:*"
},
"scripts": {
"typecheck": "tsc --noEmit",
"check": "gitbook check",
"publish-integrations-staging": "gitbook publish ."
}
}
41 changes: 41 additions & 0 deletions integrations/intercom-conversations/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { IntercomClient } from 'intercom-client';
import { IntercomRuntimeContext } from './types';
import { ExposableError, getOAuthToken, OAuthConfig } from '@gitbook/runtime';

/**
* Get the OAuth configuration for the Intercom integration.
*/
export function getIntercomOAuthConfig(context: IntercomRuntimeContext) {
const config: OAuthConfig = {
redirectURL: `${context.environment.integration.urls.publicEndpoint}/oauth`,
clientId: context.environment.secrets.CLIENT_ID,
clientSecret: context.environment.secrets.CLIENT_SECRET,
scopes: ['conversations.read'],
authorizeURL: () => 'https://app.intercom.com/oauth',
accessTokenURL: () => 'https://api.intercom.io/auth/eagle/token',
};

return config;
}

/**
* Initialize an Intercom API client for a given installation.
*/
export async function getIntercomClient(context: IntercomRuntimeContext) {
const { installation } = context.environment;

if (!installation) {
throw new Error('Installation not found');
}

const { oauth_credentials } = installation.configuration;
if (!oauth_credentials) {
throw new Error('Intercom OAuth credentials not found');
}

const token = await getOAuthToken(oauth_credentials, getIntercomOAuthConfig(context), context);

return new IntercomClient({
token,
});
}
44 changes: 44 additions & 0 deletions integrations/intercom-conversations/src/config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { createComponent, InstallationConfigurationProps } from '@gitbook/runtime';
import { IntercomRuntimeContext, IntercomRuntimeEnvironment } from './types';

/**
* Configuration component for the Intercom integration.
*/
export const configComponent = createComponent<
InstallationConfigurationProps<IntercomRuntimeEnvironment>,
{},
undefined,
IntercomRuntimeContext
>({
componentId: 'config',
render: async (element, context) => {
const { installation } = context.environment;
if (!installation) {
return null;
}

return (
<configuration>
<input
label="Authenticate"
hint="Authorize GitBook to access your Intercom account."
element={
<button
style="secondary"
disabled={false}
label={
element.props.installation.configuration.oauth_credentials
? 'Re-authorize'
: 'Authorize'
}
onPress={{
action: '@ui.url.open',
url: `${installation?.urls.publicEndpoint}/oauth`,
}}
/>
}
/>
</configuration>
);
},
});
140 changes: 140 additions & 0 deletions integrations/intercom-conversations/src/conversations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import pMap from 'p-map';
import { IntercomClient, Intercom } from 'intercom-client';
import { ConversationInput } from '@gitbook/api';
import { IntercomRuntimeContext } from './types';
import { getIntercomClient } from './client';

/**
* Ingest the last closed conversations from Intercom.
*/
export async function ingestConversations(context: IntercomRuntimeContext) {
const { installation } = context.environment;
if (!installation) {
throw new Error('Installation not found');
}

const intercom = await getIntercomClient(context);

let pageIndex = 0;
const perPage = 100;
const maxPages = 10;

let page = await intercom.conversations.search(
{
query: {
operator: 'AND',
value: [
{
field: 'open',
operator: '=',
// @ts-ignore
value: false,
},
],
},
pagination: { per_page: perPage },
},
{
// https://github.com/intercom/intercom-node/issues/460
headers: { Accept: 'application/json' },
},
);

while (pageIndex < maxPages) {
pageIndex += 1;
console.log(`Found ${page.data.length} conversations`);

const gitbookConversations = await pMap(
page.data,
async (conversation) => {
return await parseConversationAsGitBook(context, intercom, conversation);
},
{
concurrency: 3,
},
);

if (gitbookConversations.length > 0) {
await context.api.orgs.ingestConversation(
installation.target.organization,
gitbookConversations,
);
}

if (!page.hasNextPage()) {
break;
}
page = await page.getNextPage();
}
}

/**
* Parse an Intercom conversation into a GitBook conversation.
*/
export async function parseConversationAsGitBook(
context: IntercomRuntimeContext,
intercom: IntercomClient,
partialConversation: Intercom.Conversation,
): Promise<ConversationInput> {
if (partialConversation.state !== 'closed') {
throw new Error(`Conversation ${partialConversation.id} is not closed`);
}

const { installation } = context.environment;
if (!installation) {
throw new Error('Installation not found');
}

const resultConversation: ConversationInput = {
id: partialConversation.id,
metadata: {
url: `https://app.intercom.com/a/inbox/_/inbox/conversation/${partialConversation.id}`,
attributes: {},
createdAt: new Date(partialConversation.created_at * 1000).toISOString(),
},
parts: [],
};

if (partialConversation.source.subject) {
resultConversation.subject = partialConversation.source.subject;
}

if (partialConversation.source.body) {
resultConversation.parts.push({
type: 'message',
role: 'user',
body: partialConversation.source.body,
});
}

const conversation = await intercom.conversations.find(
{
conversation_id: partialConversation.id,
},
{
// https://github.com/intercom/intercom-node/issues/460
headers: { Accept: 'application/json' },
},
);
for (const part of conversation.conversation_parts?.conversation_parts ?? []) {
if (part.author.type === 'bot') {
continue;
}

switch (part.part_type) {
case 'open':
case 'comment': {
if (part.body) {
resultConversation.parts.push({
type: 'message',
role: part.author.type === 'user' ? 'user' : 'assistant',
body: part.body,
});
}
break;
}
}
}

return resultConversation;
}
Loading