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