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,266 @@
import { createAction } from '@activepieces/pieces-framework';
import { propsValidation, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { cloudconvertAuth, CloudConvertClient, archiveFileSchema } from '../common';
import { Property } from '@activepieces/pieces-framework';
const archiveFileProps = () => ({
import_method: Property.StaticDropdown({
displayName: 'Import Method',
description: 'How to import the files for archiving',
required: true,
options: {
options: [
{ label: 'File Upload', value: 'upload' },
{ label: 'File URL', value: 'url' },
{ label: 'Stored File ID', value: 'stored_file' },
]
},
defaultValue: 'upload'
}),
files: Property.Array({
displayName: 'Files to Archive',
description: 'List of files to include in the archive',
required: true,
properties: {
url: Property.ShortText({
displayName: 'File URL',
description: 'URL of the file to archive',
required: false,
}),
file: Property.File({
displayName: 'File',
description: 'File to upload and archive',
required: false,
}),
stored_file_id: Property.ShortText({
displayName: 'Stored File ID',
description: 'ID of a previously stored file in Activepieces to archive',
required: false,
}),
filename: Property.ShortText({
displayName: 'Filename in Archive',
description: 'Optional filename for this file within the archive',
required: false,
})
}
}),
output_format: Property.StaticDropdown({
displayName: 'Archive Format',
description: 'The archive format to create',
required: true,
options: {
options: [
{ label: 'ZIP', value: 'zip' },
{ label: 'RAR', value: 'rar' },
{ label: '7Z', value: '7z' },
{ label: 'TAR', value: 'tar' },
{ label: 'TAR.GZ', value: 'targz' },
{ label: 'TAR.BZ2', value: 'tarbz2' },
]
},
defaultValue: 'zip'
}),
filename: Property.ShortText({
displayName: 'Archive Filename',
description: 'Choose a filename (including extension) for the archive file',
required: false,
defaultValue: 'archive.zip'
}),
engine: Property.StaticDropdown({
displayName: 'Engine',
description: 'Use a specific engine for the archiving',
required: false,
options: {
options: [
{ label: '7-Zip', value: '7z' },
{ label: 'Archive Tool (Default)', value: 'archivetool' },
]
}
}),
engine_version: Property.ShortText({
displayName: 'Engine Version',
description: 'Use a specific engine version for the archiving',
required: false,
}),
timeout: Property.Number({
displayName: 'Timeout (seconds)',
description: 'Timeout in seconds after which the task will be cancelled',
required: false,
}),
wait_for_completion: Property.Checkbox({
displayName: 'Wait for Completion',
description: 'Wait for the archive creation to complete before returning',
required: true,
defaultValue: true,
}),
store_file: Property.Checkbox({
displayName: 'Store File',
description: 'Download and store the created archive in Activepieces',
required: false,
defaultValue: true,
}),
});
export const archiveFile = createAction({
name: 'archive_file',
displayName: 'Archive File',
description: 'Creates a ZIP, RAR, 7Z, TAR, TAR.GZ or TAR.BZ2 archive',
auth: cloudconvertAuth,
requireAuth: true,
props: archiveFileProps(),
async run(context) {
await archiveFileSchema.parseAsync(context.propsValue);
const { import_method, files, output_format, filename, engine, engine_version, timeout, wait_for_completion, store_file } = context.propsValue;
if (!files || files.length === 0) {
throw new Error('At least one file is required to create an archive');
}
const client = new CloudConvertClient(context.auth);
try {
const jobTasks: Record<string, any> = {};
files.forEach((file: any, index: number) => {
const taskName = `import-file-${index + 1}`;
if (import_method === 'url') {
if (!file.url) {
throw new Error(`File URL is required for file ${index + 1} when using URL import method`);
}
jobTasks[taskName] = {
operation: 'import/url',
url: file.url,
...(file.filename && { filename: file.filename })
};
} else if (import_method === 'stored_file') {
if (!file.stored_file_id) {
throw new Error(`Stored File ID is required for file ${index + 1} when using stored file import method`);
}
if (!context.server?.apiUrl) {
throw new Error('Server API URL is not available. Please check your Activepieces server configuration.');
}
const baseUrl = context.server.apiUrl.replace(/\/$/, '');
const fileUrl = `${baseUrl}/v1/step-files/${file.stored_file_id}`;
try {
new URL(fileUrl);
} catch (urlError) {
throw new Error(`Invalid file URL constructed: ${fileUrl}. URL Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`);
}
jobTasks[taskName] = {
operation: 'import/url',
url: fileUrl,
...(file.filename && { filename: file.filename })
};
} else if (import_method === 'upload') {
if (!file.file || !file.file.base64) {
throw new Error(`Please select a file to upload for file ${index + 1}`);
}
throw new Error('Upload method for multiple files is not yet supported with the new job format. Please use URL or stored file methods.');
} else {
throw new Error('Invalid import method selected');
}
});
// Collect all import task names
const importTaskNames = files.map((_, index) => `import-file-${index + 1}`);
// Archive task
const archiveOptions: any = {
input: importTaskNames.length === 1 ? importTaskNames[0] : importTaskNames,
output_format,
};
if (filename) archiveOptions.filename = filename;
if (engine) archiveOptions.engine = engine;
if (engine_version) archiveOptions.engine_version = engine_version;
if (timeout) archiveOptions.timeout = timeout;
jobTasks['archive-files'] = {
operation: 'archive',
...archiveOptions
};
// Export task
jobTasks['export-file'] = {
operation: 'export/url',
input: 'archive-files'
};
const job = await client.createJob(jobTasks, `archive-${Date.now()}`);
if (wait_for_completion) {
let attempts = 0;
const maxAttempts = 60;
while (attempts < maxAttempts) {
const currentJob = await client.getJob(job.id);
if (currentJob.status === 'finished') {
// Find the export task by name in the job
const exportTaskData = currentJob.tasks?.find((task: any) => task.name === 'export-file');
const downloadUrl = exportTaskData?.result?.files?.[0]?.url;
const outputFilename = exportTaskData?.result?.files?.[0]?.filename || `archive.${output_format}`;
let storedFileId: string | undefined;
if (store_file && downloadUrl) {
try {
const fileResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: downloadUrl,
});
if (fileResponse.status === 200 && fileResponse.body) {
let fileData: Buffer;
if (typeof fileResponse.body === 'string') {
fileData = Buffer.from(fileResponse.body, 'binary');
} else {
fileData = Buffer.from(fileResponse.body as ArrayBuffer);
}
storedFileId = await context.files.write({
data: fileData,
fileName: outputFilename,
});
}
} catch (error) {
// Continue without throwing
}
}
return {
job: currentJob,
download_url: downloadUrl,
filename: outputFilename,
stored_file_id: storedFileId,
file_count: files.length,
output_format,
size: exportTaskData?.result?.files?.[0]?.size || 0
};
} else if (currentJob.status === 'error') {
throw new Error(`Archive job failed: ${currentJob.message || 'Unknown error'}`);
}
await new Promise(resolve => setTimeout(resolve, 5000));
attempts++;
}
throw new Error('Archive creation did not complete within the timeout period');
}
return {
job,
file_count: files.length,
output_format,
status: 'processing'
};
} catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error(`Archive creation failed: ${String(error)}`);
}
},
});

View File

