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,57 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { bitlyApiCall } from '../common/client';
import { bitlyAuth } from '../common/auth';
import { bitlinkDropdown, groupGuid } from '../common/props';
export const archiveBitlinkAction = createAction({
auth: bitlyAuth,
name: 'archive_bitlink',
displayName: 'Archive Bitlink',
description: 'Archive a Bitlink to stop redirects.',
props: {
group_guid: groupGuid,
bitlink: bitlinkDropdown,
},
async run(context) {
const { bitlink } = context.propsValue;
try {
const body = {
archived: true,
};
return await bitlyApiCall({
method: HttpMethod.PATCH,
auth: context.auth.props,
resourceUri: `/bitlinks/${bitlink}`,
body,
});
} catch (error: any) {
const errorMessage = error.response?.data?.description || error.response?.data?.message || error.message;
if (error.response?.status === 429) {
throw new Error(
'Rate limit exceeded. Please wait before trying again.'
);
}
if (error.response?.status === 404) {
throw new Error(
`Bitlink not found: ${errorMessage}. Please verify the link (e.g., 'bit.ly/xyz123') is correct.`
);
}
if (error.response?.status === 401 || error.response?.status === 403) {
throw new Error(
`Authentication failed or forbidden: ${errorMessage}. Please check your Access Token and permissions.`
);
}
throw new Error(
`Failed to archive Bitlink: ${errorMessage || 'Unknown error occurred'}`
);
}
},
});

View File

@@ -0,0 +1,156 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { bitlyApiCall } from '../common/client';
import { bitlyAuth } from '../common/auth';
import { groupGuid, domain } from '../common/props';
export const createBitlinkAction = createAction({
auth: bitlyAuth,
name: 'create_bitlink',
displayName: 'Create Bitlink',
description: 'Shorten a long URL with optional customization.',
props: {
long_url: Property.ShortText({
displayName: 'Long URL',
description: 'The URL to shorten (must include http:// or https://).',
required: true,
}),
group_guid: groupGuid,
domain: {
...domain,
defaultValue: 'bit.ly',
},
title: Property.ShortText({
displayName: 'Title',
description: 'Custom title for the Bitlink.',
required: false,
}),
tags: Property.Array({
displayName: 'Tags',
description: 'Tags to apply to the Bitlink.',
required: false,
}),
force_new_link: Property.Checkbox({
displayName: 'Force New Link',
description: 'Create new link even if one exists for this URL.',
required: false,
defaultValue: false,
}),
// Mobile App Deeplink Configuration
app_id: Property.ShortText({
displayName: 'Mobile App ID',
description: 'Mobile app identifier (e.g., com.yourapp.name).',
required: false,
}),
app_uri_path: Property.ShortText({
displayName: 'App URI Path',
description: 'Path within the mobile app (e.g., /product/123).',
required: false,
}),
install_url: Property.LongText({
displayName: 'App Install URL',
description: 'URL where users can install the mobile app.',
required: false,
}),
install_type: Property.StaticDropdown({
displayName: 'Install Type',
description: 'How to handle app installation.',
required: false,
options: {
disabled: false,
options: [
{ label: 'No Install', value: 'no_install' },
{ label: 'Auto Install', value: 'auto_install' },
{ label: 'Promote Install', value: 'promote_install' },
],
},
}),
},
async run(context) {
const {
long_url,
group_guid,
domain,
title,
tags,
force_new_link,
app_id,
app_uri_path,
install_url,
install_type,
} = context.propsValue;
try {
// Pre-flight validation
if (!long_url.startsWith('http://') && !long_url.startsWith('https://')) {
throw new Error(
"Invalid Long URL. It must start with 'http://' or 'https://'."
);
}
const body: Record<string, unknown> = { long_url };
if (group_guid) {
body['group_guid'] = group_guid;
}
if (domain) {
body['domain'] = domain;
}
if (title) {
body['title'] = title;
}
if (tags && tags.length > 0) {
body['tags'] = tags;
}
if (force_new_link) {
body['force_new_link'] = force_new_link;
}
// Build deeplinks array if app configuration is provided
if (app_id && app_uri_path && install_url && install_type) {
body['deeplinks'] = [
{
app_id,
app_uri_path,
install_url,
install_type,
},
];
}
return await bitlyApiCall({
method: HttpMethod.POST,
auth: context.auth.props,
resourceUri: '/bitlinks',
body,
});
} catch (error: any) {
const errorMessage =
error.response?.data?.description ||
error.response?.data?.message ||
error.message;
if (error.response?.status === 429) {
throw new Error(
'Rate limit exceeded. Please wait before trying again.'
);
}
if (error.response?.status === 422) {
throw new Error(
`Unprocessable Entity: ${errorMessage}. Please check the format of your Long URL or other inputs.`
);
}
if (error.response?.status === 401 || error.response?.status === 403) {
throw new Error(
`Authentication failed or forbidden: ${errorMessage}. Please check your Access Token and permissions.`
);
}
throw new Error(
`Failed to create Bitlink: ${errorMessage || 'Unknown error occurred'}`
);
}
},
});

