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,31 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { googleChatApiAuth, googleChatCommon } from '../common';
import { peoplesDropdown, spacesDropdown, spacesMembersDropdown } from '../common/props';
import { googleChatAPIService } from '../common/requests';
export const addASpaceMember = createAction({
auth: googleChatApiAuth,
name: 'addASpaceMember',
displayName: 'Add a Space Member',
description: 'Add a user to a Google Chat space.',
props: {
spaceId: spacesDropdown({ refreshers: ['auth'], required: true }),
personId: peoplesDropdown(['auth']),
},
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, googleChatCommon.addSpaceMemberSchema);
const { spaceId, personId } = propsValue;
const userId = (personId as string).replace('people', 'users');
const response = await googleChatAPIService.AddASpaceMember({
accessToken: auth.access_token,
spaceId: spaceId as string,
userId: userId as string
})
return response;
},
});

View File

@@ -0,0 +1,60 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { googleChatApiAuth, googleChatCommon } from '../common';
import { spacesDropdown } from '../common/props';
import { googleChatAPIService } from '../common/requests';
export const findMember = createAction({
auth: googleChatApiAuth,
name: 'findMember',
displayName: 'Find Member',
description: 'Search space member by email',
props: {
spaceId: spacesDropdown({ refreshers: ['auth'], required: true }),
email: Property.ShortText({
displayName: 'Member Email',
description: 'The email address of the member to find',
required: true,
}),
},
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, googleChatCommon.findMemberSchema);
const { spaceId, email } = propsValue;
const accessToken = auth.access_token;
try {
const people = await googleChatAPIService.fetchPeople(
accessToken
);
const person = people.find((p: any) =>
p?.emailAddresses?.some(
(e: any) => e.value.toLowerCase() === email.toLowerCase()
)
);
if (!person) return { error: `No user found with email ${email}` };
const spaceMemberId = (person.resourceName as string).replace('people', 'users');
const spaceMembers = await googleChatAPIService.fetchSpaceMembers(
accessToken,
spaceId as string
);
const matchingMember = spaceMembers.find(
(m: any) => m.member?.name === spaceMemberId
);
if (!matchingMember) return {
error: `${email} is not a member of space ${spaceId}`,
};
return matchingMember;
} catch (e) {
console.error('Error finding member by email', e);
return { error: 'Failed to retrieve member information' };
}
},
});

View File

@@ -0,0 +1,27 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { googleChatApiAuth, googleChatCommon } from '../common';
import { directMessagesDropdown, spacesDropdown } from '../common/props';
import { googleChatAPIService } from '../common/requests';
export const getDirectMessageDetails = createAction({
auth: googleChatApiAuth,
name: 'getDirectMessageDetails',
displayName: 'Get Direct Message Details',
description: 'Retrieve details of a specific direct message by ID.',
props: {
directMessageId: directMessagesDropdown({ refreshers: ['auth'], required: true }),
},
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, googleChatCommon.getDirectMessageDetailsSchema);
const { directMessageId } = propsValue;
const response = await googleChatAPIService.getSpace({
accessToken: auth.access_token,
spaceId: directMessageId as string,
});
return response;
},
});

View File

@@ -0,0 +1,30 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { googleChatApiAuth, googleChatCommon } from '../common';
import { googleChatAPIService } from '../common/requests';
export const getMessageDetails = createAction({
auth: googleChatApiAuth,
name: 'getMessageDetails',
displayName: 'Get Message Details',
description: 'Retrieve details of a specific message by ID. Supports both system-generated and custom message IDs.',
props: {
name: Property.ShortText({
displayName: 'Message Resource Name',
description: 'The full resource name of the message. Format: spaces/{space}/messages/{message}',
required: true,
}),
},
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, googleChatCommon.getMessageSchema);
const { name } = propsValue;
const message = await googleChatAPIService.getMessage(
auth.access_token,
name
);
return message;
},
});

View File

