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,92 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import {
|
||||
HttpRequest,
|
||||
HttpMethod,
|
||||
AuthenticationType,
|
||||
httpClient,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { googleContactsCommon } from '../common';
|
||||
import { googleContactsAuth } from '../../';
|
||||
|
||||
export const googleContactsAddContactAction = createAction({
|
||||
auth: googleContactsAuth,
|
||||
name: 'add_contact',
|
||||
description: 'Add a contact to a Google Contacts account',
|
||||
displayName: 'Add Contact',
|
||||
props: {
|
||||
firstName: Property.ShortText({
|
||||
displayName: 'First Name',
|
||||
description: 'The first name of the contact',
|
||||
required: true,
|
||||
}),
|
||||
middleName: Property.ShortText({
|
||||
displayName: 'Middle Name',
|
||||
description: 'The middle name of the contact',
|
||||
required: false,
|
||||
}),
|
||||
lastName: Property.ShortText({
|
||||
displayName: 'Last Name',
|
||||
description: 'The last name of the contact',
|
||||
required: true,
|
||||
}),
|
||||
jobTitle: Property.ShortText({
|
||||
displayName: 'Job Title',
|
||||
description: 'The job title of the contact',
|
||||
required: false,
|
||||
}),
|
||||
company: Property.ShortText({
|
||||
displayName: 'Company',
|
||||
description: 'The company of the contact',
|
||||
required: false,
|
||||
}),
|
||||
email: Property.ShortText({
|
||||
displayName: 'Email',
|
||||
description: 'The email address of the contact',
|
||||
required: false,
|
||||
}),
|
||||
phoneNumber: Property.ShortText({
|
||||
displayName: 'Phone Number',
|
||||
description: 'The phone number of the contact',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
let requestBody = {
|
||||
names: [
|
||||
{
|
||||
givenName: context.propsValue['firstName'],
|
||||
middleName: context.propsValue['middleName'],
|
||||
familyName: context.propsValue['lastName'],
|
||||
},
|
||||
],
|
||||
};
|
||||
const contact: Record<string, unknown> = {};
|
||||
if (context.propsValue['email']) {
|
||||
contact['emailAddresses'] = [{ value: context.propsValue['email'] }];
|
||||
}
|
||||
|
||||
if (context.propsValue['phoneNumber']) {
|
||||
contact['phoneNumbers'] = [{ value: context.propsValue['phoneNumber'] }];
|
||||
}
|
||||
|
||||
if (context.propsValue['company'] || context.propsValue['jobTitle']) {
|
||||
contact['organizations'] = [
|
||||
{
|
||||
name: context.propsValue['company'] || undefined,
|
||||
title: context.propsValue['jobTitle'] || undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
requestBody = { ...requestBody, ...contact };
|
||||
const request: HttpRequest<Record<string, unknown>> = {
|
||||
method: HttpMethod.POST,
|
||||
url: `${googleContactsCommon.baseUrl}:createContact`,
|
||||
body: requestBody,
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: context.auth.access_token,
|
||||
},
|
||||
};
|
||||
return (await httpClient.sendRequest(request)).body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import {
|
||||
HttpRequest,
|
||||
HttpMethod,
|
||||
AuthenticationType,
|
||||
httpClient,
|
||||
QueryParams,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { googleContactsCommon } from '../common';
|
||||
import { googleContactsAuth } from '../../';
|
||||
|
||||
export const googleContactsSearchContactsAction = createAction({
|
||||
auth: googleContactsAuth,
|
||||
name: 'search_contact',
|
||||
description: 'Search contacts in Google Contacts account.',
|
||||
displayName: 'Search Contacts',
|
||||
props: {
|
||||
query: Property.ShortText({
|
||||
displayName: 'Query',
|
||||
description: `The plain-text query for the request.The query is used to match prefix phrases of the fields on a person. For example, a person with name "foo name" matches queries such as "f", "fo", "foo", "foo n", "nam", etc., but not "oo n".`,
|
||||
required: true,
|
||||
}),
|
||||
readMask: Property.StaticMultiSelectDropdown({
|
||||
displayName: 'Read Mask',
|
||||
description:
|
||||
'A field mask to restrict which fields on each person are returned.',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'addresses', value: 'addresses' },
|
||||
{ label: 'ageRanges', value: 'ageRanges' },
|
||||
{ label: 'biographies', value: 'biographies' },
|
||||
{ label: 'birthdays', value: 'birthdays' },
|
||||
{ label: 'calendarUrls', value: 'calendarUrls' },
|
||||
{ label: 'clientData', value: 'clientData' },
|
||||
{ label: 'coverPhotos', value: 'coverPhotos' },
|
||||
{ label: 'emailAddresses', value: 'emailAddresses' },
|
||||
{ label: 'events', value: 'events' },
|
||||
{ label: 'externalIds', value: 'externalIds' },
|
||||
{ label: 'genders', value: 'genders' },
|
||||
{ label: 'imClients', value: 'imClients' },
|
||||
{ label: 'interests', value: 'interests' },
|
||||
{ label: 'locales', value: 'locales' },
|
||||
{ label: 'locations', value: 'locations' },
|
||||
{ label: 'memberships', value: 'memberships' },
|
||||
{ label: 'metadata', value: 'metadata' },
|
||||
{ label: 'miscKeywords', value: 'miscKeywords' },
|
||||
{ label: 'names', value: 'names' },
|
||||
{ label: 'nicknames', value: 'nicknames' },
|
||||
{ label: 'occupations', value: 'occupations' },
|
||||
{ label: 'organizations', value: 'organizations' },
|
||||
{ label: 'phoneNumbers', value: 'phoneNumbers' },
|
||||
{ label: 'photos', value: 'photos' },
|
||||
{ label: 'relations', value: 'relations' },
|
||||
{ label: 'sipAddresses', value: 'sipAddresses' },
|
||||
{ label: 'skills', value: 'skills' },
|
||||
{ label: 'urls', value: 'urls' },
|
||||
{ label: 'userDefined', value: 'userDefined' },
|
||||
],
|
||||
},
|
||||
defaultValue: ['names', 'emailAddresses'],
|
||||
}),
|
||||
pageSize: Property.Number({
|
||||
displayName: 'Page Size',
|
||||
description: 'The number of results to return. Maximum 30.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const qs: QueryParams = {
|
||||
query: context.propsValue['query'],
|
||||
readMask: context.propsValue['readMask'].join(','),
|
||||
};
|
||||
if (context.propsValue['pageSize']) {
|
||||
qs['pageSize'] = String(context.propsValue['pageSize']);
|
||||
}
|
||||
const request: HttpRequest<Record<string, unknown>> = {
|
||||
method: HttpMethod.GET,
|
||||
url: `${googleContactsCommon.baseUrl}:searchContacts`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: context.auth.access_token,
|
||||
},
|
||||
queryParams: qs,
|
||||
};
|
||||
return (await httpClient.sendRequest(request)).body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
createAction,
|
||||
Property,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
HttpRequest,
|
||||
HttpMethod,
|
||||
AuthenticationType,
|
||||
httpClient,
|
||||
QueryParams,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { googleContactsCommon } from '../common';
|
||||
import { googleContactsAuth } from '../../';
|
||||
|
||||
export const googleContactsUpdateContactAction = createAction({
|
||||
auth: googleContactsAuth,
|
||||
name: 'update_contact',
|
||||
description: 'Update a contact in Google Contacts account.',
|
||||
displayName: 'Update Contact',
|
||||
props: {
|
||||
resourceName: Property.ShortText({
|
||||
displayName: 'Resource Name',
|
||||
description:
|
||||
'The resource name for the person, assigned by the server. An ASCII string in the form of people/{person_id}.',
|
||||
required: true,
|
||||
}),
|
||||
etag: Property.ShortText({
|
||||
displayName: 'Etag',
|
||||
description:
|
||||
"The `etag` ensures contact updates only apply if the contact hasn't changed since last retrieved.",
|
||||
required: true,
|
||||
}),
|
||||
updatePersonFields: Property.StaticMultiSelectDropdown({
|
||||
displayName: 'Update Field Mask',
|
||||
description:
|
||||
'A field mask to restrict which fields on the person are updated.',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Names', value: 'names' },
|
||||
{ label: 'Email', value: 'emailAddresses' },
|
||||
{ label: 'Phone Number', value: 'phoneNumbers' },
|
||||
{ label: 'Job Title / Company', value: 'organizations' },
|
||||
],
|
||||
},
|
||||
defaultValue: ['names', 'emailAddresses'],
|
||||
}),
|
||||
firstName: Property.ShortText({
|
||||
displayName: 'First Name',
|
||||
description: 'The first name of the contact',
|
||||
required: false,
|
||||
}),
|
||||
middleName: Property.ShortText({
|
||||
displayName: 'Middle Name',
|
||||
description: 'The middle name of the contact',
|
||||
required: false,
|
||||
}),
|
||||
lastName: Property.ShortText({
|
||||
displayName: 'Last Name',
|
||||
description: 'The last name of the contact',
|
||||
required: false,
|
||||
}),
|
||||
jobTitle: Property.ShortText({
|
||||
displayName: 'Job Title',
|
||||
description: 'The job title of the contact',
|
||||
required: false,
|
||||
}),
|
||||
company: Property.ShortText({
|
||||
displayName: 'Company',
|
||||
description: 'The company of the contact',
|
||||
required: false,
|
||||
}),
|
||||
email: Property.ShortText({
|
||||
displayName: 'Email',
|
||||
description: 'The email address of the contact',
|
||||
required: false,
|
||||
}),
|
||||
phoneNumber: Property.ShortText({
|
||||
displayName: 'Phone Number',
|
||||
description: 'The phone number of the contact',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const resourceName = context.propsValue['resourceName'].substring(6);
|
||||
const requestBody: Record<string, unknown> = {
|
||||
etag: context.propsValue['etag'],
|
||||
};
|
||||
const qs: QueryParams = {
|
||||
updatePersonFields: context.propsValue['updatePersonFields'].join(','),
|
||||
};
|
||||
if (
|
||||
context.propsValue['firstName'] ||
|
||||
context.propsValue['middleName'] ||
|
||||
context.propsValue['lastName']
|
||||
) {
|
||||
requestBody['names'] = [
|
||||
{
|
||||
givenName: context.propsValue['firstName'] || undefined,
|
||||
middleName: context.propsValue['middleName'] || undefined,
|
||||
familyName: context.propsValue['lastName'] || undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (context.propsValue['email']) {
|
||||
requestBody['emailAddresses'] = [{ value: context.propsValue['email'] }];
|
||||
}
|
||||
if (context.propsValue['phoneNumber']) {
|
||||
requestBody['phoneNumbers'] = [
|
||||
{ value: context.propsValue['phoneNumber'] },
|
||||
];
|
||||
}
|
||||
if (context.propsValue['company'] || context.propsValue['jobTitle']) {
|
||||
requestBody['organizations'] = [
|
||||
{
|
||||
name: context.propsValue['company'] || undefined,
|
||||
title: context.propsValue['jobTitle'] || undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
const request: HttpRequest<Record<string, unknown>> = {
|
||||
method: HttpMethod.PATCH,
|
||||
url: `${googleContactsCommon.baseUrl}${resourceName}:updateContact`,
|
||||
body: requestBody,
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: context.auth.access_token,
|
||||
},
|
||||
queryParams: qs,
|
||||
};
|
||||
return (await httpClient.sendRequest(request)).body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export const googleContactsCommon = {
|
||||
baseUrl: `https://people.googleapis.com/v1/people`,
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
import {
|
||||
AppConnectionValueForAuthProperty,
|
||||
createTrigger,
|
||||
OAuth2PropertyValue,
|
||||
TriggerStrategy,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
Polling,
|
||||
DedupeStrategy,
|
||||
pollingHelper,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { googleContactsAuth } from '../../';
|
||||
import { google } from 'googleapis';
|
||||
import { OAuth2Client } from 'googleapis-common';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const polling: Polling<AppConnectionValueForAuthProperty<typeof googleContactsAuth>, Record<string, never>> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ store, auth }) => {
|
||||
const authClient = new OAuth2Client();
|
||||
authClient.setCredentials(auth);
|
||||
|
||||
const contactsClient = google.people({ version: 'v1', auth: authClient });
|
||||
|
||||
let nextPageToken;
|
||||
const contactItems: Array<{ data: any; epochMilliSeconds: number }> = [];
|
||||
|
||||
do {
|
||||
const response: any = await contactsClient.people.connections.list({
|
||||
resourceName: 'people/me',
|
||||
pageToken: nextPageToken,
|
||||
pageSize: 100,
|
||||
sortOrder: 'LAST_MODIFIED_DESCENDING',
|
||||
personFields: [
|
||||
'addresses',
|
||||
'ageRanges',
|
||||
'biographies',
|
||||
'birthdays',
|
||||
'calendarUrls',
|
||||
'clientData',
|
||||
'coverPhotos',
|
||||
'emailAddresses',
|
||||
'events',
|
||||
'externalIds',
|
||||
'genders',
|
||||
'imClients',
|
||||
'interests',
|
||||
'locales',
|
||||
'locations',
|
||||
'memberships',
|
||||
'metadata',
|
||||
'miscKeywords',
|
||||
'names',
|
||||
'nicknames',
|
||||
'occupations',
|
||||
'organizations',
|
||||
'phoneNumbers',
|
||||
'photos',
|
||||
'relations',
|
||||
'sipAddresses',
|
||||
'skills',
|
||||
'urls',
|
||||
'userDefined',
|
||||
].join(),
|
||||
});
|
||||
|
||||
for (const contact of response.data.connections || []) {
|
||||
if (contact.metadata?.deleted !== true) {
|
||||
contactItems.push({
|
||||
data: contact,
|
||||
epochMilliSeconds: dayjs(
|
||||
contact.metadata?.sources?.[0].updateTime
|
||||
).valueOf(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
nextPageToken = response.data.nextPageToken;
|
||||
} while (nextPageToken);
|
||||
|
||||
return contactItems;
|
||||
},
|
||||
};
|
||||
|
||||
export const googleContactNewOrUpdatedContact = createTrigger({
|
||||
auth: googleContactsAuth,
|
||||
name: 'new_or_updated_contact',
|
||||
displayName: 'New Or Updated Contact',
|
||||
description: 'Triggers when there is a new or updated contact',
|
||||
props: {},
|
||||
sampleData: {
|
||||
resourceName: 'people/c4278485694217203807',
|
||||
etag: '%EiMBAgMFBgcICQoLDA0ODxATFBUWGSEiIyQlJicuNDU3PT4/QBoEAQIFByIMZFVwNlJPNEVKUzg9',
|
||||
metadata: {
|
||||
sources: [
|
||||
{
|
||||
type: 'CONTACT',
|
||||
id: '3b603c120c68305f',
|
||||
etag: '#dUp6RO4EJS8=',
|
||||
updateTime: '2023-01-30T14:35:18.142565Z',
|
||||
},
|
||||
],
|
||||
objectType: 'PERSON',
|
||||
},
|
||||
names: [
|
||||
{
|
||||
metadata: {
|
||||
primary: true,
|
||||
source: {
|
||||
type: 'CONTACT',
|
||||
id: '3b603c120c68305f',
|
||||
},
|
||||
},
|
||||
displayName: 'Shahed Mashni',
|
||||
familyName: 'Mashni',
|
||||
givenName: 'Shahed',
|
||||
displayNameLastFirst: 'Mashni, Shahed',
|
||||
unstructuredName: 'Shahed Mashni',
|
||||
},
|
||||
],
|
||||
photos: [
|
||||
{
|
||||
metadata: {
|
||||
primary: true,
|
||||
source: {
|
||||
type: 'CONTACT',
|
||||
id: '3b603c120c68305f',
|
||||
},
|
||||
},
|
||||
url: 'https://lh3.googleusercontent.com/cm/AAkddurmZojs4vCcxrpkfSxH9tnqcH-hI82ESDnwv6eq86nZeLStcjYEIe_TCx8r8g5Y=s100',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
memberships: [
|
||||
{
|
||||
metadata: {
|
||||
source: {
|
||||
type: 'CONTACT',
|
||||
id: '3b603c120c68305f',
|
||||
},
|
||||
},
|
||||
contactGroupMembership: {
|
||||
contactGroupId: 'myContacts',
|
||||
contactGroupResourceName: 'contactGroups/myContacts',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
type: TriggerStrategy.POLLING,
|
||||
async onEnable(ctx) {
|
||||
return await pollingHelper.onEnable(polling, {
|
||||
store: ctx.store,
|
||||
auth: ctx.auth,
|
||||
propsValue: {},
|
||||
});
|
||||
},
|
||||
async onDisable(ctx) {
|
||||
return await pollingHelper.onEnable(polling, {
|
||||
store: ctx.store,
|
||||
auth: ctx.auth,
|
||||
propsValue: {},
|
||||
});
|
||||
},
|
||||
async run(ctx) {
|
||||
return await pollingHelper.poll(polling, {
|
||||
store: ctx.store,
|
||||
auth: ctx.auth,
|
||||
propsValue: {},
|
||||
files: ctx.files,
|
||||
});
|
||||
},
|
||||
test: async (ctx) => {
|
||||
return await pollingHelper.test(polling, {
|
||||
store: ctx.store,
|
||||
auth: ctx.auth,
|
||||
propsValue: {},
|
||||
files: ctx.files,
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user