View File

@@ -0,0 +1,396 @@
import { HttpMethod } from '@activepieces/pieces-common';
import {
createAction,
Property,
DynamicPropsValue,
} from '@activepieces/pieces-framework';
import { bitlyApiCall } from '../common/client';
import { bitlyAuth } from '../common/auth';
import { groupGuid } from '../common/props';
export const createQrCodeAction = createAction({
auth: bitlyAuth,
name: 'create_qr_code',
displayName: 'Create QR Code',
description: 'Generate a customized QR code for a Bitlink.',
props: {
group_guid: groupGuid,
destination_type: Property.StaticDropdown({
displayName: 'Destination Type',
required: true,
defaultValue: 'long_url',
options: {
options: [
{ label: 'Long URL', value: 'long_url' },
{ label: 'Existing Bitlink', value: 'bitlink_id' },
],
},
}),
destination: Property.DynamicProperties({
auth: bitlyAuth,
displayName: 'Destination',
required: true,
refreshers: ['destination_type'],
props: async (
propsValue: Record<string, unknown>,
) => {
const destination_type = propsValue[
'destination_type'
] as unknown as string;
const props: DynamicPropsValue = {};
if (destination_type === 'long_url') {
props['long_url'] = Property.ShortText({
displayName: 'Long URL',
required: true,
});
} else if (destination_type === 'bitlink_id') {
props['bitlink_id'] = Property.ShortText({
displayName: 'Bitlink (e.g., bit.ly/xyz)',
required: true,
});
}
return props;
},
}),
title: Property.ShortText({
displayName: 'Title',
description: 'Internal title for the QR Code.',
required: false,
}),
archived: Property.Checkbox({
displayName: 'Archive on Create',
description: 'Archive the QR code upon creation.',
required: false,
}),
background_color: Property.ShortText({
displayName: 'Style: Background Color',
description: 'Hex code (e.g., #FFFFFF)',
required: false,
}),
dot_pattern_color: Property.ShortText({
displayName: 'Style: Dot Pattern Color',
description: 'Hex code (e.g., #000000)',
required: false,
}),
dot_pattern_type: Property.StaticDropdown({
displayName: 'Style: Dot Pattern Type',
required: false,
options: {
options: [
{ label: 'Standard', value: 'standard' },
{ label: 'Circle', value: 'circle' },
{ label: 'Block', value: 'block' },
{ label: 'Blob', value: 'blob' },
{ label: 'Rounded', value: 'rounded' },
{ label: 'Vertical', value: 'vertical' },
{ label: 'Horizontal', value: 'horizontal' },
{ label: 'Triangle', value: 'triangle' },
{ label: 'Heart', value: 'heart' },
{ label: 'Star', value: 'star' },
{ label: 'Diamond', value: 'diamond' },
],
},
}),
corner_1_shape: Property.StaticDropdown({
displayName: 'Corner 1 (Top-Left): Shape',
required: false,
options: {
options: [
{ label: 'Standard', value: 'standard' },
{ label: 'Slightly Round', value: 'slightly_round' },
{ label: 'Rounded', value: 'rounded' },
{ label: 'Extra Round', value: 'extra_round' },
{ label: 'Leaf', value: 'leaf' },
{ label: 'Leaf Inner', value: 'leaf_inner' },
{ label: 'Leaf Outer', value: 'leaf_outer' },
{ label: 'Target', value: 'target' },
{ label: 'Concave', value: 'concave' },
],
},
}),
corner_1_inner_color: Property.ShortText({
displayName: 'Corner 1 (Top-Left): Inner Color',
required: false,
}),
corner_1_outer_color: Property.ShortText({
displayName: 'Corner 1 (Top-Left): Outer Color',
required: false,
}),
corner_2_shape: Property.StaticDropdown({
displayName: 'Corner 2 (Top-Right): Shape',
required: false,
options: {
options: [
{ label: 'Standard', value: 'standard' },
{ label: 'Slightly Round', value: 'slightly_round' },
{ label: 'Rounded', value: 'rounded' },
{ label: 'Extra Round', value: 'extra_round' },
{ label: 'Leaf', value: 'leaf' },
{ label: 'Leaf Inner', value: 'leaf_inner' },
{ label: 'Leaf Outer', value: 'leaf_outer' },
{ label: 'Target', value: 'target' },
{ label: 'Concave', value: 'concave' },
],
},
}),
corner_2_inner_color: Property.ShortText({
displayName: 'Corner 2 (Top-Right): Inner Color',
required: false,
}),
corner_2_outer_color: Property.ShortText({
displayName: 'Corner 2 (Top-Right): Outer Color',
required: false,
}),
corner_3_shape: Property.StaticDropdown({
displayName: 'Corner 3 (Bottom-Right): Shape',
required: false,
options: {
options: [
{ label: 'Standard', value: 'standard' },
{ label: 'Slightly Round', value: 'slightly_round' },
{ label: 'Rounded', value: 'rounded' },
{ label: 'Extra Round', value: 'extra_round' },
{ label: 'Leaf', value: 'leaf' },
{ label: 'Leaf Inner', value: 'leaf_inner' },
{ label: 'Leaf Outer', value: 'leaf_outer' },
{ label: 'Target', value: 'target' },
{ label: 'Concave', value: 'concave' },
],
},
}),
corner_3_inner_color: Property.ShortText({
displayName: 'Corner 3 (Bottom-Right): Inner Color',
required: false,
}),
corner_3_outer_color: Property.ShortText({
displayName: 'Corner 3 (Bottom-Right): Outer Color',
required: false,
}),
gradient_style: Property.StaticDropdown({
displayName: 'Gradient: Style',
required: false,
options: {
options: [
{ label: 'No Gradient', value: 'no_gradient' },
{ label: 'Linear', value: 'linear' },
{ label: 'Radial', value: 'radial' },
],
},
}),
gradient_color_1: Property.ShortText({
displayName: 'Gradient: Color 1',
description: 'First gradient color (hex code)',
required: false,
}),
gradient_color_2: Property.ShortText({
displayName: 'Gradient: Color 2',
description: 'Second gradient color (hex code)',
required: false,
}),
gradient_angle: Property.Number({
displayName: 'Gradient: Angle (for Linear)',
required: false,
}),
gradient_exclude_corners: Property.Checkbox({
displayName: 'Gradient: Exclude Corners',
required: false,
}),
frame_id: Property.StaticDropdown({
displayName: 'Frame: Type',
required: false,
options: {
options: [
{ label: 'None', value: 'none' },
{ label: 'Border Only', value: 'border_only' },
{ label: 'Text Bottom', value: 'text_bottom' },
{ label: 'Tooltip Bottom', value: 'tooltip_bottom' },
{ label: 'Arrow', value: 'arrow' },
{ label: 'Text Top', value: 'text_top' },
{ label: 'Text Bottom In Frame', value: 'text_bottom_in_frame' },
{ label: 'Script', value: 'script' },
{ label: 'Text Top and Bottom', value: 'text_top_and_bottom' },
{ label: 'URL', value: 'url' },
{ label: 'Instagram', value: 'instagram' },
],
},
}),
frame_primary_color: Property.ShortText({
displayName: 'Frame: Primary Color',
required: false,
}),
frame_secondary_color: Property.ShortText({
displayName: 'Frame: Secondary Color',
required: false,
}),
frame_background_color: Property.ShortText({
displayName: 'Frame: Background Color',
required: false,
}),
frame_text: Property.ShortText({
displayName: 'Frame: Text',
description: 'Primary text for frames that support it.',
required: false,
}),
logo_image_guid: Property.ShortText({
displayName: 'Branding: Logo Image GUID',
description: 'A GUID for a logo image previously uploaded to Bitly.',
required: false,
}),
bitly_brand: Property.Checkbox({
displayName: 'Branding: Show Bitly Logo',
description: 'Show the Bitly logo in the bottom right corner.',
required: false,
defaultValue: true,
}),
error_correction: Property.StaticDropdown({
displayName: 'Specs: Error Correction',
required: false,
options: {
options: [
{ label: 'Low (1)', value: 1 },
{ label: 'Medium (2)', value: 2 },
{ label: 'Quartile (3)', value: 3 },
{ label: 'High (4)', value: 4 },
],
},
}),
},
async run(context) {
const props = context.propsValue;
try {
const body: any = {
group_guid: props.group_guid,
destination: { ...props.destination },
};
if (props.title) body.title = props.title;
if (props.archived) body.archived = props.archived;
const customizations: any = {};
if (props.background_color)
customizations.background_color = props.background_color;
if (props.dot_pattern_color)
customizations.dot_pattern_color = props.dot_pattern_color;
if (props.dot_pattern_type)
customizations.dot_pattern_type = props.dot_pattern_type;
const corners: any = {};
if (
props.corner_1_shape ||
props.corner_1_inner_color ||
props.corner_1_outer_color
)
corners.corner_1 = {
shape: props.corner_1_shape,
inner_color: props.corner_1_inner_color,
outer_color: props.corner_1_outer_color,
};
if (
props.corner_2_shape ||
props.corner_2_inner_color ||
props.corner_2_outer_color
)
corners.corner_2 = {
shape: props.corner_2_shape,
inner_color: props.corner_2_inner_color,
outer_color: props.corner_2_outer_color,
};
if (
props.corner_3_shape ||
props.corner_3_inner_color ||
props.corner_3_outer_color
)
corners.corner_3 = {
shape: props.corner_3_shape,
inner_color: props.corner_3_inner_color,
outer_color: props.corner_3_outer_color,
};
if (Object.keys(corners).length > 0) customizations.corners = corners;
const gradient: any = {};
if (props.gradient_style) gradient.style = props.gradient_style;
if (props.gradient_angle) gradient.angle = props.gradient_angle;
if (props.gradient_exclude_corners)
gradient.exclude_corners = props.gradient_exclude_corners;
// Build gradient colors array from individual color inputs
if (props.gradient_color_1 || props.gradient_color_2) {
const colors = [];
if (props.gradient_color_1) {
colors.push({ color: props.gradient_color_1, offset: 0 });
}
if (props.gradient_color_2) {
colors.push({ color: props.gradient_color_2, offset: 100 });
}
gradient.colors = colors;
}
if (Object.keys(gradient).length > 0) customizations.gradient = gradient;
const frame: any = {};
if (props.frame_id) frame.id = props.frame_id;
const frameColors: any = {};
if (props.frame_primary_color)
frameColors.primary = props.frame_primary_color;
if (props.frame_secondary_color)
frameColors.secondary = props.frame_secondary_color;
if (props.frame_background_color)
frameColors.background = props.frame_background_color;
if (Object.keys(frameColors).length > 0) frame.colors = frameColors;
if (props.frame_text)
frame.text = { primary: { content: props.frame_text } };
if (Object.keys(frame).length > 0) customizations.frame = frame;
const branding: any = {};
if (props.bitly_brand !== undefined)
branding.bitly_brand = props.bitly_brand;
if (Object.keys(branding).length > 0) customizations.branding = branding;
const logo: any = {};
if (props.logo_image_guid) logo.image_guid = props.logo_image_guid;
if (Object.keys(logo).length > 0) customizations.logo = logo;
const specSettings: any = {};
if (props.error_correction)
specSettings.error_correction = props.error_correction;
if (Object.keys(specSettings).length > 0)
customizations.spec_settings = specSettings;
if (Object.keys(customizations).length > 0)
body.render_customizations = customizations;
return await bitlyApiCall({
method: HttpMethod.POST,
auth: context.auth.props,
resourceUri: '/qr-codes',
body,
});
} catch (error: any) {
const errorMessage =
error.response?.data?.description ||
error.response?.data?.message ||
error.message;
if (error.response?.status === 429) {
throw new Error(
'Rate limit exceeded. Please wait before trying again.'
);
}
if (error.response?.status === 422) {
throw new Error(
`Unprocessable Entity: ${errorMessage}. Please check the format of your Long URL or other inputs.`
);
}
if (error.response?.status === 401 || error.response?.status === 403) {
throw new Error(
`Authentication failed or forbidden: ${errorMessage}. Please check your Access Token and permissions.`
);
}
throw new Error(
`Failed to create QR Code: ${errorMessage || 'Unknown error occurred'}`
);
}
},
});

