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,102 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { cambaiAuth } from '../../index';
import { API_BASE_URL, MAX_POLLING_ATTEMPTS, POLLING_INTERVAL_MS } from '../common';
import { listFoldersDropdown } from '../common';
export const createTextToSound = createAction({
auth: cambaiAuth,
name: 'create_text_to_sound',
displayName: 'Create Text-to-Sound',
description: 'Convert input text into “sound effects” using an AI model.',
props: {
prompt: Property.LongText({
displayName: 'Prompt',
description: 'A clear, descriptive explanation of the desired audio effect. Concise prompts yield more accurate results.',
required: true,
}),
duration: Property.Number({
displayName: 'Duration (seconds)',
description: 'The desired length of the audio in seconds (max 10). Defaults to 8 if not set.',
required: false,
}),
project_name: Property.ShortText({
displayName: 'Project Name',
description: 'A memorable name for your project to help organize tasks in your Camb.ai workspace.',
required: false,
}),
project_description: Property.LongText({
displayName: 'Project Description',
description: 'Provide details about your project\'s goals and specifications for documentation purposes.',
required: false,
}),
folder_id: listFoldersDropdown,
},
async run(context) {
const { auth } = context;
const { prompt, duration, project_name, project_description, folder_id } = context.propsValue;
const payload: Record<string, unknown> = { prompt };
if (duration) payload['duration'] = duration;
if (project_name) payload['project_name'] = project_name;
if (project_description) payload['project_description'] = project_description;
if (folder_id) payload['folder_id'] = folder_id;
const initialResponse = await httpClient.sendRequest<{ task_id: string }>({
method: HttpMethod.POST,
url: `${API_BASE_URL}/text-to-sound`,
headers: {
'x-api-key': auth.secret_text,
'Content-Type': 'application/json'
},
body: payload,
});
const taskId = initialResponse.body.task_id;
let attempts = 0;
let run_id: string | null = null;
while (attempts < MAX_POLLING_ATTEMPTS) {
const statusResponse = await httpClient.sendRequest<{
status: string, run_id?: string
}>({
method: HttpMethod.GET,
url: `${API_BASE_URL}/text-to-sound/${taskId}`,
headers: {
'x-api-key': auth.secret_text,
},
});
const status = statusResponse.body.status;
if (status === 'SUCCESS') {
run_id = statusResponse.body.run_id ?? null;
break;
}
if (status === 'FAILED') {
throw new Error(`Sound generation task failed: ${JSON.stringify(statusResponse.body)}`);
}
await new Promise(resolve => setTimeout(resolve, POLLING_INTERVAL_MS));
attempts++;
}
if (!run_id) {
throw new Error("Sound generation task timed out or failed to return a run_id.");
}
const audioResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${API_BASE_URL}/text-to-sound-result/${run_id}`,
headers: { 'x-api-key': auth.secret_text },
responseType: 'arraybuffer',
});
return { audio: audioResponse.body };
},
});

View File

@@ -0,0 +1,119 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient, HttpResponse } from '@activepieces/pieces-common';
import { cambaiAuth } from '../../index';
import { API_BASE_URL, listSourceLanguagesDropdown, listVoicesDropdown ,POLLING_INTERVAL_MS,MAX_POLLING_ATTEMPTS } from '../common';
import { listFoldersDropdown } from '../common';
export const createTextToSpeech = createAction({
auth: cambaiAuth,
name: 'create_text_to_speech',
displayName: 'Create Text-to-Speech',
description: 'Convert text into speech using a specified voice, language, gender, and age group.',
props: {
text: Property.LongText({
displayName: 'Text',
description: 'The text to be converted to speech.',
required: true,
}),
language: listSourceLanguagesDropdown,
voice_id: listVoicesDropdown,
gender: Property.StaticDropdown({
displayName: 'Gender',
description: 'The gender of the speaker.',
required: false,
options: {
options: [
{ label: 'Male', value: 1 },
{ label: 'Female', value: 2 },
{ label: 'Neutral', value: 0 },
{ label: 'Unspecified', value: 9 },
],
}
}),
age: Property.Number({
displayName: 'Age',
description: 'The age of the speaker to be generated.',
required: false,
}),
project_name: Property.ShortText({
displayName: 'Project Name',
description: 'A memorable name for your project to help organize tasks in your Camb.ai workspace.',
required: false,
}),
project_description: Property.LongText({
displayName: 'Project Description',
description: 'Provide details about your project\'s goals and specifications for documentation purposes.',
required: false,
}),
folder_id: listFoldersDropdown,
},
async run(context) {
const { auth } = context;
const { text, language, voice_id, gender, age, project_name, project_description, folder_id } = context.propsValue;
const payload: Record<string, unknown> = { text, language: Number(language), voice_id: Number(voice_id) };
if (gender !== undefined) payload['gender'] = gender;
if (age) payload['age'] = age;
if (project_name) payload['project_name'] = project_name;
if (project_description) payload['project_description'] = project_description;
if (folder_id) payload['folder_id'] = folder_id;
const initialResponse = await httpClient.sendRequest<{ task_id: string }>({
method: HttpMethod.POST,
url: `${API_BASE_URL}/tts`,
headers: { 'x-api-key': auth.secret_text, 'Content-Type': 'application/json' },
body: payload,
});
const taskId = initialResponse.body.task_id;
let attempts = 0;
let run_id: string | null = null;
while (attempts < MAX_POLLING_ATTEMPTS) {
const statusResponse = await httpClient.sendRequest<{ status: string; run_id?: string }>({
method: HttpMethod.GET,
url: `${API_BASE_URL}/tts/${taskId}`,
headers: { 'x-api-key': auth.secret_text },
});
if (statusResponse.body.status === 'SUCCESS') {
run_id = statusResponse.body.run_id ?? null;
break;
}
if (statusResponse.body.status === 'FAILED') {
throw new Error(`Text-to-Speech task failed: ${JSON.stringify(statusResponse.body)}`);
}
await new Promise(resolve => setTimeout(resolve, POLLING_INTERVAL_MS));
attempts++;
}
if (!run_id) {
throw new Error("Text-to-Speech task timed out or failed to return a run_id.");
}
const audioResponse: HttpResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${API_BASE_URL}/tts-result/${run_id}`,
headers: { 'x-api-key': auth.secret_text },
responseType: 'arraybuffer',
});
const fileName = `speech_${run_id}.wav`;
const fileData = Buffer.from(audioResponse.body as ArrayBuffer);
const fileUrl = await context.files.write({
fileName,
data: fileData,
});
return {
message: "Speech generated successfully.",
audio_url: fileUrl,
run_id: run_id,
};
},
});

