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:
@@ -0,0 +1,49 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { famulorAuth } from '../..';
|
||||
import { famulorCommon } from '../common';
|
||||
|
||||
export const addLead = createAction({
|
||||
auth: famulorAuth,
|
||||
name: 'addLead',
|
||||
displayName: 'Add Lead to Campaign',
|
||||
description: 'Add a lead to an outbound campaign to be called by an AI assistant.',
|
||||
props: famulorCommon.addLeadProperties(),
|
||||
async run({ auth, propsValue }) {
|
||||
await propsValidation.validateZod(propsValue, famulorCommon.addLeadSchema);
|
||||
|
||||
// Process secondary contacts if provided
|
||||
let secondaryContacts: Array<{ phone_number: string; variables?: Record<string, any> }> | undefined;
|
||||
|
||||
if (propsValue.secondary_contacts && propsValue.num_secondary_contacts) {
|
||||
const secondaryContactsData = propsValue.secondary_contacts as Record<string, any>;
|
||||
const numContacts = Math.min(Number(propsValue.num_secondary_contacts) || 0, 10);
|
||||
const contacts: Array<{ phone_number: string; variables?: Record<string, any> }> = [];
|
||||
|
||||
for (let i = 1; i <= numContacts; i++) {
|
||||
const phoneNumber = secondaryContactsData[`contact_${i}_phone`];
|
||||
const variables = secondaryContactsData[`contact_${i}_variables`];
|
||||
|
||||
if (phoneNumber && phoneNumber.trim() !== '') {
|
||||
contacts.push({
|
||||
phone_number: phoneNumber,
|
||||
variables: variables || { customer_name: '' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (contacts.length > 0) {
|
||||
secondaryContacts = contacts;
|
||||
}
|
||||
}
|
||||
|
||||
return await famulorCommon.addLead({
|
||||
auth: auth.secret_text,
|
||||
campaign_id: propsValue.campaign,
|
||||
phone_number: propsValue.phone_number!,
|
||||
variable: propsValue.variables,
|
||||
allow_dupplicate: propsValue.allow_dupplicate,
|
||||
secondary_contacts: secondaryContacts,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { famulorAuth } from '../..';
|
||||
import { famulorCommon } from '../common';
|
||||
|
||||
export const campaignControl = createAction({
|
||||
auth: famulorAuth,
|
||||
name: 'campaignControl',
|
||||
displayName: 'Start/Stop Campaign',
|
||||
description: 'Start or stop an outbound calling campaign. Starting requires sufficient leads; stopping cancels ongoing calls.',
|
||||
props: famulorCommon.campaignControlProperties(),
|
||||
async run({ auth, propsValue }) {
|
||||
await propsValidation.validateZod(propsValue, famulorCommon.campaignControlSchema);
|
||||
|
||||
return await famulorCommon.campaignControl({
|
||||
auth: auth.secret_text,
|
||||
campaign_id: propsValue.campaign,
|
||||
action: propsValue.action as 'start' | 'stop',
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { famulorAuth } from '../..';
|
||||
import { famulorCommon } from '../common';
|
||||
|
||||
export const deleteLead = createAction({
|
||||
auth: famulorAuth,
|
||||
name: 'deleteLead',
|
||||
displayName: 'Delete Lead',
|
||||
description: '⚠️ Permanently delete a lead from the system. This action cannot be undone and will abort any ongoing calls.',
|
||||
props: famulorCommon.deleteLeadProperties(),
|
||||
async run({ auth, propsValue }) {
|
||||
await propsValidation.validateZod(propsValue, famulorCommon.deleteLeadSchema);
|
||||
|
||||
return await famulorCommon.deleteLead({
|
||||
auth: auth.secret_text,
|
||||
lead_id: propsValue.lead_id,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { famulorAuth } from '../..';
|
||||
import { famulorCommon } from '../common';
|
||||
|
||||
export const makePhoneCall = createAction({
|
||||
auth: famulorAuth,
|
||||
name: 'makePhoneCall',
|
||||
displayName: 'Make Phone Call',
|
||||
description: 'Initiate an AI-powered phone call to a customer using a selected assistant.',
|
||||
props: famulorCommon.makePhoneCallProperties(),
|
||||
async run({ auth, propsValue }) {
|
||||
await propsValidation.validateZod(propsValue, famulorCommon.makePhoneCallSchema);
|
||||
|
||||
return await famulorCommon.makePhoneCall({
|
||||
auth: auth.secret_text,
|
||||
assistant_id: propsValue.assistant_id as number,
|
||||
phone_number: propsValue.phone_number!,
|
||||
variable: propsValue.variable,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { famulorAuth } from '../..';
|
||||
import { famulorCommon } from '../common';
|
||||
|
||||
export const sendSms = createAction({
|
||||
auth: famulorAuth,
|
||||
name: 'sendSms',
|
||||
displayName: 'Send SMS',
|
||||
description: 'Send an SMS message using your purchased phone numbers. Costs are automatically deducted from your account.',
|
||||
props: famulorCommon.sendSmsProperties(),
|
||||
async run({ auth, propsValue }) {
|
||||
await propsValidation.validateZod(propsValue, famulorCommon.sendSmsSchema);
|
||||
|
||||
return await famulorCommon.sendSms({
|
||||
auth: auth.secret_text,
|
||||
from: propsValue.from as number,
|
||||
to: propsValue.to!,
|
||||
bodysuit: propsValue.bodysuit!,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import * as properties from './properties';
|
||||
import * as schemas from './schemas';
|
||||
import {
|
||||
ListCampaignsResponse,
|
||||
AddLeadParams,
|
||||
SendSmsParams,
|
||||
MakePhoneCallParams,
|
||||
CampaignControlParams,
|
||||
DeleteLeadParams,
|
||||
LeadResponse
|
||||
} from './types';
|
||||
|
||||
export const baseApiUrl = 'https://app.famulor.de/';
|
||||
|
||||
export const famulorCommon = {
|
||||
baseHeaders: (auth: string) => ({
|
||||
'Authorization': `Bearer ${auth}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}),
|
||||
|
||||
// Properties
|
||||
addLeadProperties: properties.addLead,
|
||||
sendSmsProperties: properties.sendSms,
|
||||
makePhoneCallProperties: properties.makePhoneCall,
|
||||
campaignControlProperties: properties.campaignControl,
|
||||
deleteLeadProperties: properties.deleteLead,
|
||||
|
||||
// Schemas
|
||||
addLeadSchema: schemas.addLead,
|
||||
sendSmsSchema: schemas.sendSms,
|
||||
makePhoneCallSchema: schemas.makePhoneCall,
|
||||
campaignControlSchema: schemas.campaignControl,
|
||||
deleteLeadSchema: schemas.deleteLead,
|
||||
|
||||
// Methods
|
||||
listAllAssistants: async ({ auth, per_page = 10, page = 1, type }: { auth: string; per_page?: number; page?: number; type?: string }) => {
|
||||
const queryParams: Record<string, string> = {
|
||||
per_page: per_page.toString(),
|
||||
page: page.toString(),
|
||||
};
|
||||
|
||||
if (type && type !== '') {
|
||||
queryParams['type'] = type;
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${baseApiUrl}api/user/assistants/get`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
queryParams,
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch assistants: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
},
|
||||
|
||||
listPhoneNumbers: async ({ auth }: { auth: string }) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${baseApiUrl}api/user/assistants/phone-numbers`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch phone numbers: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body || [];
|
||||
},
|
||||
|
||||
listAssistants: async ({ auth }: { auth: string }) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${baseApiUrl}api/user/assistants/outbound`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch assistants: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body || [];
|
||||
},
|
||||
|
||||
listLeads: async ({ auth }: { auth: string }) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${baseApiUrl}api/user/leads`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch leads: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.leads || response.body;
|
||||
},
|
||||
|
||||
listCampaigns: async ({ auth }: { auth: string }) => {
|
||||
const response = await httpClient.sendRequest<ListCampaignsResponse>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${baseApiUrl}api/user/campaigns`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch campaigns: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body.campaigns || response.body;
|
||||
},
|
||||
|
||||
addLead: async (params: AddLeadParams): Promise<LeadResponse> => {
|
||||
const { auth, ...body } = params;
|
||||
|
||||
const response = await httpClient.sendRequest<LeadResponse>({
|
||||
method: HttpMethod.POST,
|
||||
url: `${baseApiUrl}api/user/lead`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
body,
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to add lead: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
},
|
||||
|
||||
sendSms: async (params: SendSmsParams) => {
|
||||
const { auth, ...body } = params;
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${baseApiUrl}api/user/sms`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
body,
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to send SMS: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
},
|
||||
|
||||
makePhoneCall: async (params: MakePhoneCallParams) => {
|
||||
const { auth, ...body } = params;
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${baseApiUrl}api/user/make_call`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
body,
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to make phone call: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
},
|
||||
|
||||
campaignControl: async (params: CampaignControlParams) => {
|
||||
const { auth, ...body } = params;
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${baseApiUrl}api/user/campaigns/update-status`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
body,
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to control campaign: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
},
|
||||
|
||||
deleteLead: async (params: DeleteLeadParams) => {
|
||||
const { auth, lead_id } = params;
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.DELETE,
|
||||
url: `${baseApiUrl}api/user/leads/${lead_id}`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to delete lead: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
},
|
||||
|
||||
enableInboundWebhook: async ({ auth, assistant_id, webhook_url }: { auth: string; assistant_id: number; webhook_url: string }) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${baseApiUrl}api/user/assistants/enable-inbound-webhook`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
body: {
|
||||
assistant_id,
|
||||
webhook_url,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to enable inbound webhook: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
},
|
||||
|
||||
disableInboundWebhook: async ({ auth, assistant_id }: { auth: string; assistant_id: number }) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${baseApiUrl}api/user/assistants/disable-inbound-webhook`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
body: {
|
||||
assistant_id,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to disable inbound webhook: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
},
|
||||
|
||||
enablePostCallWebhook: async ({ auth, assistant_id, webhook_url }: { auth: string; assistant_id: number; webhook_url: string }) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${baseApiUrl}api/user/assistants/enable-webhook`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
body: {
|
||||
assistant_id,
|
||||
webhook_url,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to enable post-call webhook: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
},
|
||||
|
||||
disablePostCallWebhook: async ({ auth, assistant_id }: { auth: string; assistant_id: number }) => {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${baseApiUrl}api/user/assistants/disable-webhook`,
|
||||
headers: famulorCommon.baseHeaders(auth),
|
||||
body: {
|
||||
assistant_id,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to disable post-call webhook: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,281 @@
|
||||
import { Property } from '@activepieces/pieces-framework';
|
||||
import { famulorCommon } from '.';
|
||||
import { famulorAuth } from '../..';
|
||||
|
||||
// Dynamic Properties
|
||||
const campaignDropdown = () =>
|
||||
Property.Dropdown({
|
||||
auth: famulorAuth,
|
||||
displayName: 'Campaign',
|
||||
description: 'Select the campaign',
|
||||
required: true,
|
||||
refreshers: ['auth'],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please authenticate first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const campaigns = await famulorCommon.listCampaigns({ auth: auth.secret_text });
|
||||
|
||||
if (!campaigns || campaigns.length === 0) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'No campaigns found. Create one first.',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
options: campaigns.map((campaign: any) => ({
|
||||
label: campaign.name,
|
||||
value: campaign.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to fetch campaigns',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const phoneNumberProperty = (displayName: string, description: string, required = true) =>
|
||||
Property.ShortText({
|
||||
displayName,
|
||||
description: `${description} (E.164 format: +1234567890)`,
|
||||
required,
|
||||
});
|
||||
|
||||
const variablesProperty = (displayName: string, description: string, required = false) =>
|
||||
Property.Object({
|
||||
displayName,
|
||||
description,
|
||||
required,
|
||||
defaultValue: {
|
||||
customer_name: 'John Doe',
|
||||
},
|
||||
});
|
||||
|
||||
// Action Properties
|
||||
export const addLead = () => ({
|
||||
campaign: campaignDropdown(),
|
||||
phone_number: phoneNumberProperty('Customer Phone Number', 'Enter the phone number of the customer'),
|
||||
variables: variablesProperty('Variables', 'Variables to pass to the assistant'),
|
||||
allow_dupplicate: Property.Checkbox({
|
||||
displayName: 'Allow Duplicates',
|
||||
description: 'Allow the same phone number to be added to the campaign more than once',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
num_secondary_contacts: Property.Number({
|
||||
displayName: 'Number of Secondary Contacts',
|
||||
description: 'How many secondary contacts do you want to add? (Max: 10)',
|
||||
required: false,
|
||||
defaultValue: 0,
|
||||
}),
|
||||
secondary_contacts: Property.DynamicProperties({
|
||||
displayName: 'Secondary Contacts',
|
||||
description: 'Add secondary contacts for this lead. Each contact can have its own phone number and variables.',
|
||||
required: false,
|
||||
refreshers: ['num_secondary_contacts'],
|
||||
auth: famulorAuth,
|
||||
props: async ({ num_secondary_contacts }) => {
|
||||
const contacts: any = {};
|
||||
const numContacts = Math.min(Number(num_secondary_contacts) || 0, 10);
|
||||
|
||||
for (let i = 1; i <= numContacts; i++) {
|
||||
contacts[`contact_${i}_phone`] = phoneNumberProperty(
|
||||
`Contact ${i} - Phone Number`,
|
||||
`Phone number for secondary contact ${i}`
|
||||
);
|
||||
|
||||
contacts[`contact_${i}_variables`] = variablesProperty(
|
||||
`Contact ${i} - Variables`,
|
||||
`Variables for secondary contact ${i} as key-value pairs`,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return contacts;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const phoneNumberDropdown = () =>
|
||||
Property.Dropdown({
|
||||
auth: famulorAuth,
|
||||
displayName: 'From Phone Number',
|
||||
description: 'Select an SMS-capable phone number to send from',
|
||||
required: true,
|
||||
refreshers: ['auth'],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please authenticate first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const phoneNumbers = await famulorCommon.listPhoneNumbers({ auth: auth.secret_text });
|
||||
|
||||
if (!phoneNumbers || phoneNumbers.length === 0) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'No phone numbers found. Purchase an SMS-capable phone number first.',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
options: phoneNumbers.map((phoneNumber: any) => ({
|
||||
label: `${phoneNumber.phone_number} (${phoneNumber.country_code})`,
|
||||
value: phoneNumber.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to fetch phone numbers',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const sendSms = () => ({
|
||||
from: phoneNumberDropdown(),
|
||||
to: phoneNumberProperty('Recipient Phone Number', 'Enter the recipient\'s phone number'),
|
||||
bodysuit: Property.LongText({
|
||||
displayName: 'Message',
|
||||
description: 'SMS message content (max 300 characters). Long messages may be split into multiple segments.',
|
||||
required: true,
|
||||
}),
|
||||
});
|
||||
|
||||
const assistantDropdown = () =>
|
||||
Property.Dropdown({
|
||||
auth: famulorAuth,
|
||||
displayName: 'Assistant',
|
||||
description: 'Select the AI assistant to use for the call',
|
||||
required: true,
|
||||
refreshers: ['auth'],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please authenticate first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const assistants = await famulorCommon.listAssistants({ auth: auth.secret_text });
|
||||
|
||||
if (!assistants || assistants.length === 0) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'No outbound assistants found. Create one first.',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
options: assistants.map((assistant: any) => ({
|
||||
label: assistant.name,
|
||||
value: assistant.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to fetch assistants',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const makePhoneCall = () => ({
|
||||
assistant_id: assistantDropdown(),
|
||||
phone_number: phoneNumberProperty('Customer Phone Number', 'Enter the phone number to call'),
|
||||
variable: Property.Object({
|
||||
displayName: 'Variables',
|
||||
description: 'Variables to pass to the assistant during the call',
|
||||
required: false,
|
||||
defaultValue: {
|
||||
customer_name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const campaignControl = () => ({
|
||||
campaign: campaignDropdown(),
|
||||
action: Property.StaticDropdown({
|
||||
displayName: 'Action',
|
||||
description: 'Select the action to perform on the campaign',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Start Campaign', value: 'start' },
|
||||
{ label: 'Stop Campaign', value: 'stop' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const leadDropdown = () =>
|
||||
Property.Dropdown<number,true,typeof famulorAuth>({
|
||||
auth: famulorAuth,
|
||||
displayName: 'Lead',
|
||||
description: 'Select the lead to delete',
|
||||
required: true,
|
||||
refreshers: ['auth'],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please authenticate first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const leads = await famulorCommon.listLeads({ auth: auth.secret_text });
|
||||
|
||||
if (!leads || leads.length === 0) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'No leads found.',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
options: leads.map((lead: any) => ({
|
||||
label: `${lead.phone_number} - ${lead.campaign?.name || 'Unknown Campaign'}`,
|
||||
value: lead.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to fetch leads',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteLead = () => ({
|
||||
lead_id: leadDropdown(),
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import z from 'zod';
|
||||
|
||||
export const addLead = {
|
||||
campaign_id: z.number().int().positive(),
|
||||
phone_number: z.string().regex(/^\+[1-9]\d{1,14}$/, 'Phone number must be in E.164 format (e.g. +1234567890)'),
|
||||
variable: z.record(z.string(), z.any()).optional(),
|
||||
allow_dupplicate: z.boolean().optional(),
|
||||
secondary_contacts: z.array(z.object({
|
||||
phone_number: z.string().regex(/^\+[1-9]\d{1,14}$/, 'Phone number must be in E.164 format'),
|
||||
variables: z.record(z.string(), z.any()).optional(),
|
||||
})).optional(),
|
||||
};
|
||||
|
||||
export const sendSms = {
|
||||
from: z.number().int().positive('Phone number ID is required'),
|
||||
to: z.string().regex(/^\+[1-9]\d{1,14}$/, 'Phone number must be in E.164 format (e.g. +1234567890)'),
|
||||
bodysuit: z.string().max(300, 'Message must be 300 characters or less'),
|
||||
};
|
||||
|
||||
export const makePhoneCall = {
|
||||
assistant_id: z.number().int().positive(),
|
||||
phone_number: z.string().regex(/^\+[1-9]\d{1,14}$/, 'Phone number must be in E.164 format (e.g. +1234567890)'),
|
||||
variable: z.object({
|
||||
customer_name: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
}).catchall(z.any()).optional(),
|
||||
};
|
||||
|
||||
export const campaignControl = {
|
||||
campaign_id: z.number().int().positive(),
|
||||
action: z.enum(['start', 'stop']),
|
||||
};
|
||||
|
||||
export const deleteLead = {
|
||||
lead_id: z.number().int().positive('Lead ID must be a positive integer'),
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
export interface Campaign {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
max_calls_in_parallel: number;
|
||||
mark_complete_when_no_leads: boolean;
|
||||
allowed_hours_start_time: string;
|
||||
allowed_hours_end_time: string;
|
||||
allowed_days: string[];
|
||||
max_retries: number;
|
||||
retry_interval: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ListCampaignsResponse {
|
||||
campaigns: Campaign[];
|
||||
}
|
||||
|
||||
export interface AddLeadParams {
|
||||
auth: string;
|
||||
campaign_id: number;
|
||||
phone_number: string;
|
||||
variable?: Record<string, any>;
|
||||
allow_dupplicate?: boolean;
|
||||
secondary_contacts?: Array<{
|
||||
phone_number: string;
|
||||
variables?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SendSmsParams {
|
||||
auth: string;
|
||||
from: number;
|
||||
to: string;
|
||||
bodysuit: string;
|
||||
}
|
||||
|
||||
export interface MakePhoneCallParams {
|
||||
auth: string;
|
||||
assistant_id: number;
|
||||
phone_number: string;
|
||||
variable?: {
|
||||
customer_name?: string;
|
||||
email?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CampaignControlParams {
|
||||
auth: string;
|
||||
campaign_id: number;
|
||||
action: 'start' | 'stop';
|
||||
}
|
||||
|
||||
export interface DeleteLeadParams {
|
||||
auth: string;
|
||||
lead_id: number;
|
||||
}
|
||||
|
||||
export interface LeadResponse {
|
||||
message: string;
|
||||
data: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { createTrigger, TriggerStrategy, PiecePropValueSchema, Property } from '@activepieces/pieces-framework';
|
||||
import { DedupeStrategy, Polling, pollingHelper } from '@activepieces/pieces-common';
|
||||
import { famulorAuth } from '../..';
|
||||
import { famulorCommon } from '../common';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const polling: Polling<PiecePropValueSchema<any>, { type?: string; per_page?: number }> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ auth, propsValue }) => {
|
||||
const perPage = propsValue['per_page'] || 10;
|
||||
const type = propsValue['type'];
|
||||
|
||||
// Get all assistants with pagination
|
||||
let allAssistants: any[] = [];
|
||||
let currentPage = 1;
|
||||
let hasMorePages = true;
|
||||
|
||||
while (hasMorePages) {
|
||||
const assistants = await famulorCommon.listAllAssistants({
|
||||
auth: auth as string,
|
||||
per_page: perPage,
|
||||
page: currentPage,
|
||||
type
|
||||
});
|
||||
|
||||
if (assistants.data && assistants.data.length > 0) {
|
||||
allAssistants = allAssistants.concat(assistants.data);
|
||||
hasMorePages = currentPage < assistants.last_page;
|
||||
currentPage++;
|
||||
} else {
|
||||
hasMorePages = false;
|
||||
}
|
||||
}
|
||||
|
||||
return allAssistants.map((assistant) => {
|
||||
const assistantDate = assistant.updated_at
|
||||
? dayjs(assistant.updated_at)
|
||||
: dayjs(assistant.created_at);
|
||||
return {
|
||||
epochMilliSeconds: assistantDate.valueOf(),
|
||||
data: assistant,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const getAssistants = createTrigger({
|
||||
auth: famulorAuth,
|
||||
name: 'getAssistants',
|
||||
displayName: 'New or Updated Assistant',
|
||||
description: 'Triggers when AI assistants are created or updated in your Famulor account.',
|
||||
props: {
|
||||
type: Property.StaticDropdown({
|
||||
displayName: 'Assistant Type',
|
||||
description: 'Filter assistants by type',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'All Types', value: '' },
|
||||
{ label: 'Inbound', value: 'inbound' },
|
||||
{ label: 'Outbound', value: 'outbound' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
per_page: Property.Number({
|
||||
displayName: 'Items Per Page',
|
||||
description: 'Number of assistants to fetch per page (1-100, default: 10)',
|
||||
required: false,
|
||||
defaultValue: 10,
|
||||
}),
|
||||
},
|
||||
sampleData: {
|
||||
id: 123,
|
||||
user_id: 456,
|
||||
name: "Customer Support Assistant",
|
||||
type: "inbound",
|
||||
status: "active",
|
||||
phone_number_id: 789,
|
||||
voice_id: 101,
|
||||
language_id: 1,
|
||||
language: "en-US",
|
||||
timezone: "UTC",
|
||||
initial_message: "Hello! How can I help you today?",
|
||||
system_prompt: "You are a helpful customer support assistant.",
|
||||
max_duration: 1800,
|
||||
record: true,
|
||||
created_at: "2024-01-15T10:30:00Z",
|
||||
updated_at: "2024-01-15T14:20:00Z",
|
||||
variable: {
|
||||
company_name: "ACME Corp",
|
||||
support_email: "support@acme.com"
|
||||
},
|
||||
is_webhook_active: true,
|
||||
webhook_url: "https://api.example.com/webhook"
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
async onEnable(context) {
|
||||
const { store, auth, propsValue } = context;
|
||||
await pollingHelper.onEnable(polling, { store, auth, propsValue });
|
||||
},
|
||||
async onDisable(context) {
|
||||
const { store, auth, propsValue } = context;
|
||||
await pollingHelper.onDisable(polling, { store, auth, propsValue });
|
||||
},
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { createTrigger, Property, TriggerStrategy } from '@activepieces/pieces-framework';
|
||||
import { famulorAuth } from '../..';
|
||||
import { famulorCommon } from '../common';
|
||||
|
||||
const inboundAssistantDropdown = () =>
|
||||
Property.Dropdown({
|
||||
auth: famulorAuth,
|
||||
displayName: 'Inbound Assistant',
|
||||
description: 'Select an inbound assistant to receive webhook notifications for',
|
||||
required: true,
|
||||
refreshers: ['auth'],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please authenticate first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Filter for inbound assistants only
|
||||
const assistants = await famulorCommon.listAllAssistants({
|
||||
auth: auth.secret_text,
|
||||
type: 'inbound',
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
if (!assistants.data || assistants.data.length === 0) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'No inbound assistants found. Create one first.',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
options: assistants.data.map((assistant: any) => ({
|
||||
label: `${assistant.name} (${assistant.status})`,
|
||||
value: assistant.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to fetch assistants',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const inboundCall = createTrigger({
|
||||
auth: famulorAuth,
|
||||
name: 'inboundCall',
|
||||
displayName: 'Inbound Call Received',
|
||||
description: 'Triggers when an inbound call is received by your AI assistant. Webhook must be enabled for the selected assistant.',
|
||||
props: {
|
||||
assistant_id: inboundAssistantDropdown(),
|
||||
},
|
||||
sampleData: {
|
||||
assistant_id: 123,
|
||||
customer_phone: '+16380991171',
|
||||
assistant_phone: '+16380991171',
|
||||
call_id: "call_abc123",
|
||||
timestamp: "2024-01-15T10:30:00Z",
|
||||
status: "incoming",
|
||||
variables: {
|
||||
customer_name: "John Doe",
|
||||
caller_id: "+16380991171"
|
||||
}
|
||||
},
|
||||
type: TriggerStrategy.WEBHOOK,
|
||||
async onEnable(context) {
|
||||
await famulorCommon.enableInboundWebhook({
|
||||
auth: context.auth.secret_text,
|
||||
assistant_id: context.propsValue.assistant_id as number,
|
||||
webhook_url: context.webhookUrl,
|
||||
});
|
||||
},
|
||||
async onDisable(context) {
|
||||
await famulorCommon.disableInboundWebhook({
|
||||
auth: context.auth.secret_text,
|
||||
assistant_id: context.propsValue.assistant_id as number,
|
||||
});
|
||||
},
|
||||
async run(context) {
|
||||
return [context.payload.body];
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
import { createTrigger, Property, TriggerStrategy } from '@activepieces/pieces-framework';
|
||||
import { famulorAuth } from '../..';
|
||||
import { famulorCommon } from '../common';
|
||||
|
||||
const assistantDropdownForWebhook = () =>
|
||||
Property.Dropdown({
|
||||
auth: famulorAuth,
|
||||
displayName: 'Assistant',
|
||||
description: 'Select an assistant to receive post-call webhook notifications for',
|
||||
required: true,
|
||||
refreshers: ['auth'],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please authenticate first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all assistants (both inbound and outbound can make calls)
|
||||
const assistants = await famulorCommon.listAllAssistants({
|
||||
auth: auth.secret_text,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
if (!assistants.data || assistants.data.length === 0) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'No assistants found. Create one first.',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
options: assistants.data.map((assistant: any) => ({
|
||||
label: `${assistant.name} (${assistant.type} - ${assistant.status})`,
|
||||
value: assistant.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Failed to fetch assistants',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const phoneCallEnded = createTrigger({
|
||||
auth: famulorAuth,
|
||||
name: 'phoneCallEnded',
|
||||
displayName: 'Phone Call Completed',
|
||||
description: 'Triggers when a phone call is completed, providing full call transcript, extracted variables, and call metadata.',
|
||||
props: {
|
||||
assistant_id: assistantDropdownForWebhook(),
|
||||
},
|
||||
sampleData: {
|
||||
id: 480336,
|
||||
customer_phone: "+4915123456789",
|
||||
assistant_phone: "+4912345678",
|
||||
duration: 180,
|
||||
status: "completed",
|
||||
extracted_variables: {
|
||||
customer_interested: true,
|
||||
appointment_scheduled: false,
|
||||
contact_reason: "product_inquiry",
|
||||
follow_up_needed: true,
|
||||
customer_budget: "10000-50000",
|
||||
decision_maker: true,
|
||||
next_contact_date: "2024-02-15"
|
||||
},
|
||||
input_variables: {
|
||||
customer_name: "Max Mustermann",
|
||||
company: "Beispiel GmbH"
|
||||
},
|
||||
transcript: "Assistent: Guten Tag, Herr Mustermann! Ich bin...",
|
||||
recording_url: "https://recordings.famulor.de/call-480336.mp3",
|
||||
created_at: "2024-01-15T10:30:00Z",
|
||||
finished_at: "2024-01-15T10:33:00Z",
|
||||
lead: {
|
||||
id: 12345,
|
||||
phone_number: "+4915123456789",
|
||||
variables: {
|
||||
customer_name: "Max Mustermann",
|
||||
company: "Beispiel GmbH",
|
||||
source: "website"
|
||||
},
|
||||
status: "contacted",
|
||||
created_at: "2024-01-14T09:00:00Z",
|
||||
updated_at: "2024-01-15T10:33:00Z"
|
||||
},
|
||||
campaign: {
|
||||
id: 123,
|
||||
name: "Q1 Sales Campaign"
|
||||
}
|
||||
},
|
||||
type: TriggerStrategy.WEBHOOK,
|
||||
async onEnable(context) {
|
||||
await famulorCommon.enablePostCallWebhook({
|
||||
auth: context.auth.secret_text,
|
||||
assistant_id: context.propsValue.assistant_id as number,
|
||||
webhook_url: context.webhookUrl,
|
||||
});
|
||||
},
|
||||
async onDisable(context) {
|
||||
await famulorCommon.disablePostCallWebhook({
|
||||
auth: context.auth.secret_text,
|
||||
assistant_id: context.propsValue.assistant_id as number,
|
||||
});
|
||||
},
|
||||
async run(context) {
|
||||
return [context.payload.body];
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user