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,27 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { vboutAuth } from '../..';
import { makeClient, vboutCommon } from '../common';
export const addContactAction = createAction({
auth: vboutAuth,
name: 'vbout_add_contact',
displayName: 'Add Contact to List',
description: 'Adds a contact to a given email list.',
props: {
email: Property.ShortText({
displayName: 'Email Address',
required: true,
}),
listid: vboutCommon.listid(true),
status: vboutCommon.contactStatus(true),
ipaddress: Property.ShortText({
displayName: 'IP Address',
required: false,
}),
fields: vboutCommon.listFields,
},
async run(context) {
const client = makeClient(context.auth.secret_text);
return await client.addContact(context.propsValue);
},
});

View File

@@ -0,0 +1,28 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { vboutAuth } from '../..';
import { makeClient } from '../common';
export const addTagToContactAction = createAction({
auth: vboutAuth,
name: 'vbout_add_tag',
displayName: 'Add Tag to Contact',
description: 'Adds the tag to the contact.',
props: {
email: Property.ShortText({
displayName: 'Email Address',
required: true,
}),
tagname: Property.Array({
displayName: 'Tag Name',
required: true,
}),
},
async run(context) {
const { email, tagname } = context.propsValue;
const client = makeClient(context.auth.secret_text);
return await client.addTagToContact({
email: email,
tagname: tagname as string[],
});
},
});

View File

@@ -0,0 +1,95 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { vboutAuth } from '../..';
import { makeClient } from '../common';
export const createEmailMarketingCampaignAction = createAction({
auth: vboutAuth,
name: 'vbout_add_email_marketing_campaign',
displayName: 'Create Email Marketing Campaign',
description: 'Creates a new email campaign for specific list.',
props: {
lists: Property.MultiSelectDropdown({
auth: vboutAuth,
displayName: 'Campaign Recipients List',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
const client = makeClient(auth.secret_text);
const res = await client.listEmailLists();
return {
disabled: false,
options: res.lists.items.map((list) => {
return {
label: list.name,
value: list.id,
};
}),
};
},
}),
name: Property.ShortText({
displayName: 'Campaign Name',
required: true,
}),
subject: Property.ShortText({
displayName: 'Campaign Subject',
required: true,
}),
type: Property.StaticDropdown({
displayName: 'Campaign Type',
required: true,
defaultValue: 'standard',
options: {
disabled: false,
options: [
{
label: 'Standard',
value: 'standard',
},
{
label: 'Automated',
value: 'automated',
},
],
},
}),
fromemail: Property.ShortText({
displayName: 'From Email',
required: true,
}),
from_name: Property.ShortText({
displayName: 'From Name',
required: true,
}),
reply_to: Property.ShortText({
displayName: 'Reply To',
required: true,
}),
body: Property.LongText({
displayName: 'Message Body',
required: true,
}),
},
async run(context) {
const { lists, name, from_name, fromemail, reply_to, subject, body, type } =
context.propsValue;
const client = makeClient(context.auth.secret_text);
return await client.addCampaign({
lists: lists.join(','),
name,
from_name,
fromemail,
reply_to,
subject,
body,
type,
});
},
});

View File

@@ -0,0 +1,71 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { vboutAuth } from '../..';
import { makeClient } from '../common';
export const createEmailListAction = createAction({
auth: vboutAuth,
name: 'vbout_create_email_list',
displayName: 'Create Email List',
description: 'Creates a new email list.',
props: {
name: Property.ShortText({
required: true,
displayName: 'Name',
description: 'The name of the list.',
}),
email_subject: Property.LongText({
required: false,
displayName: 'Email Subject',
description: 'The default subscription subject.',
}),
reply_to: Property.ShortText({
required: false,
displayName: 'Reply To',
description: 'The Reply to email of the list.',
}),
fromemail: Property.ShortText({
required: false,
displayName: 'From Email',
description: 'The From email of the list.',
}),
from_name: Property.ShortText({
required: false,
displayName: 'From Name',
description: 'The From name of the list.',
}),
notify_email: Property.ShortText({
required: false,
displayName: 'Notify Email',
description: 'Notification Email.',
}),
success_email: Property.ShortText({
required: false,
displayName: 'Success Email',
description: 'Subscription Success Email.',
}),
success_message: Property.ShortText({
required: false,
displayName: 'Success Message',
description: 'Subscription Success Message.',
}),
error_message: Property.ShortText({
required: false,
displayName: 'Error Message',
description: 'Subscription Error Message.',
}),
confirmation_email: Property.ShortText({
required: false,
displayName: 'Confirmation Email',
description: 'Confirmation Email Message.',
}),
confirmation_message: Property.ShortText({
required: false,
displayName: 'Confirmation Message',
}),
},
async run({ auth, propsValue }) {
const client = makeClient(auth.secret_text);
return await client.createEmailList(propsValue);
},
});