View File

@@ -0,0 +1,130 @@
import { createAction, Property, ApFile, DynamicPropsValue } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient, HttpMessageBody, HttpHeaders } from '@activepieces/pieces-common';
import { cambaiAuth } from '../../index';
import { API_BASE_URL, listSourceLanguagesDropdown, POLLING_INTERVAL_MS, LONG_MAX_POLLING_ATTEMPTS } from '../common';
import FormData from 'form-data';
import { listFoldersDropdown } from '../common';
export const createTranscription = createAction({
auth: cambaiAuth,
name: 'create_transcription',
displayName: 'Create Transcription',
description: 'Creates a task to process speech into readable text.',
props: {
language: listSourceLanguagesDropdown,
source_type: Property.StaticDropdown({
displayName: 'Media Source',
description: 'Choose whether to upload a file or provide a URL.',
required: true,
defaultValue: 'file',
options: {
options: [
{ label: 'Upload File', value: 'file' },
{ label: 'File URL', value: 'url' },
]
},
}),
media: Property.DynamicProperties({
auth: cambaiAuth,
displayName: 'Media',
required: true,
refreshers: ['source_type'],
props: async (context) => {
const sourceType = (context['source_type'] as unknown as string);
const fields: DynamicPropsValue = {};
if (sourceType === 'file') {
fields['media_file'] = Property.File({
displayName: 'Media File',
description: 'The media file (e.g., MP3, WAV, MP4) to transcribe. Max size: 20MB.',
required: true,
});
} else if (sourceType === 'url') {
fields['media_url'] = Property.ShortText({
displayName: 'Media URL',
description: 'A public URL to the media file to transcribe.',
required: true,
});
}
return fields;
}
}),
project_name: Property.ShortText({
displayName: 'Project Name',
description: 'A memorable name for your project to help organize tasks in your Camb.ai workspace.',
required: false,
}),
project_description: Property.LongText({
displayName: 'Project Description',
description: 'Provide details about your project\'s goals and specifications for documentation purposes.',
required: false,
}),
folder_id: listFoldersDropdown,
},
async run(context) {
const { auth } = context;
const { language, source_type, media, project_name, project_description, folder_id } = context.propsValue;
const formData = new FormData();
formData.append('language', Number(language).toString());
if (project_name) formData.append('project_name', project_name);
if (project_description) formData.append('project_description', project_description);
if (folder_id) formData.append('folder_id', folder_id.toString());
if (source_type === 'url') {
if (!media['media_url']) throw new Error("Media URL is required when source is 'File URL'.");
formData.append('media_url', media['media_url'] as string);
} else {
if (!media['media_file']) throw new Error("Media File is required when source is 'Upload File'.");
const fileData = media['media_file'] as ApFile;
formData.append('media_file', fileData.data, fileData.filename);
}
const requestBody = await formData.getBuffer();
const headers: HttpHeaders = {
'x-api-key': auth.secret_text,
...formData.getHeaders(),
};
const initialResponse = await httpClient.sendRequest<{ task_id: string }>({
method: HttpMethod.POST,
url: `${API_BASE_URL}/transcribe`,
headers: headers,
body: requestBody,
});
const taskId = initialResponse.body.task_id;
let run_id: string | null = null;
let attempts = 0;
while (attempts < LONG_MAX_POLLING_ATTEMPTS) {
const statusResponse = await httpClient.sendRequest<{ status: string; run_id?: string }>({
method: HttpMethod.GET,
url: `${API_BASE_URL}/transcribe/${taskId}`,
headers: { 'x-api-key': auth.secret_text },
});
if (statusResponse.body.status === 'SUCCESS') {
run_id = statusResponse.body.run_id ?? null;
break;
}
if (statusResponse.body.status === 'FAILED') {
throw new Error(`Transcription task failed: ${JSON.stringify(statusResponse.body)}`);
}
await new Promise(resolve => setTimeout(resolve, POLLING_INTERVAL_MS));
attempts++;
}
if (!run_id) {
throw new Error("Transcription task timed out or failed to return a task_id.");
}
const resultResponse = await httpClient.sendRequest<{ transcriptions: string[] }>({
method: HttpMethod.GET,
url: `${API_BASE_URL}/transcription-result/${run_id}`,
headers: { 'x-api-key': auth.secret_text },
});
return resultResponse.body;
},
});

