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,33 @@
{
"extends": [
"../../../../.eslintrc.base.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

View File

@@ -0,0 +1,7 @@
# pieces-ask-handle
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build pieces-ask-handle` to build the library.

View File

@@ -0,0 +1,10 @@
{
"name": "@activepieces/piece-ask-handle",
"version": "0.0.2",
"type": "commonjs",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"dependencies": {
"tslib": "^2.3.0"
}
}

View File

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

View File

@@ -0,0 +1,42 @@
{
"\nTo get your API Key:\n\n1. Go to https://dashboard.askhandle.com\n2. Sign in to your account\n3. Navigate to API settings\n4. Create or copy your API token\n5. Paste it here\n": "\nTo get your API Key:\n\n1. Go to https://dashboard.askhandle.com\n2. Sign in to your account\n3. Navigate to API settings\n4. Create or copy your API token\n5. Paste it here\n",
"Create Message": "Create Message",
"Create Lead": "Create Lead",
"List Rooms": "List Rooms",
"List Leads": "List Leads",
"Send a message to a room": "Send a message to a room",
"Create a new lead": "Create a new lead",
"Get a list of all rooms": "Get a list of all rooms",
"Get a list of all leads": "Get a list of all leads",
"Room": "Room",
"Message": "Message",
"Nickname": "Nickname",
"Email": "Email",
"Phone Number": "Phone Number",
"Device": "Device",
"From Page Title": "From Page Title",
"Referrer": "Referrer",
"Start Date": "Start Date",
"End Date": "End Date",
"Limit": "Limit",
"Select a room": "Select a room",
"The message content": "The message content",
"Sender nickname": "Sender nickname",
"Sender email": "Sender email",
"Sender phone number": "Sender phone number",
"Lead nickname": "Lead nickname",
"Lead email": "Lead email",
"Lead phone number": "Lead phone number",
"Device information": "Device information",
"Page title where lead originated": "Page title where lead originated",
"Referrer URL": "Referrer URL",
"Filter leads from this date": "Filter leads from this date",
"Filter leads until this date": "Filter leads until this date",
"Maximum number of leads to return": "Maximum number of leads to return",
"New Message": "New Message",
"New Lead": "New Lead",
"New Room": "New Room",
"Triggers when a new message is received": "Triggers when a new message is received",
"Triggers when a new lead is created": "Triggers when a new lead is created",
"Triggers when a new room is created": "Triggers when a new room is created"
}

View File

@@ -0,0 +1,28 @@
import { createPiece } from "@activepieces/pieces-framework";
import { askHandleAuth } from "./lib/common/auth";
import { createMessage } from "./lib/actions/create-message";
import { createLead } from "./lib/actions/create-lead";
import { listRooms } from "./lib/actions/list-rooms";
import { listLeads } from "./lib/actions/list-leads";
import { newMessageTrigger } from "./lib/triggers/new-message";
import { newLeadTrigger } from "./lib/triggers/new-lead";
import { newRoomTrigger } from "./lib/triggers/new-room";
export const askHandle = createPiece({
displayName: "AskHandle",
auth: askHandleAuth,
minimumSupportedRelease: '0.36.1',
logoUrl: "https://cdn.activepieces.com/pieces/ask-handle.png",
authors: ["onyedikachi-david"],
actions: [
createMessage,
createLead,
listRooms,
listLeads,
],
triggers: [
newMessageTrigger,
newLeadTrigger,
newRoomTrigger,
],
});

View File

@@ -0,0 +1,70 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { askHandleAuth } from '../common/auth';
import { askHandleApiCall } from '../common/client';
export const createLead = createAction({
auth: askHandleAuth,
name: 'create_lead',
displayName: 'Create Lead',
description: 'Create a new lead',
props: {
nickname: Property.ShortText({
displayName: 'Nickname',
description: 'Lead nickname',
required: false,
}),
email: Property.ShortText({
displayName: 'Email',
description: 'Lead email',
required: false,
}),
phone_number: Property.ShortText({
displayName: 'Phone Number',
description: 'Lead phone number',
required: false,
}),
device: Property.ShortText({
displayName: 'Device',
description: 'Device information',
required: false,
}),
from_page_title: Property.ShortText({
displayName: 'From Page Title',
description: 'Page title where lead originated',
required: false,
}),
referrer: Property.ShortText({
displayName: 'Referrer',
description: 'Referrer URL',
required: false,
}),
},
async run(context) {
const {
nickname,
email,
phone_number,
device,
from_page_title,
referrer,
} = context.propsValue;
const payload: any = {};
if (nickname) payload.nickname = nickname;
if (email) payload.email = email;
if (phone_number) payload.phone_number = phone_number;
if (device) payload.device = device;
if (from_page_title) payload.from_page_title = from_page_title;
if (referrer) payload.referrer = referrer;
return await askHandleApiCall(
context.auth.secret_text,
HttpMethod.POST,
'/leads/',
payload
);
},
});

View File

@@ -0,0 +1,57 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { askHandleAuth } from '../common/auth';
import { askHandleApiCall } from '../common/client';
import { roomDropdown } from '../common/props';
export const createMessage = createAction({
auth: askHandleAuth,
name: 'create_message',
displayName: 'Create Message',
description: 'Send a message to a room',
props: {
room: roomDropdown,
body: Property.LongText({
displayName: 'Message',
description: 'The message content',
required: true,
}),
nickname: Property.ShortText({
displayName: 'Nickname',
description: 'Sender nickname',
required: false,
}),
email: Property.ShortText({
displayName: 'Email',
description: 'Sender email',
required: false,
}),
phone_number: Property.ShortText({
displayName: 'Phone Number',
description: 'Sender phone number',
required: false,
}),
},
async run(context) {
const { room, body, nickname, email, phone_number } = context.propsValue;
const payload: any = {
body,
room: {
uuid: room,
},
};
if (nickname) payload.nickname = nickname;
if (email) payload.email = email;
if (phone_number) payload.phone_number = phone_number;
return await askHandleApiCall(
context.auth.secret_text,
HttpMethod.POST,
'/messages/',
payload
);
},
});

View File

@@ -0,0 +1,55 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { askHandleAuth } from '../common/auth';
import { askHandleApiCall } from '../common/client';
export const listLeads = createAction({
auth: askHandleAuth,
name: 'list_leads',
displayName: 'List Leads',
description: 'Get a list of all leads',
props: {
start_date: Property.DateTime({
displayName: 'Start Date',
description: 'Filter leads from this date',
required: false,
}),
end_date: Property.DateTime({
displayName: 'End Date',
description: 'Filter leads until this date',
required: false,
}),
limit: Property.Number({
displayName: 'Limit',
description: 'Maximum number of leads to return',
required: false,
}),
},
async run(context) {
const { start_date, end_date, limit } = context.propsValue;
const queryParams: string[] = [];
if (start_date) {
const dateStr = new Date(start_date).toISOString().split('T')[0];
queryParams.push(`start_date=${dateStr}`);
}
if (end_date) {
const dateStr = new Date(end_date).toISOString().split('T')[0];
queryParams.push(`end_date=${dateStr}`);
}
if (limit) {
queryParams.push(`limit=${limit}`);
}
const path = queryParams.length > 0
? `/leads/?${queryParams.join('&')}`
: '/leads/';
return await askHandleApiCall(
context.auth.secret_text,
HttpMethod.GET,
path
);
},
});

View File

@@ -0,0 +1,20 @@
import { createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { askHandleAuth } from '../common/auth';
import { askHandleApiCall } from '../common/client';
export const listRooms = createAction({
auth: askHandleAuth,
name: 'list_rooms',
displayName: 'List Rooms',
description: 'Get a list of all rooms',
props: {},
async run(context) {
return await askHandleApiCall(
context.auth.secret_text,
HttpMethod.GET,
'/rooms/'
);
},
});

View File

@@ -0,0 +1,54 @@
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { PieceAuth } from '@activepieces/pieces-framework';
const BASE_URL = 'https://dashboard.askhandle.com/api/v1';
export const askHandleAuth = PieceAuth.SecretText({
displayName: 'API Key',
description: `
To get your API Key:
1. Go to https://dashboard.askhandle.com
2. Sign in to your account
3. Navigate to API settings
4. Create or copy your API token
5. Paste it here
`,
required: true,
validate: async ({ auth }) => {
if (!auth) {
return {
valid: false,
error: 'API key is required',
};
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${BASE_URL}/rooms/`,
headers: {
Authorization: `Token ${auth}`,
'Content-Type': 'application/json',
},
});
if (response.status === 200) {
return {
valid: true,
};
}
return {
valid: false,
error: 'Invalid API key',
};
} catch (error: any) {
return {
valid: false,
error: 'Invalid API key. Please check your API key and try again.',
};
}
},
});

