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,25 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { makeClient, pastefyCommon } from '../common';
import { pastefyAuth } from '../..';
export default createAction({
auth: pastefyAuth,
name: 'create_folder',
displayName: 'Create Folder',
description: 'Creates a new folder',
props: {
name: Property.ShortText({
displayName: 'Name',
required: true,
}),
parent_id: pastefyCommon.folder_id(false, 'Parent Folder'),
},
async run(context) {
const client = makeClient(context.auth, context.propsValue);
const res = await client.createFolder({
name: context.propsValue.name as string,
parent: context.propsValue.parent_id,
});
return res.folder;
},
});

View File

@@ -0,0 +1,54 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { formatDate, makeClient, pastefyCommon } from '../common';
import { pastefyAuth } from '../..';
import CryptoJS from 'crypto-js';
export default createAction({
auth: pastefyAuth,
name: 'create_paste',
displayName: 'Create Paste',
description: 'Creates a new paste',
props: {
content: Property.LongText({
displayName: 'Content',
required: true,
}),
title: Property.ShortText({
displayName: 'Title',
required: false,
}),
password: Property.ShortText({
displayName: 'Encryption Password',
description: 'Encrypts the paste with this password',
required: false,
}),
folder_id: pastefyCommon.folder_id(false),
visibility: pastefyCommon.visibility(false),
expiry: Property.DateTime({
displayName: 'Expiry Date',
required: false,
}),
},
async run(context) {
const client = makeClient(context.auth, context.propsValue);
const password = context.propsValue.password;
let content = context.propsValue.content;
let title = context.propsValue.title;
if (password) {
content = CryptoJS.AES.encrypt(content, password).toString();
if (title) {
title = CryptoJS.AES.encrypt(title, password).toString();
}
}
const res = await client.createPaste({
title,
content,
encrypted: !!password,
folder: context.propsValue.folder_id,
visibility: context.propsValue.visibility,
expire_at: formatDate(context.propsValue.expiry),
});
return res.paste;
},
});

View File

@@ -0,0 +1,20 @@
import { createAction } from '@activepieces/pieces-framework';
import { makeClient, pastefyCommon } from '../common';
import { pastefyAuth } from '../..';
export default createAction({
auth: pastefyAuth,
name: 'delete_folder',
displayName: 'Delete Folder',
description: 'Deletes a folder',
props: {
folder_id: pastefyCommon.folder_id(true),
},
async run(context) {
const client = makeClient(context.auth, context.propsValue);
const res = await client.deleteFolder(
context.propsValue.folder_id as string
);
return res;
},
});

View File

@@ -0,0 +1,21 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { makeClient } from '../common';
import { pastefyAuth } from '../..';
export default createAction({
auth: pastefyAuth,
name: 'delete_paste',
displayName: 'Delete Paste',
description: 'Deletes a paste',
props: {
paste_id: Property.ShortText({
displayName: 'Paste ID',
required: true,
}),
},
async run(context) {
const client = makeClient(context.auth, context.propsValue);
const res = await client.deletePaste(context.propsValue.paste_id);
return res;
},
});

View File

@@ -0,0 +1,59 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { formatDate, makeClient, pastefyCommon } from '../common';
import { pastefyAuth } from '../..';
import CryptoJS from 'crypto-js';
export default createAction({
auth: pastefyAuth,
name: 'edit_paste',
displayName: 'Edit Paste',
description: 'Edits an existing private paste',
props: {
paste_id: Property.ShortText({
displayName: 'Paste ID',
required: true,
}),
content: Property.LongText({
displayName: 'Content',
required: false,
}),
title: Property.ShortText({
displayName: 'Title',
required: false,
}),
password: Property.ShortText({
displayName: 'Encryption Password',
description: 'Encrypts the paste with this password',
required: false,
}),
folder_id: pastefyCommon.folder_id(false),
visibility: pastefyCommon.visibility(false),
expiry: Property.DateTime({
displayName: 'Expiry Date',
required: false,
}),
},
async run(context) {
const client = makeClient(context.auth, context.propsValue);
const password = context.propsValue.password;
let content = context.propsValue.content;
let title = context.propsValue.title;
if (password) {
if (content) {
content = CryptoJS.AES.encrypt(content, password).toString();
}
if (title) {
title = CryptoJS.AES.encrypt(title, password).toString();
}
}
const res = await client.editPaste(context.propsValue.paste_id, {
title,
content,
encrypted: !!password,
folder: context.propsValue.folder_id,
visibility: context.propsValue.visibility,
expire_at: formatDate(context.propsValue.expiry),
});
return res;
},
});

View File

