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,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;
},
});

View File

@@ -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;
},
});

View File

@@ -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;
}
});

View File

@@ -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;
}
});

View File

@@ -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;
},
});

View File

@@ -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;
},
});

View File

@@ -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,
});

View File

@@ -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: [],
};
},
});

View File

@@ -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[];
}

View File

@@ -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,
},
},
});

View File

@@ -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],
},
});