@@ -0,0 +1,362 @@
import { createAction } from '@activepieces/pieces-framework';
import { propsValidation, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { cloudconvertAuth, CloudConvertClient, captureWebsiteSchema } from '../common';
import { Property } from '@activepieces/pieces-framework';
const captureWebsiteProps = () => ({
url: Property.ShortText({
displayName: 'Website URL',
description: 'The URL of the website to capture',
required: true,
}),
output_format: Property.StaticDropdown({
displayName: 'Output Format',
description: 'The target format to convert to',
required: true,
options: {
options: [
{ label: 'PDF', value: 'pdf' },
{ label: 'PNG', value: 'png' },
{ label: 'JPG', value: 'jpg' },
]
},
defaultValue: 'pdf'
}),
pages: Property.ShortText({
displayName: 'Pages',
description: 'Page range (e.g. 1-3) or comma separated list (e.g. 1,2,3) of pages',
required: false,
}),
zoom: Property.Number({
displayName: 'Zoom Level',
description: 'Zoom level to display the website. Defaults to 1',
required: false,
}),
page_width: Property.Number({
displayName: 'Page Width (cm)',
description: 'Page width in cm',
required: false,
}),
page_height: Property.Number({
displayName: 'Page Height (cm)',
description: 'Page height in cm',
required: false,
}),
page_format: Property.StaticDropdown({
displayName: 'Page Format',
description: 'Paper format type when printing a PDF. Overrides page_width and page_height',
required: false,
options: {
options: [
{ label: 'Letter', value: 'letter' },
{ label: 'Legal', value: 'legal' },
{ label: 'Tabloid', value: 'tabloid' },
{ label: 'Ledger', value: 'ledger' },
{ label: 'A0', value: 'a0' },
{ label: 'A1', value: 'a1' },
{ label: 'A2', value: 'a2' },
{ label: 'A3', value: 'a3' },
{ label: 'A4', value: 'a4' },
{ label: 'A5', value: 'a5' },
{ label: 'A6', value: 'a6' },
]
}
}),
page_orientation: Property.StaticDropdown({
displayName: 'Page Orientation',
description: 'Page orientation for PDF output',
required: false,
options: {
options: [
{ label: 'Portrait', value: 'portrait' },
{ label: 'Landscape', value: 'landscape' },
]
},
defaultValue: 'portrait'
}),
margin_top: Property.Number({
displayName: 'Top Margin (mm)',
description: 'Page top margin in mm',
required: false,
}),
margin_bottom: Property.Number({
displayName: 'Bottom Margin (mm)',
description: 'Page bottom margin in mm',
required: false,
}),
margin_left: Property.Number({
displayName: 'Left Margin (mm)',
description: 'Page left margin in mm',
required: false,
}),
margin_right: Property.Number({
displayName: 'Right Margin (mm)',
description: 'Page right margin in mm',
required: false,
}),
print_background: Property.Checkbox({
displayName: 'Print Background',
description: 'Render the background of websites',
required: false,
defaultValue: true,
}),
display_header_footer: Property.Checkbox({
displayName: 'Display Header/Footer',
description: 'Create a header and a footer with the URL and page numbers',
required: false,
}),
header_template: Property.LongText({
displayName: 'Header Template',
description: 'HTML template for the print header with classes: date, title, url, pageNumber, totalPages',
required: false,
}),
footer_template: Property.LongText({
displayName: 'Footer Template',
description: 'HTML template for the print footer with classes: date, title, url, pageNumber, totalPages',
required: false,
}),
wait_until: Property.StaticDropdown({
displayName: 'Wait Until',
description: 'When to consider navigation finished',
required: false,
options: {
options: [
{ label: 'Load Event', value: 'load' },
{ label: 'DOMContentLoaded', value: 'domcontentloaded' },
{ label: 'Network Idle (0 connections)', value: 'networkidle0' },
{ label: 'Network Idle (2 connections)', value: 'networkidle2' },
]
},
defaultValue: 'load'
}),
wait_for_element: Property.ShortText({
displayName: 'Wait for Element',
description: 'CSS selector for element to wait for (e.g. "body" or "#element")',
required: false,
}),
wait_time: Property.Number({
displayName: 'Wait Time (ms)',
description: 'Additional time in ms to wait after the page load',
required: false,
}),
css_media_type: Property.StaticDropdown({
displayName: 'CSS Media Type',
description: 'Changes the CSS media type of the page',
required: false,
options: {
options: [
{ label: 'Print', value: 'print' },
{ label: 'Screen', value: 'screen' },
]
},
defaultValue: 'print'
}),
filename: Property.ShortText({
displayName: 'Output Filename',
description: 'Choose a filename (including extension) for the output file',
required: false,
defaultValue: 'captured-website'
}),
engine: Property.StaticDropdown({
displayName: 'Engine',
description: 'Use a specific engine for the conversion',
required: false,
options: {
options: [
{ label: 'Chrome (Default)', value: 'chrome' },
{ label: 'wkhtmltopdf', value: 'wkhtml' },
]
}
}),
engine_version: Property.ShortText({
displayName: 'Engine Version',
description: 'Use a specific engine version for the conversion',
required: false,
}),
timeout: Property.Number({
displayName: 'Timeout (seconds)',
description: 'Timeout in seconds after the task will be cancelled',
required: false,
}),
wait_for_completion: Property.Checkbox({
displayName: 'Wait for Completion',
description: 'Wait for the capture to complete before returning',
required: true,
defaultValue: true,
}),
store_file: Property.Checkbox({
displayName: 'Store File',
description: 'Download and store the captured file in Activepieces',
required: false,
defaultValue: true,
}),
});
export const captureWebsite = createAction({
name: 'capture_website',
displayName: 'Capture Website',
description: 'Capture webpage as PDF, screenshot PNG, or JPG from a URL',
auth: cloudconvertAuth,
requireAuth: true,
props: captureWebsiteProps(),
async run(context) {
await captureWebsiteSchema.parseAsync(context.propsValue);
const {
url,
output_format,
pages,
zoom,
page_width,
page_height,
page_format,
page_orientation,
margin_top,
margin_bottom,
margin_left,
margin_right,
print_background,
display_header_footer,
header_template,
footer_template,
wait_until,
wait_for_element,
wait_time,
css_media_type,
filename,
engine,
engine_version,
timeout,
wait_for_completion,
store_file
} = context.propsValue;
const client = new CloudConvertClient(context.auth);
try {
const captureOptions: any = {
url,
output_format,
};
if (pages) captureOptions.pages = pages;
if (zoom !== undefined) captureOptions.zoom = zoom;
if (page_width !== undefined) captureOptions.page_width = page_width;
if (page_height !== undefined) captureOptions.page_height = page_height;
if (page_format) captureOptions.page_format = page_format;
if (page_orientation) captureOptions.page_orientation = page_orientation;
if (margin_top !== undefined) captureOptions.margin_top = margin_top;
if (margin_bottom !== undefined) captureOptions.margin_bottom = margin_bottom;
if (margin_left !== undefined) captureOptions.margin_left = margin_left;
if (margin_right !== undefined) captureOptions.margin_right = margin_right;
if (print_background !== undefined) captureOptions.print_background = print_background;
if (display_header_footer !== undefined) captureOptions.display_header_footer = display_header_footer;
if (header_template) captureOptions.header_template = header_template;
if (footer_template) captureOptions.footer_template = footer_template;
if (wait_until) captureOptions.wait_until = wait_until;
if (wait_for_element) captureOptions.wait_for_element = wait_for_element;
if (wait_time !== undefined) captureOptions.wait_time = wait_time;
if (css_media_type) captureOptions.css_media_type = css_media_type;
if (filename) captureOptions.filename = filename;
if (engine) captureOptions.engine = engine;
if (engine_version) captureOptions.engine_version = engine_version;
if (timeout !== undefined) captureOptions.timeout = timeout;
const captureTask = await client.createCaptureTask(captureOptions);
if (wait_for_completion) {
let attempts = 0;
const maxAttempts = 60;
while (attempts < maxAttempts) {
const currentTask = await client.getTask(captureTask.id);
if (currentTask.status === 'finished') {
break;
} else if (currentTask.status === 'error') {
throw new Error(`Capture task failed: ${currentTask.message || 'Unknown error'}`);
}
await new Promise(resolve => setTimeout(resolve, 5000));
attempts++;
}
if (attempts >= maxAttempts) {
throw new Error('Capture task did not complete within the timeout period');
}
}
const exportTask = await client.createExportTask(captureTask.id);
if (wait_for_completion) {
let attempts = 0;
const maxAttempts = 60;
while (attempts < maxAttempts) {
const exportTaskData = await client.getTask(exportTask.id);
if (exportTaskData.status === 'finished') {
const downloadUrl = exportTaskData.result?.files?.[0]?.url;
const filename = exportTaskData.result?.files?.[0]?.filename || 'captured-website.pdf';
let storedFileId: string | undefined;
if (store_file && downloadUrl) {
try {
const fileResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: downloadUrl,
});
if (fileResponse.status === 200 && fileResponse.body) {
let fileData: Buffer;
if (typeof fileResponse.body === 'string') {
fileData = Buffer.from(fileResponse.body, 'binary');
} else {
fileData = Buffer.from(fileResponse.body as ArrayBuffer);
}
storedFileId = await context.files.write({
data: fileData,
fileName: filename,
});
}
} catch (error) {
// Continue without throwing
}
}
return {
capture_task: captureTask,
export_task: exportTaskData,
download_url: downloadUrl,
filename,
stored_file_id: storedFileId,
url,
output_format,
};
} else if (exportTaskData.status === 'error') {
throw new Error(`Export task failed: ${exportTaskData.message || 'Unknown error'}`);
}
await new Promise(resolve => setTimeout(resolve, 5000));
attempts++;
}
throw new Error('Export did not complete within the timeout period');
}
return {
capture_task: captureTask,
export_task: exportTask,
url,
output_format,
status: 'processing',
};
} catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error(`Website capture failed: ${String(error)}`);
}
},
});

View File

