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,55 @@
|
||||
import { Property, createAction } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { circleAuth } from '../common/auth';
|
||||
import { BASE_URL, spaceIdDropdown } from '../common';
|
||||
|
||||
interface AddMemberToSpacePayload {
|
||||
space_id: number;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export const addMemberToSpace = createAction({
|
||||
auth: circleAuth,
|
||||
name: 'add_member_to_space',
|
||||
displayName: 'Add Member to Space',
|
||||
description: 'Add an existing member to a specific space by their email.',
|
||||
props: {
|
||||
space_id: spaceIdDropdown,
|
||||
email: Property.ShortText({
|
||||
displayName: 'Member Email',
|
||||
description: 'The email address of the member to add to the space.',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { space_id, email } = context.propsValue;
|
||||
|
||||
if (space_id === undefined) {
|
||||
throw new Error('Space ID is undefined, but it is a required field.');
|
||||
}
|
||||
if (email === undefined) {
|
||||
throw new Error('Email is undefined, but it is a required field.');
|
||||
}
|
||||
|
||||
const payload: AddMemberToSpacePayload = {
|
||||
space_id: space_id,
|
||||
email: email,
|
||||
};
|
||||
|
||||
const response = await httpClient.sendRequest<{
|
||||
message?: string;
|
||||
success?: boolean;
|
||||
error_details?: unknown;
|
||||
}>({
|
||||
method: HttpMethod.POST,
|
||||
url: `${BASE_URL}/space_members`,
|
||||
body: payload,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.auth.secret_text}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return response.body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Property, createAction } from '@activepieces/pieces-framework';
|
||||
import { spaceIdDropdown, postIdDropdown, BASE_URL } from '../common';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { circleAuth } from '../common/auth';
|
||||
import { isNil } from '@activepieces/shared';
|
||||
|
||||
interface CreateCommentPayload {
|
||||
post_id: number;
|
||||
body: string;
|
||||
parent_comment_id?: number;
|
||||
skip_notifications?: boolean;
|
||||
}
|
||||
|
||||
export const createComment = createAction({
|
||||
auth: circleAuth,
|
||||
name: 'create_comment',
|
||||
displayName: 'Create Comment',
|
||||
description: 'Creates a new comment on a post.',
|
||||
props: {
|
||||
space_id: spaceIdDropdown,
|
||||
post_id: postIdDropdown,
|
||||
body: Property.LongText({
|
||||
displayName: 'Comment Body',
|
||||
description: 'The content of the comment.',
|
||||
required: true,
|
||||
}),
|
||||
parent_comment_id: Property.Number({
|
||||
displayName: 'Parent Comment ID (Optional)',
|
||||
description: 'ID of the comment to reply to. Leave empty if not a reply.',
|
||||
required: false,
|
||||
}),
|
||||
skip_notifications: Property.Checkbox({
|
||||
displayName: 'Skip Notifications',
|
||||
description: 'Skip sending notifications for this comment?',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { post_id, body, parent_comment_id, skip_notifications } = context.propsValue;
|
||||
|
||||
if (post_id === undefined) {
|
||||
throw new Error('Post ID is required but was not provided.');
|
||||
}
|
||||
if (body === undefined) {
|
||||
throw new Error('Comment body is required but was not provided.');
|
||||
}
|
||||
|
||||
const payload: CreateCommentPayload = {
|
||||
post_id: post_id,
|
||||
body: body,
|
||||
};
|
||||
|
||||
if (!isNil(parent_comment_id)) {
|
||||
payload.parent_comment_id = parent_comment_id;
|
||||
}
|
||||
if (skip_notifications !== undefined) {
|
||||
payload.skip_notifications = skip_notifications;
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${BASE_URL}/comments`,
|
||||
body: payload,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.auth.secret_text}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return response.body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
import { Property, createAction } from "@activepieces/pieces-framework";
|
||||
import { httpClient, HttpMethod } from "@activepieces/pieces-common";
|
||||
import { circleAuth } from "../common/auth";
|
||||
import { BASE_URL, spaceIdDropdown } from "../common";
|
||||
|
||||
// Interface for the TipTap body structure (simplified for payload)
|
||||
interface TipTapPayloadBody {
|
||||
type: string; // "doc"
|
||||
content: any[]; // Content structure for TipTap
|
||||
}
|
||||
|
||||
// Interface for the full payload to create a post
|
||||
interface CreatePostPayload {
|
||||
space_id: number;
|
||||
name: string;
|
||||
status?: string;
|
||||
tiptap_body: { body: TipTapPayloadBody };
|
||||
slug?: string;
|
||||
cover_image?: string;
|
||||
internal_custom_html?: string;
|
||||
is_truncation_disabled?: boolean;
|
||||
is_comments_closed?: boolean;
|
||||
is_comments_enabled?: boolean;
|
||||
is_liking_enabled?: boolean;
|
||||
hide_meta_info?: boolean;
|
||||
hide_from_featured_areas?: boolean;
|
||||
meta_title?: string;
|
||||
meta_description?: string;
|
||||
opengraph_title?: string;
|
||||
opengraph_description?: string;
|
||||
published_at?: string; // ISO 8601 string
|
||||
created_at?: string; // ISO 8601 string - usually set by server
|
||||
topics?: number[];
|
||||
skip_notifications?: boolean;
|
||||
is_pinned?: boolean;
|
||||
user_email?: string;
|
||||
user_id?: number;
|
||||
}
|
||||
|
||||
export const createPost = createAction({
|
||||
auth: circleAuth,
|
||||
name: 'create_post',
|
||||
displayName: 'Create Post',
|
||||
description: 'Creates a new post in a specific space.',
|
||||
props: {
|
||||
space_id: spaceIdDropdown,
|
||||
name: Property.ShortText({
|
||||
displayName: 'Post Name/Title',
|
||||
description: 'The title of the post.',
|
||||
required: true,
|
||||
}),
|
||||
text_body: Property.LongText({
|
||||
displayName: 'Post Body (Plain Text)',
|
||||
description: "Simple plain text content for the post. Used if 'Tiptap Body JSON' is not provided.",
|
||||
required: false,
|
||||
}),
|
||||
tiptap_body_json: Property.Json({
|
||||
displayName: 'Tiptap Body JSON',
|
||||
description: "Full TipTap JSON object for the post body. If provided, 'Post Body (Plain Text)' is ignored.",
|
||||
required: false,
|
||||
}),
|
||||
status: Property.StaticDropdown({
|
||||
displayName: 'Status',
|
||||
description: 'The status of the post.',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Draft', value: 'draft' },
|
||||
{ label: 'Published', value: 'published' },
|
||||
{ label: 'Scheduled', value: 'scheduled' },
|
||||
]
|
||||
},
|
||||
defaultValue: 'published',
|
||||
}),
|
||||
published_at: Property.DateTime({
|
||||
displayName: 'Published At (for Scheduled)',
|
||||
description: "If status is 'scheduled', provide the future date and time for publishing.",
|
||||
required: false,
|
||||
}),
|
||||
is_comments_enabled: Property.Checkbox({
|
||||
displayName: 'Enable Comments',
|
||||
description: 'Allow comments on this post?',
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
}),
|
||||
skip_notifications: Property.Checkbox({
|
||||
displayName: 'Skip Notifications',
|
||||
description: 'Prevent notifications from being sent for this post?',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
user_email: Property.ShortText({
|
||||
displayName: 'Post As User Email (Optional)',
|
||||
description: 'Email of an existing community member to create this post as. If empty, posts as the authenticated admin.',
|
||||
required: false,
|
||||
})
|
||||
},
|
||||
async run(context) {
|
||||
const {
|
||||
space_id, name, text_body, tiptap_body_json,
|
||||
status, published_at, is_comments_enabled,
|
||||
skip_notifications, user_email
|
||||
} = context.propsValue;
|
||||
|
||||
if (space_id === undefined) {
|
||||
throw new Error("Space ID is undefined, but it is a required field.");
|
||||
}
|
||||
if (name === undefined) {
|
||||
throw new Error("Post Name/Title is undefined, but it is a required field.");
|
||||
}
|
||||
|
||||
let finalTiptapBody: { body: TipTapPayloadBody };
|
||||
|
||||
if (tiptap_body_json && typeof tiptap_body_json === 'object' && (tiptap_body_json as any).body) {
|
||||
finalTiptapBody = tiptap_body_json as { body: TipTapPayloadBody };
|
||||
} else if (text_body) {
|
||||
finalTiptapBody = {
|
||||
body: {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{ type: "text", text: text_body }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
} else if (!text_body && !tiptap_body_json) {
|
||||
finalTiptapBody = {
|
||||
body: { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "" }] }] }
|
||||
};
|
||||
} else {
|
||||
throw new Error("Invalid body input. Provide either 'Post Body (Plain Text)' or a valid 'Tiptap Body JSON'. If both are empty, an empty post will be created.");
|
||||
}
|
||||
|
||||
const payload: CreatePostPayload = {
|
||||
space_id: space_id,
|
||||
name: name,
|
||||
tiptap_body: finalTiptapBody,
|
||||
status: status ?? 'published',
|
||||
};
|
||||
|
||||
if (published_at && payload.status === 'scheduled') {
|
||||
payload.published_at = new Date(published_at).toISOString();
|
||||
} else if (payload.status === 'scheduled' && !published_at) {
|
||||
// It's an error to have scheduled status without a published_at date.
|
||||
// However, the API might handle this. For now, we'll let it pass,
|
||||
// but this could be a validation point.
|
||||
}
|
||||
|
||||
if (is_comments_enabled !== undefined) {
|
||||
payload.is_comments_enabled = is_comments_enabled;
|
||||
payload.is_comments_closed = !is_comments_enabled;
|
||||
}
|
||||
if (skip_notifications !== undefined) {
|
||||
payload.skip_notifications = skip_notifications;
|
||||
}
|
||||
if (user_email) {
|
||||
payload.user_email = user_email;
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.POST,
|
||||
url: `${BASE_URL}/posts`,
|
||||
body: payload,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${context.auth.secret_text}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
return response.body;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Property, createAction } from "@activepieces/pieces-framework";
|
||||
import { BASE_URL } from "../common";
|
||||
import { httpClient, HttpMethod } from "@activepieces/pieces-common";
|
||||
import { circleAuth } from "../common/auth";
|
||||
import { CommunityMemberDetails } from "../common/types";
|
||||
|
||||
export const findMemberByEmail = createAction({
|
||||
auth: circleAuth,
|
||||
name: 'find_member_by_email',
|
||||
displayName: 'Find Member by Email',
|
||||
description: 'Finds a community member by their email address.',
|
||||
props: {
|
||||
email: Property.ShortText({
|
||||
displayName: 'Email',
|
||||
description: 'The email address of the member to find.',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { email } = context.propsValue;
|
||||
|
||||
if (email === undefined) {
|
||||
throw new Error("Email is undefined, but it is a required field.");
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest<CommunityMemberDetails>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${BASE_URL}/community_members/search`,
|
||||
queryParams: {
|
||||
email: email,
|
||||
},
|
||||
headers: {
|
||||
"Authorization": `Bearer ${context.auth.secret_text}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
});
|
||||
|
||||
return response.body;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { BASE_URL, communityMemberIdDropdown } from '../common';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { circleAuth } from '../common/auth';
|
||||
import { CommunityMemberDetails } from '../common/types';
|
||||
|
||||
export const getMemberDetails = createAction({
|
||||
auth: circleAuth,
|
||||
name: 'get_member_details',
|
||||
displayName: 'Get Member Details',
|
||||
description: 'Fetches the full profile details for a specific community member.',
|
||||
props: {
|
||||
member_id: communityMemberIdDropdown,
|
||||
},
|
||||
async run(context) {
|
||||
const { member_id } = context.propsValue;
|
||||
if (member_id === undefined) {
|
||||
throw new Error('Member ID is undefined, but it is a required field.');
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest<CommunityMemberDetails>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${BASE_URL}/community_members/${member_id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.auth.secret_text}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return response.body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { BASE_URL, spaceIdDropdown, postIdDropdown } from '../common';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { circleAuth } from '../common/auth';
|
||||
import { PostDetails } from '../common/types';
|
||||
|
||||
export const getPostDetailsAction = createAction({
|
||||
auth: circleAuth,
|
||||
name: 'get_post_details',
|
||||
displayName: 'Get Post Details',
|
||||
description: 'Retrieves the complete details of a specific post.',
|
||||
props: {
|
||||
space_id: spaceIdDropdown,
|
||||
post_id: postIdDropdown,
|
||||
},
|
||||
async run(context) {
|
||||
const { post_id } = context.propsValue;
|
||||
|
||||
if (post_id === undefined) {
|
||||
throw new Error('Post ID is undefined, but it is a required field.');
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest<PostDetails>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${BASE_URL}/posts/${post_id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.auth.secret_text}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return response.body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { PieceAuth } from "@activepieces/pieces-framework";
|
||||
|
||||
export const circleAuth = PieceAuth.SecretText({
|
||||
displayName: 'API Token',
|
||||
description: `You can obtain your API token by navigating to **Settings->Developers->Tokens**.`,
|
||||
required: true,
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { ListBasicPostsResponse, ListCommunityMembersResponse, ListSpacesResponse } from './types';
|
||||
import { circleAuth } from './auth';
|
||||
|
||||
export const BASE_URL = 'https://app.circle.so/api/admin/v2';
|
||||
|
||||
export const spaceIdDropdown = Property.Dropdown({
|
||||
displayName: 'Space',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
auth: circleAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Please connect your account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
const response = await httpClient.sendRequest<ListSpacesResponse>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${BASE_URL}/spaces`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth.secret_text}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
disabled: false,
|
||||
options: response.body.records.map((space) => ({
|
||||
label: space.name,
|
||||
value: space.id,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Error fetching spaces',
|
||||
options: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const postIdDropdown = Property.Dropdown({
|
||||
displayName: 'Post',
|
||||
required: true,
|
||||
refreshers: ['space_id'],
|
||||
auth: circleAuth,
|
||||
options: async ({ auth, space_id }) => {
|
||||
if (!auth || !space_id) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: !auth ? 'Please connect your account first' : 'Select a space first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
const response = await httpClient.sendRequest<ListBasicPostsResponse>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${BASE_URL}/posts`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth.secret_text}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
queryParams: {
|
||||
space_id: (space_id as number).toString(),
|
||||
status: 'all', // Fetch all posts for selection
|
||||
},
|
||||
});
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
disabled: false,
|
||||
options: response.body.records.map((post) => ({
|
||||
label: post.name,
|
||||
value: post.id,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Error fetching posts or no posts found in space.',
|
||||
options: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const communityMemberIdDropdown = Property.Dropdown({
|
||||
displayName: 'Community Member',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
auth: circleAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return { disabled: true, placeholder: 'Please authenticate first', options: [] };
|
||||
}
|
||||
const response = await httpClient.sendRequest<ListCommunityMembersResponse>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${BASE_URL}/community_members`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth.secret_text}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
queryParams: { status: 'all' },
|
||||
});
|
||||
if (response.status === 200 && response.body.records) {
|
||||
return {
|
||||
disabled: false,
|
||||
options: response.body.records.map((member) => ({
|
||||
label: `${member.name} (${member.email})`,
|
||||
value: member.id,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Error fetching community members or no members found',
|
||||
options: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
export interface Space {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ListSpacesResponse {
|
||||
records: Space[];
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
has_next_page?: boolean;
|
||||
count?: number;
|
||||
page_count?: number;
|
||||
}
|
||||
|
||||
// Interface for individual post item based on 'List Basic Posts' records
|
||||
export interface BasicPostFromList {
|
||||
id: number;
|
||||
status: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
comments_count: number;
|
||||
hide_meta_info: boolean;
|
||||
published_at: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
is_comments_enabled: boolean;
|
||||
is_liking_enabled: boolean;
|
||||
flagged_for_approval_at: string | null;
|
||||
body: {
|
||||
id: number;
|
||||
name: string; // e.g., "body"
|
||||
body: string; // HTML content snippet
|
||||
record_type: string; // "Post"
|
||||
record_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
url: string;
|
||||
space_name: string;
|
||||
space_slug: string;
|
||||
space_id: number;
|
||||
user_id: number;
|
||||
user_email: string;
|
||||
user_name: string;
|
||||
community_id: number;
|
||||
user_avatar_url: string | null;
|
||||
cover_image_url: string | null;
|
||||
cover_image: string | null;
|
||||
cardview_thumbnail_url: string | null;
|
||||
cardview_thumbnail: string | null;
|
||||
is_comments_closed: boolean;
|
||||
custom_html: string | null;
|
||||
likes_count: number;
|
||||
member_posts_count: number;
|
||||
member_comments_count: number;
|
||||
member_likes_count: number;
|
||||
topics: number[];
|
||||
}
|
||||
|
||||
// Interface based on the 'List Basic Posts' API response
|
||||
export interface ListBasicPostsResponse {
|
||||
page: number;
|
||||
per_page: number;
|
||||
has_next_page: boolean;
|
||||
count: number;
|
||||
page_count: number;
|
||||
records: BasicPostFromList[];
|
||||
}
|
||||
|
||||
// --- Shared Member Profile Sub-Interfaces ---
|
||||
export interface ProfileFieldChoice {
|
||||
id: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface CommunityMemberProfileFieldChoice {
|
||||
id: number;
|
||||
profile_field_choice: ProfileFieldChoice;
|
||||
}
|
||||
|
||||
export interface CommunityMemberProfileFieldDetails {
|
||||
id: number;
|
||||
text: string | null;
|
||||
textarea: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
display_value: string[] | null;
|
||||
community_member_choices: CommunityMemberProfileFieldChoice[];
|
||||
}
|
||||
|
||||
export interface ProfileFieldPage {
|
||||
id: number;
|
||||
name: string;
|
||||
position: number;
|
||||
visible: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ProfileField {
|
||||
id: number;
|
||||
label: string;
|
||||
field_type: string;
|
||||
key: string;
|
||||
placeholder: string | null;
|
||||
description: string | null;
|
||||
required: boolean;
|
||||
platform_field: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
community_member_profile_field: CommunityMemberProfileFieldDetails | null;
|
||||
number_options: any | null;
|
||||
choices: ProfileFieldChoice[];
|
||||
pages: ProfileFieldPage[];
|
||||
}
|
||||
|
||||
export interface MemberTag {
|
||||
name: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface GamificationStats {
|
||||
community_member_id: number;
|
||||
total_points: number;
|
||||
current_level: number;
|
||||
current_level_name: string;
|
||||
points_to_next_level: number;
|
||||
level_progress: number;
|
||||
}
|
||||
|
||||
export interface CommunityMemberListItem {
|
||||
id: number;
|
||||
name: string; // Full name
|
||||
email: string;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
headline: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
community_id: number;
|
||||
last_seen_at: string | null;
|
||||
profile_confirmed_at: string | null;
|
||||
profile_url: string;
|
||||
public_uid: string;
|
||||
avatar_url: string | null;
|
||||
user_id: number; // This is the user_id associated with the community_member, not the community_member.id
|
||||
active: boolean;
|
||||
sso_provider_user_id: string | null;
|
||||
accepted_invitation: string | null;
|
||||
profile_fields: ProfileField[];
|
||||
flattened_profile_fields: Record<string, string[] | null>;
|
||||
member_tags: MemberTag[];
|
||||
posts_count: number;
|
||||
comments_count: number;
|
||||
gamification_stats: GamificationStats;
|
||||
}
|
||||
export interface ListCommunityMembersResponse {
|
||||
page: number;
|
||||
per_page: number;
|
||||
has_next_page: boolean;
|
||||
count: number;
|
||||
page_count: number;
|
||||
records: CommunityMemberListItem[];
|
||||
}
|
||||
|
||||
export interface CommunityMemberDetails {
|
||||
id: number;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
headline: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
community_id: number;
|
||||
last_seen_at: string | null;
|
||||
profile_confirmed_at: string | null;
|
||||
profile_url: string;
|
||||
public_uid: string;
|
||||
profile_fields: ProfileField[];
|
||||
flattened_profile_fields: Record<string, string[] | null>;
|
||||
avatar_url: string | null;
|
||||
user_id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
accepted_invitation: string | null;
|
||||
active: boolean;
|
||||
sso_provider_user_id: string | null;
|
||||
member_tags: MemberTag[];
|
||||
posts_count: number;
|
||||
comments_count: number;
|
||||
gamification_stats: GamificationStats;
|
||||
}
|
||||
|
||||
|
||||
interface PostBody {
|
||||
id: number;
|
||||
name: string; // "body"
|
||||
body: string; // HTML content
|
||||
record_type: string; // "Post"
|
||||
record_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface TipTapMark {
|
||||
type: string;
|
||||
attrs?: Record<string, unknown>; // Example: { href: 'url' } for a link mark
|
||||
}
|
||||
|
||||
interface TipTapContentItem {
|
||||
type: string;
|
||||
text?: string;
|
||||
marks?: TipTapMark[];
|
||||
attrs?: Record<string, unknown>;
|
||||
content?: TipTapContentItem[];
|
||||
circle_ios_fallback_text?: string;
|
||||
}
|
||||
|
||||
interface TipTapBody {
|
||||
body: {
|
||||
type: string; // "doc"
|
||||
content: TipTapContentItem[];
|
||||
};
|
||||
circle_ios_fallback_text?: string;
|
||||
attachments?: unknown[];
|
||||
inline_attachments?: unknown[];
|
||||
sgids_to_object_map?: Record<string, unknown>;
|
||||
format?: string; // "post"
|
||||
community_members?: unknown[];
|
||||
entities?: unknown[];
|
||||
group_mentions?: unknown[];
|
||||
polls?: unknown[];
|
||||
}
|
||||
|
||||
export interface PostDetails {
|
||||
id: number;
|
||||
status: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
comments_count: number;
|
||||
hide_meta_info: boolean;
|
||||
published_at: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
is_comments_enabled: boolean;
|
||||
is_liking_enabled: boolean;
|
||||
flagged_for_approval_at: string | null;
|
||||
body: PostBody;
|
||||
tiptap_body: TipTapBody;
|
||||
url: string;
|
||||
space_name: string;
|
||||
space_slug: string;
|
||||
space_id: number;
|
||||
user_id: number;
|
||||
user_email: string;
|
||||
user_name: string;
|
||||
community_id: number;
|
||||
user_avatar_url: string | null;
|
||||
cover_image_url: string | null;
|
||||
cover_image: string | null; // This seems to be an identifier string
|
||||
cardview_thumbnail_url: string | null;
|
||||
cardview_thumbnail: string | null; // Also an identifier
|
||||
is_comments_closed: boolean;
|
||||
custom_html: string | null;
|
||||
likes_count: number;
|
||||
member_posts_count: number;
|
||||
member_comments_count: number;
|
||||
member_likes_count: number;
|
||||
topics: number[];
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
AppConnectionValueForAuthProperty,
|
||||
PiecePropValueSchema,
|
||||
TriggerStrategy,
|
||||
createTrigger,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import { BASE_URL } from '../common';
|
||||
import {
|
||||
DedupeStrategy,
|
||||
httpClient,
|
||||
HttpMethod,
|
||||
Polling,
|
||||
pollingHelper,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { circleAuth } from '../common/auth';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { ListCommunityMembersResponse } from '../common/types';
|
||||
|
||||
const polling: Polling<AppConnectionValueForAuthProperty<typeof circleAuth>, Record<string, any>> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
async items({ auth, lastFetchEpochMS }) {
|
||||
let page = 1;
|
||||
let hasMorePages = true;
|
||||
let stopFetching = false;
|
||||
|
||||
const members = [];
|
||||
|
||||
do {
|
||||
const response = await httpClient.sendRequest<ListCommunityMembersResponse>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${BASE_URL}/community_members`,
|
||||
queryParams: {
|
||||
page: page.toString(),
|
||||
per_page: '50',
|
||||
status: 'all',
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth.secret_text}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const items = response.body.records || [];
|
||||
|
||||
for (const member of items) {
|
||||
const publishedAt = dayjs(member.created_at).valueOf();
|
||||
|
||||
if (publishedAt < lastFetchEpochMS) {
|
||||
stopFetching = true;
|
||||
break;
|
||||
}
|
||||
|
||||
members.push(member);
|
||||
}
|
||||
|
||||
if (stopFetching || lastFetchEpochMS === 0) break;
|
||||
|
||||
page++;
|
||||
hasMorePages = response.body.has_next_page;
|
||||
} while (hasMorePages);
|
||||
|
||||
return members.map((member) => {
|
||||
return {
|
||||
epochMilliSeconds: dayjs(member.created_at).valueOf(),
|
||||
data: member,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const newMemberAdded = createTrigger({
|
||||
auth: circleAuth,
|
||||
name: 'new_member_added',
|
||||
displayName: 'New Member Added',
|
||||
description: 'Triggers when a new member is added to the community.',
|
||||
props: {},
|
||||
type: TriggerStrategy.POLLING,
|
||||
async onEnable(context) {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async onDisable(context) {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
sampleData: {
|
||||
first_name: 'Gov.',
|
||||
last_name: 'Loriann Barton',
|
||||
headline: 'Sales Orchestrator',
|
||||
created_at: '2024-09-03T16:20:19.814Z',
|
||||
updated_at: '2024-09-03T16:20:19.826Z',
|
||||
community_id: 1,
|
||||
last_seen_at: null,
|
||||
profile_confirmed_at: '2024-09-03T16:20:19.000Z',
|
||||
id: 2,
|
||||
profile_url: 'http://reynolds.circledev.net:31337/u/352c3aff',
|
||||
public_uid: '352c3aff',
|
||||
profile_fields: [],
|
||||
flattened_profile_fields: {
|
||||
profile_field_key_1: null,
|
||||
},
|
||||
avatar_url: null,
|
||||
user_id: 3,
|
||||
name: 'Gov. Loriann Barton',
|
||||
email: 'raul@nitzsche.org',
|
||||
accepted_invitation: '2024-09-03 16:20:19 UTC',
|
||||
active: true,
|
||||
sso_provider_user_id: null,
|
||||
member_tags: [],
|
||||
posts_count: 0,
|
||||
comments_count: 0,
|
||||
gamification_stats: {
|
||||
community_member_id: 2,
|
||||
total_points: 0,
|
||||
current_level: 1,
|
||||
current_level_name: 'Level 1',
|
||||
points_to_next_level: 50,
|
||||
level_progress: 50,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
AppConnectionValueForAuthProperty,
|
||||
TriggerStrategy,
|
||||
createTrigger,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import { BASE_URL, spaceIdDropdown } from '../common';
|
||||
import {
|
||||
DedupeStrategy,
|
||||
httpClient,
|
||||
HttpMethod,
|
||||
Polling,
|
||||
pollingHelper,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { circleAuth } from '../common/auth';
|
||||
import { ListBasicPostsResponse } from '../common/types';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const polling: Polling<AppConnectionValueForAuthProperty<typeof circleAuth>, { space_id?: number }> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
async items({ auth, propsValue, lastFetchEpochMS }) {
|
||||
const spaceId = propsValue.space_id!;
|
||||
|
||||
let page = 1;
|
||||
let hasMorePages = true;
|
||||
let stopFetching = false;
|
||||
|
||||
const posts = [];
|
||||
|
||||
do {
|
||||
const response = await httpClient.sendRequest<ListBasicPostsResponse>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${BASE_URL}/posts`,
|
||||
queryParams: {
|
||||
space_id: spaceId.toString(),
|
||||
status: 'published',
|
||||
sort: 'latest',
|
||||
page: page.toString(),
|
||||
per_page: '60',
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const items = response.body.records || [];
|
||||
|
||||
for (const post of items) {
|
||||
const publishedAt = dayjs(post.published_at).valueOf();
|
||||
|
||||
if (publishedAt < lastFetchEpochMS) {
|
||||
stopFetching = true;
|
||||
break;
|
||||
}
|
||||
|
||||
posts.push(post);
|
||||
}
|
||||
|
||||
if (stopFetching || lastFetchEpochMS === 0) break;
|
||||
|
||||
page++;
|
||||
hasMorePages = response.body.has_next_page;
|
||||
} while (hasMorePages);
|
||||
|
||||
return posts.map((post) => {
|
||||
return {
|
||||
epochMilliSeconds: dayjs(post.published_at).valueOf(),
|
||||
data: post,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const newPostCreated = createTrigger({
|
||||
auth: circleAuth,
|
||||
name: 'new_post_created',
|
||||
displayName: 'New Post Created',
|
||||
description: 'Triggers when a new post is created in a specific space.',
|
||||
props: {
|
||||
space_id: spaceIdDropdown,
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
async onEnable(context) {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async onDisable(context) {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
sampleData: {
|
||||
id: 2,
|
||||
status: 'published',
|
||||
name: 'Second post',
|
||||
slug: 'kiehn',
|
||||
comments_count: 0,
|
||||
hide_meta_info: false,
|
||||
published_at: '2024-06-27T08:31:30.777Z',
|
||||
created_at: '2024-06-27T08:31:30.781Z',
|
||||
updated_at: '2024-06-27T08:31:30.784Z',
|
||||
is_comments_enabled: true,
|
||||
is_liking_enabled: true,
|
||||
flagged_for_approval_at: null,
|
||||
body: {
|
||||
id: 2,
|
||||
name: 'body',
|
||||
body: '<div><!--block-->Iusto sint asperiores sed.</div>',
|
||||
record_type: 'Post',
|
||||
record_id: 2,
|
||||
created_at: '2024-06-27T08:31:30.000Z',
|
||||
updated_at: '2024-06-27T08:31:30.000Z',
|
||||
},
|
||||
url: 'http://dickinson.circledev.net:31337/c/post/kiehn',
|
||||
space_name: 'post',
|
||||
space_slug: 'post',
|
||||
space_id: 1,
|
||||
user_id: 6,
|
||||
user_email: 'lyndon@frami.info',
|
||||
user_name: 'Rory Wyman',
|
||||
community_id: 1,
|
||||
user_avatar_url: 'https://example.com/avatar.png',
|
||||
cover_image_url: 'http://example.com/cover.jpeg',
|
||||
cover_image: 'identifier-string',
|
||||
cardview_thumbnail_url: 'http://example.com/thumbnail.jpeg',
|
||||
cardview_thumbnail: 'identifier-string',
|
||||
is_comments_closed: false,
|
||||
custom_html: '<div>Click Me!</div>',
|
||||
likes_count: 0,
|
||||
member_posts_count: 2,
|
||||
member_comments_count: 0,
|
||||
member_likes_count: 0,
|
||||
topics: [12, 43, 54],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user