Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,18 @@
{
"extends": ["../../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@@ -0,0 +1,7 @@
# pieces-linkedin
This library was generated with [Nx](https://nx.dev).
## Running lint
Run `nx lint pieces-linkedin` to execute the lint via [ESLint](https://eslint.org/).

View File

@@ -0,0 +1,4 @@
{
"name": "@activepieces/piece-linkedin",
"version": "0.1.20"
}

View File

@@ -0,0 +1,51 @@
{
"name": "pieces-linkedin",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/pieces/community/linkedin/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/packages/pieces/community/linkedin",
"tsConfig": "packages/pieces/community/linkedin/tsconfig.lib.json",
"packageJson": "packages/pieces/community/linkedin/package.json",
"main": "packages/pieces/community/linkedin/src/index.ts",
"assets": [
"packages/pieces/community/linkedin/*.md",
{
"input": "packages/pieces/community/linkedin/src/i18n",
"output": "./src/i18n",
"glob": "**/!(i18n.json)"
}
],
"buildableProjectDepsInPackageJsonType": "dependencies",
"updateBuildableProjectDepsInPackageJson": true
},
"dependsOn": [
"^build",
"prebuild"
]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
]
},
"prebuild": {
"executor": "nx:run-commands",
"options": {
"cwd": "packages/pieces/community/linkedin",
"command": "bun install --no-save --silent"
},
"dependsOn": [
"^build"
]
}
},
"tags": []
}

View File