View File

@@ -0,0 +1,21 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { vboutAuth } from '../../';
import { makeClient, vboutCommon } from '../common';
export const createSocialMediaMessageAction = createAction({
auth: vboutAuth,
name: 'vbout_create_social_media_message',
displayName: 'Create Social Media Message',
description: 'Post a message to one of your social media channel.',
props: {
message: Property.LongText({
displayName: 'Message',
required: true,
}),
channel: vboutCommon.socialMediaChannel,
channelid: vboutCommon.socialMediaProfile,
},
async run(context) {
const client = makeClient(context.auth.secret_text);
return await client.createSocialMediaPost(context.propsValue);
},
});

View File

@@ -0,0 +1,23 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { vboutAuth } from '../..';
import { makeClient, vboutCommon } from '../common';
export const getContactByEmailAction = createAction({
auth: vboutAuth,
name: 'vbout_get_contact_by_email',
displayName: 'Get Contact by Email',
description: 'Retrieves the contact by email.',
props: {
listid: vboutCommon.listid(false),
email: Property.ShortText({
displayName: 'Email',
required: true,
}),
},
async run({ auth, propsValue }) {
const client = makeClient(auth.secret_text);
const { listid, email } = propsValue;
return await client.getContactByEmail(email, listid);
},
});

View File

@@ -0,0 +1,19 @@
import { createAction } from '@activepieces/pieces-framework';
import { vboutAuth } from '../..';
import { makeClient, vboutCommon } from '../common';
export const getEmailListAction = createAction({
auth: vboutAuth,
name: 'vbout_get_email_list',
displayName: 'Get List Details with Custom Fields',
description: 'Retrieves specific list details with custom fields.',
props: {
listid: vboutCommon.listid(true),
},
async run({ auth, propsValue }) {
const client = makeClient(auth.secret_text);
const listId = propsValue.listid!;
return await client.getEmailList(listId);
},
});

View File

@@ -0,0 +1,26 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { vboutAuth } from '../..';
import { makeClient } from '../common';
export const removeTagFromContactAction = createAction({
auth: vboutAuth,
name: 'vbout_remove_tag',
displayName: 'Remove Tags from Contact',
description: 'Removes tags from an existing contact.',
props: {
email: Property.ShortText({
displayName: 'Email Address',
required: true,
}),
tagname: Property.ShortText({
displayName: 'Tag Name',
required: true,
description: `use comma for multiple tag e.g. **tag1,tag2**.`,
}),
},
async run(context) {
const { email, tagname } = context.propsValue;
const client = makeClient(context.auth.secret_text);
return await client.removeTagFromContact(email, tagname);
},
});

View File

@@ -0,0 +1,38 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { vboutAuth } from '../..';
import { makeClient, vboutCommon } from '../common';
import { ContactStatusValues } from '../common/models';
export const unsubscribeContactAction = createAction({
auth: vboutAuth,
name: 'vbout_unsubscribe_contact',
displayName: 'Unsubscribe Contact',
description: 'Unsubscribes an existing contact in a given email list.',
props: {
email: Property.ShortText({
displayName: 'Contact Email',
required: true,
description: 'Contact email for update.',
}),
listid: vboutCommon.listid(true),
},
async run(context) {
const client = makeClient(context.auth.secret_text);
const { email, listid } = context.propsValue;
const res = await client.getContactByEmail(
email as string,
listid as string
);
const contact = res.response.data.contact;
if ('errorCode' in contact) {
return res;
} else {
const contactId = contact[0].id;
return await client.updateContact({
id: contactId,
status: ContactStatusValues.UNSUBSCRIBE,
});
}
},
});

View File

@@ -0,0 +1,40 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { vboutAuth } from '../..';
import { makeClient, vboutCommon } from '../common';
export const updateContactAction = createAction({
auth: vboutAuth,
name: 'vbout_update_contact',
displayName: 'Update Contact',
description: 'Updates a contact in a given email list.',
props: {
email: Property.ShortText({
displayName: 'Contact Email',
required: true,
description: 'Contact email for update.',
}),
listid: vboutCommon.listid(true),
status: vboutCommon.contactStatus(false),
ipaddress: Property.ShortText({
displayName: 'IP Address',
required: false,
}),
fields: vboutCommon.listFields,
},
async run(context) {
const client = makeClient(context.auth.secret_text);
const { email } = context.propsValue;
const res = await client.getContactByEmail(email as string);
const contact = res.response.data.contact;
if ('errorCode' in contact) {
return res;
} else {
const contactId = contact[0].id;
return await client.updateContact({
id: contactId,
...context.propsValue,
});
}
},
});

View File

