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,133 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { promises as fs } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import { nanoid } from 'nanoid';
import Jimp from 'jimp';
const execPromise = promisify(exec);
const pdftoppmPath = '/usr/bin/pdftoppm';
const MAX_FILE_SIZE = 16 * 1024 * 1024;
async function isPdftoppmInstalled(): Promise<boolean> {
const { stdout, stderr } = await execPromise(`command -v ${pdftoppmPath}`);
return !stderr && stdout.trim() === pdftoppmPath;
}
async function convertPdfToImages(dataBuffer: Buffer): Promise<Buffer[]> {
const tempDir = tmpdir();
const uniqueId = nanoid();
const inputFilePath = join(tempDir, `input-${uniqueId}.pdf`);
const outputDir = join(tempDir, `output-${uniqueId}`);
try {
await fs.mkdir(outputDir);
await fs.writeFile(inputFilePath, dataBuffer as any);
const { stderr } = await execPromise(`${pdftoppmPath} -png ${inputFilePath} ${join(outputDir, 'output')}`);
if (stderr) {
throw new Error(stderr);
}
const files = await fs.readdir(outputDir);
const imageBuffers = [];
for (const file of files) {
const filePath = join(outputDir, file);
const imageBuffer = await fs.readFile(filePath);
await fs.unlink(filePath);
imageBuffers.push(imageBuffer);
}
return imageBuffers;
} finally {
await fs.unlink(inputFilePath).catch(() => void 0);
await fs.rm(outputDir, { recursive: true, force: true }).catch(() => void 0);
}
}
async function concatImagesVertically(imageBuffers: Buffer[]): Promise<Buffer> {
const images = await Promise.all(imageBuffers.map(buffer => Jimp.read(buffer)));
const totalHeight = images.reduce((sum, image) => sum + image.getHeight(), 0);
const maxWidth = Math.max(...images.map(image => image.getWidth()));
const finalImage = new Jimp(maxWidth, totalHeight);
let yOffset = 0;
for (const image of images) {
finalImage.composite(image, 0, yOffset);
yOffset += image.getHeight();
}
return finalImage.getBufferAsync(Jimp.MIME_PNG);
}
export const convertToImage = createAction({
name: 'convertToImage',
displayName: 'Convert to Image',
description: 'Convert a PDF file or URL to an image',
props: {
file: Property.File({
displayName: 'PDF File or URL',
required: true,
}),
imageOutputType: Property.StaticDropdown({
displayName: 'Output Image Type',
required: true,
options: {
options: [
{ label: 'Single Combined Image', value: 'single' },
{ label: 'Separate Image for Each Page', value: 'multiple' },
],
},
defaultValue: 'multiple',
}),
},
errorHandlingOptions: {
continueOnFailure: {
defaultValue: false,
},
retryOnFailure: {
hide: true
},
},
async run(context) {
if (!await isPdftoppmInstalled()) {
throw new Error(`${pdftoppmPath} is not installed`);
}
const file = context.propsValue.file;
const returnConcatenatedImage = context.propsValue.imageOutputType === 'single';
// To prevent a DOS attack, we limit the file size to 16MB
if (file.data.buffer.byteLength > MAX_FILE_SIZE) {
throw new Error(`File size exceeds the limit of ${MAX_FILE_SIZE / (1024 * 1024)} MB.`);
}
const dataBuffer = Buffer.from(file.data.buffer);
const imageBuffers = await convertPdfToImages(dataBuffer);
if (returnConcatenatedImage) {
const finalImageBuffer = await concatImagesVertically(imageBuffers);
const imageLink = await context.files.write({
data: finalImageBuffer,
fileName: `converted_image.png`,
});
return {
image: imageLink,
};
} else {
const imageLinks = await Promise.all(imageBuffers.map((imageBuffer, index) =>
context.files.write({
data: imageBuffer,
fileName: `converted_image_page_${index + 1}.png`,
})
));
return {
images: imageLinks,
};
}
},
});

View File