@@ -0,0 +1,30 @@
{
"LinkedIn": "LinkedIn",
"Connect and network with professionals": "Connect and network with professionals",
"Create Share Update": "Create Share Update",
"Create Company Update": "Create Company Update",
"Custom API Call": "Custom API Call",
"Create a share update on LinkedIn": "Create a share update on LinkedIn",
"Create a new company update for Company Page": "Create a new company update for Company Page",
"Make a custom API call to a specific endpoint": "Make a custom API call to a specific endpoint",
"Text": "Text",
"Visibility": "Visibility",
"Image": "Image",
"Content - URL": "Content - URL",
"Content - Title": "Content - Title",
"Content - Description": "Content - Description",
"Company Page": "Company Page",
"Method": "Method",
"Headers": "Headers",
"Query Parameters": "Query Parameters",
"Body": "Body",
"No Error on Failure": "No Error on Failure",
"Timeout (in seconds)": "Timeout (in seconds)",
"Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.",
"GET": "GET",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "DELETE",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,31 @@
{
"Connect and network with professionals": "Verbinden und Netzwerk mit Profis",
"Create Share Update": "Teilen Update erstellen",
"Create Company Update": "Unternehmensupdate erstellen",
"Custom API Call": "Eigener API-Aufruf",
"Create a share update on LinkedIn": "Ein Freigabe-Update auf LinkedIn erstellen",
"Create a new company update for Company Page": "Neues Unternehmensupdate für Unternehmensseite erstellen",
"Make a custom API call to a specific endpoint": "Einen benutzerdefinierten API-Aufruf an einen bestimmten Endpunkt machen",
"Text": "Text",
"Visibility": "Sichtbarkeit",
"Image": "Bild",
"Content - URL": "Inhalt - URL",
"Content - Title": "Inhalt - Titel",
"Content - Description": "Inhalt - Beschreibung",
"Company Page": "Firmenseite",
"Method": "Methode",
"Headers": "Kopfzeilen",
"Query Parameters": "Abfrageparameter",
"Body": "Körper",
"Response is Binary ?": "Antwort ist binär?",
"No Error on Failure": "Kein Fehler bei Fehler",
"Timeout (in seconds)": "Timeout (in Sekunden)",
"Authorization headers are injected automatically from your connection.": "Autorisierungs-Header werden automatisch von Ihrer Verbindung injiziert.",
"Enable for files like PDFs, images, etc..": "Aktivieren für Dateien wie PDFs, Bilder, etc..",
"GET": "ERHALTEN",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "LÖSCHEN",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,31 @@
{
"Connect and network with professionals": "Conectar y conectar a la red con profesionales",
"Create Share Update": "Crear Actualización de Compartir",
"Create Company Update": "Crear actualización de la empresa",
"Custom API Call": "Llamada API personalizada",
"Create a share update on LinkedIn": "Crear una actualización para compartir en LinkedIn",
"Create a new company update for Company Page": "Crear una nueva actualización de la empresa para la página de la empresa",
"Make a custom API call to a specific endpoint": "Hacer una llamada API personalizada a un extremo específico",
"Text": "Texto",
"Visibility": "Visibilidad",
"Image": "Imagen",
"Content - URL": "Contenido - URL",
"Content - Title": "Contenido - Título",
"Content - Description": "Contenido - Descripción",
"Company Page": "Página de la empresa",
"Method": "Método",
"Headers": "Encabezados",
"Query Parameters": "Parámetros de consulta",
"Body": "Cuerpo",
"Response is Binary ?": "¿Respuesta es binaria?",
"No Error on Failure": "No hay ningún error en fallo",
"Timeout (in seconds)": "Tiempo de espera (en segundos)",
"Authorization headers are injected automatically from your connection.": "Las cabeceras de autorización se inyectan automáticamente desde tu conexión.",
"Enable for files like PDFs, images, etc..": "Activar para archivos como PDFs, imágenes, etc.",
"GET": "RECOGER",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "BORRAR",
"HEAD": "LIMPIO"
}

View File

@@ -0,0 +1,31 @@
{
"Connect and network with professionals": "Se connecter et se connecter avec des professionnels",
"Create Share Update": "Créer une mise à jour de partage",
"Create Company Update": "Créer une mise à jour de la société",
"Custom API Call": "Appel API personnalisé",
"Create a share update on LinkedIn": "Créer une mise à jour de partage sur LinkedIn",
"Create a new company update for Company Page": "Créer une nouvelle mise à jour de la société pour la page de l'entreprise",
"Make a custom API call to a specific endpoint": "Passez un appel API personnalisé à un point de terminaison spécifique",
"Text": "Texte du texte",
"Visibility": "Visibilité",
"Image": "Image",
"Content - URL": "Contenu - URL",
"Content - Title": "Contenu - Titre",
"Content - Description": "Contenu - Description",
"Company Page": "Page de la société",
"Method": "Méthode",
"Headers": "En-têtes",
"Query Parameters": "Paramètres de requête",
"Body": "Corps",
"Response is Binary ?": "La réponse est Binaire ?",
"No Error on Failure": "Aucune erreur en cas d'échec",
"Timeout (in seconds)": "Délai d'attente (en secondes)",
"Authorization headers are injected automatically from your connection.": "Les en-têtes d'autorisation sont injectés automatiquement à partir de votre connexion.",
"Enable for files like PDFs, images, etc..": "Activer pour les fichiers comme les PDFs, les images, etc.",
"GET": "OBTENIR",
"POST": "POSTER",
"PATCH": "PATCH",
"PUT": "EFFACER",
"DELETE": "SUPPRIMER",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,30 @@
{
"LinkedIn": "LinkedIn",
"Connect and network with professionals": "Connect and network with professionals",
"Create Share Update": "Create Share Update",
"Create Company Update": "Create Company Update",
"Custom API Call": "Custom API Call",
"Create a share update on LinkedIn": "Create a share update on LinkedIn",
"Create a new company update for Company Page": "Create a new company update for Company Page",
"Make a custom API call to a specific endpoint": "Make a custom API call to a specific endpoint",
"Text": "Text",
"Visibility": "Visibility",
"Image": "Image",
"Content - URL": "Content - URL",
"Content - Title": "Content - Title",
"Content - Description": "Content - Description",
"Company Page": "Company Page",
"Method": "Method",
"Headers": "Headers",
"Query Parameters": "Query Parameters",
"Body": "Body",
"No Error on Failure": "No Error on Failure",
"Timeout (in seconds)": "Timeout (in seconds)",
"Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.",
"GET": "GET",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "DELETE",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,30 @@
{
"LinkedIn": "LinkedIn",
"Connect and network with professionals": "Connect and network with professionals",
"Create Share Update": "Create Share Update",
"Create Company Update": "Create Company Update",
"Custom API Call": "Custom API Call",
"Create a share update on LinkedIn": "Create a share update on LinkedIn",
"Create a new company update for Company Page": "Create a new company update for Company Page",
"Make a custom API call to a specific endpoint": "Make a custom API call to a specific endpoint",
"Text": "Text",
"Visibility": "Visibility",
"Image": "Image",
"Content - URL": "Content - URL",
"Content - Title": "Content - Title",
"Content - Description": "Content - Description",
"Company Page": "Company Page",
"Method": "Method",
"Headers": "Headers",
"Query Parameters": "Query Parameters",
"Body": "Body",
"No Error on Failure": "No Error on Failure",
"Timeout (in seconds)": "Timeout (in seconds)",
"Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.",
"GET": "GET",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "DELETE",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,31 @@
{
"Connect and network with professionals": "プロとネットワークを接続する",
"Create Share Update": "共有アップデートを作成",
"Create Company Update": "会社の更新を作成",
"Custom API Call": "カスタムAPI通話",
"Create a share update on LinkedIn": "LinkedIn で共有の更新を作成する",
"Create a new company update for Company Page": "会社ページの新しい会社更新を作成する",
"Make a custom API call to a specific endpoint": "特定のエンドポイントへのカスタム API コールを実行します。",
"Text": "テキスト",
"Visibility": "公開範囲",
"Image": "画像",
"Content - URL": "コンテンツ - URL",
"Content - Title": "コンテンツ - タイトル",
"Content - Description": "コンテンツ - 説明",
"Company Page": "勤務先ページ",
"Method": "方法",
"Headers": "ヘッダー",
"Query Parameters": "クエリパラメータ",
"Body": "本文",
"Response is Binary ?": "応答はバイナリですか?",
"No Error on Failure": "失敗時にエラーはありません",
"Timeout (in seconds)": "タイムアウト(秒)",
"Authorization headers are injected automatically from your connection.": "認証ヘッダは接続から自動的に注入されます。",
"Enable for files like PDFs, images, etc..": "PDF、画像などのファイルを有効にします。",
"GET": "取得",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "削除",
"HEAD": "頭"
}

View File

@@ -0,0 +1,31 @@
{
"Connect and network with professionals": "Verbind en netwerk met professionals",
"Create Share Update": "Maak Share Update",
"Create Company Update": "Maak bedrijfsupdate",
"Custom API Call": "Custom API Call",
"Create a share update on LinkedIn": "Maak een share update aan op LinkedIn",
"Create a new company update for Company Page": "Maak een nieuwe bedrijfsupdate voor Bedrijfspagina",
"Make a custom API call to a specific endpoint": "Maak een aangepaste API call naar een specifiek eindpunt",
"Text": "Tekstveld",
"Visibility": "Zichtbaarheid",
"Image": "Afbeelding",
"Content - URL": "Inhoud - URL",
"Content - Title": "Inhoud - Titel",
"Content - Description": "Inhoud - Beschrijving",
"Company Page": "Bedrijfs pagina",
"Method": "Methode",
"Headers": "Kopteksten",
"Query Parameters": "Query parameters",
"Body": "Lichaam",
"Response is Binary ?": "Antwoord is binair?",
"No Error on Failure": "Geen fout bij fout",
"Timeout (in seconds)": "Time-out (in seconden)",
"Authorization headers are injected automatically from your connection.": "Autorisatie headers worden automatisch geïnjecteerd vanuit uw verbinding.",
"Enable for files like PDFs, images, etc..": "Inschakelen voor bestanden zoals PDF's, afbeeldingen etc..",
"GET": "KRIJG",
"POST": "POSTE",
"PATCH": "BEKIJK",
"PUT": "PUT",
"DELETE": "VERWIJDEREN",
"HEAD": "HOOFD"
}

View File

@@ -0,0 +1,31 @@
{
"Connect and network with professionals": "Conecte-se e rede com profissionais",
"Create Share Update": "Criar Atualização de Compartilhamento",
"Create Company Update": "Criar atualização de empresa",
"Custom API Call": "Chamada de API personalizada",
"Create a share update on LinkedIn": "Criar uma atualização de compartilhamento no LinkedIn",
"Create a new company update for Company Page": "Criar uma nova atualização da empresa para a Página Empresa",
"Make a custom API call to a specific endpoint": "Faça uma chamada de API personalizada para um ponto de extremidade específico",
"Text": "texto",
"Visibility": "Visibilidade",
"Image": "Imagem:",
"Content - URL": "Conteúdo - URL",
"Content - Title": "Conteúdo - Título",
"Content - Description": "Conteúdo - Descrição",
"Company Page": "Página da Empresa",
"Method": "Método",
"Headers": "Cabeçalhos",
"Query Parameters": "Parâmetros da consulta",
"Body": "Conteúdo",
"Response is Binary ?": "A resposta é binária ?",
"No Error on Failure": "Nenhum erro no Failure",
"Timeout (in seconds)": "Tempo limite (em segundos)",
"Authorization headers are injected automatically from your connection.": "Os cabeçalhos de autorização são inseridos automaticamente a partir da sua conexão.",
"Enable for files like PDFs, images, etc..": "Habilitar para arquivos como PDFs, imagens, etc..",
"GET": "OBTER",
"POST": "POSTAR",
"PATCH": "COMPRAR",
"PUT": "COLOCAR",
"DELETE": "EXCLUIR",
"HEAD": "CABEÇA"
}

View File

@@ -0,0 +1,30 @@
{
"LinkedIn": "LinkedIn",
"Connect and network with professionals": "Связь и связь со специалистами",
"Create Share Update": "Создать обновление общего доступа",
"Create Company Update": "Создать обновление компании",
"Custom API Call": "Пользовательский вызов API",
"Create a share update on LinkedIn": "Создать общее обновление на LinkedIn",
"Create a new company update for Company Page": "Создать новое обновление компании для страницы компании",
"Make a custom API call to a specific endpoint": "Сделать пользовательский API вызов к определенной конечной точке",
"Text": "Текст",
"Visibility": "Видимость",
"Image": "Изображение",
"Content - URL": "Содержимое - URL",
"Content - Title": "Содержимое - Заголовок",
"Content - Description": "Контент - Описание",
"Company Page": "Страница компании",
"Method": "Метод",
"Headers": "Заголовки",
"Query Parameters": "Параметры запроса",
"Body": "Тело",
"No Error on Failure": "Нет ошибок при ошибке",
"Timeout (in seconds)": "Таймаут (в секундах)",
"Authorization headers are injected automatically from your connection.": "Заголовки авторизации включаются автоматически из вашего соединения.",
"GET": "ПОЛУЧИТЬ",
"POST": "ПОСТ",
"PATCH": "ПАТЧ",
"PUT": "ПОКУПИТЬ",
"DELETE": "УДАЛИТЬ",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,31 @@
{
"Connect and network with professionals": "Connect and network with professionals",
"Create Share Update": "Create Share Update",
"Create Company Update": "Create Company Update",
"Custom API Call": "Custom API Call",
"Create a share update on LinkedIn": "Create a share update on LinkedIn",
"Create a new company update for Company Page": "Create a new company update for Company Page",
"Make a custom API call to a specific endpoint": "Make a custom API call to a specific endpoint",
"Text": "Text",
"Visibility": "Visibility",
"Image": "Image",
"Content - URL": "Content - URL",
"Content - Title": "Content - Title",
"Content - Description": "Content - Description",
"Company Page": "Company Page",
"Method": "Method",
"Headers": "Headers",
"Query Parameters": "Query Parameters",
"Body": "Body",
"Response is Binary ?": "Response is Binary ?",
"No Error on Failure": "No Error on Failure",
"Timeout (in seconds)": "Timeout (in seconds)",
"Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.",
"Enable for files like PDFs, images, etc..": "Enable for files like PDFs, images, etc..",
"GET": "GET",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "DELETE",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,30 @@
{
"LinkedIn": "LinkedIn",
"Connect and network with professionals": "Connect and network with professionals",
"Create Share Update": "Create Share Update",
"Create Company Update": "Create Company Update",
"Custom API Call": "Custom API Call",
"Create a share update on LinkedIn": "Create a share update on LinkedIn",
"Create a new company update for Company Page": "Create a new company update for Company Page",
"Make a custom API call to a specific endpoint": "Make a custom API call to a specific endpoint",
"Text": "Text",
"Visibility": "Visibility",
"Image": "Image",
"Content - URL": "Content - URL",
"Content - Title": "Content - Title",
"Content - Description": "Content - Description",
"Company Page": "Company Page",
"Method": "Method",
"Headers": "Headers",
"Query Parameters": "Query Parameters",
"Body": "Body",
"No Error on Failure": "No Error on Failure",
"Timeout (in seconds)": "Timeout (in seconds)",
"Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.",
"GET": "GET",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "DELETE",
"HEAD": "HEAD"
}

View File

@@ -0,0 +1,31 @@
{
"Connect and network with professionals": "Connect and network with professionals",
"Create Share Update": "Create Share Update",
"Create Company Update": "Create Company Update",
"Custom API Call": "自定义 API 呼叫",
"Create a share update on LinkedIn": "Create a share update on LinkedIn",
"Create a new company update for Company Page": "Create a new company update for Company Page",
"Make a custom API call to a specific endpoint": "将一个自定义 API 调用到一个特定的终点",
"Text": "文本",
"Visibility": "Visibility",
"Image": "Image",
"Content - URL": "Content - URL",
"Content - Title": "Content - Title",
"Content - Description": "Content - Description",
"Company Page": "Company Page",
"Method": "方法",
"Headers": "信头",
"Query Parameters": "查询参数",
"Body": "正文内容",
"Response is Binary ?": "Response is Binary ?",
"No Error on Failure": "失败时没有错误",
"Timeout (in seconds)": "超时(秒)",
"Authorization headers are injected automatically from your connection.": "授权头自动从您的连接中注入。",
"Enable for files like PDFs, images, etc..": "Enable for files like PDFs, images, etc..",
"GET": "获取",
"POST": "帖子",
"PATCH": "PATCH",
"PUT": "弹出",
"DELETE": "删除",
"HEAD": "黑色"
}

View File

@@ -0,0 +1,52 @@
import {
OAuth2PropertyValue,
PieceAuth,
createPiece,
} from '@activepieces/pieces-framework';
import { PieceCategory } from '@activepieces/shared';
import { createCompanyUpdate } from './lib/actions/create-company-update';
import { createShareUpdate } from './lib/actions/create-share-update';
import { createCustomApiCallAction } from '@activepieces/pieces-common';
import { linkedinCommon } from './lib/common';
export const linkedinAuth = PieceAuth.OAuth2({
authUrl: 'https://www.linkedin.com/oauth/v2/authorization',
tokenUrl: 'https://www.linkedin.com/oauth/v2/accessToken',
required: true,
scope: [
'w_member_social',
'w_organization_social',
'rw_organization_admin',
'openid',
'email',
'profile',
],
});
export const linkedin = createPiece({
displayName: 'LinkedIn',
description: 'Connect and network with professionals',
minimumSupportedRelease: '0.30.0',
logoUrl: 'https://cdn.activepieces.com/pieces/linkedin.png',
categories: [PieceCategory.MARKETING],
authors: ["aasimsani","kishanprmr","MoShizzle","khaledmashaly","abuaboud", "izdrail"],
auth: linkedinAuth,
actions: [
createShareUpdate,
createCompanyUpdate,
createCustomApiCallAction({
auth: linkedinAuth,
baseUrl: () => {
return linkedinCommon.baseUrl;
},
authMapping: async (auth) => {
return {
Authorization: `Bearer ${(auth as OAuth2PropertyValue).access_token}`,
};
},
}),
],
triggers: [],
});

View File

@@ -0,0 +1,71 @@
import { createAction } from '@activepieces/pieces-framework';
import {
AuthenticationType,
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { Image, linkedinCommon, santizeText } from '../common';
import { linkedinAuth } from '../..';
export const createCompanyUpdate = createAction({
auth: linkedinAuth,
name: 'create_company_update',
displayName: 'Create Company Update',
description: 'Create a new company update for Company Page',
props: {
company: linkedinCommon.company,
imageUrl: linkedinCommon.imageUrl,
text: linkedinCommon.text,
link: linkedinCommon.link,
linkTitle: linkedinCommon.linkTitle,
linkDescription: linkedinCommon.linkDescription,
},
run: async (context) => {
const imageUrl = context.propsValue.imageUrl;
const bodyConfig: {
urn: string;
text: string;
link?: string | undefined;
linkDescription?: string | undefined;
linkTitle?: string | undefined;
visibility: string;
image?: Image | undefined;
} = {
urn: `organization:${context.propsValue.company}`,
text: santizeText(context.propsValue.text),
link: context.propsValue.link,
linkDescription: context.propsValue.linkDescription,
linkTitle: context.propsValue.linkTitle,
visibility: 'PUBLIC',
};
if (imageUrl) {
bodyConfig.image = await linkedinCommon.uploadImage(
context.auth.access_token,
`organization:${context.propsValue.company}`,
imageUrl
);
}
const requestBody = linkedinCommon.generatePostRequestBody(bodyConfig);
const createPostHeaders: any = linkedinCommon.linkedinHeaders;
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${linkedinCommon.baseUrl}/rest/posts`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
headers: createPostHeaders,
body: requestBody,
};
const response = await httpClient.sendRequest(request);
return {
success: true,
};
},
});

View File

@@ -0,0 +1,69 @@
import { createAction } from '@activepieces/pieces-framework';
import {
AuthenticationType,
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { Image, linkedinCommon, santizeText } from '../common';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { linkedinAuth } from '../..';
export const createShareUpdate = createAction({
auth: linkedinAuth,
name: 'create_share_update',
displayName: 'Create Share Update',
description: 'Create a share update on LinkedIn',
props: {
text: linkedinCommon.text,
visibility: linkedinCommon.visibility,
imageUrl: linkedinCommon.imageUrl,
link: linkedinCommon.link,
linkTitle: linkedinCommon.linkTitle,
linkDescription: linkedinCommon.linkDescription,
},
run: async (context) => {
const token = context.auth.data.id_token;
const decoded: JwtPayload = jwt.decode(token) as JwtPayload;
const imageUrl = context.propsValue.imageUrl;
const { text, link, linkDescription, linkTitle, visibility } =
context.propsValue;
let image: Image | undefined;
if (imageUrl) {
image = await linkedinCommon.uploadImage(
context.auth.access_token,
`person:${decoded.sub}`,
imageUrl
);
}
const requestBody = linkedinCommon.generatePostRequestBody({
urn: `person:${decoded.sub}`,
text: santizeText(text),
link,
linkDescription,
linkTitle,
visibility,
image,
});
const createPostHeaders: any = linkedinCommon.linkedinHeaders;
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${linkedinCommon.baseUrl}/rest/posts`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
headers: createPostHeaders,
body: requestBody,
};
const response = await httpClient.sendRequest(request);
return {
success: true,
};
},
});

View File

@@ -0,0 +1,279 @@
import { ApFile, Property } from '@activepieces/pieces-framework';
import {
HttpMethod,
httpClient,
AuthenticationType,
} from '@activepieces/pieces-common';
import FormData from 'form-data';
import { linkedinAuth } from '../..';
export const santizeText = (text: string) => {
// LinkedIn Posts API has a list of characters that need to be escaped since it's type is "LittleText"
// https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api?view=li-lms-2023-11&tabs=http
// https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/little-text-format?view=li-lms-2023-11
// eslint-disable-next-line no-useless-escape
return text.replace(/[\(*\)\[\]\{\}<>@|~_]/gm, (x: string) => '\\' + x);
};
export const linkedinCommon = {
baseUrl: 'https://api.linkedin.com',
linkedinHeaders: {
'X-Restli-Protocol-Version': '2.0.0',
'LinkedIn-Version': '202511',
},
text: Property.LongText({
displayName: 'Text',
required: true,
}),
imageUrl: Property.File({
displayName: 'Image',
required: false,
}),
link: Property.ShortText({
displayName: 'Content - URL',
required: false,
}),
linkTitle: Property.ShortText({
displayName: 'Content - Title',
required: false,
}),
linkDescription: Property.ShortText({
displayName: 'Content - Description',
required: false,
}),
visibility: Property.Dropdown({
auth: linkedinAuth,
displayName: 'Visibility',
refreshers: [],
required: true,
options: async () => {
return {
options: [
{
label: 'Public',
value: 'PUBLIC',
},
{
label: 'Connections Only',
value: 'CONNECTIONS',
},
],
};
},
}),
company: Property.Dropdown({
auth: linkedinAuth,
displayName: 'Company Page',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account',
options: [],
};
}
const authProp = auth as { access_token: string };
const companies: any = await linkedinCommon.getCompanies(
authProp.access_token
);
const options = [];
for (const company in companies) {
options.push({
label: companies[company].localizedName,
value: companies[company].id,
});
}
return {
options: options,
};
},
}),
getCompanies: async (accessToken: string) => {
const companies = (
await httpClient.sendRequest({
url: `${linkedinCommon.baseUrl}/v2/organizationalEntityAcls`,
method: HttpMethod.GET,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
queryParams: {
q: 'roleAssignee',
},
})
).body;
const companyIds = companies.elements.map(
(company: { organizationalTarget: string }) => {
return company.organizationalTarget.substr(
company.organizationalTarget.lastIndexOf(':') + 1
);
}
);
const response = await fetch(`${linkedinCommon.baseUrl}/rest/organizations?ids=List(${companyIds.join(',')})`, {
method: 'GET',
headers: {
...linkedinCommon.linkedinHeaders,
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const companySearch = await response.json();
return companySearch.results;
},
generatePostRequestBody: (data: {
urn: string;
text: any;
link?: string | undefined;
linkTitle?: string | undefined;
linkDescription?: string | undefined;
visibility: string;
image?: Image | undefined;
}) => {
const requestObject: Post = {
author: `urn:li:${data.urn}`,
lifecycleState: 'PUBLISHED',
commentary: data.text,
distribution: {
feedDistribution: 'MAIN_FEED',
},
visibility: data.visibility,
isReshareDisabledByAuthor: false,
};
if (data.link) {
requestObject.content = {
article: {
source: data.link,
title: data.linkTitle,
description: data.linkDescription,
thumbnail: data.image?.value.image,
},
};
} else if (data.image) {
requestObject.content = {
media: {
id: data.image.value.image,
},
};
}
return requestObject;
},
uploadImage: async (
accessToken: string,
urn: string,
image: ApFile
): Promise<Image> => {
const uploadData = (
await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${linkedinCommon.baseUrl}/v2/images`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
queryParams: {
action: 'initializeUpload',
},
body: {
initializeUploadRequest: {
owner: `urn:li:${urn}`,
},
},
})
).body as Image;
const uploadFormData = new FormData();
const { filename, base64 } = image;
uploadFormData.append('file', Buffer.from(base64, 'base64'), filename);
await httpClient.sendRequest({
url: uploadData.value.uploadUrl,
method: HttpMethod.POST,
body: uploadFormData,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
headers: {
'Content-Type': 'multipart/form-data',
},
});
return uploadData;
},
};
export interface UgcPost {
author: string;
lifecycleState: string;
specificContent: {
'com.linkedin.ugc.ShareContent': {
shareCommentary: {
text?: string;
};
shareMediaCategory: string;
media?: [
{
status: string;
description?: {
text: string;
};
originalUrl?: string;
thumbnail?: string;
title?: {
text: string;
};
}
];
};
};
visibility: {
'com.linkedin.ugc.MemberNetworkVisibility': string;
};
}
export interface Post {
author: string;
commentary: string;
lifecycleState: string;
visibility: string;
distribution: {
feedDistribution: string;
};
content?: {
article?: {
source: string;
thumbnail?: string | undefined;
title?: string | undefined;
description?: string | undefined;
};
media?: {
id: string;
};
};
isReshareDisabledByAuthor: boolean;
}
export interface Image {
value: {
uploadUrlExpiresAt: number;
uploadUrl: string;
image: string;
};
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../../../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
],
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}

View File

@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}