@@ -0,0 +1,257 @@
import { createAction } from '@activepieces/pieces-framework';
import { propsValidation, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { cloudconvertAuth, CloudConvertClient, convertFileProps, convertFileSchema } from '../common';
// Get the props function result
const getConvertFileProps = convertFileProps;
export const convertFile = createAction({
name: 'convert_file',
displayName: 'Convert File',
description: 'Create a basic file conversion task for a single file with desired output format',
auth: cloudconvertAuth,
requireAuth: true,
props: getConvertFileProps(),
async run(context) {
await convertFileSchema.parseAsync(context.propsValue);
const { import_method, url, file, stored_file_id, input_format, output_format, filename, engine, engine_version, timeout, wait_for_completion, store_file } = context.propsValue;
const client = new CloudConvertClient(context.auth);
try {
const supportedFormats = await client.getSupportedFormats({
inputFormat: input_format === 'auto' ? undefined : input_format,
outputFormat: output_format as string,
include: ['options', 'engine_versions']
});
if (supportedFormats.length === 0) {
const allOutputFormats = await client.getSupportedFormats({
outputFormat: output_format as string,
include: ['options', 'engine_versions']
});
throw new Error(`Conversion from ${input_format || 'auto-detected format'} to ${output_format} is not supported by CloudConvert. Available input formats for ${output_format}: ${allOutputFormats.map((f: any) => f.input_format).join(', ')}`);
}
const engines = [...new Set(supportedFormats.map((f: any) => f.engine))];
const jobTasks: Record<string, any> = {};
if (import_method === 'url') {
if (!url) {
throw new Error('File URL is required when using URL import method');
}
jobTasks['import-file'] = {
operation: 'import/url',
url: url,
...(filename && { filename })
};
} else if (import_method === 'stored_file') {
if (!stored_file_id) {
throw new Error('Stored File ID is required when using stored file import method');
}
if (!context.server?.apiUrl) {
throw new Error('Server API URL is not available. Please check your Activepieces server configuration.');
}
const baseUrl = context.server.apiUrl.replace(/\/$/, '');
const fileUrl = `${baseUrl}/v1/step-files/${stored_file_id}`;
try {
new URL(fileUrl);
} catch (urlError) {
throw new Error(`Invalid file URL constructed: ${fileUrl}. URL Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`);
}
jobTasks['import-file'] = {
operation: 'import/url',
url: fileUrl,
...(filename && { filename })
};
} else if (import_method === 'upload') {
if (!file || !file.base64) {
throw new Error('Please select a file to upload from your device');
}
const uploadTask = await client.createUploadTask(file.filename || 'uploaded-file');
const uploadUrl = uploadTask.result.form.url;
const uploadForm = uploadTask.result.form.parameters;
const formData = new FormData();
Object.entries(uploadForm).forEach(([key, value]) => {
formData.append(key, value as string);
});
if (file.base64) {
const buffer = Buffer.from(file.base64, 'base64');
const blob = new Blob([buffer], { type: file.extension ? `application/${file.extension}` : 'application/octet-stream' });
formData.append('file', blob, file.filename);
}
const uploadResponse = await httpClient.sendRequest({
method: HttpMethod.POST,
url: uploadUrl,
body: formData,
});
if (uploadResponse.status < 200 || uploadResponse.status >= 300) {
throw new Error(`Failed to upload file: HTTP ${uploadResponse.status} - ${uploadResponse.body?.message || 'Upload failed'}`);
}
jobTasks['import-file'] = {
operation: 'import/upload',
...(filename && { filename })
};
} else {
throw new Error('Invalid import method selected');
}
const convertOptions: any = {
input: 'import-file',
output_format,
};
if (input_format && input_format !== 'auto') {
convertOptions.input_format = input_format;
}
if (filename) {
convertOptions.filename = filename;
}
if (engine) {
if (!engines.includes(engine)) {
convertOptions.engine = engines[0];
} else {
convertOptions.engine = engine;
}
} else if (engines.length > 0) {
convertOptions.engine = engines[0];
}
if (engine_version) convertOptions.engine_version = engine_version;
if (timeout) convertOptions.timeout = timeout;
jobTasks['convert-file'] = {
operation: 'convert',
...convertOptions
};
jobTasks['export-file'] = {
operation: 'export/url',
input: 'convert-file'
};
const job = await client.createJob(jobTasks, `convert-${Date.now()}`);
if (wait_for_completion) {
let attempts = 0;
const maxAttempts = 60;
while (attempts < maxAttempts) {
const currentJob = await client.getJob(job.id);
if (currentJob.status === 'finished') {
const exportTaskData = currentJob.tasks?.find((task: any) => task.name === 'export-file');
const downloadUrl = exportTaskData?.result?.files?.[0]?.url;
const outputFilename = exportTaskData?.result?.files?.[0]?.filename || `converted-${output_format}`;
let storedFileId: string | undefined;
if (store_file && downloadUrl) {
try {
if (!downloadUrl || typeof downloadUrl !== 'string') {
throw new Error(`Invalid download URL: ${downloadUrl}`);
}
try {
new URL(downloadUrl);
} catch (urlError) {
throw new Error(`Invalid download URL format: ${downloadUrl} - ${urlError instanceof Error ? urlError.message : String(urlError)}`);
}
const fileResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: downloadUrl,
});
if (fileResponse.status === 200 && fileResponse.body) {
let fileData: Buffer;
if (typeof fileResponse.body === 'string') {
fileData = Buffer.from(fileResponse.body, 'binary');
} else {
fileData = Buffer.from(fileResponse.body as ArrayBuffer);
}
storedFileId = await context.files.write({
data: fileData,
fileName: outputFilename,
});
}
} catch (error) {
// Continue without throwing
}
}
return {
job: currentJob,
download_url: downloadUrl,
filename: outputFilename,
stored_file_id: storedFileId,
input_format,
output_format,
};
} else if (currentJob.status === 'error') {
const failedTasks = currentJob.tasks?.filter((task: any) => task.status === 'error') || [];
let errorMessage = `Conversion job failed: ${currentJob.message || 'Unknown error'}`;
if (failedTasks.length > 0) {
const taskErrors = failedTasks.map((task: any) => {
let taskError = `${task.name} (${task.operation})`;
if (task.code) taskError += ` - Code: ${task.code}`;
if (task.message) taskError += ` - ${task.message}`;
if (task.engine) taskError += ` - Engine: ${task.engine}`;
if (task.engine_version) taskError += ` v${task.engine_version}`;
return taskError;
}).join('; ');
errorMessage += `\n\nTask errors: ${taskErrors}`;
const convertTask = failedTasks.find((task: any) => task.operation === 'convert');
if (convertTask) {
if (convertTask.engine === 'pdftron-pdf2word' && convertTask.code === 'UNKNOWN_ERROR') {
errorMessage += '\n\nThis appears to be a file-specific issue. PDFs from website captures often contain complex layouts or images that cannot be converted to editable formats.';
errorMessage += '\n\nSuggested alternatives:';
errorMessage += '\n• Try converting to TXT (text only) or HTML format instead';
errorMessage += '\n• Use RTF format which is simpler than DOCX';
errorMessage += '\n• If this is a scanned/image-based PDF, it may need OCR processing';
errorMessage += '\n• Consider capturing the website directly to HTML instead of PDF→DOCX';
} else if (convertTask.engine === 'pdftron-pdf2word') {
errorMessage += '\n\nTip: Try converting without specifying an engine, or try a different engine for better compatibility.';
}
}
}
throw new Error(errorMessage);
}
await new Promise(resolve => setTimeout(resolve, 5000));
attempts++;
}
throw new Error('Conversion did not complete within the timeout period');
}
return {
job,
input_format,
output_format,
status: 'processing',
};
} catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error(`File conversion failed: ${String(error)}`);
}
},
});

View File

@@ -0,0 +1,195 @@
import { createAction } from '@activepieces/pieces-framework';
import { propsValidation, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { cloudconvertAuth, CloudConvertClient, downloadFileSchema } from '../common';
import { Property } from '@activepieces/pieces-framework';
const downloadFileProps = () => ({
task_id: Property.ShortText({
displayName: 'Task ID',
description: 'ID of the CloudConvert task to retrieve',
required: true,
}),
include: Property.MultiSelectDropdown({
auth: cloudconvertAuth,
displayName: 'Include Additional Data',
description: 'Additional data to include in the response',
required: false,
refreshers: [],
options: async () => ({
options: [
{ label: 'Retries', value: 'retries' },
{ label: 'Depends On Tasks', value: 'depends_on_tasks' },
{ label: 'Payload', value: 'payload' },
{ label: 'Job', value: 'job' },
]
})
}),
store_file: Property.Checkbox({
displayName: 'Store File',
description: 'Download and store the output files in Activepieces',
required: false,
defaultValue: true,
}),
});
export const downloadFile = createAction({
name: 'download_file',
displayName: 'Download a File',
description: 'Downloads output from a completed task',
auth: cloudconvertAuth,
requireAuth: true,
props: downloadFileProps(),
async run(context) {
await downloadFileSchema.parseAsync(context.propsValue);
const { task_id, include, store_file } = context.propsValue;
const client = new CloudConvertClient(context.auth);
try {
const queryParams: Record<string, string> = {};
if (include && Array.isArray(include) && include.length > 0) {
queryParams['include'] = include.join(',');
}
const task = await client.getTask(task_id, queryParams);
if (task.status !== 'finished') {
throw new Error(`Task is not finished. Current status: ${task.status}. ${task.message || ''}`);
}
let downloadTask = task;
let exportTaskId: string | undefined;
if (task.operation !== 'export/url' && task.result?.files && task.result.files.length > 0) {
const hasDownloadUrls = task.result.files.some((file: any) => file.url);
if (!hasDownloadUrls) {
try {
const exportTask = await client.createExportTask(task_id);
exportTaskId = exportTask.id;
let attempts = 0;
const maxAttempts = 60;
while (attempts < maxAttempts) {
const exportTaskData = await client.getTask(exportTask.id);
if (exportTaskData.status === 'finished') {
downloadTask = exportTaskData;
break;
} else if (exportTaskData.status === 'error') {
throw new Error(`Export task failed: ${exportTaskData.message || 'Unknown error'}. Code: ${exportTaskData.code || 'N/A'}`);
}
await new Promise(resolve => setTimeout(resolve, 2000));
attempts++;
}
if (attempts >= maxAttempts) {
throw new Error('Export task did not complete within the timeout period');
}
} catch (exportError) {
// Continue with original task
}
}
}
const result: any = {
id: task.id,
job_id: task.job_id,
name: task.name,
operation: task.operation,
status: task.status,
message: task.message,
code: task.code,
credits: task.credits,
created_at: task.created_at,
started_at: task.started_at,
ended_at: task.ended_at,
engine: task.engine,
engine_version: task.engine_version,
...(exportTaskId && { export_task_id: exportTaskId }),
};
if (task.depends_on_tasks) {
result.depends_on_tasks = task.depends_on_tasks;
}
if (task.retry_of_task_id) {
result.retry_of_task_id = task.retry_of_task_id;
}
if (task.retries) {
result.retries = task.retries;
}
if (task.payload) {
result.payload = task.payload;
}
if (downloadTask.result) {
result.result = downloadTask.result;
if (downloadTask.result.files && downloadTask.result.files.length > 0) {
result.download_urls = downloadTask.result.files.map((file: any) => ({
filename: file.filename,
url: file.url,
size: file.size,
}));
if (store_file) {
result.stored_files = [];
for (const file of downloadTask.result.files) {
try {
if (!file.url || typeof file.url !== 'string') {
continue;
}
try {
new URL(file.url);
} catch (urlError) {
continue;
}
const fileResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: file.url,
});
if (fileResponse.status === 200 && fileResponse.body) {
let fileData: Buffer;
if (typeof fileResponse.body === 'string') {
fileData = Buffer.from(fileResponse.body, 'binary');
} else {
fileData = Buffer.from(fileResponse.body as ArrayBuffer);
}
const storedFileId = await context.files.write({
data: fileData,
fileName: file.filename || `downloaded-file-${Date.now()}`,
});
result.stored_files.push({
filename: file.filename,
size: file.size,
stored_file_id: storedFileId,
original_url: file.url,
});
}
} catch (error) {
// Continue with next file
}
}
}
}
}
return result;
} catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error(`Task retrieval failed: ${String(error)}`);
}
},
});