@@ -0,0 +1,123 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { PDFDocument } from 'pdf-lib';
import { MarkdownVariant } from '@activepieces/shared';
export function pageRangeToIndexes(
startPage: number,
endPage: number,
totalPages: number
) {
if (startPage > endPage) {
throw Error(
`Range start (${startPage}) has to be less than range end (${endPage})`
);
}
if (startPage === 0 || endPage === 0) {
throw Error('Range start/end has to be a non-zero number');
}
if (startPage > totalPages || endPage > totalPages) {
throw Error(
'Range start/end has to be less or equal to the total number of pages'
);
}
if (startPage < 0 && endPage > 0) {
throw Error('Range start cannot be negative when end is positive');
}
// page is 1 indexed, handle positive case
let startIndex = startPage - 1;
// handle negative case
if (startPage < 0) {
startIndex = totalPages + startPage;
}
// page is 1 indexed, handle positive case
let endIndex = endPage - 1;
// handle negative case
if (endPage < 0) {
endIndex = totalPages + endPage;
}
return Array.from(
{ length: endIndex - startIndex + 1 },
(_, idx) => startIndex + idx
);
}
const markdownValue = `
This action can extract or rearrange the pages in a PDF.
- The order of array determines the sequence of pages.
- Each array element is one inclusive continuous range with a start page and end page.
- Pages start from 1, and 0 is not valid.
- Start page has to be less than end page.
- You can select one page by setting the same start and end page.
- To select pages from the start, specify negative pages eg. -1 is the last page, -5 is the 5th last page. start: -5, end: -1 are the last 5 pages.
- Range cannot span across 0 eg. start: -3, end: 5.
`;
export const extractPdfPages = createAction({
name: 'extractPdfPages',
displayName: 'Extract PDF Pages',
description: 'Extract or rearrange page(s)from PDF File.',
props: {
markdown: Property.MarkDown({
variant: MarkdownVariant.INFO,
value: markdownValue,
}),
file: Property.File({
displayName: 'PDF File or URL',
required: true,
}),
pageRanges: Property.Array({
displayName: 'Page Ranges',
properties: {
startPage: Property.Number({
displayName: 'Start Page',
required: true,
}),
endPage: Property.Number({
displayName: 'End Page',
required: true,
}),
},
required: true,
}),
},
errorHandlingOptions: {
continueOnFailure: {
defaultValue: false,
},
retryOnFailure: {
hide: true,
},
},
async run(context) {
try {
const srcDoc = await PDFDocument.load(context.propsValue.file.data as any);
const totalPages = srcDoc.getPageCount();
const pageIndexes = context.propsValue.pageRanges.flatMap(
(pageRange: any) =>
pageRangeToIndexes(pageRange.startPage, pageRange.endPage, totalPages)
);
const newDoc = await PDFDocument.create();
const newPages = await newDoc.copyPages(srcDoc, pageIndexes);
newPages.forEach((newPage) => newDoc.addPage(newPage));
const pdfBytes = await newDoc.save();
const base64Pdf = Buffer.from(pdfBytes).toString('base64');
return context.files.write({
data: Buffer.from(base64Pdf, 'base64'),
fileName: context.propsValue.file.filename,
});
} catch (error) {
throw new Error(`Failed to extract pages: ${(error as Error).message}`);
}
},
});

View File

@@ -0,0 +1,28 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import pdfParse from 'pdf-parse';
export const extractText = createAction({
name: 'extractText',
displayName: 'Extract Text',
description: 'Extract text from PDF file or url',
props: {
file: Property.File({
displayName: 'PDF File or URL',
required: true,
}),
},
errorHandlingOptions: {
continueOnFailure: {
defaultValue: false,
},
retryOnFailure: {
hide: true
},
},
async run(context) {
const file = context.propsValue.file;
const dataBuffer = Buffer.from(file.data.buffer);
const pdfData = await pdfParse(dataBuffer);
return pdfData.text;
},
});

View File