View File

@@ -0,0 +1,52 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { bitlyApiCall } from '../common/client';
import { bitlyAuth } from '../common/auth';
import { bitlinkDropdown, groupGuid } from '../common/props';
export const getBitlinkDetailsAction = createAction({
auth: bitlyAuth,
name: 'get_bitlink_details',
displayName: 'Get Bitlink Details',
description: 'Retrieve metadata for a Bitlink.',
props: {
group_guid: groupGuid,
bitlink: bitlinkDropdown,
},
async run(context) {
const { bitlink } = context.propsValue;
try {
return await bitlyApiCall({
method: HttpMethod.GET,
auth: context.auth.props,
resourceUri: `/bitlinks/${bitlink}`,
});
} catch (error: any) {
const errorMessage = error.response?.data?.description || error.response?.data?.message || error.message;
if (error.response?.status === 429) {
throw new Error(
'Rate limit exceeded. Please wait before trying again.'
);
}
if (error.response?.status === 404) {
throw new Error(
`Bitlink not found: ${errorMessage}. Please verify the link ID is correct.`
);
}
if (error.response?.status === 401 || error.response?.status === 403) {
throw new Error(
`Authentication failed or forbidden: ${errorMessage}. Please check your Access Token and permissions.`
);
}
throw new Error(
`Failed to get Bitlink details: ${errorMessage || 'Unknown error occurred'}`
);
}
},
});