View File

@@ -0,0 +1,252 @@
import { createAction } from '@activepieces/pieces-framework';
import { propsValidation, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { cloudconvertAuth, CloudConvertClient, mergePdfSchema } from '../common';
import { Property } from '@activepieces/pieces-framework';
const mergePdfProps = () => ({
import_method: Property.StaticDropdown({
displayName: 'Import Method',
description: 'How to import the files for merging',
required: true,
options: {
options: [
{ label: 'File Upload', value: 'upload' },
{ label: 'File URL', value: 'url' },
{ label: 'Stored File ID', value: 'stored_file' },
]
},
defaultValue: 'upload'
}),
files: Property.Array({
displayName: 'Files to Merge',
description: 'List of files to merge into a single PDF',
required: true,
properties: {
url: Property.ShortText({
displayName: 'File URL',
description: 'URL of the file to merge',
required: false,
}),
file: Property.File({
displayName: 'File',
description: 'File to upload and merge',
required: false,
}),
stored_file_id: Property.ShortText({
displayName: 'Stored File ID',
description: 'ID of a previously stored file in Activepieces to merge',
required: false,
}),
filename: Property.ShortText({
displayName: 'Filename in PDF',
description: 'Optional filename for this file within the PDF',
required: false,
})
}
}),
filename: Property.ShortText({
displayName: 'Output Filename',
description: 'Choose a filename (including extension) for the output file',
required: false,
defaultValue: 'merged-document.pdf'
}),
engine: Property.StaticDropdown({
displayName: 'Engine',
description: 'Use a specific engine for the conversion',
required: false,
options: {
options: [
{ label: '3-Heights (Default)', value: '3heights' },
{ label: 'PDFTron', value: 'pdftron' },
{ label: 'MuPDF', value: 'mupdf' },
{ label: 'Poppler', value: 'poppler' },
{ label: 'LibreOffice', value: 'libreoffice' },
]
}
}),
engine_version: Property.ShortText({
displayName: 'Engine Version',
description: 'Use a specific engine version for the conversion',
required: false,
}),
timeout: Property.Number({
displayName: 'Timeout (seconds)',
description: 'Timeout in seconds after which the task will be cancelled',
required: false,
}),
wait_for_completion: Property.Checkbox({
displayName: 'Wait for Completion',
description: 'Wait for the merge to complete before returning',
required: true,
defaultValue: true,
}),
store_file: Property.Checkbox({
displayName: 'Store File',
description: 'Download and store the merged PDF in Activepieces',
required: false,
defaultValue: true,
}),
});
export const mergePdf = createAction({
name: 'merge_pdf',
displayName: 'Merge Files to PDF',
description: 'Combine multiple documents/images into a single PDF',
auth: cloudconvertAuth,
requireAuth: true,
props: mergePdfProps(),
async run(context) {
await mergePdfSchema.parseAsync(context.propsValue);
const { import_method, files, filename, engine, engine_version, timeout, wait_for_completion, store_file } = context.propsValue;
if (!files || files.length < 2) {
throw new Error('At least 2 files are required for merging');
}
const client = new CloudConvertClient(context.auth);
try {
const jobTasks: Record<string, any> = {};
files.forEach((file: any, index: number) => {
const taskName = `import-file-${index + 1}`;
if (import_method === 'url') {
if (!file.url) {
throw new Error(`File URL is required for file ${index + 1} when using URL import method`);
}
jobTasks[taskName] = {
operation: 'import/url',
url: file.url,
...(file.filename && { filename: file.filename })
};
} else if (import_method === 'stored_file') {
if (!file.stored_file_id) {
throw new Error(`Stored File ID is required for file ${index + 1} when using stored file import method`);
}
if (!context.server?.apiUrl) {
throw new Error('Server API URL is not available. Please check your Activepieces server configuration.');
}
const baseUrl = context.server.apiUrl.replace(/\/$/, '');
const fileUrl = `${baseUrl}/v1/step-files/${file.stored_file_id}`;
try {
new URL(fileUrl);
} catch (urlError) {
throw new Error(`Invalid file URL constructed: ${fileUrl}. URL Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`);
}
jobTasks[taskName] = {
operation: 'import/url',
url: fileUrl,
...(file.filename && { filename: file.filename })
};
} else if (import_method === 'upload') {
if (!file.file || !file.file.base64) {
throw new Error(`Please select a file to upload for file ${index + 1}`);
}
throw new Error('Upload method for multiple files is not yet supported with the new job format. Please use URL or stored file methods.');
} else {
throw new Error('Invalid import method selected');
}
});
// Collect all import task names
const importTaskNames = files.map((_, index) => `import-file-${index + 1}`);
// Merge task
const mergeOptions: any = {
input: importTaskNames.length === 1 ? importTaskNames[0] : importTaskNames,
output_format: 'pdf',
};
if (filename) mergeOptions.filename = filename;
if (engine) mergeOptions.engine = engine;
if (engine_version) mergeOptions.engine_version = engine_version;
if (timeout) mergeOptions.timeout = timeout;
jobTasks['merge-files'] = {
operation: 'merge',
...mergeOptions
};
// Export task
jobTasks['export-file'] = {
operation: 'export/url',
input: 'merge-files'
};
const job = await client.createJob(jobTasks, `merge-${Date.now()}`);
if (wait_for_completion) {
let attempts = 0;
const maxAttempts = 60;
while (attempts < maxAttempts) {
const currentJob = await client.getJob(job.id);
if (currentJob.status === 'finished') {
// Find the export task by name in the job
const exportTaskData = currentJob.tasks?.find((task: any) => task.name === 'export-file');
const downloadUrl = exportTaskData?.result?.files?.[0]?.url;
const outputFilename = exportTaskData?.result?.files?.[0]?.filename || 'merged-document.pdf';
let storedFileId: string | undefined;
if (store_file && downloadUrl) {
try {
const fileResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: downloadUrl,
});
if (fileResponse.status === 200 && fileResponse.body) {
let fileData: Buffer;
if (typeof fileResponse.body === 'string') {
fileData = Buffer.from(fileResponse.body, 'binary');
} else {
fileData = Buffer.from(fileResponse.body as ArrayBuffer);
}
storedFileId = await context.files.write({
data: fileData,
fileName: outputFilename,
});
}
} catch (error) {
// Continue without throwing
}
}
return {
job: currentJob,
download_url: downloadUrl,
filename: outputFilename,
stored_file_id: storedFileId,
file_count: files.length,
output_format: 'pdf',
};
} else if (currentJob.status === 'error') {
throw new Error(`Merge job failed: ${currentJob.message || 'Unknown error'}`);
}
await new Promise(resolve => setTimeout(resolve, 5000));
attempts++;
}
throw new Error('Merge did not complete within the timeout period');
}
return {
job,
file_count: files.length,
output_format: 'pdf',
status: 'processing',
};
} catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error(`Merge failed: ${String(error)}`);
}
},
});

View File