@@ -0,0 +1,45 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { googleChatApiAuth, googleChatCommon } from '../common';
import { allSpacesDropdown } from '../common/props';
import { googleChatAPIService } from '../common/requests';
export const searchMessages = createAction({
auth: googleChatApiAuth,
name: 'searchMessages',
displayName: 'Search Messages',
description: 'Search within Chat for messages matching keywords or filters.',
props: {
spaceId: allSpacesDropdown({ refreshers: ['auth'], required: true }),
keyword: Property.ShortText({
displayName: 'Keyword',
description: 'Search for messages containing this text',
required: true,
}),
limit: Property.Number({
displayName: 'Max Results',
description: 'Maximum number of messages to return',
required: false,
defaultValue: 50,
}),
},
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, googleChatCommon.searchMessagesSchema);
const { spaceId, keyword, limit } = propsValue;
const response = await googleChatAPIService.listMessages(
auth.access_token,
spaceId as string,
limit
);
const messages = response.messages || [];
const filtered = messages.filter((msg: any) =>
msg.text?.toLowerCase().includes(keyword.toLowerCase())
);
return filtered;
},
});

View File

@@ -0,0 +1,110 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { googleChatApiAuth, googleChatCommon } from '../common';
import { allSpacesDropdown, spacesDropdown, peoplesDropdown, threadsDropdown } from '../common/props';
import { googleChatAPIService } from '../common/requests';
export const sendAMessage = createAction({
auth: googleChatApiAuth,
name: 'sendAMessage',
displayName: 'Send a Message',
description: 'Send a message to a space or direct conversation.',
props: {
spaceId: allSpacesDropdown({ refreshers: ['auth'], required: true }),
text: Property.LongText({
displayName: 'Message',
description: 'The message content to send. Supports basic formatting like *bold*, _italic_, and @mentions.',
required: true,
}),
thread: threadsDropdown({ refreshers: ['auth', 'spaceId'], required: false }),
messageReplyOption: Property.StaticDropdown({
displayName: 'Reply Behavior',
description: 'How to handle replies when thread ID is provided.',
required: false,
options: {
options: [
{
label: 'Reply or start new thread',
value: 'REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD',
},
{
label: 'Reply only (fail if thread not found)',
value: 'REPLY_MESSAGE_OR_FAIL',
},
],
},
}),
customMessageId: Property.ShortText({
displayName: 'Custom Message ID',
description: 'Optional unique ID for this message (auto-generated if empty). Useful for deduplication.',
required: false,
}),
isPrivate: Property.Checkbox({
displayName: 'Send as Private Message',
description: 'Send this message privately to a specific user. Requires app authentication.',
required: false,
}),
privateMessageViewer: Property.Dropdown({
displayName: 'Private Message Recipient',
description: 'Select the user who can view this private message.',
required: false,
refreshers: ['auth'],
auth: googleChatApiAuth,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Google account first',
options: [],
};
}
try {
const members = await googleChatAPIService.fetchPeople(
auth.access_token
);
return {
options: members
.map((member: any) => {
const nameObj =
member.names?.find((n: any) => n.metadata.primary) ||
member.names?.[0];
if (!nameObj) return null;
return {
label: nameObj.displayName,
value: member.resourceName,
};
})
.filter(Boolean),
};
} catch (e) {
console.error('Failed to fetch people', e);
return {
options: [],
placeholder: 'Unable to load people',
};
}
},
}),
},
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, googleChatCommon.sendMessageSchema);
const { spaceId, text, thread, messageReplyOption, customMessageId, isPrivate, privateMessageViewer } = propsValue;
const response = await googleChatAPIService.sendMessage({
accessToken: auth.access_token,
spaceId: spaceId as string,
text,
thread,
messageReplyOption,
customMessageId,
isPrivate,
privateMessageViewer,
});
return response;
},
});

View File

@@ -0,0 +1,22 @@
import { PieceAuth } from "@activepieces/pieces-framework";
export const googleChatApiAuth = PieceAuth.OAuth2({
authUrl: 'https://accounts.google.com/o/oauth2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
required: true,
scope: [
'https://www.googleapis.com/auth/chat.messages',
'https://www.googleapis.com/auth/chat.spaces',
'https://www.googleapis.com/auth/chat.memberships',
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/directory.readonly',
],
});
export const GOOGLE_SERVICE_ENTITIES = {
chat: 'chat',
cloudresourcemanager: 'cloudresourcemanager',
pubsub: 'pubsub',
workspaceevents: 'workspaceevents',
people: 'people',
};

