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