@@ -0,0 +1,304 @@
import { createAction } from '@activepieces/pieces-framework';
import { propsValidation, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { cloudconvertAuth, CloudConvertClient, optimizeFileSchema } from '../common';
import { Property } from '@activepieces/pieces-framework';
const optimizeFileProps = () => ({
import_method: Property.StaticDropdown({
displayName: 'Import Method',
description: 'How to import the file for optimization',
required: true,
options: {
options: [
{ label: 'File Upload', value: 'upload' },
{ label: 'File URL', value: 'url' },
{ label: 'Stored File ID', value: 'stored_file' },
]
},
defaultValue: 'upload'
}),
file: Property.File({
displayName: 'File',
description: 'File to upload and optimize (PDF, PNG, JPG)',
required: false,
}),
url: Property.ShortText({
displayName: 'File URL',
description: 'URL of the file to optimize',
required: false,
}),
stored_file_id: Property.ShortText({
displayName: 'Stored File ID',
description: 'ID of a previously stored file in Activepieces to optimize',
required: false,
}),
input_format: Property.StaticDropdown({
displayName: 'Input Format',
description: 'The current format of the file. If not set, the extension of the input file is used',
required: false,
options: {
options: [
{ label: 'PDF', value: 'pdf' },
{ label: 'PNG', value: 'png' },
{ label: 'JPG', value: 'jpg' },
]
}
}),
profile: Property.StaticDropdown({
displayName: 'Optimization Profile',
description: 'Optimization profile for specific target needs',
required: false,
defaultValue: 'web',
options: {
options: [
{ label: 'Web - Remove redundant data for the web', value: 'web' },
{ label: 'Print - Optimized for printing', value: 'print' },
{ label: 'Archive - Optimized for archiving purposes', value: 'archive' },
{ label: 'MRC - Optimized for scanned images', value: 'mrc' },
{ label: 'Max - Maximal size reduction', value: 'max' },
]
}
}),
flatten_signatures: Property.Checkbox({
displayName: 'Flatten Signatures',
description: 'Flatten visible signatures and keep them as non-editable graphics',
required: false,
defaultValue: false,
}),
colorspace: Property.StaticDropdown({
displayName: 'Color Space',
description: 'Color space of raster images',
required: false,
options: {
options: [
{ label: 'Unchanged', value: 'unchanged' },
{ label: 'RGB', value: 'rgb' },
{ label: 'CMYK', value: 'cmyk' },
{ label: 'Grayscale', value: 'grayscale' },
]
}
}),
filename: Property.ShortText({
displayName: 'Output Filename',
description: 'Choose a filename (including extension) for the output file',
required: false,
defaultValue: 'optimized-document.pdf'
}),
engine: Property.StaticDropdown({
displayName: 'Engine',
description: 'Use a specific engine for the optimization',
required: false,
options: {
options: [
{ label: '3-Heights', value: '3heights' },
{ label: 'PDF Tools', value: 'pdftools' },
]
}
}),
engine_version: Property.ShortText({
displayName: 'Engine Version',
description: 'Use a specific engine version for the optimization',
required: false,
}),
timeout: Property.Number({
displayName: 'Timeout (seconds)',
description: 'Timeout in seconds after which the task will be cancelled',
required: false,
}),
wait_for_completion: Property.Checkbox({
displayName: 'Wait for Completion',
description: 'Wait for the optimization to complete before returning',
required: true,
defaultValue: true,
}),
store_file: Property.Checkbox({
displayName: 'Store File',
description: 'Download and store the optimized file in Activepieces',
required: false,
defaultValue: true,
}),
});
export const optimizeFile = createAction({
name: 'optimize_file',
displayName: 'Optimize File',
description: 'Creates a task to optimize and compress a file',
auth: cloudconvertAuth,
requireAuth: true,
props: optimizeFileProps(),
async run(context) {
await optimizeFileSchema.parseAsync(context.propsValue);
const { import_method, url, file, stored_file_id, input_format, profile, flatten_signatures, colorspace, filename, engine, engine_version, timeout, wait_for_completion, store_file } = context.propsValue;
const client = new CloudConvertClient(context.auth);
try {
const jobTasks: Record<string, any> = {};
// Import task
if (import_method === 'url') {
if (!url) {
throw new Error('File URL is required when using URL import method');
}
jobTasks['import-file'] = {
operation: 'import/url',
url: url,
...(filename && { filename })
};
} else if (import_method === 'stored_file') {
if (!stored_file_id) {
throw new Error('Stored File ID is required when using stored file import method');
}
if (!context.server?.apiUrl) {
throw new Error('Server API URL is not available. Please check your Activepieces server configuration.');
}
const baseUrl = context.server.apiUrl.replace(/\/$/, '');
const fileUrl = `${baseUrl}/v1/step-files/${stored_file_id}`;
try {
new URL(fileUrl);
} catch (urlError) {
throw new Error(`Invalid file URL constructed: ${fileUrl}. URL Error: ${urlError instanceof Error ? urlError.message : String(urlError)}`);
}
jobTasks['import-file'] = {
operation: 'import/url',
url: fileUrl,
...(filename && { filename })
};
} else if (import_method === 'upload') {
if (!file || !file.base64) {
throw new Error('Please select a file to upload from your device');
}
const uploadTask = await client.createUploadTask(file.filename || 'uploaded-file');
const uploadUrl = uploadTask.result.form.url;
const uploadForm = uploadTask.result.form.parameters;
const formData = new FormData();
Object.entries(uploadForm).forEach(([key, value]) => {
formData.append(key, value as string);
});
if (file.base64) {
const buffer = Buffer.from(file.base64, 'base64');
const blob = new Blob([buffer], { type: file.extension ? `application/${file.extension}` : 'application/octet-stream' });
formData.append('file', blob, file.filename);
}
const uploadResponse = await httpClient.sendRequest({
method: HttpMethod.POST,
url: uploadUrl,
body: formData,
});
if (uploadResponse.status < 200 || uploadResponse.status >= 300) {
throw new Error(`Failed to upload file: HTTP ${uploadResponse.status} - ${uploadResponse.body?.message || 'Upload failed'}`);
}
jobTasks['import-file'] = {
operation: 'import/upload',
...(filename && { filename })
};
} else {
throw new Error('Invalid import method selected');
}
const optimizeOptions: any = {
input: 'import-file',
};
if (input_format) optimizeOptions.input_format = input_format;
if (profile) optimizeOptions.profile = profile;
if (flatten_signatures !== undefined) optimizeOptions.flatten_signatures = flatten_signatures;
if (colorspace) optimizeOptions.colorspace = colorspace;
if (filename) optimizeOptions.filename = filename;
if (engine) optimizeOptions.engine = engine;
if (engine_version) optimizeOptions.engine_version = engine_version;
if (timeout) optimizeOptions.timeout = timeout;
jobTasks['optimize-file'] = {
operation: 'optimize',
...optimizeOptions
};
jobTasks['export-file'] = {
operation: 'export/url',
input: 'optimize-file'
};
const job = await client.createJob(jobTasks, `optimize-${Date.now()}`);
if (wait_for_completion) {
let attempts = 0;
const maxAttempts = 60;
while (attempts < maxAttempts) {
const currentJob = await client.getJob(job.id);
if (currentJob.status === 'finished') {
const exportTaskData = currentJob.tasks?.find((task: any) => task.name === 'export-file');
const downloadUrl = exportTaskData?.result?.files?.[0]?.url;
const outputFilename = exportTaskData?.result?.files?.[0]?.filename || `optimized-${input_format || 'file'}`;
let storedFileId: string | undefined;
if (store_file && downloadUrl) {
try {
const fileResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: downloadUrl,
});
if (fileResponse.status === 200 && fileResponse.body) {
let fileData: Buffer;
if (typeof fileResponse.body === 'string') {
fileData = Buffer.from(fileResponse.body, 'binary');
} else {
fileData = Buffer.from(fileResponse.body as ArrayBuffer);
}
storedFileId = await context.files.write({
data: fileData,
fileName: outputFilename,
});
}
} catch (error) {
// Continue without throwing
}
}
return {
job: currentJob,
download_url: downloadUrl,
filename: outputFilename,
stored_file_id: storedFileId,
input_format,
profile,
size: exportTaskData?.result?.files?.[0]?.size || 0,
};
} else if (currentJob.status === 'error') {
throw new Error(`Optimize job failed: ${currentJob.message || 'Unknown error'}`);
}
await new Promise(resolve => setTimeout(resolve, 5000));
attempts++;
}
throw new Error('Optimization did not complete within the timeout period');
}
return {
job,
input_format,
profile,
status: 'processing',
};
} catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error(`File optimization failed: ${String(error)}`);
}
},
});

View File

@@ -0,0 +1,26 @@
import { OAuth2GrantType } from '@activepieces/shared';
import { PieceAuth, Property } from '@activepieces/pieces-framework';
export const cloudconvertAuth = PieceAuth.OAuth2({
description: 'Connect your CloudConvert account using OAuth2',
authUrl: 'https://cloudconvert.com/oauth/authorize',
tokenUrl: 'https://cloudconvert.com/oauth/token',
required: true,
grantType: OAuth2GrantType.AUTHORIZATION_CODE,
scope: ['task.read', 'task.write'],
props: {
region: Property.StaticDropdown({
displayName: 'Region',
description: 'CloudConvert processing region',
required: true,
options: {
options: [
{ label: 'Auto (Nearest)', value: 'auto' },
{ label: 'EU Central (Germany)', value: 'eu-central' },
{ label: 'US East (Virginia)', value: 'us-east' },
]
},
defaultValue: 'auto'
})
}
});

View File

