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,34 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { assignBadgeToMember } from '../api';
import { buildBadgesDropdown } from '../props';
import { bettermodeAuth } from '../auth';
export const assignBadgeAction = createAction({
name: 'assign_badge',
auth: bettermodeAuth,
displayName: 'Assign Badge to Member',
description: 'Assign an existing badge to a member by email',
props: {
badgeId: Property.Dropdown({
auth: bettermodeAuth,
displayName: 'Badge',
description: 'The badge to assign',
required: true,
refreshers: [],
options: async ({ auth }) =>
await buildBadgesDropdown(auth?.props),
}),
email: Property.ShortText({
displayName: 'Email',
description: 'The email of the member to assign the badge to',
required: true,
}),
},
async run(context) {
return await assignBadgeToMember(
context.auth.props,
context.propsValue.badgeId,
context.propsValue.email
);
},
});

View File

@@ -0,0 +1,53 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { createDiscussion } from '../api';
import { buildMemberSpacesDropdown } from '../props';
import { bettermodeAuth } from '../auth';
export const createDiscussionAction = createAction({
name: 'create_discussion',
auth: bettermodeAuth,
displayName: 'Create Discussion Post',
description: 'Create a new discussion post in a space',
props: {
spaceId: Property.Dropdown({
auth: bettermodeAuth,
displayName: 'Space',
description: 'The space to create the discussion in',
required: true,
refreshers: [],
options: async ({ auth }) =>
await buildMemberSpacesDropdown(auth?.props),
}),
title: Property.ShortText({
displayName: 'Title',
description: 'The title of the discussion',
required: true,
}),
content: Property.LongText({
displayName: 'Content',
description: 'The content of the discussion',
required: true,
}),
tagNames: Property.ShortText({
displayName: 'Tags',
description: 'The tags to add to the discussion',
required: false,
}),
locked: Property.Checkbox({
displayName: 'Locked',
description: 'If the discussion should be locked',
required: false,
defaultValue: false,
}),
},
async run(context) {
return await createDiscussion(
context.auth.props,
context.propsValue.spaceId,
context.propsValue.tagNames ?? '',
context.propsValue.title,
context.propsValue.content,
context.propsValue.locked
);
},
});

View File

@@ -0,0 +1,53 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { createQuestion } from '../api';
import { buildMemberSpacesDropdown } from '../props';
import { bettermodeAuth } from '../auth';
export const createQuestionAction = createAction({
name: 'create_question',
auth: bettermodeAuth,
displayName: 'Create Question Post',
description: 'Create a new question post in a space',
props: {
spaceId: Property.Dropdown({
auth: bettermodeAuth,
displayName: 'Space',
description: 'The space to create the question in',
required: true,
refreshers: [],
options: async ({ auth }) =>
await buildMemberSpacesDropdown(auth?.props),
}),
title: Property.ShortText({
displayName: 'Title',
description: 'The title of the question',
required: true,
}),
content: Property.LongText({
displayName: 'Content',
description: 'The content of the question',
required: true,
}),
tagNames: Property.ShortText({
displayName: 'Tags',
description: 'The tags to add to the question',
required: false,
}),
locked: Property.Checkbox({
displayName: 'Locked',
description: 'If the question should be locked',
required: false,
defaultValue: false,
}),
},
async run(context) {
return await createQuestion(
context.auth.props,
context.propsValue.spaceId,
context.propsValue.tagNames ?? '',
context.propsValue.title,
context.propsValue.content,
context.propsValue.locked
);
},
});

View File

@@ -0,0 +1,34 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { revokeBadgeFromMember } from '../api';
import { buildBadgesDropdown } from '../props';
import { bettermodeAuth } from '../auth';
export const revokeBadgeAction = createAction({
name: 'revoke_badge',
auth: bettermodeAuth,
displayName: 'Revoke Badge from Member',
description: 'Revoke a badge from a member by email',
props: {
badgeId: Property.Dropdown({
auth: bettermodeAuth,
displayName: 'Badge',
description: 'The badge to revoke',
required: true,
refreshers: [],
options: async ({ auth }) =>
await buildBadgesDropdown(auth?.props),
}),
email: Property.ShortText({
displayName: 'Email',
description: 'The email of the member to revoke the badge from',
required: true,
}),
},
async run(context) {
return await revokeBadgeFromMember(
context.auth.props,
context.propsValue.badgeId,
context.propsValue.email
);
},
});