View File

@@ -0,0 +1,113 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { cambaiAuth } from '../../index';
import { API_BASE_URL, listSourceLanguagesDropdown, listTargetLanguagesDropdown ,POLLING_INTERVAL_MS,MAX_POLLING_ATTEMPTS} from '../common';
export const createTranslation = createAction({
auth: cambaiAuth,
name: 'create_translation',
displayName: 'Create Translation',
description: 'Translate text from a source language to a target language.',
props: {
texts: Property.LongText({
displayName: 'Text to Translate',
description: 'The text to be translated. You can enter multiple lines; each line will be treated as a separate text segment.',
required: true,
}),
source_language: listSourceLanguagesDropdown,
target_language: listTargetLanguagesDropdown,
formality: Property.StaticDropdown({
displayName: 'Formality',
description: 'Adjust the formality level to match your context.',
required: false,
options: {
options: [
{ label: 'Formal', value: 1 },
{ label: 'Informal', value: 2 },
]
}
}),
gender: Property.StaticDropdown({
displayName: 'Gender',
description: 'Specify grammatical gender preferences when relevant in the target language.',
required: false,
options: {
options: [
{ label: 'Male', value: 1 },
{ label: 'Female', value: 2 },
{ label: 'Neutral', value: 0 },
{ label: 'Unspecified', value: 9 },
],
}
}),
age: Property.Number({
displayName: 'Audience Age',
description: 'Helps adjust vocabulary and expressions to be age-appropriate.',
required: false,
}),
project_name: Property.ShortText({
displayName: 'Project Name',
description: 'A memorable name for your project to help organize tasks in your Camb.ai workspace.',
required: false,
}),
},
async run(context) {
const { auth } = context;
const { texts, source_language, target_language, formality, gender, age, project_name } = context.propsValue;
const payload: Record<string, unknown> = {
texts: texts.split('\n').filter(line => line.trim().length > 0),
source_language: Number(source_language),
target_language: Number(target_language),
};
if (formality !== undefined) payload['formality'] = formality;
if (gender !== undefined) payload['gender'] = gender;
if (age) payload['age'] = age;
if (project_name) payload['project_name'] = project_name;
const initialResponse = await httpClient.sendRequest<{ task_id: string }>({
method: HttpMethod.POST,
url: `${API_BASE_URL}/translate`,
headers: { 'x-api-key': auth.secret_text, 'Content-Type': 'application/json' },
body: payload,
});
const taskId = initialResponse.body.task_id;
let run_id: string | null = null;
let attempts = 0;
while (attempts < MAX_POLLING_ATTEMPTS) {
const statusResponse = await httpClient.sendRequest<{ status: string; run_id?: string }>({
method: HttpMethod.GET,
url: `${API_BASE_URL}/translate/${taskId}`,
headers: { 'x-api-key': auth.secret_text },
});
if (statusResponse.body.status === 'SUCCESS') {
run_id = statusResponse.body.run_id ?? null;
break;
}
if (statusResponse.body.status === 'ERROR' || statusResponse.body.status === 'FAILED') {
throw new Error(`Translation task failed: ${JSON.stringify(statusResponse.body)}`);
}
await new Promise(resolve => setTimeout(resolve, POLLING_INTERVAL_MS));
attempts++;
}
if (!run_id) {
throw new Error("Translation task timed out or failed to return a task_id.");
}
const resultResponse = await httpClient.sendRequest<{ translations: string[] }>({
method: HttpMethod.GET,
url: `${API_BASE_URL}/translation-result/${run_id}`,
headers: { 'x-api-key': auth.secret_text },
});
return resultResponse.body;
},
});