View File

@@ -0,0 +1,17 @@
export * from './schemas';
export * from './constants';
export * from './props';
export * from './requests';
import * as schemas from './schemas';
export const googleChatCommon = {
sendMessageSchema: schemas.sendMessageSchema,
getMessageSchema: schemas.getMessageSchema,
addSpaceMemberSchema: schemas.addSpaceMemberSchema,
findMemberSchema: schemas.findMemberSchema,
searchMessagesSchema: schemas.searchMessagesSchema,
getDirectMessageDetailsSchema: schemas.getDirectMessageDetailsSchema,
newMessageTriggerSchema: schemas.newMessageTriggerSchema,
newMentionTriggerSchema: schemas.newMentionTriggerSchema,
};

View File

@@ -0,0 +1,332 @@
import { Property } from '@activepieces/pieces-framework';
import { googleChatAPIService } from './requests';
import { googleChatApiAuth } from './constants';
export const projectsDropdown = (refreshers: string[]) =>
Property.Dropdown({
displayName: 'Project',
description: 'Select a Google Cloud Project',
required: true,
refreshers,
auth: googleChatApiAuth,
async options({ auth }: any) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Google account first',
options: [],
};
}
try {
const projects = await googleChatAPIService.fetchProjects(
auth.access_token
);
return {
options: projects.map((project: any) => ({
label: project.name,
value: project.projectId,
})),
};
} catch (e) {
console.error('Failed to fetch projects', e);
return {
options: [],
placeholder: 'Unable to load projects',
};
}
},
});
export const spacesDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.Dropdown({
auth: googleChatApiAuth,
displayName: 'Space',
description: `Select a Space${
required ? '' : ', leave empty for all spaces'
}`,
required,
refreshers,
async options({ auth }: any) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Google account first',
options: [],
};
}
try {
const spaces = await googleChatAPIService.fetchSpaces(
auth.access_token
);
return {
options: spaces.map((space: any) => ({
label: space.displayName || space.name,
value: space.name,
})),
};
} catch (e) {
console.error('Failed to fetch spaces', e);
return {
options: [],
placeholder: 'Unable to load spaces',
};
}
},
});
export const allSpacesDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.Dropdown({
auth: googleChatApiAuth,
displayName: 'Space',
description: `Select a Space${
required ? '' : ', leave empty for all spaces'
}`,
required,
refreshers,
async options({ auth }: any) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Google account first',
options: [],
};
}
try {
const spaces = await googleChatAPIService.fetchAllSpaces(
auth.access_token
);
return {
options: spaces.map((space: any) => ({
label: space.displayName || space.name,
value: space.name,
})),
};
} catch (e) {
console.error('Failed to fetch spaces', e);
return {
options: [],
placeholder: 'Unable to load spaces',
};
}
},
});
export const directMessagesDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.Dropdown({
auth: googleChatApiAuth,
displayName: 'Direct Message',
description: `Select a Direct Message${
required ? '' : ', leave empty for all spaces'
}`,
required,
refreshers,
async options({ auth }: any) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Google account first',
options: [],
};
}
try {
const spaces = await googleChatAPIService.fetchDirectMessages(
auth.access_token
);
return {
options: spaces.map((space: any) => ({
label: space.displayName || space.name,
value: space.name,
})),
};
} catch (e) {
console.error('Failed to fetch spaces', e);
return {
options: [],
placeholder: 'Unable to load spaces',
};
}
},
});
export const spacesMembersDropdown = (refreshers: string[]) =>
Property.Dropdown({
auth: googleChatApiAuth,
displayName: 'Space Member',
description: 'Select a space member, leave empty for all members',
required: false,
refreshers,
async options({ auth, spaceId }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Google account first',
options: [],
};
}
if (!spaceId || typeof spaceId !== 'string') {
return {
disabled: true,
placeholder: 'Please select a space first',
options: [],
};
}
try {
const members = await googleChatAPIService.fetchSpaceMembers(
auth.access_token,
spaceId
);
return {
options: members.map((member: any) => ({
label: member.member.name,
value: member.member.name,
})),
};
} catch (e) {
console.error('Failed to fetch space members', e);
return {
options: [],
placeholder: 'Unable to load space members',
};
}
},
});
export const peoplesDropdown = (refreshers: string[]) =>
Property.Dropdown({
auth: googleChatApiAuth,
displayName: 'Select A Person',
description: 'Select a person',
required: true,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Google account first',
options: [],
};
}
try {
const members = await googleChatAPIService.fetchPeople(
auth.access_token
);
return {
options: members
.map((member: any) => {
const nameObj =
member.names?.find((n: any) => n.metadata.primary) ||
member.names?.[0];
if (!nameObj) return null;
return {
label: nameObj.displayName,
value: member.resourceName,
};
})
.filter(Boolean),
};
} catch (e) {
console.error('Failed to fetch people', e);
return {
options: [],
placeholder: 'Unable to load people',
};
}
},
});
export const threadsDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.Dropdown({
auth: googleChatApiAuth,
displayName: 'Thread',
description: `Select a thread to reply to${required ? '' : ', leave empty for new thread'}`,
required,
refreshers,
async options({ auth, spaceId }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Google account first',
options: [],
};
}
if (!spaceId || typeof spaceId !== 'string') {
return {
disabled: true,
placeholder: 'Please select a space first',
options: [],
};
}
if (!spaceId.startsWith('spaces/')) {
return {
disabled: true,
placeholder: 'Invalid space ID format. Please select a valid space.',
options: [],
};
}
try {
const threads = await googleChatAPIService.fetchThreads(
auth.access_token,
spaceId
);
const options = threads.map((thread: any) => ({
label: thread.displayName || thread.name,
value: thread.name,
}));
options.unshift({
label: 'Start new thread',
value: '',
});
return { options };
} catch (e) {
console.error('Failed to fetch threads', e);
return {
options: [],
placeholder: 'Unable to load threads',
};
}
},
});