@@ -0,0 +1,20 @@
import { createAction } from '@activepieces/pieces-framework';
import { makeClient, pastefyCommon } from '../common';
import { pastefyAuth } from '../..';
export default createAction({
auth: pastefyAuth,
name: 'get_folder_hierarchy',
displayName: 'Get Folder Hierarchy',
description: 'Retrieves a hierarchy of all folders',
props: {
parent_id: pastefyCommon.folder_id(false, 'Start Folder'),
},
async run(context) {
const client = makeClient(context.auth, context.propsValue);
const hierarchy = await client.getFolderHierarchy(
context.propsValue.parent_id
);
return hierarchy;
},
});

View File

@@ -0,0 +1,20 @@
import { createAction } from '@activepieces/pieces-framework';
import { makeClient, pastefyCommon } from '../common';
import { pastefyAuth } from '../..';
export default createAction({
auth: pastefyAuth,
name: 'get_folder',
displayName: 'Get Folder',
description: 'Retrieves information about a folder',
props: {
folder_id: pastefyCommon.folder_id(true),
},
async run(context) {
const client = makeClient(context.auth, context.propsValue);
const folder = await client.getFolder(
context.propsValue.folder_id as string
);
return folder;
},
});

View File

@@ -0,0 +1,38 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { makeClient } from '../common';
import { pastefyAuth } from '../..';
import CryptoJS from 'crypto-js';
export default createAction({
auth: pastefyAuth,
name: 'get_paste',
displayName: 'Get Paste',
description: 'Retrieves a paste',
props: {
paste_id: Property.ShortText({
displayName: 'Paste ID',
required: true,
}),
password: Property.ShortText({
displayName: 'Encryption Password',
description: 'Decrypts the paste with this password',
required: false,
}),
},
async run(context) {
const client = makeClient(context.auth, context.propsValue);
const password = context.propsValue.password;
const paste = await client.getPaste(context.propsValue.paste_id);
if (paste.encrypted && password) {
paste.content = CryptoJS.AES.decrypt(paste.content, password).toString(
CryptoJS.enc.Utf8
);
if (paste.title) {
paste.title = CryptoJS.AES.decrypt(paste.title, password).toString(
CryptoJS.enc.Utf8
);
}
}
return paste;
},
});

View File

@@ -0,0 +1,19 @@
import createFolder from './create-folder';
import createPaste from './create-paste';
import deleteFolder from './delete-folder';
import deletePaste from './delete-paste';
import editPaste from './edit-paste';
import getFolder from './get-folder';
import getFolderHierarchy from './get-folder-hierarchy';
import getPaste from './get-paste';
export default [
createPaste,
getPaste,
editPaste,
deletePaste,
createFolder,
getFolder,
getFolderHierarchy,
deleteFolder,
];

View File

@@ -0,0 +1,156 @@
import {
Authentication,
AuthenticationType,
HttpMessageBody,
HttpMethod,
QueryParams,
httpClient,
} from '@activepieces/pieces-common';
import {
Folder,
FolderCreateRequest,
FolderCreateResponse,
FolderGetRequest,
FolderHierarchy,
FolderListRequest,
} from './models/folder';
import { ActionResponse, prepareQueryRequest } from './models/common';
import {
Paste,
PasteCreateRequest,
PasteCreateResponse,
PasteEditRequest,
PasteListRequest,
PasteShareRequest,
} from './models/paste';
function ensureSuccessfulResponse<T extends ActionResponse>(res: T): T {
if (!res.success) {
throw 'Request failed';
}
return res;
}
export class PastefyClient {
constructor(
private apiKey?: string,
private instanceUrl = 'https://pastefy.app'
) {}
async makeRequest<T extends HttpMessageBody>(
method: HttpMethod,
url: string,
query?: QueryParams,
body?: object
): Promise<T> {
const authentication: Authentication | undefined = this.apiKey
? {
type: AuthenticationType.BEARER_TOKEN,
token: this.apiKey,
}
: undefined;
const res = await httpClient.sendRequest<T>({
method,
url: this.instanceUrl + '/api/v2' + url,
queryParams: query,
body,
authentication,
});
return res.body;
}
async createFolder(
request: FolderCreateRequest
): Promise<FolderCreateResponse> {
return ensureSuccessfulResponse<FolderCreateResponse>(
await this.makeRequest(HttpMethod.POST, '/folder', undefined, request)
);
}
async listFolders(request: FolderListRequest): Promise<Folder[]> {
return await this.makeRequest(
HttpMethod.GET,
'/folder',
prepareQueryRequest(request)
);
}
async getFolder(id: string, request?: FolderGetRequest): Promise<Folder> {
return await this.makeRequest(
HttpMethod.GET,
'/folder/' + id,
prepareQueryRequest(request)
);
}
async getFolderHierarchy(parentId?: string): Promise<FolderHierarchy[]> {
const folders = await this.listFolders({
page_size: 99999,
filter: {
parent: parentId || 'null',
},
});
const hierarchies: FolderHierarchy[] = [];
for (const folder of folders) {
hierarchies.push({
id: folder.id,
name: folder.name,
children: await this.getFolderHierarchy(folder.id),
});
}
return hierarchies;
}
async deleteFolder(id: string): Promise<ActionResponse> {
return ensureSuccessfulResponse(
await this.makeRequest(HttpMethod.DELETE, '/folder/' + id)
);
}
async createPaste(request: PasteCreateRequest): Promise<PasteCreateResponse> {
return ensureSuccessfulResponse<PasteCreateResponse>(
await this.makeRequest(HttpMethod.POST, '/paste', undefined, request)
);
}
async listPastes(request: PasteListRequest): Promise<Paste[]> {
return await this.makeRequest(
HttpMethod.GET,
'/paste',
prepareQueryRequest(request)
);
}
async getPaste(id: string): Promise<Paste> {
return await this.makeRequest(HttpMethod.GET, '/paste/' + id);
}
async editPaste(
id: string,
request: PasteEditRequest
): Promise<ActionResponse> {
return ensureSuccessfulResponse(
await this.makeRequest(HttpMethod.PUT, '/paste/' + id, undefined, request)
);
}
async deletePaste(id: string): Promise<ActionResponse> {
return ensureSuccessfulResponse(
await this.makeRequest(HttpMethod.DELETE, '/paste/' + id)
);
}
async sharePaste(
id: string,
request: PasteShareRequest
): Promise<ActionResponse> {
return ensureSuccessfulResponse(
await this.makeRequest(
HttpMethod.POST,
'/paste/' + id + '/friend',
undefined,
request
)
);
}
}