View File

@@ -0,0 +1,151 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { bitlyApiCall } from '../common/client';
import { bitlyAuth } from '../common/auth';
import { bitlinkDropdown, groupGuid } from '../common/props';
export const updateBitlinkAction = createAction({
auth: bitlyAuth,
name: 'update_bitlink',
displayName: 'Update Bitlink',
description: 'Modify properties of an existing Bitlink.',
props: {
group_guid: groupGuid,
bitlink: bitlinkDropdown,
title: Property.ShortText({
displayName: 'Title',
description: 'New title for the Bitlink.',
required: false,
}),
archived: Property.Checkbox({
displayName: 'Archived',
description: 'Archive or unarchive the Bitlink.',
required: false,
}),
tags: Property.Array({
displayName: 'Tags',
description: 'Tags to apply (overwrites existing tags).',
required: false,
}),
// Mobile App Deeplink Configuration
app_uri_path: Property.ShortText({
displayName: 'App URI Path',
description: 'Path within the mobile app (e.g., /product/123).',
required: false,
}),
install_url: Property.LongText({
displayName: 'App Install URL',
description: 'URL where users can install the mobile app.',
required: false,
}),
os: Property.StaticDropdown({
displayName: 'Mobile OS',
description: 'Target mobile operating system.',
required: false,
options: {
disabled: false,
options: [
{ label: 'iOS', value: 'ios' },
{ label: 'Android', value: 'android' },
],
},
}),
install_type: Property.StaticDropdown({
displayName: 'Install Type',
description: 'How to handle app installation.',
required: false,
options: {
disabled: false,
options: [
{ label: 'No Install', value: 'no_install' },
{ label: 'Auto Install', value: 'auto_install' },
{ label: 'Promote Install', value: 'promote_install' },
],
},
}),
},
async run(context) {
const {
bitlink,
title,
archived,
tags,
app_uri_path,
install_url,
os,
install_type
} = context.propsValue;
try {
const body: Record<string, unknown> = {};
if (title !== undefined && title !== null) {
body['title'] = title;
}
if (archived !== undefined && archived !== null) {
body['archived'] = archived;
}
if (tags !== undefined && tags !== null && Array.isArray(tags)) {
body['tags'] = tags;
}
// Build deeplinks array if app configuration is provided
if (app_uri_path || install_url || os || install_type) {
const deeplink: Record<string, unknown> = {};
if (app_uri_path) deeplink['app_uri_path'] = app_uri_path;
if (install_url) deeplink['install_url'] = install_url;
if (os) deeplink['os'] = os;
if (install_type) deeplink['install_type'] = install_type;
if (Object.keys(deeplink).length > 0) {
body['deeplinks'] = [deeplink];
}
}
if (Object.keys(body).length === 0) {
throw new Error(
'No fields were provided to update. Please provide a title, tags, archive status, or deeplinks.'
);
}
return await bitlyApiCall({
method: HttpMethod.PATCH,
auth: context.auth.props,
resourceUri: `/bitlinks/${bitlink}`,
body,
});
} catch (error: any) {
const errorMessage =
error.response?.data?.description ||
error.response?.data?.message ||
error.message;
if (error.response?.status === 429) {
throw new Error(
'Rate limit exceeded. Please wait before trying again.'
);
}
if (error.response?.status === 404) {
throw new Error(
`Bitlink not found: ${errorMessage}. Please verify the link (e.g., 'bit.ly/xyz123') is correct.`
);
}
if (error.response?.status === 401 || error.response?.status === 403) {
throw new Error(
`Authentication failed or forbidden: ${errorMessage}. Please check your Access Token and permissions.`
);
}
if (error.message.includes('Invalid JSON format')) {
throw error;
}
throw new Error(
`Failed to update Bitlink: ${errorMessage || 'Unknown error occurred'}`
);
}
},
});