View File

@@ -0,0 +1,480 @@
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { GOOGLE_SERVICE_ENTITIES } from './constants';
async function fireHttpRequest({
method,
path,
entity,
body,
accessToken,
}: {
accessToken: string;
method: HttpMethod;
path: string;
entity: keyof typeof GOOGLE_SERVICE_ENTITIES;
body?: unknown;
}) {
const BASE_URL = `https://${GOOGLE_SERVICE_ENTITIES[entity]}.googleapis.com`;
return await httpClient
.sendRequest({
method,
url: `${BASE_URL}${path}`,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body,
})
.then((res) => res.body)
.catch((err) => {
throw new Error(err);
});
}
export const googleChatAPIService = {
async fetchProjects(accessToken: string) {
const response = await fireHttpRequest({
method: HttpMethod.GET,
path: '/v1/projects',
entity: 'cloudresourcemanager',
accessToken,
});
return response.projects;
},
async sendMessage({
accessToken,
spaceId,
text,
thread,
messageReplyOption,
customMessageId,
isPrivate,
privateMessageViewer,
}: {
accessToken: string;
spaceId: string;
text: string;
thread?: string;
messageReplyOption?: string;
customMessageId?: string;
isPrivate?: boolean;
privateMessageViewer?: string;
}) {
const body: any = { text };
if (thread) {
body.thread = { name: thread };
}
if (messageReplyOption) {
body.messageReplyOption = messageReplyOption;
}
if (customMessageId) {
const cleanId = customMessageId.toLowerCase().replace(/[^a-z0-9-]/g, '');
body.messageId = `client-${cleanId}`;
}
if (isPrivate && privateMessageViewer) {
body.privateMessageViewer = { name: privateMessageViewer };
}
return await fireHttpRequest({
method: HttpMethod.POST,
entity: 'chat',
accessToken,
path: `/v1/${spaceId}/messages`,
body,
});
},
async AddASpaceMember({
accessToken,
spaceId,
userId,
}: {
accessToken: string;
spaceId: string;
userId: string;
}) {
return await fireHttpRequest({
method: HttpMethod.POST,
accessToken: accessToken,
entity: 'chat',
path: `/v1/${spaceId}/members`,
body: {
member: {
name: userId,
type: 'HUMAN',
},
},
});
},
async getMessage(accessToken: string, messageName: string) {
return await fireHttpRequest({
method: HttpMethod.GET,
entity: 'chat',
accessToken,
path: `/v1/${messageName}`,
});
},
async getSpace({
accessToken,
spaceId,
}: {
accessToken: string;
spaceId: string;
}) {
return await fireHttpRequest({
method: HttpMethod.GET,
entity: 'chat',
path: `/v1/${spaceId}`,
accessToken,
});
},
async listMessages(
accessToken: string,
spaceId: string,
pageSize = 50
) {
return await fireHttpRequest({
method: HttpMethod.GET,
entity: 'chat',
accessToken,
path: `/v1/${spaceId}/messages?pageSize=${pageSize}`,
});
},
async fetchAllSpaces(accessToken: string) {
const response = await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1/spaces`,
entity: 'chat',
accessToken,
});
return response.spaces;
},
async fetchSpaces(accessToken: string) {
const filter = encodeURIComponent('spaceType = "SPACE"');
const response = await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1/spaces?filter=${filter}`,
entity: 'chat',
accessToken,
});
return response.spaces;
},
async fetchDirectMessages(accessToken: string) {
const filter = encodeURIComponent('spaceType = "DIRECT_MESSAGE"');
const response = await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1/spaces?filter=${filter}`,
entity: 'chat',
accessToken,
});
return response.spaces;
},
async fetchThreads(accessToken: string, spaceId: string) {
const response = await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1/${spaceId}/messages?pageSize=50`,
entity: 'chat',
accessToken,
});
const threads = new Map();
response.messages?.forEach((message: any) => {
if (message.thread && message.thread.name && !message.threadReply) {
threads.set(message.thread.name, {
name: message.thread.name,
displayName: message.text?.substring(0, 50) + (message.text?.length > 50 ? '...' : ''),
lastActivity: message.createTime,
});
}
});
return Array.from(threads.values());
},
async fetchSpaceMembers(accessToken: string, spaceId: string) {
const response = await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1/${spaceId}/members`,
entity: 'chat',
accessToken,
});
return response.memberships;
},
async fetchPeople(accessToken: string) {
const response = await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1/people:listDirectoryPeople?sources=DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE&sources=DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT&readMask=names,emailAddresses`,
entity: 'people',
accessToken,
});
return response.people;
},
async deleteWorkspaceEventsSubscription({
accessToken,
subscriptionName,
}: {
accessToken: string;
subscriptionName: string;
}) {
return await fireHttpRequest({
method: HttpMethod.DELETE,
entity: 'workspaceevents',
accessToken,
path: `/v1/${subscriptionName}`,
});
},
async grantTopicPermissions({
accessToken,
projectId,
topicName,
}: {
accessToken: string;
projectId: string;
topicName: string;
}) {
const policy = {
bindings: [
{
role: 'roles/pubsub.publisher',
members: [`serviceAccount:chat-api-push@system.gserviceaccount.com`],
},
],
};
return await fireHttpRequest({
method: HttpMethod.POST,
entity: 'pubsub',
accessToken,
path: `/v1/projects/${projectId}/topics/${topicName}:setIamPolicy`,
body: { policy },
});
},
async createPubSubTopic({
accessToken,
projectId,
topicName,
}: {
accessToken: string;
projectId: string;
topicName: string;
}) {
return await fireHttpRequest({
method: HttpMethod.PUT,
entity: 'pubsub',
accessToken,
path: `/v1/projects/${projectId}/topics/${topicName}`,
});
},
async deletePubSubTopic({
accessToken,
projectId,
topicName,
}: {
accessToken: string;
projectId: string;
topicName: string;
}) {
return await fireHttpRequest({
method: HttpMethod.DELETE,
entity: 'pubsub',
accessToken,
path: `/v1/projects/${projectId}/topics/${topicName}`,
});
},
async createWorkspaceEventsSubscription({
accessToken,
projectId,
subscriptionName,
targetResource,
eventTypes,
topicName,
includeResource = false,
}: {
accessToken: string;
projectId: string;
subscriptionName: string;
targetResource: string;
eventTypes: string[];
topicName: string;
includeResource?: boolean;
}) {
const subscription = {
targetResource,
eventTypes,
notificationEndpoint: {
pubsubTopic: `projects/${projectId}/topics/${topicName}`,
},
payloadOptions: {
includeResource,
},
};
return await fireHttpRequest({
method: HttpMethod.POST,
entity: 'workspaceevents',
accessToken,
path: `/v1/subscriptions`,
body: subscription,
});
},
async createPubSubSubscription({
accessToken,
projectId,
subscriptionName,
topicName,
pushEndpoint,
}: {
accessToken: string;
projectId: string;
subscriptionName: string;
topicName: string;
pushEndpoint: string;
}) {
const subscription = {
topic: `projects/${projectId}/topics/${topicName}`,
pushConfig: {
pushEndpoint,
},
ackDeadlineSeconds: 600,
};
return await fireHttpRequest({
method: HttpMethod.PUT,
entity: 'pubsub',
accessToken,
path: `/v1/projects/${projectId}/subscriptions/${subscriptionName}`,
body: subscription,
});
},
async createWebhookSubscription({
accessToken,
projectId,
topic,
subscriptionName,
webhookUrl,
eventTypes,
targetResource,
}: {
accessToken: string;
projectId: string;
topic: string;
webhookUrl: string;
subscriptionName: string;
eventTypes: string[];
targetResource: string;
}) {
const workspaceSubscription = await this.createWorkspaceEventsSubscription({
accessToken,
projectId,
subscriptionName,
targetResource,
eventTypes,
topicName: topic,
includeResource: true,
});
const pubsubSubscription = await this.createPubSubSubscription({
accessToken,
projectId,
subscriptionName,
topicName: topic,
pushEndpoint: webhookUrl,
});
return {
workspaceSubscription,
pubsubSubscription,
};
},
async deletePubSubSubscription({
accessToken,
projectId,
subscriptionName,
}: {
accessToken: string;
projectId: string;
subscriptionName: string;
}) {
return await fireHttpRequest({
method: HttpMethod.DELETE,
entity: 'pubsub',
accessToken,
path: `/v1/projects/${projectId}/subscriptions/${subscriptionName}`,
});
},
async deleteWebhookSubscription({
accessToken,
projectId,
subscriptionName,
topicName,
event_type,
}: {
accessToken: string;
projectId: string;
subscriptionName: string;
topicName: string;
event_type: string;
}) {
return await this.cleanupWebhookResources({
accessToken,
projectId,
event_type,
});
},
async fetchWorkSpaceSubscriptions({
accessToken,
event_type,
}: {
accessToken: string;
event_type: string;
}) {
const response = await fireHttpRequest({
method: HttpMethod.GET,
entity: 'workspaceevents',
accessToken,
path: `/v1/subscriptions?filter=event_types:"${event_type}"`,
});
return response.subscriptions;
},
async cleanupWebhookResources({
accessToken,
projectId,
event_type,
}: {
accessToken: string;
projectId: string;
event_type: string;
}) {
try {
const workspaceSubscriptions = await this.fetchWorkSpaceSubscriptions({
accessToken,
event_type,
});
for (const sub of workspaceSubscriptions || []) {
try {
await this.deleteWorkspaceEventsSubscription({
accessToken,
subscriptionName: sub.name,
});
} catch (err: any) {
console.log(err);
}
}
} catch (err: any) {
console.error('Error cleaning up workspace subscriptions:', err);
throw err;
}
},
};