@@ -0,0 +1,210 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { PDFDocument, PDFImage, RotationTypes, PageSizes } from 'pdf-lib';
export const imageToPdf = createAction({
name: 'imageToPdf',
displayName: 'Image to PDF',
description: 'Convert image to PDF',
props: {
image: Property.File({
displayName: 'image',
description:
'Image has to be png, jpeg or jpg and it will be scaled down to fit the page when image is larger than an A4 page',
required: true,
}),
},
errorHandlingOptions: {
continueOnFailure: {
defaultValue: false,
},
retryOnFailure: {
hide: true,
},
},
async run(context) {
try {
const image = context.propsValue.image;
const imageExtension = image.extension?.toLowerCase();
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage();
const [pageWidth, pageHeight] = PageSizes.A4;
page.setSize(pageWidth, pageHeight);
const xMargin = 30;
const yMargin = 30;
const [maxCardWidth, maxCardHeight] = [pageWidth - xMargin * 2, pageHeight - yMargin * 2];
let result: PDFImage | null = null;
if (imageExtension === 'png') {
result = await pdfDoc.embedPng(image.data as any);
} else if (imageExtension === 'jpg' || imageExtension === 'jpeg') {
result = await pdfDoc.embedJpg(image.data as any);
} else {
throw new Error(`Unsupported image format: ${imageExtension}`);
}
if (result === null) {
throw new Error('Failed to embed image');
}
const exifOrientation = getImageOrientation(image.data.buffer as any);
const orientationCorrection = getOrientationCorrection(exifOrientation);
let scaledImage, correctedWidth, correctedHeight;
switch (exifOrientation) {
case ImageOrientation.FlipHorizontalRotate90:
case ImageOrientation.Rotate90:
case ImageOrientation.FlipVerticalRotate90:
case ImageOrientation.Rotate270:
// The uploaded image is rotated +/- 90 degrees
scaledImage = result.scaleToFit(maxCardHeight, maxCardWidth);
correctedWidth = scaledImage.height;
correctedHeight = scaledImage.width;
break;
default:
scaledImage = result.scaleToFit(maxCardWidth, maxCardHeight);
correctedWidth = scaledImage.width;
correctedHeight = scaledImage.height;
}
let xShift, yShift;
const yOffset = pageHeight - yMargin;
switch (exifOrientation) {
case ImageOrientation.FlipHorizontal:
xShift = pageWidth - xMargin - correctedWidth;
yShift = yOffset - correctedHeight;
break;
case ImageOrientation.Rotate180:
xShift = xMargin + correctedWidth;
yShift = yOffset;
break;
case ImageOrientation.FlipVertical:
xShift = pageWidth - xMargin;
yShift = yOffset;
break;
case ImageOrientation.FlipHorizontalRotate90:
xShift = xMargin + correctedWidth;
yShift = pageHeight - yOffset;
break;
case ImageOrientation.Rotate90:
xShift = xMargin;
yShift = yOffset;
break;
case ImageOrientation.FlipVerticalRotate90:
xShift = xMargin;
yShift = pageHeight - yOffset + correctedHeight;
break;
case ImageOrientation.Rotate270:
xShift = xMargin + correctedWidth;
yShift = yOffset - correctedHeight;
break;
default:
xShift = xMargin;
yShift = yOffset - correctedHeight;
}
page.drawImage(result, {
x: xShift,
y: yShift,
height: scaledImage.height,
width: scaledImage.width,
rotate: { angle: orientationCorrection.degrees, type: RotationTypes.Degrees },
});
const pdfBytes = await pdfDoc.save();
const base64Pdf = Buffer.from(pdfBytes).toString('base64');
return context.files.write({
data: Buffer.from(base64Pdf, 'base64'),
fileName: `${image.filename}.pdf`,
});
} catch (error) {
throw new Error(`Failed to convert text to PDF: ${(error as Error).message}`);
}
},
});
// https://sirv.com/help/articles/rotate-photos-to-be-upright/#exif-orientation-values
enum ImageOrientation {
Normal = 1, // "Image is in normal orientation, no rotation or flipping"
Rotate90 = 6, // "Image is rotated 90 degrees"
Rotate180 = 3, // "Image is rotated 180 degrees"
Rotate270 = 8, // "Image is rotated 270 degrees"
FlipHorizontal = 2, // "Image is flipped horizontally"
FlipVertical = 4, // "Image is flipped horizontally and rotated 180 degrees"
FlipHorizontalRotate90 = 5, // "Image is rotated 90 degrees and flipped horizontally"
FlipVerticalRotate90 = 7, // "Image is rotated 270 degrees and flipped horizontally"
Unknown= -1
}
// https://github.com/Hopding/pdf-lib/issues/1284
// https://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side/32490603#32490603
function getImageOrientation(file: ArrayBuffer): ImageOrientation {
const view = new DataView(file);
const length = view.byteLength;
let offset = 2;
while (offset < length) {
if (view.getUint16(offset + 2, false) <= 8) return ImageOrientation.Unknown;
const marker = view.getUint16(offset, false);
offset += 2;
// If EXIF buffer segment exists find the orientation
if (marker == 0xffe1) {
if (view.getUint32((offset += 2), false) != 0x45786966) {
return ImageOrientation.Unknown;
}
const little = view.getUint16((offset += 6), false) == 0x4949;
offset += view.getUint32(offset + 4, little);
const tags = view.getUint16(offset, little);
offset += 2;
for (let i = 0; i < tags; i++) {
if (view.getUint16(offset + i * 12, little) == 0x0112) {
const orientation = view.getUint16(offset + i * 12 + 8, little);
switch (orientation) {
case 1: return ImageOrientation.Normal;
case 3: return ImageOrientation.Rotate180;
case 6: return ImageOrientation.Rotate90;
case 8: return ImageOrientation.Rotate270;
case 2: return ImageOrientation.FlipHorizontal;
case 4: return ImageOrientation.FlipVertical;
case 5: return ImageOrientation.FlipHorizontalRotate90;
case 7: return ImageOrientation.FlipVerticalRotate90;
default: return ImageOrientation.Unknown;
}
}
}
} else if ((marker & 0xff00) != 0xff00) {
break;
} else {
offset += view.getUint16(offset, false);
}
}
return ImageOrientation.Unknown;
}
function getOrientationCorrection(orientation: number): { degrees: number; mirrored?: 'x' | 'y' } {
switch (orientation) {
case ImageOrientation.FlipHorizontal:
return { degrees: 0, mirrored: 'x' };
case ImageOrientation.Rotate180:
return { degrees: -180 };
case ImageOrientation.FlipVertical:
return { degrees: 180, mirrored: 'x' };
case ImageOrientation.FlipHorizontalRotate90:
return { degrees: 90, mirrored: 'y' };
case ImageOrientation.Rotate90:
return { degrees: -90 };
case ImageOrientation.FlipVerticalRotate90:
return { degrees: -90, mirrored: 'y' };
case ImageOrientation.Rotate270:
return { degrees: 90 };
default:
return { degrees: 0 };
}
}