View File

@@ -0,0 +1,292 @@
import {
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { BettermodeAuthType } from './auth';
type KeyValuePair = { [key: string]: string | boolean | object | undefined };
const bettermodeAPI = async (
auth: BettermodeAuthType,
query: string,
variables: KeyValuePair = {}
) => {
const request: HttpRequest = {
method: HttpMethod.POST,
url: auth.region,
headers: {
'Content-Type': 'application/json',
Authorization: auth.token ? `Bearer ${auth.token}` : undefined,
},
body: JSON.stringify({
query: query,
variables: variables,
}),
};
const response = await httpClient.sendRequest(request);
if (response.body['errors']) {
throw new Error(response.body['errors'][0]['message']);
}
return response.body['data'];
};
const getGuestToken = async (auth: BettermodeAuthType) => {
const query = `query GetGuestToken($domain: String!) {
tokens(networkDomain: $domain) {
accessToken
}
}`;
const variables = { domain: auth.domain };
const response = await bettermodeAPI(auth, query, variables);
auth.token = response.tokens.accessToken;
return auth;
};
export const getAuthToken = async (auth: BettermodeAuthType) => {
const query = `mutation getAuthToken($email: String!, $password: String!) {
loginNetwork(input:{usernameOrEmail: $email, password: $password}) {
accessToken
member {
id
name
}
}
}`;
const variables = {
email: auth.email,
password: auth.password,
};
auth = await getGuestToken(auth);
const response = await bettermodeAPI(auth, query, variables);
auth.token = response.loginNetwork.accessToken;
auth.memberId = response.loginNetwork.member.id;
return auth;
};
const getPostType = async (auth: BettermodeAuthType, postTypeName: string) => {
const query = `query getPostType($postTypeName: String!) {
postTypes(limit: 1, query: $postTypeName) {
nodes {
id
name
}
}
}`;
if (!auth.memberId) auth = await getAuthToken(auth);
const variables = { postTypeName: postTypeName };
const response = await bettermodeAPI(auth, query, variables);
return response.postTypes.nodes[0];
};
export const listBadges = async (auth: BettermodeAuthType) => {
const query = `query {
network {
badges {
id
name
}
}
}`;
if (!auth.memberId) auth = await getAuthToken(auth);
const response = await bettermodeAPI(auth, query);
return response.network.badges;
};
export const listMemberSpaces = async (auth: BettermodeAuthType) => {
const query = `query listMemberSpaces($memberId: ID!) {
spaces(memberId: $memberId, limit: 100) {
nodes {
id
name
}
}
}`;
if (!auth.memberId) auth = await getAuthToken(auth);
const variables = { memberId: auth.memberId };
const response = await bettermodeAPI(auth, query, variables);
return response.spaces.nodes;
};
const getMemberByEmail = async (auth: BettermodeAuthType, email: string) => {
const query = `query getMemberId($email: String!) {
members(limit: 1, query: $email) {
nodes {
id
name
}
}
}`;
if (!auth.memberId) auth = await getAuthToken(auth);
const variables = { email: email };
const response = await bettermodeAPI(auth, query, variables);
if (response.members.nodes.length == 0) {
throw new Error(`Member with email ${email} not found`);
}
return response.members.nodes[0];
};
export const assignBadgeToMember = async (
auth: BettermodeAuthType,
badgeId: string,
email: string
) => {
const query = `mutation assignBadgeToMember($badgeId: String!, $memberId: String!) {
assignBadge(
id: $badgeId,
input: {
memberId: $memberId,
}
) {
status
}
}`;
if (!auth.memberId) auth = await getAuthToken(auth);
const member = await getMemberByEmail(auth, email);
const variables = { badgeId: badgeId, memberId: member.id };
const response = await bettermodeAPI(auth, query, variables);
return response.assignBadge;
};
export const revokeBadgeFromMember = async (
auth: BettermodeAuthType,
badgeId: string,
email: string
) => {
const query = `mutation revokeBadgeFromMember($badgeId: String!, $memberId: String!) {
revokeBadge(
id: $badgeId,
input: {
memberId: $memberId,
}
) {
status
}
}`;
if (!auth.memberId) auth = await getAuthToken(auth);
const member = await getMemberByEmail(auth, email);
const variables = { badgeId: badgeId, memberId: member.id };
const response = await bettermodeAPI(auth, query, variables);
return response.revokeBadge;
};
export const createPostOfType = async (
auth: BettermodeAuthType,
postTypeName: string,
spaceId: string,
tagNames: string,
title: string,
content: string,
locked = false
) => {
const query = `mutation createPostOfType($spaceId: ID!, $postTypeId: String!, $locked: Boolean!, $tagNames: [String!], $title: String!, $content: String!) {
createPost(
spaceId : $spaceId,
input : {
postTypeId : $postTypeId,
locked : $locked,
publish : true,
tagNames : $tagNames,
mappingFields : [
{
key : "title",
type : text,
value : $title
},
{
key : "content",
type : html,
value : $content
}
]
}
) {
url
createdAt
}
}`;
auth = await getAuthToken(auth);
const postType = await getPostType(auth, postTypeName);
const variables = {
spaceId: spaceId,
postTypeId: postType.id,
locked: locked,
tagNames: tagNames.split(',').map((tag: string) => tag.trim()),
title: JSON.stringify(title),
content: JSON.stringify(content),
};
const response = await bettermodeAPI(auth, query, variables);
return response.createPost;
};
export const createDiscussion = async (
auth: BettermodeAuthType,
spaceId: string,
tagNames: string,
title: string,
content: string,
locked = false
) => {
return await createPostOfType(
auth,
'Discussion',
spaceId,
tagNames,
title,
content,
locked
);
};
export const createQuestion = async (
auth: BettermodeAuthType,
spaceId: string,
tagNames: string,
title: string,
content: string,
locked = false
) => {
return await createPostOfType(
auth,
'Question',
spaceId,
tagNames,
title,
content,
locked
);
};
// TODO: assignBadge to member

View File

@@ -0,0 +1,77 @@
import {
PieceAuth,
Property,
} from '@activepieces/pieces-framework';
import { z } from 'zod';
import { propsValidation } from '@activepieces/pieces-common';
import { getAuthToken } from './api';
export type BettermodeAuthType = {
region: string;
domain: string;
email: string;
password: string;
token?: string;
memberId?: string;
};
export const bettermodeAuth = PieceAuth.CustomAuth({
description:
'Your domain should be the base URL of your Bettermode community. Example: community.example.com',
props: {
region: Property.StaticDropdown({
displayName: 'Region',
description: 'The region of your Bettermode account',
required: true,
options: {
options: [
{ label: 'US Region', value: 'https://api.bettermode.com' },
{ label: 'EU Region', value: 'https://api.bettermode.de' },
],
},
}),
domain: Property.ShortText({
displayName: 'BetterMode Domain',
description: 'The domain of your Bettermode account',
required: true,
}),
email: Property.ShortText({
displayName: 'Email',
description: 'Email address for your Bettermode account',
required: true,
}),
password: PieceAuth.SecretText({
displayName: 'Password',
description: 'Password for your Bettermode account',
required: true,
}),
},
validate: async ({ auth }) => {
try {
await validateAuth(auth);
return {
valid: true,
};
} catch (e) {
return {
valid: false,
error: (e as Error)?.message,
};
}
},
required: true,
});
const validateAuth = async (auth: BettermodeAuthType) => {
await propsValidation.validateZod(auth, {
domain: z.string().url(),
email: z.string().email(),
});
const response = await getAuthToken(auth);
if (!response.memberId) {
throw new Error(
'Authentication failed. Please check your credentials and try again.'
);
}
};

View File

@@ -0,0 +1,36 @@
import { BettermodeAuthType } from './auth';
import { listMemberSpaces, listBadges } from './api';
export async function buildMemberSpacesDropdown(auth?: BettermodeAuthType) {
if (!auth) {
return {
options: [],
disabled: true,
placeholder: 'Please authenticate first',
};
}
const spaces = await listMemberSpaces(auth as BettermodeAuthType);
const options = spaces.map((space: { name: string; id: string }) => {
return { label: space.name, value: space.id };
});
return {
options: options,
};
}
export async function buildBadgesDropdown(auth?: BettermodeAuthType) {
if (!auth) {
return {
options: [],
disabled: true,
placeholder: 'Please authenticate first',
};
}
const badges = await listBadges(auth as BettermodeAuthType);
const options = badges.map((badge: { name: string; id: string }) => {
return { label: badge.name, value: badge.id };
});
return {
options: options,
};
}