View File

@@ -0,0 +1,147 @@
import z from 'zod';
export const sendMessageSchema = {
spaceId: z.string().min(1, 'Space ID is required'),
text: z.string().min(1, 'Message text is required'),
thread: z.string().optional(),
messageReplyOption: z.enum([
'REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD',
'REPLY_MESSAGE_OR_FAIL'
]).optional(),
customMessageId: z.string().regex(
/^[a-z0-9-]+$/,
'Custom message ID must contain only lowercase letters, numbers, and hyphens'
).max(63, 'Custom message ID must be 63 characters or less').optional(),
isPrivate: z.boolean().optional(),
privateMessageViewer: z.string().optional(),
};
export const spaceIdSchema = z.string().regex(
/^spaces\/[a-zA-Z0-9_-]+$/,
'Space ID must be in format: spaces/{space}'
);
export const threadIdSchema = z.string().regex(
/^spaces\/[a-zA-Z0-9_-]+\/threads\/[a-zA-Z0-9_-]+$/,
'Thread ID must be in format: spaces/{space}/threads/{thread}'
);
export const userResourceNameSchema = z.string().regex(
/^people\/[a-zA-Z0-9_-]+$/,
'User resource name must be in format: people/{person}'
);
export const customMessageIdSchema = z.string()
.regex(
/^client-[a-z0-9-]+$/,
'Custom message ID must start with "client-" and contain only lowercase letters, numbers, and hyphens'
)
.max(63, 'Custom message ID must be 63 characters or less');
export const messageTextSchema = z.string()
.min(1, 'Message text cannot be empty')
.max(32000, 'Message text cannot exceed 32,000 characters');
export const validateSpaceFromDropdown = z.object({
value: spaceIdSchema,
label: z.string(),
});
export const validateThreadFromDropdown = z.object({
value: z.union([z.literal(''), threadIdSchema]),
label: z.string(),
});
export const validateUserFromDropdown = z.object({
value: userResourceNameSchema,
label: z.string(),
});
export const getMessageSchema = {
name: z.string()
.min(1, 'Message resource name is required')
.regex(
/^spaces\/[a-zA-Z0-9_-]+\/messages\/[a-zA-Z0-9_-]+$/,
'Message resource name must be in format: spaces/{space}/messages/{message}'
),
};
export const addSpaceMemberSchema = {
spaceId: z.string()
.min(1, 'Space ID is required')
.regex(
/^spaces\/[a-zA-Z0-9_-]+$/,
'Space ID must be in format: spaces/{space}'
),
personId: z.string()
.min(1, 'Person ID is required')
.regex(
/^people\/[a-zA-Z0-9_-]+$/,
'Person ID must be in format: people/{person}'
),
};
export const findMemberSchema = {
spaceId: z.string()
.min(1, 'Space ID is required')
.regex(
/^spaces\/[a-zA-Z0-9_-]+$/,
'Space ID must be in format: spaces/{space}'
),
email: z.string()
.min(1, 'Email is required')
.email('Please enter a valid email address'),
};
export const searchMessagesSchema = {
spaceId: z.string()
.min(1, 'Space ID is required')
.regex(
/^spaces\/[a-zA-Z0-9_-]+$/,
'Space ID must be in format: spaces/{space}'
),
keyword: z.string()
.min(1, 'Search keyword is required')
.min(2, 'Search keyword must be at least 2 characters long'),
limit: z.number()
.min(1, 'Limit must be at least 1')
.max(1000, 'Limit cannot exceed 1000')
.optional(),
};
export const getDirectMessageDetailsSchema = {
directMessageId: z.string()
.min(1, 'Direct message ID is required')
.regex(
/^spaces\/[a-zA-Z0-9_-]+$/,
'Direct message ID must be in format: spaces/{space}'
),
};
export const newMessageTriggerSchema = {
projectId: z.string()
.min(1, 'Project ID is required'),
spaceId: z.string()
.regex(
/^spaces\/[a-zA-Z0-9_-]+$/,
'Space ID must be in format: spaces/{space}'
)
.optional(),
};
export const newMentionTriggerSchema = {
projectId: z.string()
.min(1, 'Project ID is required'),
spaceId: z.string()
.regex(
/^spaces\/[a-zA-Z0-9_-]+$/,
'Space ID must be in format: spaces/{space}'
)
.optional(),
spaceMemberId: z.string()
.regex(
/^users\/[a-zA-Z0-9_-]+$/,
'Space member ID must be in format: users/{user}'
)
.optional(),
};