View File

@@ -0,0 +1,37 @@
import { PieceAuth } from '@activepieces/pieces-framework';
import { bitlyApiCall } from './client';
import { HttpMethod } from '@activepieces/pieces-common';
export const bitlyAuth = PieceAuth.CustomAuth({
description: `
To get your Access Token:
1. Log in to your Bitly account.
2. Click your profile icon in the top right corner.
3. Go to **Profile Settings**.
4. Navigate to the **Developer settings** section.
5. Click on **API**.
6. Click the **Generate token** button and enter your password to get your access token.
`,
props: {
accessToken: PieceAuth.SecretText({
displayName: 'Access Token',
required: true,
}),
},
validate: async ({ auth }) => {
try {
await bitlyApiCall({
method: HttpMethod.GET,
auth,
resourceUri: '/user',
});
return { valid: true };
} catch (e) {
return {
valid: false,
error: 'Invalid Access Token',
};
}
},
required: true,
});

View File

@@ -0,0 +1,122 @@
import {
httpClient,
HttpMethod,
HttpRequest,
HttpMessageBody,
QueryParams,
} from '@activepieces/pieces-common';
export type BitlyAuthProps = {
accessToken: string;
};
export type BitlyApiCallParams = {
method: HttpMethod;
resourceUri: string;
query?: Record<string, string | number | string[] | undefined>;
body?: any;
auth: BitlyAuthProps;
};
export async function bitlyApiCall<T extends HttpMessageBody>({
method,
resourceUri,
query,
body,
auth,
}: BitlyApiCallParams): Promise<T> {
const { accessToken } = auth;
if (!accessToken) {
throw new Error('Bitly Access Token is required for authentication');
}
const queryParams: QueryParams = {};
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== null && value !== undefined) {
queryParams[key] = String(value);
}
}
}
const baseUrl = 'https://api-ssl.bitly.com/v4';
const request: HttpRequest = {
method,
url: `${baseUrl}${resourceUri}`,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
queryParams,
body,
};
try {
const response = await httpClient.sendRequest<T>(request);
return response.body;
} catch (error: any) {
const statusCode = error.response?.status;
const errorData = error.response?.data;
const errorMessage = errorData?.description || errorData?.message || 'Unknown error occurred';
switch (statusCode) {
case 400:
throw new Error(
`Bad Request: ${errorMessage}. Please check your input parameters.`
);
case 401:
throw new Error(
'Authentication Failed: Invalid Access Token. Please verify your Bitly credentials in the connection settings.'
);
case 402:
throw new Error(
`Payment Required: ${errorMessage}. Your account has been suspended or you have reached a usage limit.`
);
case 403:
throw new Error(
`Access Forbidden: ${errorMessage}. You do not have permission to access this resource.`
);
case 404:
throw new Error(
`Resource Not Found: ${errorMessage}. The requested resource does not exist.`
);
case 417:
throw new Error(
`Expectation Failed: ${errorMessage}. You must agree to the latest terms of service.`
);
case 422:
throw new Error(
`Unprocessable Entity: ${errorMessage}. The request was well-formed but was unable to be followed due to semantic errors.`
);
case 429:
throw new Error(
`Rate Limit Exceeded: ${errorMessage}. Too many requests. Please wait before trying again.`
);
case 500:
throw new Error(
'Internal Server Error: Bitly is experiencing technical difficulties. Please try again later.'
);
case 503:
throw new Error(
'Service Unavailable: Bitly service is temporarily unavailable. Please try again in a few minutes.'
);
default:
throw new Error(
`Bitly API Error (${statusCode || 'Unknown'}): ${errorMessage}`
);
}
}
}

