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,100 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { shortIoApiCall } from '../common/client';
import { shortIoAuth } from '../common/auth';
import { domainIdDropdown, linkIdDropdown } from '../common/props';
export const createCountryTargetingRuleAction = createAction({
auth: shortIoAuth,
name: 'create-country-targeting-rule',
displayName: 'Create Country Targeting Rule',
description: 'Set geographic targeting rules for a link with specific destination per country.',
props: {
domain: domainIdDropdown,
linkId: linkIdDropdown,
country: Property.ShortText({
displayName: 'Country Code',
description: 'ISO 3166-1 alpha-2 country code (e.g., US, GB, CA, IN). Must be exactly 2 characters.',
required: true,
}),
originalURL: Property.ShortText({
displayName: 'Country-Specific Redirect URL',
description: 'The destination URL when users from this country access the link. Must be a valid URL.',
required: true,
}),
},
async run({ propsValue, auth }) {
const { linkId, domain: domainString, country, originalURL } = propsValue;
if (!country || country.trim().length !== 2) {
throw new Error('Country code must be exactly 2 characters (e.g., US, GB, CA)');
}
const countryCode = country.trim().toUpperCase();
try {
new URL(originalURL);
} catch (error) {
throw new Error('Invalid URL format. Please provide a valid URL starting with http:// or https://');
}
const query: Record<string, string> = {};
if (domainString) {
const domainObject = JSON.parse(domainString as string);
query['domainId'] = String(domainObject.id);
}
const body = {
country: countryCode,
originalURL,
};
try {
const response = await shortIoApiCall({
method: HttpMethod.POST,
auth,
resourceUri: `/link_country/${linkId}`,
query,
body,
});
return {
success: true,
message: `Country targeting rule for ${countryCode} created successfully`,
data: response,
};
} catch (error: any) {
if (error.message.includes('400')) {
throw new Error(
'Invalid request parameters. Please check the country code format and URL validity.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication failed. Please check your API key and permissions.'
);
}
if (error.message.includes('404')) {
throw new Error(
'Short link not found. Please check the link ID and try again.'
);
}
if (error.message.includes('409')) {
throw new Error(
`A country targeting rule for ${countryCode} already exists for this link.`
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate limit exceeded. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to create country targeting rule: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,287 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { shortIoApiCall } from '../common/client';
import { shortIoAuth } from '../common/auth';
import { domainIdDropdown, folderIdDropdown } from '../common/props';
export const createShortLinkAction = createAction({
auth: shortIoAuth,
name: 'create-short-link',
displayName: 'Create Short Link',
description:
'Create a new short link with optional parameters (title, UTM tags, expiration, cloaking, etc.)',
props: {
originalURL: Property.ShortText({
displayName: 'Original URL',
required: true,
}),
domain: domainIdDropdown,
title: Property.ShortText({
displayName: 'Title',
required: false,
}),
folderId: folderIdDropdown,
cloaking: Property.Checkbox({
displayName: 'Enable Cloaking',
required: false,
}),
password: Property.ShortText({
displayName: 'Password',
required: false,
}),
redirectType: Property.StaticDropdown({
displayName: 'Redirect Type',
description:
'The HTTP status code for the redirect. The default is 302 (Found).',
required: false,
options: {
disabled: false,
options: [
{ label: '301 (Moved Permanently)', value: '301' },
{ label: '302 (Found)', value: '302' },
{ label: '303 (See Other)', value: '303' },
{ label: '307 (Temporary Redirect)', value: '307' },
{ label: '308 (Permanent Redirect)', value: '308' },
],
},
}),
expiresAt: Property.DateTime({
displayName: 'Expiration Date',
description: 'The date and time when the link will become inactive.',
required: false,
}),
expiredURL: Property.ShortText({
displayName: 'Expired Redirect URL',
required: false,
}),
tags: Property.Array({
displayName: 'Tags',
description: 'Array of tags for the link',
required: false,
}),
utmSource: Property.ShortText({
displayName: 'UTM Source',
required: false,
}),
utmMedium: Property.ShortText({
displayName: 'UTM Medium',
required: false,
}),
utmCampaign: Property.ShortText({
displayName: 'UTM Campaign',
required: false,
}),
utmTerm: Property.ShortText({
displayName: 'UTM Term',
required: false,
}),
utmContent: Property.ShortText({
displayName: 'UTM Content',
required: false,
}),
ttl: Property.Number({
displayName: 'Time to Live (in seconds)',
description:
'⚠️ CAUTION: Link will be PERMANENTLY DELETED after this many seconds. This action cannot be undone. Use with extreme caution.',
required: false,
}),
androidURL: Property.ShortText({
displayName: 'Android URL',
required: false,
}),
iphoneURL: Property.ShortText({
displayName: 'iPhone URL',
required: false,
}),
createdAt: Property.DateTime({
displayName: 'Creation Time',
description: 'Overrides the creation time of the link.',
required: false,
}),
clicksLimit: Property.Number({
displayName: 'Clicks Limit',
description: 'Disable link after specified number of clicks (minimum: 1)',
required: false,
}),
passwordContact: Property.Checkbox({
displayName: 'Show Contact for Password Recovery',
required: false,
}),
skipQS: Property.Checkbox({
displayName: 'Skip Query String Merge',
required: false,
}),
archived: Property.Checkbox({
displayName: 'Archive Link',
required: false,
}),
splitURL: Property.ShortText({
displayName: 'Split URL',
required: false,
}),
splitPercent: Property.Number({
displayName: 'Split Percent',
description: 'Split URL percentage (1-100)',
required: false,
}),
integrationAdroll: Property.ShortText({
displayName: 'Adroll Integration ID',
required: false,
}),
integrationFB: Property.ShortText({
displayName: 'Facebook Integration ID',
required: false,
}),
integrationGA: Property.ShortText({
displayName: 'Google Analytics ID',
required: false,
}),
integrationGTM: Property.ShortText({
displayName: 'Google Tag Manager ID',
required: false,
}),
allowDuplicates: Property.Checkbox({
displayName: 'Allow Duplicates',
required: false,
}),
path: Property.ShortText({
displayName: 'Path',
description: 'Custom path for the short link (optional).',
required: false,
}),
},
async run({ propsValue, auth }) {
const {
originalURL,
domain: domainString,
path,
title,
cloaking,
password,
redirectType,
expiresAt,
expiredURL,
tags,
utmSource,
utmMedium,
utmCampaign,
utmTerm,
utmContent,
ttl,
androidURL,
iphoneURL,
createdAt,
clicksLimit,
passwordContact,
skipQS,
archived,
splitURL,
splitPercent,
integrationAdroll,
integrationFB,
integrationGA,
integrationGTM,
allowDuplicates,
folderId,
} = propsValue;
// Input validation
if (clicksLimit && clicksLimit < 1) {
throw new Error('Clicks limit must be at least 1');
}
if (splitPercent && (splitPercent < 1 || splitPercent > 100)) {
throw new Error('Split percent must be between 1 and 100');
}
if (redirectType && !['301', '302', '303', '307', '308'].includes(redirectType)) {
throw new Error('Invalid redirect type. Must be one of: 301, 302, 303, 307, 308');
}
const domainObject = JSON.parse(domainString as string);
const body: Record<string, unknown> = {
originalURL,
domain: domainObject.hostname,
};
const optionalParams = {
path,
title,
cloaking,
password,
redirectType,
expiresAt,
expiredURL,
tags,
utmSource,
utmMedium,
utmCampaign,
utmTerm,
utmContent,
ttl: ttl ? ttl * 1000 : undefined,
androidURL,
iphoneURL,
createdAt,
clicksLimit,
passwordContact,
skipQS,
archived,
splitURL,
splitPercent,
integrationAdroll,
integrationFB,
integrationGA,
integrationGTM,
allowDuplicates,
folderId,
};
for (const [key, value] of Object.entries(optionalParams)) {
if (value !== null && value !== undefined && value !== '') {
body[key] = value;
}
}
try {
const response = await shortIoApiCall({
method: HttpMethod.POST,
auth,
resourceUri: '/links',
body,
});
return {
success: true,
message: 'Short link created successfully',
data: response,
};
} catch (error: any) {
if (error.message.includes('409')) {
throw new Error(
'A short link with this path already exists but points to a different original URL.'
);
}
if (error.message.includes('400')) {
throw new Error(
'Invalid request parameters. Please check your input values and try again.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication failed. Please check your API key and permissions.'
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate limit exceeded. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to create short link: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,65 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { shortIoApiCall } from '../common/client';
import { shortIoAuth } from '../common/auth';
export const deleteShortLinkAction = createAction({
auth: shortIoAuth,
name: 'delete-short-link',
displayName: 'Delete Short Link',
description: 'Permanently delete a short link by its unique link ID.',
props: {
linkId: Property.ShortText({
displayName: 'Link ID',
description: 'The ID of the short link you want to delete (e.g., lnk_61Mb_0dnRUg3vvtmAPZh3dhQh6)',
required: true,
}),
},
async run({ propsValue, auth }) {
const { linkId } = propsValue;
if (!linkId || linkId.trim() === '') {
throw new Error('Link ID is required and cannot be empty');
}
try {
const response = await shortIoApiCall({
method: HttpMethod.DELETE,
auth,
resourceUri: `/links/${linkId}`,
});
return {
success: true,
message: `Short link with ID ${linkId} deleted successfully`,
data: response,
};
} catch (error: any) {
if (error.message.includes('400')) {
throw new Error(
'Invalid request. Please check the link ID format and try again.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication failed. Please check your API key and permissions.'
);
}
if (error.message.includes('404')) {
throw new Error(
`Short link with ID ${linkId} not found. It may have already been deleted.`
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate limit exceeded. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to delete short link: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,153 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { shortIoApiCall } from '../common/client';
import { shortIoAuth } from '../common/auth';
import { domainIdDropdown } from '../common/props';
export const domainStatisticsAction = createAction({
auth: shortIoAuth,
name: 'get-domain-statistics',
displayName: 'Domain Statistics',
description: 'Retrieve usage stats (clicks, conversions) for a domain within a time period.',
props: {
domainId: {
...domainIdDropdown,
required: true,
description: 'Select the domain to get statistics for',
},
period: Property.StaticDropdown({
displayName: 'Time Period',
description: 'Select a predefined time interval or choose "Custom" to set specific dates.',
required: true,
defaultValue: 'last30',
options: {
disabled: false,
options: [
{ label: 'Today', value: 'today' },
{ label: 'Yesterday', value: 'yesterday' },
{ label: 'Last 7 Days', value: 'last7' },
{ label: 'Last 30 Days', value: 'last30' },
{ label: 'This Month', value: 'thisMonth' },
{ label: 'Last Month', value: 'lastMonth' },
{ label: 'Custom Date Range', value: 'custom' },
],
},
}),
startDate: Property.DateTime({
displayName: 'Start Date',
description: 'Start date for statistics (required when period is "Custom").',
required: false,
}),
endDate: Property.DateTime({
displayName: 'End Date',
description: 'End date for statistics (required when period is "Custom").',
required: false,
}),
tz: Property.ShortText({
displayName: 'Timezone',
description: 'Timezone for statistics (e.g., UTC, America/New_York, Europe/London). Defaults to UTC.',
required: false,
}),
clicksChartInterval: Property.StaticDropdown({
displayName: 'Chart Interval',
description: 'Granularity for click statistics chart data.',
required: false,
options: {
disabled: false,
options: [
{ label: 'Hour', value: 'hour' },
{ label: 'Day', value: 'day' },
{ label: 'Week', value: 'week' },
{ label: 'Month', value: 'month' },
],
},
}),
},
async run({ propsValue, auth }) {
const { domainId: domainString, period, startDate, endDate, tz, clicksChartInterval } = propsValue;
if (!domainString) {
throw new Error('Domain is required. Please select a domain.');
}
const domainObject = JSON.parse(domainString as string);
if (period === 'custom') {
if (!startDate || !endDate) {
throw new Error('Start Date and End Date are both required when using custom period.');
}
const start = new Date(startDate);
const end = new Date(endDate);
if (start >= end) {
throw new Error('Start date must be before end date.');
}
}
if (tz && tz.trim() !== '') {
const tzValue = tz.trim();
if (tzValue.length < 3 || tzValue.includes(' ')) {
throw new Error('Invalid timezone format. Use standard timezone names like UTC, America/New_York, Europe/London.');
}
}
const query: Record<string, string> = {
period,
};
if (period === 'custom' && startDate && endDate) {
query['startDate'] = new Date(startDate).toISOString().split('T')[0];
query['endDate'] = new Date(endDate).toISOString().split('T')[0];
}
if (tz && tz.trim() !== '') query['tz'] = tz.trim();
if (clicksChartInterval) query['clicksChartInterval'] = clicksChartInterval;
try {
const response = await shortIoApiCall({
method: HttpMethod.GET,
auth,
url: `https://statistics.short.io/statistics/domain/${domainObject.id}`,
query,
});
const stats = response as any;
const clicks = stats['clicks'] || 0;
const links = stats['links'] || 0;
const periodLabel = period === 'custom' ? 'custom period' : period.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
return {
success: true,
message: `Retrieved statistics for ${periodLabel}: ${clicks} clicks across ${links} links`,
data: response,
};
} catch (error: any) {
if (error.message.includes('400')) {
throw new Error(
'Invalid request parameters. Please check your date range, timezone, and other settings.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication failed or insufficient permissions. Please check your API key and domain access.'
);
}
if (error.message.includes('404')) {
throw new Error(
'Domain not found. Please verify the domain exists and you have access to its statistics.'
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate limit exceeded. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to retrieve domain statistics: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,110 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { shortIoApiCall } from '../common/client';
import { shortIoAuth } from '../common/auth';
import { domainIdDropdown, linkIdDropdown } from '../common/props';
export const expireShortLinkAction = createAction({
auth: shortIoAuth,
name: 'expire-short-link',
displayName: 'Expire Short Link',
description: 'Set an expiration date or click limit to deactivate a short link.',
props: {
domain: domainIdDropdown,
linkId: linkIdDropdown,
expiresAt: Property.DateTime({
displayName: 'Expiration Date',
description: 'The date and time when the link will become inactive.',
required: false,
}),
clicksLimit: Property.Number({
displayName: 'Clicks Limit',
description: 'Disable link after specified number of clicks (minimum: 1)',
required: false,
}),
expiredURL: Property.ShortText({
displayName: 'Expired Redirect URL',
description: 'Optional URL to redirect users when the link has expired.',
required: false,
}),
},
async run({ propsValue, auth }) {
const {
linkId,
domain: domainString,
expiresAt,
clicksLimit,
expiredURL,
} = propsValue;
if (!expiresAt && !clicksLimit) {
throw new Error('You must provide either an expiration date or a clicks limit to expire the link.');
}
if (clicksLimit && clicksLimit < 1) {
throw new Error('Clicks limit must be at least 1');
}
const query: Record<string, string> = {};
if (domainString) {
const domainObject = JSON.parse(domainString as string);
query['domain_id'] = String(domainObject.id);
}
const body: Record<string, unknown> = {};
if (expiresAt) {
body['expiresAt'] = expiresAt;
}
if (clicksLimit) {
body['clicksLimit'] = clicksLimit;
}
if (expiredURL && expiredURL.trim() !== '') {
body['expiredURL'] = expiredURL;
}
try {
const response = await shortIoApiCall({
method: HttpMethod.POST,
auth,
resourceUri: `/links/${linkId}`,
query,
body,
});
return {
success: true,
message: 'Short link expiration settings updated successfully',
data: response,
};
} catch (error: any) {
if (error.message.includes('400')) {
throw new Error(
'Invalid request parameters. Please check your expiration settings and try again.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication failed. Please check your API key and permissions.'
);
}
if (error.message.includes('404')) {
throw new Error(
'Short link not found. Please check the link ID and try again.'
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate limit exceeded. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to set link expiration: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,85 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { shortIoApiCall } from '../common/client';
import { shortIoAuth } from '../common/auth';
import { domainIdDropdown } from '../common/props';
export const getLinkByPathAction = createAction({
auth: shortIoAuth,
name: 'get-short-link-info-by-path',
displayName: 'Get Link by Path',
description: 'Retrieve detailed information about a short link using its domain and path.',
props: {
domain: domainIdDropdown,
path: Property.ShortText({
displayName: 'Link Path',
description: 'The path/slug of the short link (e.g., "abc123", "my-link"). Do not include domain or slashes.',
required: true,
}),
},
async run({ propsValue, auth }) {
const { domain: domainString, path } = propsValue;
if (!domainString) {
throw new Error('Domain is required. Please select a domain first.');
}
if (!path || path.trim() === '') {
throw new Error('Path is required and cannot be empty.');
}
const cleanPath = path.trim();
if (cleanPath.includes('/') || cleanPath.includes('http')) {
throw new Error('Path should only contain the slug (e.g., "abc123"), not the full URL or domain.');
}
const domainObject = JSON.parse(domainString as string);
const query = {
domain: domainObject.hostname,
path: cleanPath,
};
try {
const response = await shortIoApiCall({
method: HttpMethod.GET,
auth,
resourceUri: `/links/expand`,
query,
});
return {
success: true,
message: `Link information for "${cleanPath}" retrieved successfully`,
data: response,
};
} catch (error: any) {
if (error.message.includes('400')) {
throw new Error(
'Invalid request parameters. Please check the domain and path values.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication failed. Please check your API key and permissions.'
);
}
if (error.message.includes('404')) {
throw new Error(
`Short link with path "${cleanPath}" not found on domain "${domainObject.hostname}". Please verify the path exists.`
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate limit exceeded. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to retrieve link information: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,113 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { shortIoApiCall } from '../common/client';
import { shortIoAuth } from '../common/auth';
import { domainIdDropdown } from '../common/props';
export const getLinkClicksAction = createAction({
auth: shortIoAuth,
name: 'get-link-clicks',
displayName: 'Get Link Clicks',
description: 'Retrieve click statistics for specific short links by their IDs.',
props: {
domain: {
...domainIdDropdown,
required: true,
description: 'Select the domain containing the links',
},
ids: Property.Array({
displayName: 'Link IDs',
description: 'List of link IDs to fetch click statistics for (e.g., lnk_61Mb_0dnRUg3vvtmAPZh3dhQh6).',
required: true,
}),
startDate: Property.DateTime({
displayName: 'Start Date',
description: 'Start date for click statistics (optional).',
required: false,
}),
endDate: Property.DateTime({
displayName: 'End Date',
description: 'End date for click statistics (optional).',
required: false,
}),
},
async run({ propsValue, auth }) {
const { domain: domainString, ids, startDate, endDate } = propsValue;
if (!domainString) {
throw new Error('Domain is required. Please select a domain.');
}
if (!ids || ids.length === 0) {
throw new Error('At least one Link ID is required.');
}
if ((startDate && !endDate) || (!startDate && endDate)) {
throw new Error('Both Start Date and End Date must be provided if using a date range filter.');
}
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
if (start >= end) {
throw new Error('Start date must be before end date.');
}
}
const domainObject = JSON.parse(domainString as string);
const query: Record<string, string> = {
ids: (ids as string[]).join(','),
};
if (startDate && endDate) {
query['startDate'] = new Date(startDate).toISOString().split('T')[0]; // YYYY-MM-DD format
query['endDate'] = new Date(endDate).toISOString().split('T')[0];
}
try {
const response = await shortIoApiCall({
method: HttpMethod.GET,
auth,
url: `https://statistics.short.io/statistics/domain/${domainObject.id}/link_clicks`,
query,
});
const linkCount = ids.length;
const dateRange = startDate && endDate ? ` for ${query['startDate']} to ${query['endDate']}` : '';
return {
success: true,
message: `Retrieved click statistics for ${linkCount} link${linkCount > 1 ? 's' : ''}${dateRange}`,
data: response,
};
} catch (error: any) {
if (error.message.includes('400')) {
throw new Error(
'Invalid request parameters. Please check your link IDs and date range.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication failed or insufficient permissions. Please check your API key and domain access.'
);
}
if (error.message.includes('404')) {
throw new Error(
'Domain or links not found. Please verify the domain and link IDs exist.'
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate limit exceeded. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to retrieve link click statistics: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,142 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { shortIoApiCall } from '../common/client';
import { shortIoAuth } from '../common/auth';
import { domainIdDropdown, folderIdDropdown } from '../common/props';
export const listLinksAction = createAction({
auth: shortIoAuth,
name: 'list-short-links',
displayName: 'List Links',
description:
'Retrieve all links on a domain, with pagination and date-range filters.',
props: {
domain: {
...domainIdDropdown,
required: true,
description: 'Select the domain to retrieve links from',
},
folderId: folderIdDropdown,
limit: Property.Number({
displayName: 'Limit',
description: 'Number of results to return (1-150). Default is 50.',
required: false,
}),
idString: Property.ShortText({
displayName: 'Link ID Filter',
description: 'Filter by specific link ID (optional).',
required: false,
}),
createdAt: Property.ShortText({
displayName: 'Exact Creation Time',
description: 'Filter by exact creation time (ISO format or timestamp).',
required: false,
}),
beforeDate: Property.DateTime({
displayName: 'Before Date',
description: 'Return links created before this date and time.',
required: false,
}),
afterDate: Property.DateTime({
displayName: 'After Date',
description: 'Return links created after this date and time.',
required: false,
}),
dateSortOrder: Property.StaticDropdown({
displayName: 'Date Sort Order',
description: 'Order links by creation date.',
required: false,
options: {
disabled: false,
options: [
{ label: 'Ascending (Oldest First)', value: 'asc' },
{ label: 'Descending (Newest First)', value: 'desc' },
],
},
}),
pageToken: Property.ShortText({
displayName: 'Page Token',
description: 'Token for paginated results (get this from previous response).',
required: false,
}),
},
async run({ propsValue, auth }) {
const {
domain: domainString,
limit,
idString,
createdAt,
beforeDate,
afterDate,
dateSortOrder,
pageToken,
folderId,
} = propsValue;
if (!domainString) {
throw new Error('Domain is required. Please select a domain.');
}
if (limit && (limit < 1 || limit > 150)) {
throw new Error('Limit must be between 1 and 150.');
}
const query: Record<string, string> = {};
const domainObject = JSON.parse(domainString as string);
query['domain_id'] = String(domainObject.id);
if (limit) query['limit'] = String(limit);
if (idString && idString.trim() !== '') query['idString'] = String(idString);
if (createdAt && createdAt.trim() !== '') query['createdAt'] = String(createdAt);
if (beforeDate) query['beforeDate'] = beforeDate;
if (afterDate) query['afterDate'] = afterDate;
if (dateSortOrder) query['dateSortOrder'] = String(dateSortOrder);
if (pageToken && pageToken.trim() !== '') query['pageToken'] = String(pageToken);
if (folderId) query['folderId'] = String(folderId);
try {
const response = await shortIoApiCall({
method: HttpMethod.GET,
auth,
resourceUri: '/api/links',
query,
});
const linkCount = (response as any)['count'] || ((response as any)['links'] ? (response as any)['links'].length : 0);
const hasNextPage = (response as any)['nextPageToken'] ? ' (more pages available)' : '';
return {
success: true,
message: `Retrieved ${linkCount} links successfully${hasNextPage}`,
data: response,
};
} catch (error: any) {
if (error.message.includes('400')) {
throw new Error(
'Invalid request parameters. Please check your filter values and try again.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication failed or insufficient permissions. Please check your API key and domain access.'
);
}
if (error.message.includes('404')) {
throw new Error(
'Domain not found. Please verify the domain exists and you have access to it.'
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate limit exceeded. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to retrieve links: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,283 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { shortIoApiCall } from '../common/client';
import { shortIoAuth } from '../common/auth';
import { domainIdDropdown, linkIdDropdown, folderIdDropdown } from '../common/props';
export const updateShortLinkAction = createAction({
auth: shortIoAuth,
name: 'update-short-link',
displayName: 'Update Short Link',
description:
"Update an existing short link's original URL, path, title, or other properties using its link ID.",
props: {
domain: domainIdDropdown,
linkId: linkIdDropdown,
originalURL: Property.ShortText({
displayName: 'Original URL',
required: false,
}),
path: Property.ShortText({
displayName: 'Custom Path (Slug)',
required: false,
}),
title: Property.ShortText({
displayName: 'Title',
required: false,
}),
folderId: folderIdDropdown,
cloaking: Property.Checkbox({
displayName: 'Enable Cloaking',
required: false,
}),
password: Property.ShortText({
displayName: 'Password',
required: false,
}),
redirectType: Property.StaticDropdown({
displayName: 'Redirect Type',
description:
'The HTTP status code for the redirect. The default is 302 (Found).',
required: false,
options: {
disabled: false,
options: [
{ label: '301 (Moved Permanently)', value: '301' },
{ label: '302 (Found)', value: '302' },
{ label: '303 (See Other)', value: '303' },
{ label: '307 (Temporary Redirect)', value: '307' },
{ label: '308 (Permanent Redirect)', value: '308' },
],
},
}),
expiresAt: Property.DateTime({
displayName: 'Expiration Date',
description: 'The date and time when the link will become inactive.',
required: false,
}),
expiredURL: Property.ShortText({
displayName: 'Expired Redirect URL',
required: false,
}),
tags: Property.Array({
displayName: 'Tags',
description: 'Array of tags for the link',
required: false,
}),
utmSource: Property.ShortText({
displayName: 'UTM Source',
required: false,
}),
utmMedium: Property.ShortText({
displayName: 'UTM Medium',
required: false,
}),
utmCampaign: Property.ShortText({
displayName: 'UTM Campaign',
required: false,
}),
utmTerm: Property.ShortText({
displayName: 'UTM Term',
required: false,
}),
utmContent: Property.ShortText({
displayName: 'UTM Content',
required: false,
}),
ttl: Property.Number({
displayName: 'Time to Live (in seconds)',
description:
'⚠️ CAUTION: Link will be PERMANENTLY DELETED after this many seconds. This action cannot be undone. Use with extreme caution.',
required: false,
}),
androidURL: Property.ShortText({
displayName: 'Android URL',
required: false,
}),
iphoneURL: Property.ShortText({
displayName: 'iPhone URL',
required: false,
}),
createdAt: Property.DateTime({
displayName: 'Creation Time',
description: 'Overrides the creation time of the link.',
required: false,
}),
clicksLimit: Property.Number({
displayName: 'Clicks Limit',
description: 'Disable link after specified number of clicks (minimum: 1)',
required: false,
}),
passwordContact: Property.Checkbox({
displayName: 'Show Contact for Password Recovery',
required: false,
}),
skipQS: Property.Checkbox({
displayName: 'Skip Query String Merge',
required: false,
}),
archived: Property.Checkbox({
displayName: 'Archive Link',
required: false,
}),
splitURL: Property.ShortText({
displayName: 'Split URL',
required: false,
}),
splitPercent: Property.Number({
displayName: 'Split Percent',
description: 'Split URL percentage (1-100)',
required: false,
}),
integrationAdroll: Property.ShortText({
displayName: 'Adroll Integration ID',
required: false,
}),
integrationFB: Property.ShortText({
displayName: 'Facebook Integration ID',
required: false,
}),
integrationGA: Property.ShortText({
displayName: 'Google Analytics ID',
required: false,
}),
integrationGTM: Property.ShortText({
displayName: 'Google Tag Manager ID',
required: false,
}),
},
async run({ propsValue, auth }) {
const {
linkId,
domain: domainString,
originalURL,
path,
title,
folderId,
cloaking,
password,
redirectType,
expiresAt,
expiredURL,
tags,
utmSource,
utmMedium,
utmCampaign,
utmTerm,
utmContent,
ttl,
androidURL,
iphoneURL,
createdAt,
clicksLimit,
passwordContact,
skipQS,
archived,
splitURL,
splitPercent,
integrationAdroll,
integrationFB,
integrationGA,
integrationGTM,
} = propsValue;
if (clicksLimit && clicksLimit < 1) {
throw new Error('Clicks limit must be at least 1');
}
if (splitPercent && (splitPercent < 1 || splitPercent > 100)) {
throw new Error('Split percent must be between 1 and 100');
}
if (redirectType && !['301', '302', '303', '307', '308'].includes(redirectType)) {
throw new Error('Invalid redirect type. Must be one of: 301, 302, 303, 307, 308');
}
const query: Record<string, string> = {};
if (domainString) {
const domainObject = JSON.parse(domainString as string);
query['domain_id'] = String(domainObject.id);
}
const optionalParams = {
originalURL,
path,
title,
folderId,
cloaking,
password,
redirectType,
expiresAt,
expiredURL,
tags,
utmSource,
utmMedium,
utmCampaign,
utmTerm,
utmContent,
ttl: ttl ? ttl * 1000 : undefined,
androidURL,
iphoneURL,
createdAt,
clicksLimit,
passwordContact,
skipQS,
archived,
splitURL,
splitPercent,
integrationAdroll,
integrationFB,
integrationGA,
integrationGTM,
};
const body: Record<string, unknown> = {};
for (const [key, value] of Object.entries(optionalParams)) {
if (value !== null && value !== undefined && value !== '') {
body[key] = value;
}
}
try {
const response = await shortIoApiCall({
method: HttpMethod.POST,
auth,
resourceUri: `/links/${linkId}`,
query,
body,
});
return {
success: true,
message: 'Short link updated successfully',
data: response,
};
} catch (error: any) {
if (error.message.includes('400')) {
throw new Error(
'Invalid request parameters. Please check your input values and try again.'
);
}
if (error.message.includes('401') || error.message.includes('403')) {
throw new Error(
'Authentication failed. Please check your API key and permissions.'
);
}
if (error.message.includes('404')) {
throw new Error(
'Short link not found. Please check the link ID and try again.'
);
}
if (error.message.includes('429')) {
throw new Error(
'Rate limit exceeded. Please wait a moment before trying again.'
);
}
throw new Error(`Failed to update short link: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,33 @@
import { PieceAuth } from '@activepieces/pieces-framework';
import { shortIoApiCall } from './client';
import { HttpMethod } from '@activepieces/pieces-common';
import { AppConnectionType } from '@activepieces/shared';
export const shortIoAuth = PieceAuth.CustomAuth({
description: 'Enter your Short.io API Key',
props: {
apiKey: PieceAuth.SecretText({
displayName: 'API Key',
required: true,
}),
},
validate: async ({ auth }) => {
try {
await shortIoApiCall({
method: HttpMethod.GET,
auth: {
type: AppConnectionType.CUSTOM_AUTH,
props: auth,
},
resourceUri: '/api/domains',
});
return { valid: true };
} catch (e) {
return {
valid: false,
error: 'Invalid API Key',
};
}
},
required: true,
});

View File

@@ -0,0 +1,66 @@
import {
httpClient,
HttpMethod,
HttpRequest,
HttpMessageBody,
QueryParams,
} from '@activepieces/pieces-common';
import { AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
import { shortIoAuth } from './auth';
export type ShortioAuthProps = AppConnectionValueForAuthProperty<typeof shortIoAuth>;
export type ShortioApiCallParams = {
method: HttpMethod;
resourceUri?: string;
url?: string;
query?: Record<string, string | number | string[] | undefined>;
body?: unknown;
auth: ShortioAuthProps;
};
export async function shortIoApiCall<T extends HttpMessageBody>({
method,
resourceUri,
url,
query,
body,
auth,
}: ShortioApiCallParams): Promise<T> {
const finalUrl = url ?? `https://api.short.io${resourceUri ?? ''}`;
const queryParams: QueryParams = {};
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== null && value !== undefined) {
queryParams[key] = String(value);
}
}
}
const request: HttpRequest = {
method,
url: finalUrl,
headers: {
authorization: auth.props.apiKey,
'Content-Type': 'application/json',
accept: 'application/json',
},
queryParams,
body,
};
try {
const response = await httpClient.sendRequest<T>(request);
return response.body;
} catch (error: any) {
const status = error.response?.status;
const message =
error.response?.body?.message ||
error.message ||
'Unknown Short.io API error';
throw new Error(
`Short.io API Error (${status || 'No Status'}): ${message}`
);
}
}

View File

@@ -0,0 +1,180 @@
import { Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { shortIoApiCall, ShortioAuthProps } from './client';
import { shortIoAuth } from './auth';
interface ShortIoDomain {
id: number;
hostname: string;
}
interface ShortIoLink {
idString: string;
path: string;
originalURL: string;
}
interface ShortIoLinksResponse {
links: ShortIoLink[];
}
interface ShortIoFolder {
id: string;
name: string;
}
interface ShortIoFoldersResponse {
linkFolders: ShortIoFolder[];
}
export const domainIdDropdown =Property.Dropdown({
auth: shortIoAuth,
displayName: 'Domain',
description: 'Select the domain to use for the link',
required: true,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Connect your Short.io account first',
};
}
try {
const domains = await shortIoApiCall<ShortIoDomain[]>({
auth: auth as ShortioAuthProps,
method: HttpMethod.GET,
resourceUri: '/api/domains',
});
return {
disabled: false,
options: domains.map((domain) => ({
label: domain.hostname,
value: JSON.stringify({
id: domain.id,
hostname: domain.hostname,
}),
})),
placeholder:
domains.length === 0 ? 'No domains available' : 'Select a domain',
};
} catch (error: any) {
return {
disabled: true,
options: [],
placeholder: `Error loading domains: ${error.message}`,
};
}
},
});
export const linkIdDropdown =Property.Dropdown({
auth: shortIoAuth,
displayName: 'Short Link',
description: 'Select the short link from the domain',
required: true,
refreshers: ['auth', 'domain'],
options: async ({ auth, domain }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Connect your Short.io account first',
};
}
if (!domain) {
return {
disabled: true,
options: [],
placeholder: 'Select a domain first',
};
}
try {
const domainObject = JSON.parse(domain as string);
const response = await shortIoApiCall<ShortIoLinksResponse>({
auth: auth as ShortioAuthProps,
method: HttpMethod.GET,
resourceUri: '/api/links',
query: {
domain_id: domainObject.id,
limit: 100,
},
});
return {
disabled: false,
options: response.links.map((link) => ({
label: `${link.path || '(auto)'}${link.originalURL}`,
value: link.idString,
})),
placeholder:
response.links.length === 0 ? 'No links available' : 'Select a link',
};
} catch (error: any) {
return {
disabled: true,
options: [],
placeholder: `Error loading links: ${error.message}`,
};
}
},
});
export const folderIdDropdown =Property.Dropdown({
auth: shortIoAuth,
displayName: 'Folder',
description: 'Select the folder to add the link to.',
required: false,
refreshers: ['auth', 'domain'],
options: async ({ auth, domain }) => {
if (!auth || !domain) {
return {
disabled: true,
options: [],
placeholder: 'Select a domain first',
};
}
try {
const domainObject = JSON.parse(domain as string);
const response = await shortIoApiCall<ShortIoFoldersResponse>({
auth: auth as ShortioAuthProps,
method: HttpMethod.GET,
resourceUri: `/links/folders/${domainObject.id}`,
});
const foldersArray = response.linkFolders;
if (!foldersArray || foldersArray.length === 0) {
return {
disabled: true,
options: [],
placeholder: 'No folders found in this domain',
};
}
return {
disabled: false,
options: foldersArray.map((folder: ShortIoFolder) => ({
label: folder.name,
value: folder.id,
})),
placeholder: 'Select a folder',
};
} catch (error: any) {
return {
disabled: true,
options: [],
placeholder: `Error: Could not load folders. ${error.message}`,
};
}
},
});

View File

@@ -0,0 +1,317 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { shortIoAuth } from '../common/auth';
import { shortIoApiCall } from '../common/client';
import { domainIdDropdown } from '../common/props';
const LAST_LINK_IDS_KEY = 'shortio-last-link-ids';
export const newLinkCreatedTrigger = createTrigger({
auth: shortIoAuth,
name: 'new_link_created',
displayName: 'New Link Created',
description: 'Fires when a new short link is created on a domain. Useful to sync newly created links to other systems.',
type: TriggerStrategy.POLLING,
props: {
domain: {
...domainIdDropdown,
required: true,
description: 'Select the domain to monitor for new links',
},
pollingInterval: Property.StaticDropdown({
displayName: 'Polling Interval',
description: 'How often to check for new links. More frequent polling may hit rate limits.',
required: true,
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' },
],
},
}),
},
async onEnable(context) {
const { domain: domainString } = context.propsValue;
if (!domainString) {
throw new Error('Domain is required. Please select a domain.');
}
const domainObject = JSON.parse(domainString as string);
const domainId = domainObject.id;
try {
const response = await shortIoApiCall<{ links: ShortIoLink[] }>({
auth: context.auth,
method: HttpMethod.GET,
resourceUri: `/api/links`,
query: {
domain_id: domainId,
limit: 100,
dateSortOrder: 'desc',
},
});
const currentIds = (response.links || []).map((link) => link.idString);
await context.store.put<string[]>(LAST_LINK_IDS_KEY, currentIds);
} catch (error: any) {
throw new Error(`Failed to initialize trigger: ${error.message}`);
}
},
async onDisable() {
// No-op
},
async run(context) {
const { domain: domainString } = context.propsValue;
if (!domainString) {
throw new Error('Domain is required. Please select a domain.');
}
const domainObject = JSON.parse(domainString as string);
const domainId = domainObject.id;
const previousIds = await context.store.get<string[]>(LAST_LINK_IDS_KEY) || [];
try {
const response = await shortIoApiCall<{ links: ShortIoLink[] }>({
auth: context.auth,
method: HttpMethod.GET,
resourceUri: `/api/links`,
query: {
domain_id: domainId,
limit: 100,
dateSortOrder: 'desc',
},
});
const allLinks = response.links || [];
const currentIds = allLinks.map((link) => link.idString);
await context.store.put<string[]>(LAST_LINK_IDS_KEY, currentIds);
const newLinks = allLinks.filter((link) => !previousIds.includes(link.idString));
return newLinks.map((link) => ({
id: link.idString,
originalURL: link.originalURL,
shortURL: link.shortURL,
secureShortURL: link.secureShortURL,
title: link.title,
path: link.path,
createdAt: link.createdAt,
updatedAt: link.updatedAt,
tags: link.tags,
redirectType: link.redirectType,
domainId: link.DomainId || link.domainId,
folderId: link.FolderId,
ownerId: link.OwnerId,
hasPassword: link.hasPassword,
cloaking: link.cloaking,
password: link.password,
expiresAt: link.expiresAt,
expiredURL: link.expiredURL,
clicksLimit: link.clicksLimit,
archived: link.archived,
utmSource: link.utmSource,
utmMedium: link.utmMedium,
utmCampaign: link.utmCampaign,
utmTerm: link.utmTerm,
utmContent: link.utmContent,
androidURL: link.androidURL,
iphoneURL: link.iphoneURL,
triggerInfo: {
source: 'short.io',
type: 'new_link_created',
detectedAt: new Date().toISOString(),
domain: domainObject.hostname,
},
raw: link,
}));
} catch (error: any) {
if (error.message.includes('403')) {
throw new Error(`Access denied to domain. Please check your API key permissions for domain: ${domainObject.hostname}`);
}
if (error.message.includes('404')) {
throw new Error(`Domain not found: ${domainObject.hostname}. Please verify the domain exists.`);
}
if (error.message.includes('429')) {
throw new Error('Rate limit exceeded. Consider increasing the polling interval.');
}
throw new Error(`Failed to check for new links: ${error.message}`);
}
},
async test(context) {
const { domain: domainString } = context.propsValue;
if (!domainString) {
throw new Error('Domain is required. Please select a domain.');
}
const domainObject = JSON.parse(domainString as string);
const domainId = domainObject.id;
try {
const response = await shortIoApiCall<{ links: ShortIoLink[] }>({
auth: context.auth,
method: HttpMethod.GET,
resourceUri: `/api/links`,
query: {
domain_id: domainId,
limit: 1,
dateSortOrder: 'desc',
},
});
const link = response.links?.[0];
if (link) {
return [{
id: link.idString,
originalURL: link.originalURL,
shortURL: link.shortURL,
secureShortURL: link.secureShortURL,
title: link.title,
path: link.path,
createdAt: link.createdAt,
updatedAt: link.updatedAt,
tags: link.tags,
redirectType: link.redirectType,
domainId: link.DomainId || link.domainId,
folderId: link.FolderId,
ownerId: link.OwnerId,
hasPassword: link.hasPassword,
cloaking: link.cloaking,
password: link.password,
expiresAt: link.expiresAt,
expiredURL: link.expiredURL,
clicksLimit: link.clicksLimit,
archived: link.archived,
utmSource: link.utmSource,
utmMedium: link.utmMedium,
utmCampaign: link.utmCampaign,
utmTerm: link.utmTerm,
utmContent: link.utmContent,
androidURL: link.androidURL,
iphoneURL: link.iphoneURL,
triggerInfo: {
source: 'short.io',
type: 'new_link_created',
detectedAt: new Date().toISOString(),
domain: domainObject.hostname,
},
raw: link,
}];
}
return [];
} catch (error: any) {
if (error.message.includes('403')) {
throw new Error(`Access denied to domain. Please check your API key permissions for domain: ${domainObject.hostname}`);
}
if (error.message.includes('404')) {
throw new Error(`Domain not found: ${domainObject.hostname}. Please verify the domain exists.`);
}
throw new Error(`Failed to test trigger: ${error.message}`);
}
},
sampleData: {
id: 'lnk_61Mb_0dnRUg3vvtmAPZh3dhQh6',
originalURL: 'https://example.com/page',
shortURL: 'https://yourdomain.short.io/abc123',
secureShortURL: 'https://yourdomain.short.io/abc123',
title: 'Example Page Title',
path: 'abc123',
createdAt: '2025-01-15T12:00:00.000Z',
updatedAt: '2025-01-15T12:00:00.000Z',
tags: ['marketing', 'campaign'],
redirectType: 302,
domainId: 123456,
folderId: null,
ownerId: 789,
hasPassword: false,
cloaking: false,
password: null,
expiresAt: null,
expiredURL: null,
clicksLimit: null,
archived: false,
utmSource: 'newsletter',
utmMedium: 'email',
utmCampaign: 'january_promo',
utmTerm: null,
utmContent: null,
androidURL: null,
iphoneURL: null,
triggerInfo: {
source: 'short.io',
type: 'new_link_created',
detectedAt: '2025-01-15T12:01:00.000Z',
domain: 'yourdomain.short.io',
},
raw: {},
},
});
interface ShortIoLink {
idString: string;
id: string;
originalURL: string;
shortURL: string;
secureShortURL: string;
title?: string;
path: string;
createdAt: string;
updatedAt?: string;
tags: string[];
redirectType?: number;
DomainId?: number;
domainId?: number;
FolderId?: string | null;
OwnerId?: number;
hasPassword?: boolean;
cloaking?: boolean;
password?: string | null;
expiresAt?: string | number | null;
expiredURL?: string | null;
clicksLimit?: number | null;
passwordContact?: boolean | null;
skipQS?: boolean;
archived?: boolean;
splitURL?: string | null;
splitPercent?: number | null;
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
utmTerm?: string;
utmContent?: string;
ttl?: string | number | null;
androidURL?: string | null;
iphoneURL?: string | null;
integrationAdroll?: string | null;
integrationFB?: string | null;
integrationGA?: string | null;
integrationGTM?: string | null;
User?: {
id: number;
name: string | null;
email: string;
photoURL: string | null;
};
[key: string]: any;
}