View File

@@ -0,0 +1,111 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { googleChatApiAuth, googleChatCommon } from '../common';
import { projectsDropdown, spacesDropdown, spacesMembersDropdown } from '../common/props';
import { googleChatAPIService } from '../common/requests';
export const newMention = createTrigger({
auth: googleChatApiAuth,
name: 'newMention',
displayName: 'New Mention',
description: 'Triggers when a new mention is received in Google Chat.',
props: {
projectId: projectsDropdown(['auth']),
spaceId: spacesDropdown({refreshers: ['auth']}),
spaceMemberId: spacesMembersDropdown(['auth', 'spaceId']),
},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable({ auth, propsValue, webhookUrl, store }) {
await propsValidation.validateZod(propsValue, googleChatCommon.newMentionTriggerSchema);
const { projectId, spaceId } = propsValue;
const accessToken = auth.access_token;
const topicName = `acp-topic-${Date.now()}`;
const subscriptionName = `acp-sub-${Date.now()}`;
await googleChatAPIService
.cleanupWebhookResources({
accessToken,
event_type: 'google.workspace.chat.message.v1.created',
projectId: projectId as string,
})
.catch((err) => {
console.log('Error cleaning up webhook resources', err);
});
await googleChatAPIService.createPubSubTopic({
accessToken,
projectId: projectId as string,
topicName,
});
await googleChatAPIService.grantTopicPermissions({
accessToken,
projectId: projectId as string,
topicName,
});
const targetResource = `//chat.googleapis.com/${
spaceId ? spaceId : 'spaces/-'
}`;
await googleChatAPIService.createWebhookSubscription({
accessToken,
projectId: projectId as string,
topic: topicName,
subscriptionName,
webhookUrl,
eventTypes: ['google.workspace.chat.message.v1.created'],
targetResource,
});
},
async onDisable({ auth, propsValue: { projectId }, store }) {
const accessToken = auth.access_token;
await googleChatAPIService
.cleanupWebhookResources({
accessToken,
event_type: 'google.workspace.chat.message.v1.created',
projectId: projectId as string,
})
.catch((err) => {
console.log('Error cleaning up webhook resources during disable', err);
});
},
async run(context) {
const messageData = JSON.parse(
Buffer.from(
(context.payload.body as any).message.data,
'base64'
).toString('utf-8')
);
const { spaceMemberId } = context.propsValue;
if (!messageData.message?.annotations) {
return [];
}
const mentions = messageData.message.annotations.filter(
(a: any) => a.type === 'USER_MENTION'
);
if (mentions.length === 0) {
return [];
}
if (spaceMemberId) {
const isMatch = mentions.some(
(m: any) => m.userMention?.user?.name === spaceMemberId
);
if (!isMatch) {
return [];
}
}
return [messageData];
},
});

