Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions integrations/linkedin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Dependencies
node_modules/

# OS
.DS_Store

# Generated files
.botpress/
.claude/

# Planning
plan/
75 changes: 75 additions & 0 deletions integrations/linkedin/definitions/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as sdk from '@botpress/sdk'

const { z } = sdk

export const actions = {
createPost: {
title: 'Create Post',
description: 'Create a LinkedIn post with optional image or article link',
input: {
schema: z.object({
text: z
.string()
.min(1)
.max(3000)
.title('Post Text')
.describe('The text content of your LinkedIn post (required, max 3000 characters)'),
visibility: z
.enum(['PUBLIC', 'CONNECTIONS'])
.title('Visibility')
.describe('Who can see this post: PUBLIC (anyone) or CONNECTIONS (1st degree only)'),
imageUrl: z
.string()
.url()
.optional()
.title('Image URL')
.describe(
'URL of an image to attach (JPG, PNG, GIF, max 8MB). The image will be downloaded and uploaded to LinkedIn. If both imageUrl and articleUrl are provided, only the image will be used.'
),
articleUrl: z
.string()
.url()
.optional()
.title('Article URL')
.describe(
'URL of an article/link to share. LinkedIn will generate a preview card. Ignored if imageUrl is provided.'
),
articleTitle: z.string().optional().title('Article Title').describe('Custom title for the article preview.'),
articleDescription: z
.string()
.optional()
.title('Article Description')
.describe(
'Custom description for the article preview (optional - LinkedIn will scrape from URL if not provided)'
),
}),
},
output: {
schema: z.object({
postUrn: z
.string()
.title('Post URN')
.describe('The URN identifier of the created post (e.g., urn:li:ugcPost:123456)'),
postUrl: z.string().title('Post URL').describe('Direct link to view the post on LinkedIn'),
}),
},
},

deletePost: {
title: 'Delete Post',
description: 'Delete a LinkedIn post created by the authenticated user',
input: {
schema: z.object({
postUrn: z
.string()
.title('Post URN')
.describe('The URN of the post to delete (e.g., urn:li:ugcPost:123456 or urn:li:share:123456)'),
}),
},
output: {
schema: z.object({
success: z.boolean().title('Success').describe('Whether the post was successfully deleted'),
}),
},
},
} as const satisfies sdk.IntegrationDefinitionProps['actions']
32 changes: 32 additions & 0 deletions integrations/linkedin/definitions/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as sdk from '@botpress/sdk'

const { z } = sdk

export const configuration = {
identifier: {
linkTemplateScript: 'linkTemplate.vrl',
required: true,
},
schema: z.object({}),
} as const satisfies sdk.IntegrationDefinitionProps['configuration']

export const configurations = {
manual: {
title: 'Manual Configuration',
description: 'Configure with your own LinkedIn Developer application',
schema: z.object({
clientId: z.string().min(1).title('Client ID').describe('Client ID from your LinkedIn Developer application'),
clientSecret: z
.string()
.min(1)
.secret()
.title('Client Secret')
.describe('Primary Client Secret from your LinkedIn Developer application'),
authorizationCode: z.string().min(1).secret().title('Authorization Code').describe('Authorization Code'),
}),
},
} as const satisfies sdk.IntegrationDefinitionProps['configurations']

export const identifier = {
extractScript: 'extract.vrl',
} as const satisfies sdk.IntegrationDefinitionProps['identifier']
4 changes: 4 additions & 0 deletions integrations/linkedin/definitions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './secrets'
export * from './states'
export * from './configuration'
export * from './actions'
12 changes: 12 additions & 0 deletions integrations/linkedin/definitions/secrets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { posthogHelper } from '@botpress/common'
import * as sdk from '@botpress/sdk'

export const secrets = {
CLIENT_ID: {
description: 'Botpress LinkedIn OAuth Client ID',
},
CLIENT_SECRET: {
description: 'Botpress LinkedIn OAuth Client Secret',
},
...posthogHelper.COMMON_SECRET_NAMES,
} as const satisfies sdk.IntegrationDefinitionProps['secrets']
39 changes: 39 additions & 0 deletions integrations/linkedin/definitions/states.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as sdk from '@botpress/sdk'