View File

@@ -0,0 +1,123 @@
import { Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { bitlyApiCall } from './client';
import { BitlyAuthProps } from './client';
import { bitlyAuth } from './auth';
interface BitlyGroup {
guid: string;
name: string;
bsds: Array<{ domain: string }>;
}
interface Bitlink {
id: string;
title: string;
}
const getBitlyGroups = async (auth: BitlyAuthProps): Promise<BitlyGroup[]> => {
const response = await bitlyApiCall<{ groups: BitlyGroup[] }>({
auth,
method: HttpMethod.GET,
resourceUri: '/groups',
});
return response.groups || [];
};
export const groupGuid = Property.Dropdown({
displayName: 'Group',
description: 'The group where the item will be managed.',
required: true,
refreshers: [],
auth:bitlyAuth ,
options: async ({ auth }) => {
if (!auth) {
return { disabled: true, options: [], placeholder: 'Please connect your Bitly account first.' };
}
const { accessToken } = auth.props;
if (!accessToken) {
return { disabled: true, options: [], placeholder: 'Please connect your Bitly account first.' };
}
try {
const groups = await getBitlyGroups({ accessToken });
if (groups.length === 0) {
return { disabled: true, options: [], placeholder: 'No groups found in your account.' };
}
return {
disabled: false,
options: groups.map((group) => ({
label: group.name,
value: group.guid,
})),
};
} catch (e) {
return { disabled: true, options: [], placeholder: `Error fetching groups: ${(e as Error).message}` };
}
},
});
export const domain = Property.Dropdown({
auth: bitlyAuth,
displayName: 'Domain',
description: 'Domain to use for the Bitlink.',
required: false,
refreshers: ['group_guid'],
options: async ({ auth, group_guid }) => {
if (!auth) {
return { disabled: true, options: [], placeholder: 'Please connect your Bitly account first.' };
}
const { accessToken } = auth.props;
if (!accessToken || !group_guid) {
return { disabled: true, options: [], placeholder: 'Please select a group first.' };
}
try {
const groups = await getBitlyGroups({ accessToken });
const selectedGroup = groups.find(g => g.guid === group_guid);
const customDomains = selectedGroup?.bsds?.map(bsd => bsd.domain) || [];
const allDomains = ['bit.ly', ...customDomains];
return {
disabled: false,
options: allDomains.map(d => ({
label: d,
value: d,
})),
};
} catch (e) {
return { disabled: true, options: [], placeholder: `Error fetching domains: ${(e as Error).message}` };
}
},
});
export const bitlinkDropdown = Property.Dropdown({
displayName: 'Bitlink',
description: 'Select the Bitlink to modify.',
required: true,
refreshers: ['group_guid'],
auth: bitlyAuth,
options: async ({ auth, group_guid }) => {
if (!auth) {
return { disabled: true, options: [], placeholder: 'Please connect your Bitly account first.' };
}
const { accessToken } = auth.props;
if (!accessToken) return { disabled: true, options: [], placeholder: 'Please connect your Bitly account first.' };
if (!group_guid) return { disabled: true, options: [], placeholder: 'Please select a group first.' };
try {
const response = await bitlyApiCall<{ links: Bitlink[] }>({
auth: { accessToken },
method: HttpMethod.GET,
resourceUri: `/groups/${group_guid as string}/bitlinks`,
});
return {
disabled: false,
options: response.links.map(link => ({
label: `${link.title || 'No Title'} (${link.id})`,
value: link.id
}))
};
} catch (e) {
return { disabled: true, options: [], placeholder: `Error fetching Bitlinks: ${(e as Error).message}` };
}
}
});

View File

@@ -0,0 +1,358 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { bitlyAuth } from '../common/auth';
import { bitlyApiCall } from '../common/client';
import { groupGuid } from '../common/props';
const LAST_BITLINK_IDS_KEY = 'bitly-last-bitlink-ids';
export const newBitlinkCreatedTrigger = createTrigger({
auth: bitlyAuth,
name: 'new_bitlink_created',
displayName: 'New Bitlink Created',
description: 'Fires when a new Bitlink is created.',
type: TriggerStrategy.POLLING,
props: {
pollingInterval: Property.StaticDropdown({
displayName: 'Polling Interval',
description: 'How frequently to check for new Bitlinks.',
required: false,
defaultValue: '5',
options: {
disabled: false,
options: [
{ label: 'Every 1 minute', value: '1' },
{ label: 'Every 5 minutes', value: '5' },
{ label: 'Every 15 minutes', value: '15' },
{ label: 'Every 30 minutes', value: '30' },
{ label: 'Every hour', value: '60' },
],
},
}),
group_guid: groupGuid,
titleFilter: Property.ShortText({
displayName: 'Title Filter (Optional)',
description: 'Only trigger for Bitlinks containing this text in their title.',
required: false,
}),
tagFilter: Property.ShortText({
displayName: 'Tag Filter (Optional)',
description: 'Only trigger for Bitlinks containing this tag.',
required: false,
}),
includeArchived: Property.Checkbox({
displayName: 'Include Archived Bitlinks',
description: 'Include archived Bitlinks in monitoring.',
required: false,
defaultValue: false,
}),
},
async onEnable(context) {
const { group_guid } = context.propsValue;
const { accessToken } = context.auth.props;
try {
const response = await bitlyApiCall<{ links: BitlyLink[] }>({
auth: { accessToken },
method: HttpMethod.GET,
resourceUri: `/groups/${group_guid}/bitlinks`,
query: {
size: 50,
archived: context.propsValue.includeArchived ? 'both' : 'off',
},
});
const linkIds = response.links.map((link) => link.id);
await context.store.put<string[]>(LAST_BITLINK_IDS_KEY, linkIds);
console.log(`Bitly New Bitlink trigger initialized with ${linkIds.length} existing links`);
} catch (error: any) {
if (error.response?.status === 401) {
throw new Error(
'Authentication failed: Please check your access token. Make sure your token has permission to access Bitlinks.'
);
}
if (error.response?.status === 403) {
throw new Error(
'Access denied: You do not have permission to list Bitlinks. Please check your Bitly account permissions.'
);
}
throw new Error(
`Failed to initialize Bitlink monitoring: ${error.message || 'Unknown error occurred'}. Please check your Bitly connection.`
);
}
},
async onDisable() {
console.log('Bitly New Bitlink trigger disabled and cleaned up');
},
async run(context) {
const { group_guid, titleFilter, tagFilter, includeArchived } = context.propsValue;
const { accessToken } = context.auth.props;
try {
const previousLinkIds = await context.store.get<string[]>(LAST_BITLINK_IDS_KEY) || [];
const response = await bitlyApiCall<{ links: BitlyLink[] }>({
auth: { accessToken },
method: HttpMethod.GET,
resourceUri: `/groups/${group_guid}/bitlinks`,
query: {
size: 50,
archived: includeArchived ? 'both' : 'off',
},
});
const allLinks = response.links || [];
const currentLinkIds = allLinks.map((l) => l.id);
await context.store.put<string[]>(LAST_BITLINK_IDS_KEY, currentLinkIds);
let newLinks = allLinks.filter((link) => !previousLinkIds.includes(link.id));
if (titleFilter && titleFilter.trim()) {
const filterText = titleFilter.trim().toLowerCase();
newLinks = newLinks.filter((link) =>
link.title && link.title.toLowerCase().includes(filterText)
);
}
if (tagFilter && tagFilter.trim()) {
const filterTag = tagFilter.trim().toLowerCase();
newLinks = newLinks.filter((link) =>
link.tags && Array.isArray(link.tags) &&
link.tags.some(tag => tag.toLowerCase().includes(filterTag))
);
}
const processedLinks = newLinks.map((link) => ({
id: link.id,
link: link.link,
longUrl: link.long_url,
title: link.title,
tags: link.tags || [],
isArchived: link.archived,
createdAt: link.created_at,
modifiedAt: link.modified_at,
customBitlinks: link.custom_bitlinks || [],
references: {
group: link.references?.group,
},
rawLinkData: link,
triggerInfo: {
detectedAt: new Date().toISOString(),
source: 'bitly',
type: 'new_bitlink',
},
}));
return processedLinks;
} catch (error: any) {
if (error.response?.status === 401) {
throw new Error(
'Authentication failed: Your access token may have expired. Please check your Bitly authentication.'
);
}
if (error.response?.status === 429) {
throw new Error(
'Rate limit exceeded: Bitly API rate limit reached. Consider increasing your polling interval.'
);
}
if (error.response?.status === 403) {
throw new Error(
'Access denied: You do not have permission to list Bitlinks. Please check your account permissions.'
);
}
throw new Error(
`Failed to check for new Bitlinks: ${error.message || 'Unknown error occurred'}. The trigger will retry on the next polling interval.`
);
}
},
async test(context) {
const { group_guid, includeArchived } = context.propsValue;
const { accessToken } = context.auth.props;
try {
const response = await bitlyApiCall<{ links: BitlyLink[] }>({
auth: { accessToken },
method: HttpMethod.GET,
resourceUri: `/groups/${group_guid}/bitlinks`,
query: {
size: 1,
archived: includeArchived ? 'both' : 'off',
},
});
const links = response.links || [];
if (links.length > 0) {
const testLink = links[0];
return [
{
id: testLink.id,
link: testLink.link,
longUrl: testLink.long_url,
title: testLink.title,
tags: testLink.tags || [],
isArchived: testLink.archived,
createdAt: testLink.created_at,
modifiedAt: testLink.modified_at,
customBitlinks: testLink.custom_bitlinks || [],
references: {
group: testLink.references?.group,
},
rawLinkData: testLink,
triggerInfo: {
detectedAt: new Date().toISOString(),
source: 'bitly',
type: 'new_bitlink',
},
},
];
} else {
return [
{
id: 'bit.ly/test123',
link: 'https://bit.ly/test123',
longUrl: 'https://example.com/very-long-url',
title: 'Sample Bitlink',
tags: ['sample', 'test'],
isArchived: false,
createdAt: '2025-01-15T10:00:00+0000',
modifiedAt: '2025-01-15T10:00:00+0000',
customBitlinks: [],
references: {
group: 'Ba1bc23dE4F',
},
rawLinkData: {
id: 'bit.ly/test123',
link: 'https://bit.ly/test123',
long_url: 'https://example.com/very-long-url',
title: 'Sample Bitlink',
tags: ['sample', 'test'],
archived: false,
created_at: '2025-01-15T10:00:00+0000',
modified_at: '2025-01-15T10:00:00+0000',
custom_bitlinks: [],
references: {
group: 'Ba1bc23dE4F',
},
},
triggerInfo: {
detectedAt: new Date().toISOString(),
source: 'bitly',
type: 'new_bitlink',
},
},
];
}
} catch (error: any) {
return [
{
id: 'bit.ly/test123',
link: 'https://bit.ly/test123',
longUrl: 'https://example.com/test-url',
title: 'Test Bitlink',
tags: ['test'],
isArchived: false,
createdAt: '2025-01-15T10:00:00+0000',
modifiedAt: '2025-01-15T10:00:00+0000',
customBitlinks: [],
references: {
group: 'Ba1bc23dE4F',
},
rawLinkData: {
id: 'bit.ly/test123',
link: 'https://bit.ly/test123',
long_url: 'https://example.com/test-url',
title: 'Test Bitlink',
tags: ['test'],
archived: false,
created_at: '2025-01-15T10:00:00+0000',
modified_at: '2025-01-15T10:00:00+0000',
custom_bitlinks: [],
references: {
group: 'Ba1bc23dE4F',
},
},
triggerInfo: {
detectedAt: new Date().toISOString(),
source: 'bitly',
type: 'new_bitlink',
},
},
];
}
},
sampleData: {
id: 'bit.ly/3XYZ123',
link: 'https://bit.ly/3XYZ123',
longUrl: 'https://example.com/marketing-campaign-landing-page',
title: 'Marketing Campaign Landing Page',
tags: ['marketing', 'campaign', '2025'],
isArchived: false,
createdAt: '2025-01-15T09:30:00+0000',
modifiedAt: '2025-01-15T09:30:00+0000',
customBitlinks: [],
references: {
group: 'Ba1bc23dE4F',
},
rawLinkData: {
id: 'bit.ly/3XYZ123',
link: 'https://bit.ly/3XYZ123',
long_url: 'https://example.com/marketing-campaign-landing-page',
title: 'Marketing Campaign Landing Page',
tags: ['marketing', 'campaign', '2025'],
archived: false,
created_at: '2025-01-15T09:30:00+0000',
modified_at: '2025-01-15T09:30:00+0000',
custom_bitlinks: [],
references: {
group: 'Ba1bc23dE4F',
},
},
triggerInfo: {
detectedAt: '2025-01-15T09:30:00.000Z',
source: 'bitly',
type: 'new_bitlink',
},
},
});
/**
* Interface for Bitly link data structure
*/
interface BitlyLink {
id: string;
link: string;
long_url: string;
title: string;
tags: string[];
archived: boolean;
created_at: string;
modified_at: string;
custom_bitlinks: string[];
references: {
group: string;
};
[key: string]: any;
}