View File

@@ -0,0 +1,86 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { googleChatApiAuth, googleChatCommon } from '../common';
import { projectsDropdown, spacesDropdown } from '../common/props';
import { googleChatAPIService } from '../common/requests';
export const newMessage = createTrigger({
auth: googleChatApiAuth,
name: 'newMessage',
displayName: 'New Message',
description: 'Triggers when a new message is received in Google Chat.',
props: {
projectId: projectsDropdown(['auth']),
spaceId: spacesDropdown({ refreshers: ['auth'] }),
},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable({ auth, propsValue, webhookUrl, store }) {
await propsValidation.validateZod(propsValue, googleChatCommon.newMessageTriggerSchema);
const { projectId, spaceId } = propsValue;
const accessToken = auth.access_token;
const topicName = `acp-topic-${Date.now()}`;
const subscriptionName = `acp-sub-${Date.now()}`;
await googleChatAPIService
.cleanupWebhookResources({
accessToken,
event_type: 'google.workspace.chat.message.v1.created',
projectId: projectId as string,
})
.catch((err) => {
console.log('Error cleaning up webhook resources', err);
});
await googleChatAPIService.createPubSubTopic({
accessToken,
projectId: projectId as string,
topicName,
});
await googleChatAPIService.grantTopicPermissions({
accessToken,
projectId: projectId as string,
topicName,
});
const targetResource = `//chat.googleapis.com/${
spaceId ? spaceId : 'spaces/-'
}`;
await googleChatAPIService.createWebhookSubscription({
accessToken,
projectId: projectId as string,
topic: topicName,
subscriptionName,
webhookUrl,
eventTypes: ['google.workspace.chat.message.v1.created'],
targetResource,
});
},
async onDisable({ auth, propsValue: { projectId }, store }) {
const accessToken = auth.access_token;
await googleChatAPIService
.cleanupWebhookResources({
accessToken,
event_type: 'google.workspace.chat.message.v1.created',
projectId: projectId as string,
})
.catch((err) => {
console.log('Error cleaning up webhook resources during disable', err);
});
},
async run(context) {
const messageData = JSON.parse(
Buffer.from(
(context.payload.body as any).message.data,
'base64'
).toString('utf-8')
);
return [messageData];
},
});