const { z } = sdk

export const states = {
oauthCredentials: {
type: 'integration',
schema: z.object({
accessToken: z
.object({
token: z.string().secret(),
issuedAt: z.number(),
expiresAt: z.number(),
})
.title('Access Token')
.describe('The access token generated by LinkedIn'),
refreshToken: z
.object({
token: z.string().secret(),
issuedAt: z.number(),
expiresAt: z.number().optional(),
})
.optional()
.title('Refresh Token')
.describe('The refresh token generated by LinkedIn'),
grantedScopes: z.array(z.string()).title('Granted Scopes').describe('The scopes granted by the user'),
linkedInUserId: z.string().title('LinkedIn User ID').describe('The user ID of the authenticated user'),
}),
},
processedNotifications: {
type: 'integration',
schema: z.object({
notificationIds: z
.array(z.string())
.title('Notification IDs')
.describe('Rolling list of recently processed notification IDs to prevent duplicate processing'),
}),
},
} as const satisfies sdk.IntegrationDefinitionProps['states']
17 changes: 17 additions & 0 deletions integrations/linkedin/extract.vrl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Parse incoming webhook body
body = parse_json!(.body)

# LinkedIn webhooks contain actor/author URNs like "urn:li:person:abc123"
# We need to extract just the ID to match our OAuth identifier
# Fallback chain: actor -> author -> owner

actorUrn = body.actor ?? body.author ?? body.owner ?? ""

# Extract ID from URN format: "urn:li:person:abc123" -> "abc123"
# Split by colon and take the last segment
if actorUrn != "" {
parts = split(actorUrn, ":")
parts[length(parts) - 1]
} else {
null
}
73 changes: 73 additions & 0 deletions integrations/linkedin/hub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# LinkedIn

Connect your Botpress chatbot to LinkedIn to share posts and engage with your professional network. This integration enables your bot to interact with LinkedIn's API using OAuth authentication.

## Configuration

The LinkedIn integration requires OAuth authentication to establish a secure connection between Botpress and LinkedIn. You can configure the integration using either automatic or manual configuration methods.

### Automatic configuration with OAuth

To set up the LinkedIn integration using automatic configuration, click the authorization button and follow the on-screen instructions to connect your Botpress chatbot to LinkedIn.

When using this configuration mode, a Botpress-managed LinkedIn application will be used to connect to your LinkedIn account. Actions taken by the bot will be attributed to the LinkedIn account that authorized the connection.

#### Configuring the integration in Botpress

1. Authorize the LinkedIn integration by clicking the authorization button.
2. Follow the on-screen instructions to connect your Botpress chatbot to LinkedIn.
3. Once the connection is established, you can save the configuration and enable the integration.

### Manual configuration with OAuth

To set up the LinkedIn integration manually, you must create a LinkedIn application and configure OAuth credentials. You will also need to obtain an authorization code and configure the integration in Botpress.

#### Creating a LinkedIn Application

1. Go to the [LinkedIn Developer Portal](https://www.linkedin.com/developers/apps).
2. Click the `Create app` button.
3. Fill in the required information:
- App name
- LinkedIn Page (you must associate your app with a LinkedIn Page)
- App logo
- Legal agreement
4. Click `Create app` to create your application.

#### Configuring OAuth Settings

1. In your LinkedIn application settings, navigate to the `Products` tab.
2. Request access to the following products:
- `Share on LinkedIn` - Required for posting content
- `Sign In with LinkedIn using OpenID Connect` - Required for authentication
3. Wait for approval (this may be instant or require review by LinkedIn).
4. Navigate to the `Auth` tab.
5. Under `OAuth 2.0 settings`, add the following redirect URL:
```
https://webhook.botpress.cloud/oauth
```
6. Copy your **Client ID** and **Client Secret** for use in the next steps.

#### Authorizing the OAuth Application

1. Construct the authorization URL with your Client ID:

```
https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https://webhook.botpress.cloud/oauth&scope=openid%20profile%20email%20w_member_social&state=manual
```

> Replace `YOUR_CLIENT_ID` with your actual Client ID.

2. Visit this URL in your browser while logged into the LinkedIn account you want to use with the integration.
3. Follow the on-screen instructions to authorize the application.
4. You will be redirected to `webhook.botpress.cloud`. **Do not close this page**.
5. Copy the **authorization code** from the URL in your browser's address bar.
> The authorization code is the string that appears after `code=` in the URL.
6. You may now safely close this page.

#### Configuring the integration in Botpress

1. Select the `Manual` configuration mode in the Botpress integration settings.
2. Enter your LinkedIn **Client ID** and **Client Secret**.
3. Enter the **authorization code** you obtained in the previous step.
> The authorization code is only valid for a short period of time. If the code has expired, you will need to repeat the authorization steps to obtain a new code.
4. Save the configuration and enable the integration.
4 changes: 4 additions & 0 deletions integrations/linkedin/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions integrations/linkedin/integration.definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IntegrationDefinition } from '@botpress/sdk'
import { configuration, configurations, identifier, states, secrets, actions } from './definitions'