@@ -0,0 +1,379 @@
import { httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common';
export class CloudConvertClient {
private readonly auth: any;
private readonly baseUrl: string;
constructor(auth: any) {
this.auth = auth;
const region = auth.region || 'auto';
switch (region) {
case 'eu-central':
this.baseUrl = 'https://eu-central.api.cloudconvert.com/v2';
break;
case 'us-east':
this.baseUrl = 'https://us-east.api.cloudconvert.com/v2';
break;
default:
this.baseUrl = 'https://api.cloudconvert.com/v2';
}
}
async apiCall({
method,
resourceUri,
body = undefined,
queryParams = undefined,
}: {
method: HttpMethod;
resourceUri: string;
body?: any;
queryParams?: Record<string, string>;
}) {
// OAuth2 authentication
const authConfig = {
type: AuthenticationType.BEARER_TOKEN as const,
token: this.auth.access_token,
};
const response = await httpClient.sendRequest({
method: method,
url: `${this.baseUrl}${resourceUri}`,
body,
queryParams,
authentication: authConfig
});
return response;
}
async createImportTask(fileUrl: string, filename?: string) {
const response = await this.apiCall({
method: HttpMethod.POST,
resourceUri: '/import/url',
body: {
url: fileUrl,
...(filename && { filename })
}
});
if (response.status !== 201) {
throw new Error(`Failed to create import task: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async createUploadTask(filename: string) {
const response = await this.apiCall({
method: HttpMethod.POST,
resourceUri: '/import/upload',
body: {
filename
}
});
if (response.status !== 201) {
throw new Error(`Failed to create upload task: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async createImportBase64Task(fileContent: string, filename: string) {
const response = await this.apiCall({
method: HttpMethod.POST,
resourceUri: '/import/base64',
body: {
file: fileContent,
filename
}
});
if (response.status !== 201) {
throw new Error(`Failed to create base64 import task: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async createArchiveTask(input: string | string[], outputFormat: string, options?: {
filename?: string;
engine?: string;
engineVersion?: string;
timeout?: number;
}) {
const body: any = {
input: Array.isArray(input) && input.length === 1 ? input[0] : input,
output_format: outputFormat,
};
if (options?.filename) {
body.filename = options.filename;
}
if (options?.engine) {
body.engine = options.engine;
if (options.engineVersion) {
body.engine_version = options.engineVersion;
}
}
if (options?.timeout) {
body.timeout = options.timeout;
}
const response = await this.apiCall({
method: HttpMethod.POST,
resourceUri: '/archive',
body
});
if (response.status !== 201) {
throw new Error(`Failed to create archive task: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async createCaptureTask(options: {
url: string;
output_format: string;
pages?: string;
zoom?: number;
page_width?: number;
page_height?: number;
page_format?: string;
page_orientation?: string;
margin_top?: number;
margin_bottom?: number;
margin_left?: number;
margin_right?: number;
print_background?: boolean;
display_header_footer?: boolean;
header_template?: string;
footer_template?: string;
wait_until?: string;
wait_for_element?: string;
wait_time?: number;
css_media_type?: string;
filename?: string;
engine?: string;
engine_version?: string;
timeout?: number;
}) {
const response = await this.apiCall({
method: HttpMethod.POST,
resourceUri: '/capture-website',
body: options
});
if (response.status !== 201) {
throw new Error(`Failed to create capture task: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async createConvertTask(options: {
input: string | string[];
input_format?: string;
output_format: string;
filename?: string;
engine?: string;
engine_version?: string;
timeout?: number;
[key: string]: any;
}) {
const response = await this.apiCall({
method: HttpMethod.POST,
resourceUri: '/convert',
body: options
});
if (response.status !== 201) {
throw new Error(`Failed to create convert task: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async createMergeTask(options: {
input: string | string[];
output_format: string;
filename?: string;
engine?: string;
engine_version?: string;
timeout?: number;
[key: string]: any;
}) {
const response = await this.apiCall({
method: HttpMethod.POST,
resourceUri: '/merge',
body: options
});
if (response.status !== 201) {
throw new Error(`Failed to create merge task: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async createOptimizeTask(options: {
input: string | string[];
input_format?: string;
profile?: string;
flatten_signatures?: boolean;
colorspace?: string;
filename?: string;
engine?: string;
engine_version?: string;
timeout?: number;
[key: string]: any;
}) {
const response = await this.apiCall({
method: HttpMethod.POST,
resourceUri: '/optimize',
body: options
});
if (response.status !== 201) {
throw new Error(`Failed to create optimize task: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async createExportTask(inputTaskId: string) {
const response = await this.apiCall({
method: HttpMethod.POST,
resourceUri: '/export/url',
body: {
input: inputTaskId
}
});
if (response.status !== 201) {
throw new Error(`Failed to create export task: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async createJob(tasks: Record<string, any>, tag?: string) {
const response = await this.apiCall({
method: HttpMethod.POST,
resourceUri: '/jobs',
body: {
tasks,
...(tag && { tag })
}
});
if (response.status !== 201) {
throw new Error(`Failed to create job: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async getJob(jobId: string) {
const response = await this.apiCall({
method: HttpMethod.GET,
resourceUri: `/jobs/${jobId}`
});
if (response.status !== 200) {
throw new Error(`Failed to get job: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async getTask(taskId: string, queryParams?: Record<string, string>) {
const response = await this.apiCall({
method: HttpMethod.GET,
resourceUri: `/tasks/${taskId}`,
queryParams
});
if (response.status !== 200) {
throw new Error(`Failed to get task: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data;
}
async getSupportedFormats(options?: {
inputFormat?: string;
outputFormat?: string;
engine?: string;
include?: string[];
}) {
const queryParams: Record<string, string> = {};
if (options?.inputFormat) {
queryParams['filter[input_format]'] = options.inputFormat;
}
if (options?.outputFormat) {
queryParams['filter[output_format]'] = options.outputFormat;
}
if (options?.engine) {
queryParams['filter[engine]'] = options.engine;
}
if (options?.include) {
queryParams['include'] = options.include.join(',');
}
const response = await this.apiCall({
method: HttpMethod.GET,
resourceUri: '/convert/formats',
queryParams
});
if (response.status !== 200) {
throw new Error(`Failed to get supported formats: HTTP ${response.status} - ${response.body?.message || 'Unknown error'}`);
}
return response.body.data || [];
}
}
export const cloudconvertCommon = {
baseUrl: (region = 'auto') => {
switch (region) {
case 'eu-central':
return 'https://eu-central.api.cloudconvert.com/v2';
case 'us-east':
return 'https://us-east.api.cloudconvert.com/v2';
default:
return 'https://api.cloudconvert.com/v2';
}
},
createClient(auth: any) {
return new CloudConvertClient(auth);
},
async apiCall({
auth,
method,
resourceUri,
body = undefined,
queryParams = undefined,
}: {
auth: any;
method: HttpMethod;
resourceUri: string;
body?: any;
queryParams?: Record<string, string>;
}) {
const client = new CloudConvertClient(auth);
return await client.apiCall({
method,
resourceUri,
body,
queryParams
});
},
};

View File

@@ -0,0 +1,26 @@
import { CloudConvertClient } from './client';
export { cloudconvertAuth } from './auth';
export { CloudConvertClient } from './client';
export { convertFileProps } from './properties';
export { convertFileSchema, captureWebsiteSchema, mergePdfSchema, downloadFileSchema, archiveFileSchema, optimizeFileSchema } from './schemas';
export const cloudconvertCommon = {
baseUrl: (region = 'auto') => {
switch (region) {
case 'eu-central':
return 'https://eu-central.api.cloudconvert.com/v2';
case 'us-east':
return 'https://us-east.api.cloudconvert.com/v2';
default:
return 'https://api.cloudconvert.com/v2';
}
},
createClient(auth: any) {
return new CloudConvertClient(auth);
},
};

View File

@@ -0,0 +1,200 @@
import { Property } from '@activepieces/pieces-framework';
import { CloudConvertClient } from './client';
import { cloudconvertAuth } from './auth';
const outputFormatDropdown = () =>
Property.Dropdown({
displayName: 'Output Format',
description: 'The target format to convert to',
required: true,
refreshers: ['auth'],
auth: cloudconvertAuth,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your CloudConvert account first',
options: [],
};
}
try {
const client = new CloudConvertClient(auth.props);
const formats = await client.getSupportedFormats();
if (formats.length === 0) {
return {
disabled: true,
placeholder: 'No supported formats found',
options: [],
};
}
const formatGroups: { [key: string]: Array<{ label: string; value: string }> } = {};
formats.forEach((format: any) => {
const group = format.meta?.group || 'Other';
if (!formatGroups[group]) {
formatGroups[group] = [];
}
formatGroups[group].push({
label: format.output_format.toUpperCase(),
value: format.output_format,
});
});
const popularFormats = ['pdf', 'docx', 'jpg', 'png', 'mp4', 'mp3'];
const options: Array<{ label: string; value: string }> = [];
popularFormats.forEach(format => {
const formatOption = formats.find((f: any) => f.output_format === format);
if (formatOption) {
options.push({
label: formatOption.output_format.toUpperCase(),
value: formatOption.output_format,
});
}
});
formats.forEach((format: any) => {
if (!popularFormats.includes(format.output_format)) {
options.push({
label: format.output_format.toUpperCase(),
value: format.output_format,
});
}
});
return {
options: options.slice(0, 50),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Error loading formats - please try again',
options: [],
};
}
},
});
const engineDropdown = ({ required = false }: { required?: boolean }) =>
Property.Dropdown({
auth: cloudconvertAuth,
displayName: 'Engine',
description: 'Use a specific engine for the conversion',
required,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your CloudConvert account first',
options: [],
};
}
const engines = [
{ label: 'LibreOffice (Default)', value: 'libreoffice' },
{ label: 'Microsoft Office', value: 'office' },
{ label: 'OnlyOffice', value: 'onlyoffice' },
{ label: 'Chrome/Puppeteer', value: 'chrome' },
{ label: 'ImageMagick', value: 'imagemagick' },
{ label: 'Poppler', value: 'poppler' },
{ label: 'GraphicsMagick', value: 'graphicsmagick' },
{ label: 'FFmpeg', value: 'ffmpeg' },
{ label: 'Calibre', value: 'calibre' },
{ label: 'Pandoc', value: 'pandoc' },
{ label: '3-Heights', value: '3heights' },
{ label: 'PDFTron', value: 'pdftron' },
{ label: 'MuPDF', value: 'mupdf' },
{ label: 'Inkscape', value: 'inkscape' },
];
return {
options: engines,
};
},
});
export const convertFileProps = () => ({
import_method: Property.StaticDropdown({
displayName: 'Import Method',
description: 'How to import the file for conversion',
required: true,
options: {
options: [
{ label: 'File Upload', value: 'upload' },
{ label: 'File URL', value: 'url' },
{ label: 'Stored File ID', value: 'stored_file' },
]
},
defaultValue: 'upload'
}),
file: Property.File({
displayName: 'File',
description: 'File to upload and convert (select from your device)',
required: false,
}),
url: Property.ShortText({
displayName: 'File URL',
description: 'URL of the file to convert',
required: false,
}),
stored_file_id: Property.ShortText({
displayName: 'Stored File ID',
description: 'ID of a previously stored file in Activepieces to convert',
required: false,
}),
input_format: Property.StaticDropdown({
displayName: 'Input Format',
description: 'The format of the input file. Leave as "Auto-detect" to let CloudConvert detect automatically',
required: false,
options: {
options: [
{ label: 'Auto-detect', value: 'auto' },
{ label: 'PDF', value: 'pdf' },
{ label: 'DOCX', value: 'docx' },
{ label: 'DOC', value: 'doc' },
{ label: 'TXT', value: 'txt' },
{ label: 'JPG', value: 'jpg' },
{ label: 'PNG', value: 'png' },
{ label: 'HTML', value: 'html' },
{ label: 'Other', value: 'other' },
]
},
defaultValue: 'auto'
}),
output_format: outputFormatDropdown(),
filename: Property.ShortText({
displayName: 'Output Filename',
description: 'Choose a filename (including extension) for the output file',
required: false,
defaultValue: 'converted-file'
}),
engine: engineDropdown({ required: false }),
engine_version: Property.ShortText({
displayName: 'Engine Version',
description: 'Use a specific engine version for the conversion',
required: false,
}),
timeout: Property.Number({
displayName: 'Timeout (seconds)',
description: 'Timeout in seconds after which the task will be cancelled',
required: false,
}),
wait_for_completion: Property.Checkbox({
displayName: 'Wait for Completion',
description: 'Wait for the conversion to complete before returning',
required: true,
defaultValue: true,
}),
store_file: Property.Checkbox({
displayName: 'Store File',
description: 'Download and store the converted file in Activepieces',
required: false,
defaultValue: true,
}),
});

View File

@@ -0,0 +1,171 @@
import z from 'zod';
export const convertFileSchema = z.object({
import_method: z.enum(['upload', 'url', 'stored_file']),
file: z.object({
base64: z.string(),
filename: z.string().optional(),
extension: z.string().optional(),
}).nullable().optional(),
url: z.string().url().nullable().optional(),
stored_file_id: z.string().nullable().optional(),
input_format: z.string().nullable().optional().refine((val) => !val || val === 'auto' || val.length > 0, {
message: "Input format must be either 'auto' or a valid format"
}),
output_format: z.string().min(1, 'Output format is required'),
filename: z.string().nullable().optional(),
engine: z.string().nullable().optional(),
engine_version: z.string().nullable().optional(),
timeout: z.number().positive().nullable().optional(),
wait_for_completion: z.boolean().default(true),
store_file: z.boolean().default(true),
}).refine((data) => {
if (data.import_method === 'upload') {
return data.file && data.file.base64;
} else if (data.import_method === 'url') {
return data.url;
} else if (data.import_method === 'stored_file') {
return data.stored_file_id;
}
return false;
}, {
message: "File is required for upload method, URL is required for URL method, Stored File ID is required for stored file method",
path: ["file"]
});
export const captureWebsiteSchema = z.object({
url: z.string().url('Valid website URL is required'),
output_format: z.string().min(1, 'Output format is required'),
pages: z.string().nullable().optional(),
zoom: z.number().positive().nullable().optional(),
page_width: z.number().positive().nullable().optional(),
page_height: z.number().positive().nullable().optional(),
page_format: z.string().nullable().optional(),
page_orientation: z.string().nullable().optional(),
margin_top: z.number().nonnegative().nullable().optional(),
margin_bottom: z.number().nonnegative().nullable().optional(),
margin_left: z.number().nonnegative().nullable().optional(),
margin_right: z.number().nonnegative().nullable().optional(),
print_background: z.boolean().nullable().optional(),
display_header_footer: z.boolean().nullable().optional(),
header_template: z.string().nullable().optional(),
footer_template: z.string().nullable().optional(),
wait_until: z.string().nullable().optional(),
wait_for_element: z.string().nullable().optional(),
wait_time: z.number().positive().nullable().optional(),
css_media_type: z.string().nullable().optional(),
filename: z.string().nullable().optional(),
engine: z.string().nullable().optional(),
engine_version: z.string().nullable().optional(),
timeout: z.number().positive().nullable().optional(),
wait_for_completion: z.boolean().default(true),
store_file: z.boolean().default(true),
});
export const mergePdfSchema = z.object({
import_method: z.enum(['upload', 'url', 'stored_file']),
files: z.array(z.object({
url: z.string().nullable().optional(),
file: z.object({
base64: z.string(),
filename: z.string().optional(),
extension: z.string().optional(),
}).nullable().optional(),
stored_file_id: z.string().nullable().optional(),
filename: z.string().nullable().optional(),
})).min(2, 'At least 2 files are required for merging'),
filename: z.string().nullable().optional(),
engine: z.string().nullable().optional(),
engine_version: z.string().nullable().optional(),
timeout: z.number().positive().nullable().optional(),
wait_for_completion: z.boolean().default(true),
store_file: z.boolean().default(true),
}).refine((data) => {
return data.files.every(fileItem => {
if (data.import_method === 'upload') {
return fileItem.file && fileItem.file.base64;
} else if (data.import_method === 'url') {
return fileItem.url;
} else if (data.import_method === 'stored_file') {
return fileItem.stored_file_id;
}
return false;
});
}, {
message: "All files must have valid input based on the selected import method",
path: ["files"]
});
export const downloadFileSchema = z.object({
task_id: z.string().min(1, 'Task ID is required'),
include: z.array(z.string()).nullable().optional(),
store_file: z.boolean().optional(),
});
export const archiveFileSchema = z.object({
import_method: z.enum(['upload', 'url', 'stored_file']),
files: z.array(z.object({
url: z.string().nullable().optional(),
file: z.object({
base64: z.string(),
filename: z.string().optional(),
extension: z.string().optional(),
}).nullable().optional(),
stored_file_id: z.string().nullable().optional(),
filename: z.string().nullable().optional(),
})).min(1, 'At least one file is required to create an archive'),
output_format: z.string().min(1, 'Output format is required'),
filename: z.string().nullable().optional(),
engine: z.string().nullable().optional(),
engine_version: z.string().nullable().optional(),
timeout: z.number().positive().nullable().optional(),
wait_for_completion: z.boolean().default(true),
store_file: z.boolean().default(true),
}).refine((data) => {
return data.files.every(fileItem => {
if (data.import_method === 'upload') {
return fileItem.file && fileItem.file.base64;
} else if (data.import_method === 'url') {
return fileItem.url;
} else if (data.import_method === 'stored_file') {
return fileItem.stored_file_id;
}
return false;
});
}, {
message: "All files must have valid input based on the selected import method",
path: ["files"]
});
export const optimizeFileSchema = z.object({
import_method: z.enum(['upload', 'url', 'stored_file']),
file: z.object({
base64: z.string(),
filename: z.string().optional(),
extension: z.string().optional(),
}).nullable().optional(),
url: z.string().url().nullable().optional(),
stored_file_id: z.string().nullable().optional(),
input_format: z.string().nullable().optional(),
profile: z.string().nullable().optional(),
flatten_signatures: z.boolean().nullable().optional(),
colorspace: z.string().nullable().optional(),
filename: z.string().nullable().optional(),
engine: z.string().nullable().optional(),
engine_version: z.string().nullable().optional(),
timeout: z.number().positive().nullable().optional(),
wait_for_completion: z.boolean().default(true),
store_file: z.boolean().default(true),
}).refine((data) => {
if (data.import_method === 'upload') {
return data.file && data.file.base64;
} else if (data.import_method === 'url') {
return data.url;
} else if (data.import_method === 'stored_file') {
return data.stored_file_id;
}
return false;
}, {
message: "File is required for upload method, URL is required for URL method, Stored File ID is required for stored file method",
path: ["file"]
});

View File

@@ -0,0 +1,121 @@
import {
HttpMethod,
HttpRequest,
httpClient,
AuthenticationType,
} from '@activepieces/pieces-common';
import {
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { cloudconvertAuth, cloudconvertCommon } from '../common';
export const jobFailed = createTrigger({
name: 'job_failed',
displayName: 'Job Failed',
description: 'Triggers when a CloudConvert job has failed',
auth: cloudconvertAuth,
type: TriggerStrategy.WEBHOOK,
props: {},
sampleData: {
event: 'job.failed',
job: {
id: 'job_123456789',
tag: 'example-job',
status: 'error',
created_at: '2023-12-01T12:00:00Z',
started_at: '2023-12-01T12:00:05Z',
ended_at: '2023-12-01T12:00:15Z',
message: 'Task failed: File conversion error',
code: 'CONVERSION_FAILED',
tasks: [
{
id: 'task_123456789',
name: 'convert-my-file',
operation: 'convert',
status: 'error',
message: 'File conversion error',
code: 'CONVERSION_FAILED',
created_at: '2023-12-01T12:00:00Z',
started_at: '2023-12-01T12:00:05Z',
ended_at: '2023-12-01T12:00:15Z',
engine: 'office',
engine_version: '2016',
result: null
}
]
}
},
async test(context) {
return [this.sampleData];
},
onEnable: async (context) => {
const webhookUrl = context.webhookUrl;
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${cloudconvertCommon.baseUrl((context.auth).props?.['region'] as string || 'auto')}/webhooks`,
body: {
url: webhookUrl,
events: ['job.failed']
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
headers: {
'Content-Type': 'application/json',
},
};
const response = await httpClient.sendRequest(request);
if (response.status !== 201) {
throw new Error(`Failed to register webhook. Status: ${response.status}`);
}
},
onDisable: async (context) => {
const webhookUrl = context.webhookUrl;
// First, find the webhook by URL
const listRequest: HttpRequest = {
method: HttpMethod.GET,
url: `${cloudconvertCommon.baseUrl((context.auth).props?.['region'] as string || 'auto')}/users/me/webhooks`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
};
try {
const listResponse = await httpClient.sendRequest(listRequest);
if (listResponse.status === 200 && listResponse.body.data) {
const webhook = listResponse.body.data.find((w: any) => w.url === webhookUrl);
if (webhook) {
const deleteRequest: HttpRequest = {
method: HttpMethod.DELETE,
url: `${cloudconvertCommon.baseUrl((context.auth).props?.['region'] as string || 'auto')}/webhooks/${webhook.id}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
};
await httpClient.sendRequest(deleteRequest);
}
}
} catch (error) {
// Continue silently
}
},
run: async (context) => {
const payload = context.payload.body as any;
if (payload?.event === 'job.failed' && payload?.job) {
return [payload];
}
return [];
},
});

View File

@@ -0,0 +1,160 @@
import {
HttpMethod,
HttpRequest,
httpClient,
AuthenticationType,
} from '@activepieces/pieces-common';
import {
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { cloudconvertAuth, cloudconvertCommon } from '../common';
export const jobFinished = createTrigger({
name: 'job_finished',
displayName: 'Job Finished',
description: 'Triggers when a CloudConvert job has been completed',
auth: cloudconvertAuth,
type: TriggerStrategy.WEBHOOK,
props: {},
sampleData: {
event: 'job.finished',
job: {
id: '6559c281-ed85-4728-80db-414561c631e9',
tag: 'myjob-123',
status: 'finished',
created_at: '2018-09-19T14:42:58+00:00',
started_at: '2018-09-19T14:42:58+00:00',
ended_at: '2018-09-19T14:43:08+00:00',
tasks: [
{
id: '1f34c1b5-9ee8-4c8c-890f-bf44cda1deb7',
name: 'import-my-file',
operation: 'import/url',
status: 'finished',
created_at: '2018-09-19T14:42:58+00:00',
started_at: '2018-09-19T14:42:58+00:00',
ended_at: '2018-09-19T14:42:59+00:00',
result: {
files: [
{
filename: 'input.pdf',
size: 10240
}
]
}
},
{
id: '48c6e72b-cb8e-4ecc-bf3d-ead5477b4741',
name: 'convert-my-file',
operation: 'convert',
status: 'finished',
created_at: '2018-09-19T14:42:58+00:00',
started_at: '2018-09-19T14:42:59+00:00',
ended_at: '2018-09-19T14:43:08+00:00',
engine: 'office',
engine_version: '2016',
result: {
files: [
{
filename: 'output.docx',
url: 'https://storage.cloudconvert.com/48c6e72b-cb8e-4ecc-bf3d-ead5477b4741/output.docx',
size: 15360
}
]
}
},
{
id: '36af6f54-1c01-45cc-bcc3-97dd23d2f93d',
name: 'export-my-file',
operation: 'export/url',
status: 'finished',
created_at: '2018-09-19T14:42:58+00:00',
started_at: '2018-09-19T14:43:08+00:00',
ended_at: '2018-09-19T14:43:08+00:00',
result: {
files: [
{
filename: 'output.docx',
url: 'https://storage.cloudconvert.com/36af6f54-1c01-45cc-bcc3-97dd23d2f93d/output.docx',
size: 15360
}
]
}
}
]
}
},
async test(context) {
return [this.sampleData];
},
onEnable: async (context) => {
const webhookUrl = context.webhookUrl;
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${cloudconvertCommon.baseUrl((context.auth).props?.['region'] as string || 'auto')}/webhooks`,
body: {
url: webhookUrl,
events: ['job.finished']
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
headers: {
'Content-Type': 'application/json',
},
};
const response = await httpClient.sendRequest(request);
if (response.status !== 201) {
throw new Error(`Failed to register webhook. Status: ${response.status}`);
}
},
onDisable: async (context) => {
const webhookUrl = context.webhookUrl;
// First, find the webhook by URL
const listRequest: HttpRequest = {
method: HttpMethod.GET,
url: `${cloudconvertCommon.baseUrl((context.auth as any).region || 'auto')}/users/me/webhooks`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
};
try {
const listResponse = await httpClient.sendRequest(listRequest);
if (listResponse.status === 200 && listResponse.body.data) {
const webhook = listResponse.body.data.find((w: any) => w.url === webhookUrl);
if (webhook) {
const deleteRequest: HttpRequest = {
method: HttpMethod.DELETE,
url: `${cloudconvertCommon.baseUrl((context.auth as any).region || 'auto')}/webhooks/${webhook.id}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
};
await httpClient.sendRequest(deleteRequest);
}
}
} catch (error) {
// Continue silently
}
},
run: async (context) => {
const payload = context.payload.body as any;
if (payload?.event === 'job.finished' && payload?.job) {
return [payload];
}
return [];
},
});

View File

@@ -0,0 +1,135 @@
import {
HttpMethod,
HttpRequest,
httpClient,
AuthenticationType,
} from '@activepieces/pieces-common';
import {
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { cloudconvertAuth, cloudconvertCommon } from '../common';
export const newJob = createTrigger({
name: 'new_job',
displayName: 'New Job Event',
description: 'Triggers when a new job has been created',
auth: cloudconvertAuth,
type: TriggerStrategy.WEBHOOK,
props: {},
sampleData: {
event: 'job.created',
job: {
id: '6559c281-ed85-4728-80db-414561c631e9',
tag: 'myjob-123',
status: 'waiting',
created_at: '2018-09-19T14:42:58+00:00',
started_at: null,
ended_at: null,
tasks: [
{
id: '7f110c42-3245-41cf-8555-37087c729ed2',
name: 'import-my-file',
operation: 'import/s3',
status: 'waiting',
created_at: '2018-09-19T14:42:58+00:00',
started_at: null,
ended_at: null
},
{
id: '7a142bd0-fa20-493e-abf5-99cc9b5fd7e9',
name: 'convert-my-file',
operation: 'convert',
status: 'waiting',
created_at: '2018-09-19T14:42:58+00:00',
started_at: null,
ended_at: null,
engine: 'office',
engine_version: '2016'
},
{
id: '36af6f54-1c01-45cc-bcc3-97dd23d2f93d',
name: 'export-my-file',
operation: 'export/s3',
status: 'waiting',
created_at: '2018-09-19T14:42:58+00:00',
started_at: null,
ended_at: null
}
]
}
},
async test(context) {
return [this.sampleData];
},
onEnable: async (context) => {
const webhookUrl = context.webhookUrl;
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${cloudconvertCommon.baseUrl((context.auth).props?.['region'] as string || 'auto')}/webhooks`,
body: {
url: webhookUrl,
events: ['job.created']
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
headers: {
'Content-Type': 'application/json',
},
};
const response = await httpClient.sendRequest(request);
if (response.status !== 201) {
throw new Error(`Failed to register webhook. Status: ${response.status}`);
}
},
onDisable: async (context) => {
const webhookUrl = context.webhookUrl;
// First, find the webhook by URL
const listRequest: HttpRequest = {
method: HttpMethod.GET,
url: `${cloudconvertCommon.baseUrl((context.auth as any).region || 'auto')}/users/me/webhooks`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
};
try {
const listResponse = await httpClient.sendRequest(listRequest);
if (listResponse.status === 200 && listResponse.body.data) {
const webhook = listResponse.body.data.find((w: any) => w.url === webhookUrl);
if (webhook) {
const deleteRequest: HttpRequest = {
method: HttpMethod.DELETE,
url: `${cloudconvertCommon.baseUrl((context.auth as any).region || 'auto')}/webhooks/${webhook.id}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
};
await httpClient.sendRequest(deleteRequest);
}
}
} catch (error) {
// Continue silently
}
},
run: async (context) => {
const payload = context.payload.body as any;
if (payload?.event === 'job.created' && payload?.job) {
return [payload];
}
return [];
},
});