@@ -0,0 +1,170 @@
import {
HttpMessageBody,
HttpMethod,
QueryParams,
httpClient,
} from '@activepieces/pieces-common';
import { vboutCommon } from '.';
import {
CampaignCreateRequest,
ContactCreateRequest,
ContactList,
ContactUpdateRequest,
EmailListCreateRequest,
SocialMediaChannelListResponse,
SocialMediaPostCreateRequest,
TagCreateRequest,
VboutResponseBody,
} from './models';
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 prepareQuery(request?: Record<string, any>): QueryParams {
const params: QueryParams = {};
if (!request) return params;
Object.keys(request)
.filter(emptyValueFilter((k) => request[k]))
.forEach((k: string) => {
params[k] = (request as Record<string, any>)[k].toString();
});
return params;
}
export class VboutClient {
constructor(private apiKey: string) {}
async makeRequest<T extends HttpMessageBody>(
method: HttpMethod,
url: string,
query?: QueryParams,
body?: object
): Promise<T> {
const res = await httpClient.sendRequest<T>({
method,
url: vboutCommon.baseUrl + url,
queryParams: { key: this.apiKey, ...query },
body,
});
return res.body;
}
async validateAuth() {
return await this.makeRequest(HttpMethod.GET, '/app/me');
}
async listEmailLists() {
return (
await this.makeRequest<
VboutResponseBody<{
lists: {
count: number;
items: ContactList[];
};
}>
>(HttpMethod.GET, '/emailmarketing/getlists')
).response.data;
}
async getContactByEmail(email: string, listId?: string) {
return await this.makeRequest<
VboutResponseBody<{
contact: {
id: string;
email: string;
listid: string;
list_name: string;
[key: string]: any;
}[];
}>
>(
HttpMethod.GET,
'/emailmarketing/getcontactbyemail',
prepareQuery({ email: email, listid: listId })
);
}
async getEmailList(listId: string) {
return await this.makeRequest<VboutResponseBody<{ list: ContactList }>>(
HttpMethod.GET,
'/emailmarketing/getlist',
prepareQuery({ id: listId })
);
}
async createEmailList(request: EmailListCreateRequest) {
return await this.makeRequest(
HttpMethod.POST,
'/emailMarketing/AddList',
undefined,
request
);
}
async addContact(request: ContactCreateRequest) {
return await this.makeRequest(
HttpMethod.POST,
'/emailMarketing/AddContact',
undefined,
request
);
}
async updateContact(request: ContactUpdateRequest) {
return await this.makeRequest(
HttpMethod.POST,
'/emailMarketing/EditContact',
undefined,
request
);
}
async addTagToContact(request: TagCreateRequest) {
return await this.makeRequest(
HttpMethod.POST,
'/emailMarketing/AddTag',
undefined,
request
);
}
async removeTagFromContact(email: string, tagname: string) {
return await this.makeRequest(
HttpMethod.DELETE,
'/emailMarketing/RemoveTag',
prepareQuery({
email,
tagname,
})
);
}
async addCampaign(request: CampaignCreateRequest) {
return await this.makeRequest(
HttpMethod.POST,
'/emailMarketing/AddCampaign',
undefined,
request
);
}
async listSocialMediaChannels() {
return (
await this.makeRequest<
VboutResponseBody<{ channels: SocialMediaChannelListResponse }>
>(HttpMethod.GET, '/socialMedia/Channels')
).response.data;
}
async createSocialMediaPost(request: SocialMediaPostCreateRequest) {
return await this.makeRequest(
HttpMethod.POST,
'/socialMedia/AddPost',
undefined,
request
);
}
}

View File