export const INTEGRATION_NAME = 'linkedin'
export const INTEGRATION_VERSION = '0.1.0'

export default new IntegrationDefinition({
name: INTEGRATION_NAME,
version: INTEGRATION_VERSION,
title: 'LinkedIn',
description: 'Connect to LinkedIn to share posts and engage with your professional network.',
readme: 'hub.md',
icon: 'icon.svg',
configuration,
configurations,
identifier,
states,
secrets,
actions,
})
12 changes: 12 additions & 0 deletions integrations/linkedin/linkTemplate.vrl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
webhookId = to_string!(.webhookId)
webhookUrl = to_string!(.webhookUrl)
env = to_string!(.env)

clientId = "789ku4ucqpvndz"
if env == "production" {
clientId = "786rl66mwxted8"
}

redirectUri = "{{ webhookUrl }}/oauth"

"https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id={{ clientId }}&redirect_uri={{ redirectUri }}&state={{ webhookId }}&scope=openid%20profile%20email%20w_member_social"
19 changes: 19 additions & 0 deletions integrations/linkedin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@botpresshub/linkedin",
"scripts": {
"build": "bp add -y && bp build",
"check:type": "tsc --noEmit",
"check:bplint": "bp lint"
},
"private": true,
"dependencies": {
"@botpress/sdk": "workspace:*"
},
"devDependencies": {
"@botpress/cli": "workspace:*",
"@botpress/client": "workspace:*",
"@botpress/common": "workspace:*",
"@types/node": "^22.16.4",
"typescript": "^5.6.3"
}
}
26 changes: 26 additions & 0 deletions integrations/linkedin/src/actions/create-post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LinkedInClient } from '../linkedin-api'
import * as bp from '.botpress'

export const createPost: bp.IntegrationProps['actions']['createPost'] = async ({ client, ctx, input, logger }) => {
const { text, visibility, imageUrl, articleUrl, articleTitle, articleDescription } = input

const linkedIn = await LinkedInClient.create({ client, ctx, logger })

if (imageUrl) {
logger.forBot().info('Processing image for LinkedIn post...')
}

const result = await linkedIn.posts.createPost({
authorUrn: linkedIn.authorUrn,
text,
visibility,
imageUrl,
articleUrl,
articleTitle,
articleDescription,
})

logger.forBot().info('Post created successfully', { postUrn: result.postUrn })

return result
}
14 changes: 14 additions & 0 deletions integrations/linkedin/src/actions/delete-post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { LinkedInClient } from '../linkedin-api'
import * as bp from '.botpress'

export const deletePost: bp.IntegrationProps['actions']['deletePost'] = async ({ client, ctx, input, logger }) => {
const { postUrn } = input

const linkedIn = await LinkedInClient.create({ client, ctx, logger })

await linkedIn.posts.deletePost(postUrn)

logger.forBot().info('Post deleted successfully', { postUrn })

return { success: true }
}
2 changes: 2 additions & 0 deletions integrations/linkedin/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { createPost } from './create-post'
export { deletePost } from './delete-post'
Loading
Loading