View File

@@ -0,0 +1,101 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { PDFDocument } from 'pdf-lib';
export const mergePdfs = createAction({
name: 'mergePdfs',
displayName: 'Merge PDFs',
description: 'Merges multiple PDF files into a single PDF document.',
props: {
pdfFiles: Property.Array({
displayName: 'PDF Files',
description: 'Array of PDF files to merge',
required: true,
properties: {
file: Property.File({
displayName: 'PDF File',
required: true,
}),
},
}),
outputFileName: Property.ShortText({
displayName: 'Output File Name',
description: 'Name for the merged PDF file (without extension)',
required: false,
defaultValue: 'merged-document',
}),
},
async run(context) {
try {
const { pdfFiles, outputFileName } = context.propsValue;
// pdfFiles is already an array from Property.Array
if (!pdfFiles || !Array.isArray(pdfFiles) || pdfFiles.length < 2) {
throw new Error('At least 2 PDF files are required for merging');
}
const mergedPdf = await PDFDocument.create();
for (let i = 0; i < pdfFiles.length; i++) {
const fileItem = pdfFiles[i] as any;
const file = fileItem.file;
if (!file) {
throw new Error(`File at index ${i} is null or undefined`);
}
// Handle PDF files only
let fileData: Buffer;
const fileName = file.filename || file.name || `file-${i}.pdf`;
// Validate it's a PDF file
if (fileName && !fileName.toLowerCase().endsWith('.pdf')) {
throw new Error(`File at index ${i} (${fileName}) is not a PDF file`);
}
if (file.data) {
// Handle base64 strings
if (typeof file.data === 'string') {
fileData = Buffer.from(file.data, 'base64');
}
// Handle Buffer objects serialized as JSON
else if (file.data.type === 'Buffer' && Array.isArray(file.data.data)) {
fileData = Buffer.from(file.data.data);
}
// Handle direct Buffer
else if (Buffer.isBuffer(file.data)) {
fileData = file.data;
}
else {
throw new Error(`Unsupported data format for PDF file ${i}: ${typeof file.data}`);
}
}
else {
throw new Error(`PDF file at index ${i} has no data property`);
}
try {
const pdfDoc = await PDFDocument.load(new Uint8Array(fileData));
const pageCount = pdfDoc.getPageCount();
const pageIndices = Array.from({ length: pageCount }, (_, idx) => idx);
const copiedPages = await mergedPdf.copyPages(pdfDoc, pageIndices);
copiedPages.forEach((page) => mergedPdf.addPage(page));
} catch (error) {
throw new Error(`Failed to process PDF file at index ${i} (${fileName}): ${(error as Error).message}`);
}
}
const pdfBytes = await mergedPdf.save();
return context.files.write({
data: Buffer.from(pdfBytes),
fileName: `${outputFileName || 'merged-document'}.pdf`,
});
} catch (error) {
throw new Error(`Failed to merge PDFs: ${(error as Error).message}`);
}
},
});