View File

@@ -0,0 +1,92 @@
import {
AppConnectionValueForAuthProperty,
PiecePropValueSchema,
PiecePropertyMap,
Property,
StaticPropsValue,
} from '@activepieces/pieces-framework';
import { PastefyClient } from './client';
import { FolderHierarchy } from './models/folder';
import { PasteVisibility } from './models/paste';
import { pastefyAuth } from '../..';
interface FlatFolder {
id: string;
name: string;
}
function flattenFolderHierarchy(hierarchy: FolderHierarchy[]): FlatFolder[] {
const folders: FlatFolder[] = [];
for (const h of hierarchy) {
folders.push({ id: h.id, name: h.name });
flattenFolderHierarchy(h.children).forEach((e) => {
folders.push({
id: e.id,
name: h.name + ' / ' + e.name,
});
});
}
return folders;
}
export function formatDate(date?: string): string | undefined {
if (!date) return date;
return date
.replace('T', ' ')
.replace('Z', '')
.replace(/\.[0-9]{3}/, '');
}
export const pastefyCommon = {
folder_id: (required = true, displayName = 'Folder') =>
Property.Dropdown({
auth: pastefyAuth,
description: 'A folder',
displayName: displayName,
required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'setup authentication first',
options: [],
};
}
const client = makeClient(
auth,
{ ...auth.props }
);
const folders = await client.getFolderHierarchy();
return {
disabled: false,
options: flattenFolderHierarchy(folders).map((folder) => {
return {
label: folder.name,
value: folder.id,
};
}),
};
},
}),
visibility: (required = true) =>
Property.StaticDropdown({
displayName: 'Visibility',
required,
options: {
options: [
{ label: 'Public', value: PasteVisibility.PUBLIC },
{ label: 'Unlisted', value: PasteVisibility.UNLISTED },
{ label: 'Private', value: PasteVisibility.PRIVATE },
],
},
}),
};
export function makeClient(
auth: AppConnectionValueForAuthProperty<typeof pastefyAuth>,
propsValue: StaticPropsValue<PiecePropertyMap>
): PastefyClient {
return new PastefyClient(auth.props.token || undefined, propsValue.instance_url);
}

View File

@@ -0,0 +1,48 @@
import { QueryParams } from '@activepieces/pieces-common';
export interface ActionResponse {
success: boolean;
}
export interface ListRequest {
page?: number;
page_size?: number;
search?: string;
filter?: Record<string, any>;
}
function emptyValueFilter(
accessor: (key: string) => any
): (key: string) => boolean {
return (key: string) => {
const val = accessor(key);
return (
val !== null &&
val !== undefined &&
(typeof val != 'string' || val.length > 0)
);
};
}
export function prepareQueryRequest(
request?: ListRequest | Record<string, any | undefined>
): QueryParams {
const params: QueryParams = {};
if (!request) return params;
const requestObj = request as Record<string, any>;
Object.keys(request)
.filter((k) => k != 'filter')
.filter(emptyValueFilter((k) => requestObj[k]))
.forEach((k: string) => {
params[k] = (request as Record<string, any>)[k].toString();
});
if (request.filter) {
const filter = request.filter; // For some reason required to pass the unidentified check
Object.keys(request.filter)
.filter(emptyValueFilter((k) => filter[k]))
.forEach((k) => {
params['filter[' + k + ']'] = filter[k].toString();
});
}
return params;
}