View File

@@ -0,0 +1,67 @@
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
const BASE_URL = 'https://dashboard.askhandle.com/api/v1';
export async function askHandleApiCall(
apiKey: string,
method: HttpMethod,
path: string,
body?: unknown
) {
try {
const response = await httpClient.sendRequest({
method,
url: `${BASE_URL}${path}`,
headers: {
Authorization: `Token ${apiKey}`,
'Content-Type': 'application/json',
},
body,
});
if (response.status >= 200 && response.status < 300) {
return response.body;
}
throw new Error(
`AskHandle API error: ${response.status} ${JSON.stringify(response.body)}`
);
} catch (error: any) {
const statusCode = error.response?.status;
if (statusCode) {
switch (statusCode) {
case 400:
throw new Error(
`Bad Request: Invalid request parameters. Please check your data.`
);
case 401:
throw new Error(
'Authentication Failed: Invalid API key. Please verify your AskHandle credentials.'
);
case 403:
throw new Error(
'Access Forbidden: You do not have permission to access this resource.'
);
case 404:
throw new Error(
'Resource Not Found: The requested resource does not exist.'
);
case 500:
case 502:
case 503:
case 504:
throw new Error(
`Server Error (${statusCode}): AskHandle API is experiencing issues. Please try again later.`
);
default:
throw new Error(
`AskHandle API Error (${statusCode}): ${error.message || 'Unknown error occurred'}`
);
}
}
throw new Error(`Unexpected error: ${error.message || String(error)}`);
}
}