View File

@@ -0,0 +1,30 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { PDFDocument } from 'pdf-lib';
export const pdfPageCount = createAction({
name: 'pdfPageCount',
displayName: 'PDF Page Count',
description: 'Get page count of PDF file.',
props: {
file: Property.File({
displayName: 'PDF File or URL',
required: true,
}),
},
errorHandlingOptions: {
continueOnFailure: {
defaultValue: false,
},
retryOnFailure: {
hide: true,
},
},
async run({ propsValue }) {
try {
const pdfDoc = await PDFDocument.load(propsValue.file.data as any);
return pdfDoc.getPageCount();
} catch (error) {
throw new Error(`Failed to get page count: ${(error as Error).message}`);
}
},
});

View File

@@ -0,0 +1,103 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { PDFDocument, StandardFonts } from 'pdf-lib';
export const textToPdf = createAction({
name: 'textToPdf',
displayName: 'Text to PDF',
description: 'Convert text to PDF',
props: {
text: Property.LongText({
displayName: 'text',
description: 'Enter text to convert',
required: true,
}),
},
errorHandlingOptions: {
continueOnFailure: {
defaultValue: false,
},
retryOnFailure: {
hide: true,
},
},
async run(context) {
const text = context.propsValue.text;
const pageSize: [number, number] = [595, 842]; // Standard A4 size
const margin = 50;
const topMargin = 70;
const fontSize = 12;
const lineSpacing = 5;
const paragraphSpacing = 8;
const fontType: StandardFonts = StandardFonts.Helvetica;
try {
const pdfDoc = await PDFDocument.create();
let page = pdfDoc.addPage(pageSize);
const { width, height } = page.getSize();
const font = await pdfDoc.embedFont(fontType);
const lineHeight = font.heightAtSize(fontSize) + lineSpacing;
const maxWidth = width - margin * 2;
const paragraphs = text.split('\n');
let yPosition = height - topMargin;
paragraphs.forEach((paragraph) => {
const words = paragraph.split(' ');
let line = '';
words.forEach((word) => {
const testLine = line + word + ' ';
const testLineWidth = font.widthOfTextAtSize(testLine, fontSize);
if (testLineWidth > maxWidth) {
page.drawText(line.trim(), {
x: margin,
y: yPosition,
size: fontSize,
font,
});
line = word + ' ';
yPosition -= lineHeight;
if (yPosition < margin + lineHeight) {
page = pdfDoc.addPage(pageSize);
yPosition = height - topMargin;
}
} else {
line = testLine;
}
});
if (line.trim()) {
page.drawText(line.trim(), {
x: margin,
y: yPosition,
size: fontSize,
font,
});
yPosition -= lineHeight;
}
yPosition -= paragraphSpacing;
if (yPosition < margin + lineHeight) {
page = pdfDoc.addPage(pageSize);
yPosition = height - topMargin;
}
});
const pdfBytes = await pdfDoc.save();
const base64Pdf = Buffer.from(pdfBytes).toString('base64');
return context.files.write({
data: Buffer.from(base64Pdf, 'base64'),
fileName: 'text.pdf',
});
} catch (error) {
throw new Error(`Failed to convert text to PDF: ${(error as Error).message}`);
}
},
});