@@ -0,0 +1,175 @@
import { DynamicPropsValue, Property } from '@activepieces/pieces-framework';
import { VboutClient } from './client';
import {
ContactStatusValues,
SocialMediaChannelValues,
SocialMediaProfile,
} from './models';
import { vboutAuth } from '../..';
export function makeClient(apiKey: string): VboutClient {
return new VboutClient(apiKey);
}
export const vboutCommon = {
baseUrl: 'https://api.vbout.com/1',
listid: (required = true) =>
Property.Dropdown({
auth: vboutAuth,
displayName: 'List ID',
required: required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
const client = makeClient(auth.secret_text);
const res = await client.listEmailLists();
return {
disabled: false,
options: res.lists.items.map((list) => {
return {
label: list.name,
value: list.id,
};
}),
};
},
}),
listFields: Property.DynamicProperties({
auth: vboutAuth,
displayName: 'Fields',
required: true,
refreshers: ['listid'],
props: async ({ auth, listid }) => {
if (!auth || !listid) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account and select Email List.',
};
}
const fields: DynamicPropsValue = {};
const client = makeClient(auth.secret_text);
const contactList = await client.getEmailList(
listid as unknown as string
);
const contactListFields = contactList.response.data.list.fields;
Object.keys(contactListFields).forEach((key) => {
fields[key] = Property.ShortText({
displayName: contactListFields[key],
required: false,
});
});
return fields;
},
}),
contactStatus: (required = true) =>
Property.StaticDropdown({
displayName: 'Contact Status',
required: required,
options: {
disabled: false,
options: [
{
label: 'Unconfirmed',
value: ContactStatusValues.UNCONFIRMED,
},
{
label: 'Active',
value: ContactStatusValues.ACTIVE,
},
{
label: 'Unsubscribe',
value: ContactStatusValues.UNSUBSCRIBE,
},
{
label: 'Bounced Email',
value: ContactStatusValues.BOUNCED_EMAIL,
},
],
},
}),
socialMediaChannel: Property.StaticDropdown({
displayName: 'Social Media Channel',
required: true,
options: {
disabled: false,
options: [
{
label: 'Twitter',
value: SocialMediaChannelValues.TWITTER,
},
{
label: 'LinkedIn',
value: SocialMediaChannelValues.LINKEDIN,
},
{
label: 'Facebook',
value: SocialMediaChannelValues.FACEBOOK,
},
],
},
}),
socialMediaProfile: Property.Dropdown({
auth: vboutAuth,
displayName: 'Social Media Account',
required: true,
refreshers: ['channel'],
options: async ({ auth, channel }) => {
if (!auth || !channel) {
return {
disabled: true,
options: [],
placeholder:
'Please connect your account and select social media channel.',
};
}
const client = makeClient(auth.secret_text);
const { channels } = await client.listSocialMediaChannels();
let options: { label: string; value: string }[] = [];
switch (channel as string) {
case SocialMediaChannelValues.TWITTER: {
options = [
...options,
...mapSocialMediaProfile(channels.Twitter.profiles),
];
break;
}
case SocialMediaChannelValues.FACEBOOK: {
options = [
...options,
...mapSocialMediaProfile(channels.Facebook.pages),
];
break;
}
case SocialMediaChannelValues.LINKEDIN: {
options = [
...options,
...mapSocialMediaProfile(channels.Linkedin.companies),
...mapSocialMediaProfile(channels.Linkedin.profiles),
];
break;
}
}
return {
disabled: false,
options: options,
};
},
}),
};
function mapSocialMediaProfile(
profiles: SocialMediaProfile[]
): { label: string; value: string }[] {
return profiles.map((profile) => {
return {
label: profile.name,
value: profile.id,
};
});
}

View File

@@ -0,0 +1,122 @@
import { HttpMessageBody } from '@activepieces/pieces-common';
export interface VboutResponseBody<T> extends HttpMessageBody {
response: {
header: {
status: string;
dataType: string;
limit: string;
cached?: string;
};
data: T;
};
}
export interface EmailListCreateRequest {
name: string;
email_subject?: string;
reply_to?: string;
fromemail?: string;
from_name?: string;
doubleOption?: string;
notify?: string;
notify_email?: string;
success_email?: string;
success_message?: string;
error_message?: string;
confirmation_email?: string;
confirmation_message?: string;
communications?: boolean;
}
export interface ContactList {
id: string;
name: string;
form_title: string;
email_subject: string;
reply_to: string;
from_email: string;
from_name: string;
confirmation_email: string;
success_email: string;
confirmation_message: string;
success_message: string;
error_message: string;
doubleOption: string;
notify_email: string;
creation_date: string;
fields: {
[key: string]: string;
};
}
export interface ContactCreateRequest {
listid?: string;
status?: string;
email: string;
ipaddress?: string;
fields?: {
[key: string]: string;
};
}
export interface ContactUpdateRequest {
id: string;
listid?: string;
status?: string;
email?: string;
ipaddress?: string;
fields?: {
[key: string]: string;
};
}
export interface TagCreateRequest {
email: string;
tagname: string[];
}
export interface CampaignCreateRequest {
name: string;
subject: string;
fromemail: string;
from_name: string;
reply_to: string;
body: string;
type: string;
lists: string;
}
export interface SocialMediaProfile {
id: string;
name: string;
}
export interface SocialMediaChannelListResponse {
Facebook: {
count: number;
pages: SocialMediaProfile[];
};
Twitter: {
count: number;
profiles: SocialMediaProfile[];
};
Linkedin: {
count: number;
profiles: SocialMediaProfile[];
companies: SocialMediaProfile[];
};
}
export interface SocialMediaPostCreateRequest {
message: string;
channel: string;
channelid: string;
}
export enum SocialMediaChannelValues {
TWITTER = 'twitter',
LINKEDIN = 'linkedin',
FACEBOOK = 'facebook',
}
export enum ContactStatusValues {
UNCONFIRMED = '0',
ACTIVE = '1',
UNSUBSCRIBE = '2',
BOUNCED_EMAIL = '3',
}