View File

@@ -0,0 +1,43 @@
import { ActionResponse, ListRequest } from './common';
import { Paste } from './paste';
export interface Folder {
exists: boolean;
id: string;
name: string;
user_id: string;
children?: Folder[];
pastes?: Paste[];
created: string;
}
export interface FolderCreateRequest {
name: string;
parent?: string;
}
export interface FolderCreateResponse extends ActionResponse {
folder: Folder;
}
export interface FolderEditRequest {
name?: string;
}
export interface FolderEditResponse extends ActionResponse {
folder: Folder;
}
export interface FolderGetRequest {
hide_children?: string;
}
export interface FolderListRequest extends ListRequest {
user_id?: string;
}
export interface FolderHierarchy {
id: string;
name: string;
children: FolderHierarchy[];
}

View File

@@ -0,0 +1,75 @@
import { ActionResponse } from './common';
export enum PasteType {
PASTE = 'PASTE',
MULTI_PASTE = 'MULTI_PASTE',
}
export enum PasteVisibility {
PUBLIC = 'PUBLIC',
UNLISTED = 'UNLISTED',
PRIVATE = 'PRIVATE',
}
export interface Paste {
exists: boolean;
id: string;
content: string;
title: string;
encrypted: boolean;
folder: string;
user_id?: string;
visibility: PasteVisibility;
forked_from?: string;
raw_url: string;
type: PasteType;
created_at: string;
expire_at?: string;
}
export interface PasteCreateRequest {
title?: string;
content: string;
encrypted?: boolean;
folder?: string;
expire_at?: string;
forked_from?: string;
visibility?: PasteVisibility;
type?: PasteType;
}
export interface PasteCreateResponse extends ActionResponse {
paste: Paste;
}
export interface PasteEditRequest {
title?: string;
content?: string;
encrypted?: boolean;
folder?: string;
type?: PasteType;
visibility?: PasteVisibility;
expire_at?: string;
}
export interface PasteEditResponse extends ActionResponse {
paste: Paste;
}
export interface PasteShareRequest {
friend: string;
}
export interface PasteListRequest {
page?: number;
page_size?: number;
search?: string;
shorten_content?: boolean;
}
export interface PasteListTrendingRequest {
page?: number;
page_size?: number;
trending?: boolean;
shorten_content?: boolean;
}

View File

@@ -0,0 +1,21 @@
export enum UserType {
USER,
ADMIN,
BLOCKED,
AWAITING_ACCESS,
}
export interface User {
name: string;
avatar?: string;
displayName: string;
}
export interface DetailedUser extends User {
id: string;
color: string;
profile_picture?: string;
logged_in: true;
auth_type: string;
type: UserType;
}

View File

@@ -0,0 +1,3 @@
import pasteChanged from './paste-changed';
export default [pasteChanged];

View File

@@ -0,0 +1,72 @@
import {
createTrigger,
Property,
StoreScope,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { makeClient } from '../common';
import { createHash } from 'crypto';
import { pastefyAuth } from '../..';
export default createTrigger({
auth: pastefyAuth,
name: 'paste_changed',
displayName: 'Paste Changed',
description: 'Triggers when the content (or title) of the paste changes',
type: TriggerStrategy.POLLING,
props: {
paste_id: Property.ShortText({
displayName: 'Paste ID',
required: true,
}),
include_title: Property.Checkbox({
displayName: 'Include Title',
required: false,
}),
},
sampleData: {},
onEnable: async (context) => {
const client = makeClient(context.auth, context.propsValue);
const paste = await client.getPaste(context.propsValue.paste_id);
const hash = createHash('md5')
.update(
paste.content + (context.propsValue.include_title ? paste.title : '')
)
.digest('hex');
await context.store.put(
'paste_changed_trigger_hash',
hash,
StoreScope.FLOW
);
},
onDisable: async (context) => {
await context.store.delete('paste_changed_trigger_hash', StoreScope.FLOW);
},
run: async (context) => {
const oldHash = await context.store.get(
'paste_changed_trigger_hash',
StoreScope.FLOW
);
const client = makeClient(context.auth, context.propsValue);
const paste = await client.getPaste(context.propsValue.paste_id);
const newHash = createHash('md5')
.update(
paste.content + (context.propsValue.include_title ? paste.title : '')
)
.digest('hex');
if (oldHash != newHash) {
await context.store.put(
'paste_changed_trigger_hash',
newHash,
StoreScope.FLOW
);
return [paste];
}
return [];
},
test: async (context) => {
const client = makeClient(context.auth, context.propsValue);
const paste = await client.getPaste(context.propsValue.paste_id);
return [paste];
},
});