View File

@@ -0,0 +1,165 @@
import { Property } from "@activepieces/pieces-framework";
import { HttpMethod, httpClient } from "@activepieces/pieces-common";
import { cambaiAuth } from "../..";
export const API_BASE_URL = "https://client.camb.ai/apis";
export const POLLING_INTERVAL_MS = 5000;
export const LONG_POLLING_INTERVAL_MS = 10000;
export const MAX_POLLING_ATTEMPTS = 10;
export const LONG_MAX_POLLING_ATTEMPTS = 120;
type Voice = {
id: number;
voice_name: string;
};
type Folder = {
folder_id: number;
folder_name: string;
};
type Language = {
id: number;
language: string;
short_name: string;
};
export const listVoicesDropdown = Property.Dropdown({
auth: cambaiAuth,
displayName: 'Voice',
description: 'Select the voice to generate the speech.',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const response = await httpClient.sendRequest<Voice[]>({
method: HttpMethod.GET,
url: `${API_BASE_URL}/list-voices`,
headers: {
'x-api-key': auth.secret_text,
},
});
const voices = response.body ?? [];
return {
disabled: false,
options: voices.map((voice) => ({
label: voice.voice_name,
value: voice.id,
})),
};
},
});
export const listSourceLanguagesDropdown = Property.Dropdown({
auth: cambaiAuth,
displayName: 'Source Language',
description: 'Select the original language of the input text.',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const response = await httpClient.sendRequest<Language[]>({
method: HttpMethod.GET,
url: `${API_BASE_URL}/source-languages`,
headers: {
'x-api-key': auth.secret_text,
},
});
const languages = response.body ?? [];
return {
disabled: false,
options: languages.map((lang) => ({
label: `${lang.language} (${lang.short_name})`,
value: lang.id,
})),
};
},
});
export const listTargetLanguagesDropdown = Property.Dropdown({
displayName: 'Target Language',
description: 'Select the language to translate the text into.',
auth: cambaiAuth,
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
const response = await httpClient.sendRequest<Language[]>({
method: HttpMethod.GET,
url: `${API_BASE_URL}/target-languages`,
headers: {
'x-api-key': auth.secret_text,
},
});
const languages = response.body ?? [];
return {
disabled: false,
options: languages.map((lang) => ({
label: `${lang.language} (${lang.short_name})`,
value: lang.id,
})),
};
},
});
export const listFoldersDropdown = Property.Dropdown({
displayName: 'Folder',
auth: cambaiAuth,
description: 'Select the folder to save the task in.',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first',
};
}
try {
const response = await httpClient.sendRequest<Folder[]>({
method: HttpMethod.GET,
url: `${API_BASE_URL}/folders`,
headers: {
'x-api-key': auth.secret_text,
},
});
const folders = response.body ?? [];
return {
disabled: false,
options: folders.map((folder) => ({
label: folder.folder_name,
value: folder.folder_id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: "Could not load folders."
}
}
},
});