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,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
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
@@ -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.'
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user