View File

@@ -0,0 +1,87 @@
import { Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { askHandleApiCall } from './client';
import { askHandleAuth } from './auth';
export const roomDropdown = Property.Dropdown({
auth: askHandleAuth,
displayName: 'Room',
description: 'Select a room',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
try {
const response = await askHandleApiCall(
auth.secret_text,
HttpMethod.GET,
'/rooms/'
);
const rooms = Array.isArray(response) ? response : (response)?.results || [];
return {
disabled: false,
options: rooms.map((room: any) => ({
label: room.name || room.label || `Room ${room.uuid}`,
value: room.uuid,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading rooms',
};
}
},
});
export const leadDropdown = Property.Dropdown({
auth: askHandleAuth,
displayName: 'Lead',
description: 'Select a lead',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
try {
const response = await askHandleApiCall(
auth.secret_text,
HttpMethod.GET,
'/leads/'
);
const leads = Array.isArray(response) ? response : (response as any)?.results || [];
return {
disabled: false,
options: leads.map((lead: any) => ({
label: lead.nickname || lead.email || `Lead ${lead.uuid}`,
value: lead.uuid,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading leads',
};
}
},
});

View File

@@ -0,0 +1,79 @@
import {
createTrigger,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { askHandleAuth } from '../common/auth';
const BASE_URL = 'https://dashboard.askhandle.com/api/v1';
export const newLeadTrigger = createTrigger({
auth: askHandleAuth,
name: 'new_lead',
displayName: 'New Lead',
description: 'Triggers when a new lead is created',
type: TriggerStrategy.WEBHOOK,
props: {},
sampleData: {
uuid: 'd8a2e337-9d4d-4515-979a-6f590379848f',
nickname: 'John',
email: 'john@example.com',
phone_number: '+1234567890',
device: 'Desktop',
from_page_title: 'Homepage',
referrer: 'https://example.com',
created_at: '2021-09-15T12:08:50.676405Z',
},
async onEnable(context) {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${BASE_URL}/webhooks/`,
headers: {
Authorization: `Token ${context.auth}`,
'Content-Type': 'application/json',
},
body: {
event: 'lead.added',
target: context.webhookUrl,
},
});
if (response.status === 200 || response.status === 201) {
const webhook = response.body as { uuid: string };
await context.store.put('_askhandle_webhook_lead', webhook.uuid);
}
} catch (error: any) {
throw new Error(
`Failed to enable webhook: ${error.message || String(error)}`
);
}
},
async onDisable(context) {
try {
const webhookId = await context.store.get<string>(
'_askhandle_webhook_lead'
);
if (webhookId) {
await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: `${BASE_URL}/webhooks/${webhookId}/`,
headers: {
Authorization: `Token ${context.auth}`,
'Content-Type': 'application/json',
},
});
await context.store.delete('_askhandle_webhook_lead');
}
} catch (error: any) {
console.warn('Failed to delete webhook during disable:', error);
}
},
async run(context) {
const payload = context.payload.body as { data: any };
return [payload.data || payload];
},
});

View File

@@ -0,0 +1,77 @@
import {
createTrigger,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { askHandleAuth } from '../common/auth';
const BASE_URL = 'https://dashboard.askhandle.com/api/v1';
export const newMessageTrigger = createTrigger({
auth: askHandleAuth,
name: 'new_message',
displayName: 'New Message',
description: 'Triggers when a new message is received',
type: TriggerStrategy.WEBHOOK,
props: {},
sampleData: {
uuid: 'ffb84155-5f12-4bee-bd50-3c868097e473',
nickname: 'Mary',
email: 'mary@example.com',
body: 'Hello!',
is_support_sender: false,
sent_at: '2021-09-15T12:08:50.676405Z',
},
async onEnable(context) {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${BASE_URL}/webhooks/`,
headers: {
Authorization: `Token ${context.auth}`,
'Content-Type': 'application/json',
},
body: {
event: 'message.added',
target: context.webhookUrl,
},
});
if (response.status === 200 || response.status === 201) {
const webhook = response.body as { uuid: string };
await context.store.put('_askhandle_webhook_message', webhook.uuid);
}
} catch (error: any) {
throw new Error(
`Failed to enable webhook: ${error.message || String(error)}`
);
}
},
async onDisable(context) {
try {
const webhookId = await context.store.get<string>(
'_askhandle_webhook_message'
);
if (webhookId) {
await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: `${BASE_URL}/webhooks/${webhookId}/`,
headers: {
Authorization: `Token ${context.auth}`,
'Content-Type': 'application/json',
},
});
await context.store.delete('_askhandle_webhook_message');
}
} catch (error: any) {
console.warn('Failed to delete webhook during disable:', error);
}
},
async run(context) {
const payload = context.payload.body as { data: any };
return [payload.data || payload];
},
});

View File

@@ -0,0 +1,78 @@
import {
createTrigger,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { askHandleAuth } from '../common/auth';
const BASE_URL = 'https://dashboard.askhandle.com/api/v1';
export const newRoomTrigger = createTrigger({
auth: askHandleAuth,
name: 'new_room',
displayName: 'New Room',
description: 'Triggers when a new room is created',
type: TriggerStrategy.WEBHOOK,
props: {},
sampleData: {
uuid: 'de1e39a5-a391-4d7f-836d-cf3589529af8',
label: 'room-label-123',
name: 'Customer Support Room',
rating: 5,
is_bot_use: false,
created_at: '2021-09-15T12:08:50.676405Z',
messages: [],
},
async onEnable(context) {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${BASE_URL}/webhooks/`,
headers: {
Authorization: `Token ${context.auth}`,
'Content-Type': 'application/json',
},
body: {
event: 'chat.added',
target: context.webhookUrl,
},
});
if (response.status === 200 || response.status === 201) {
const webhook = response.body as { uuid: string };
await context.store.put('_askhandle_webhook_room', webhook.uuid);
}
} catch (error: any) {
throw new Error(
`Failed to enable webhook: ${error.message || String(error)}`
);
}
},
async onDisable(context) {
try {
const webhookId = await context.store.get<string>(
'_askhandle_webhook_room'
);
if (webhookId) {
await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: `${BASE_URL}/webhooks/${webhookId}/`,
headers: {
Authorization: `Token ${context.auth}`,
'Content-Type': 'application/json',
},
});
await context.store.delete('_askhandle_webhook_room');
}
} catch (error: any) {
console.warn('Failed to delete webhook during disable:', error);
}
},
async run(context) {
const payload = context.payload.body as { data: any };
return [payload.data || payload];
},
});

View File

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

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}