diff --git a/apps/docs/content/docs/en/tools/short_io.mdx b/apps/docs/content/docs/en/tools/short_io.mdx new file mode 100644 index 0000000000..05822ee6ab --- /dev/null +++ b/apps/docs/content/docs/en/tools/short_io.mdx @@ -0,0 +1,175 @@ +--- +title: Short.io +description: Create and manage short links, domains, QR codes, and analytics +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Short.io](https://short.io/) is a white-label URL shortener that lets you create branded short links on your own domain, track clicks, and manage links at scale. Short.io is designed for businesses that want professional short URLs, QR codes, and link analytics without relying on generic shorteners. + +With Short.io in Sim, you can: + +- **Create short links**: Generate branded short URLs from long URLs using your custom domain, with optional custom paths +- **List domains**: Retrieve all Short.io domains on your account to get domain IDs for listing links +- **List links**: List short links for a domain with pagination and optional date sort order +- **Delete links**: Remove a short link by its ID (e.g. lnk_abc123_abcdef) +- **Generate QR codes**: Create QR codes for any Short.io link with optional size, color, background color, and format (PNG or SVG); returns a base64 data URL +- **Get link statistics**: Fetch click analytics for a link including total clicks, human clicks, referrer/country/browser/OS/city breakdowns, UTM dimensions, time-series data, and date interval + +These capabilities allow your Sim agents to automate link shortening, QR code generation, and analytics reporting directly in your workflows — from campaign tracking to link management and performance dashboards. +{/* MANUAL-CONTENT-END */} + +## Usage Instructions + +Integrate Short.io into your workflow for link shortening, QR codes, and analytics. Authenticate with your Short.io Secret API Key. Create links, list domains and links, delete links, generate QR codes, and fetch per-link statistics. + +## Tools + +### `short_io_create_link` + +Create a short link using your Short.io custom domain. + +#### Input + +| Parameter | Type | Required | Description | +| ------------- | ------ | -------- | ---------------------------------------------------------- | +| `domain` | string | Yes | Your registered Short.io custom domain | +| `originalURL` | string | Yes | The long URL to shorten | +| `path` | string | No | Optional custom path for the short link | +| `apiKey` | string | Yes | Short.io Secret API Key | + +#### Output + +| Parameter | Type | Description | +| ---------- | ------- | ---------------------------------- | +| `success` | boolean | Whether the link was created | +| `shortURL` | string | The generated short link URL | +| `idString` | string | The unique Short.io link ID | +| `error` | string | Error message if failed | + +### `short_io_list_domains` + +List Short.io domains. Returns domain IDs and details for use in List Links. + +#### Input + +| Parameter | Type | Required | Description | +| ------------- | ------ | -------- | ------------------------------- | +| `apiKey` | string | Yes | Short.io Secret API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ------- | ------------------------------------ | +| `success` | boolean | Success status | +| `domains` | array | List of domain objects (id, hostname) | +| `count` | number | Number of domains | +| `error` | string | Error message | + +### `short_io_list_links` + +List short links for a domain. Requires domain_id from List Domains. Max 150 per request. + +#### Input + +| Parameter | Type | Required | Description | +| --------------- | ------ | -------- | ------------------------------------ | +| `domainId` | number | Yes | Domain ID (from List Domains) | +| `limit` | number | No | Max links to return (1–150) | +| `pageToken` | string | No | Pagination token from previous call | +| `dateSortOrder`| string | No | Sort by date: asc or desc | +| `apiKey` | string | Yes | Short.io Secret API Key | + +#### Output + +| Parameter | Type | Description | +| --------------- | ------- | ---------------------------------------- | +| `success` | boolean | Success status | +| `links` | array | List of link objects (idString, shortURL, originalURL, path, etc.) | +| `count` | number | Number of links returned | +| `nextPageToken` | string | Token for next page | +| `error` | string | Error message | + +### `short_io_delete_link` + +Delete a short link by ID. Rate limit 20/s. + +#### Input + +| Parameter | Type | Required | Description | +| ------------- | ------ | -------- | ------------------------------- | +| `linkId` | string | Yes | Link ID to delete (e.g. lnk_abc123_abcdef) | +| `apiKey` | string | Yes | Short.io Secret API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ------- | -------------------------- | +| `success` | boolean | Success status | +| `deleted` | boolean | Whether the link was deleted | +| `idString`| string | Deleted link ID | +| `error` | string | Error message | + +### `short_io_get_qr_code` + +Generate a QR code for a Short.io link. Returns a base64 data URL (e.g. data:image/png;base64,...). + +#### Input + +| Parameter | Type | Required | Description | +| --------------- | ------ | -------- | ------------------------------------ | +| `linkId` | string | Yes | Link ID (e.g. lnk_abc123_abcdef) | +| `color` | string | No | QR color hex (e.g. 000000) | +| `backgroundColor` | string | No | Background color hex (e.g. FFFFFF) | +| `size` | number | No | QR size 1–99 | +| `type` | string | No | Output format: png or svg | +| `apiKey` | string | Yes | Short.io Secret API Key | + +#### Output + +| Parameter | Type | Description | +| ---------- | ------- | ------------------------------------------------ | +| `file` | file | Generated QR code image file | + +### `short_io_get_analytics` + +Fetch click statistics for a Short.io link. + +#### Input + +| Parameter | Type | Required | Description | +| ------------- | ------ | -------- | -------------------------------------------------------- | +| `linkId` | string | Yes | Link ID | +| `period` | string | Yes | Period: today, yesterday, last7, last30, total, week, month, lastmonth | +| `tz` | string | No | Timezone (default UTC) | +| `apiKey` | string | Yes | Short.io Secret API Key | + +#### Output + +| Parameter | Type | Description | +| ------------------- | ------- | ------------------------------------------------------ | +| `success` | boolean | Success status | +| `clicks` | number | Total clicks in period | +| `totalClicks` | number | Total clicks | +| `humanClicks` | number | Human clicks | +| `totalClicksChange` | string | Change vs previous period | +| `humanClicksChange` | string | Human clicks change | +| `referer` | array | Referrer breakdown (referer, score) | +| `country` | array | Country breakdown (countryName, country, score) | +| `browser` | array | Browser breakdown (browser, score) | +| `os` | array | OS breakdown (os, score) | +| `city` | array | City breakdown (city, name, countryCode, score) | +| `device` | array | Device breakdown | +| `social` | array | Social source breakdown (social, score) | +| `utmMedium` | array | UTM medium breakdown | +| `utmSource` | array | UTM source breakdown | +| `utmCampaign` | array | UTM campaign breakdown | +| `clickStatistics` | object | Time-series click data (datasets with x/y per interval) | +| `interval` | object | Date range (startDate, endDate, prevStartDate, prevEndDate, tz) | +| `error` | string | Error message | diff --git a/apps/sim/app/api/tools/short_io/qr/route.ts b/apps/sim/app/api/tools/short_io/qr/route.ts new file mode 100644 index 0000000000..39b64c2802 --- /dev/null +++ b/apps/sim/app/api/tools/short_io/qr/route.ts @@ -0,0 +1,101 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('ShortIoQrAPI') + +const ShortIoQrSchema = z.object({ + apiKey: z.string().min(1, 'API key is required'), + linkId: z.string().min(1, 'Link ID is required'), + color: z.string().optional(), + backgroundColor: z.string().optional(), + size: z.number().min(1).max(99).optional(), + type: z.enum(['png', 'svg']).optional(), + useDomainSettings: z.boolean().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Short.io QR request: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const body = await request.json() + const validated = ShortIoQrSchema.parse(body) + + const qrBody: Record = { + useDomainSettings: validated.useDomainSettings ?? true, + } + if (validated.color) qrBody.color = validated.color + if (validated.backgroundColor) qrBody.backgroundColor = validated.backgroundColor + if (validated.size) qrBody.size = validated.size + if (validated.type) qrBody.type = validated.type + + const response = await fetch(`https://api.short.io/links/qr/${validated.linkId}`, { + method: 'POST', + headers: { + Authorization: validated.apiKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(qrBody), + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => response.statusText) + logger.error(`[${requestId}] Short.io QR API error: ${errorText}`) + return NextResponse.json( + { success: false, error: `Short.io API error: ${errorText}` }, + { status: response.status } + ) + } + + const contentType = response.headers.get('Content-Type') ?? 'image/png' + const fileBuffer = Buffer.from(await response.arrayBuffer()) + const mimeType = contentType.split(';')[0]?.trim() || 'image/png' + const ext = validated.type === 'svg' ? 'svg' : 'png' + const fileName = `qr-${validated.linkId}.${ext}` + + logger.info(`[${requestId}] QR code generated`, { + linkId: validated.linkId, + size: fileBuffer.length, + mimeType, + }) + + return NextResponse.json({ + success: true, + output: { + file: { + name: fileName, + mimeType, + data: fileBuffer.toString('base64'), + size: fileBuffer.length, + }, + }, + }) + } catch (error: unknown) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { + success: false, + error: `Validation error: ${error.errors.map((e) => e.message).join(', ')}`, + }, + { status: 400 } + ) + } + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] Short.io QR error: ${message}`) + return NextResponse.json({ success: false, error: message }, { status: 500 }) + } +} diff --git a/apps/sim/blocks/blocks/short_io.ts b/apps/sim/blocks/blocks/short_io.ts new file mode 100644 index 0000000000..e82802434c --- /dev/null +++ b/apps/sim/blocks/blocks/short_io.ts @@ -0,0 +1,228 @@ +import { ShortIoIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { ToolResponse } from '@/tools/types' + +export const ShortIoBlock: BlockConfig = { + type: 'short_io', + name: 'Short.io', + description: 'Create and manage short links, domains, and analytics.', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate Short.io to generate branded short links, list domains and links, delete links, generate QR codes, and view link statistics. Requires your Short.io Secret API Key.', + docsLink: 'https://docs.sim.ai/tools/short_io', + category: 'tools', + bgColor: '#FFFFFF', + icon: ShortIoIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Link', id: 'create_link' }, + { label: 'List Domains', id: 'list_domains' }, + { label: 'List Links', id: 'list_links' }, + { label: 'Delete Link', id: 'delete_link' }, + { label: 'Get QR Code', id: 'get_qr_code' }, + { label: 'Get Link Statistics', id: 'get_analytics' }, + ], + value: () => 'create_link', + }, + { + id: 'apiKey', + title: 'Secret API Key', + type: 'short-input', + mode: 'basic', + required: true, + password: true, + placeholder: 'sk_...', + }, + { + id: 'domain', + title: 'Custom Domain', + type: 'short-input', + placeholder: 'link.yourbrand.com', + condition: { field: 'operation', value: 'create_link' }, + required: true, + }, + { + id: 'originalURL', + title: 'Original URL', + type: 'long-input', + placeholder: 'https://www.example.com/very/long/path/to/page', + condition: { field: 'operation', value: 'create_link' }, + required: true, + }, + { + id: 'path', + title: 'Custom Path (Optional)', + type: 'short-input', + placeholder: 'my-custom-path', + condition: { field: 'operation', value: 'create_link' }, + required: false, + }, + { + id: 'domainId', + title: 'Domain ID', + type: 'short-input', + placeholder: '12345', + condition: { field: 'operation', value: 'list_links' }, + required: true, + }, + { + id: 'limit', + title: 'Limit (1–150)', + type: 'short-input', + placeholder: '50', + condition: { field: 'operation', value: 'list_links' }, + required: false, + }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Next page token', + condition: { field: 'operation', value: 'list_links' }, + required: false, + }, + { + id: 'linkId', + title: 'Short.io Link ID', + type: 'short-input', + placeholder: 'lnk_abc123_abcdef', + condition: { + field: 'operation', + value: ['get_qr_code', 'get_analytics', 'delete_link'], + }, + required: true, + }, + { + id: 'type', + title: 'QR Format', + type: 'dropdown', + options: [ + { label: 'PNG', id: 'png' }, + { label: 'SVG', id: 'svg' }, + ], + condition: { field: 'operation', value: 'get_qr_code' }, + required: false, + value: () => 'png', + }, + { + id: 'size', + title: 'QR Size (1–99)', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: 'get_qr_code' }, + required: false, + }, + { + id: 'color', + title: 'QR Color (hex)', + type: 'short-input', + placeholder: '000000', + condition: { field: 'operation', value: 'get_qr_code' }, + required: false, + }, + { + id: 'backgroundColor', + title: 'Background Color (hex)', + type: 'short-input', + placeholder: 'FFFFFF', + condition: { field: 'operation', value: 'get_qr_code' }, + required: false, + }, + { + id: 'period', + title: 'Statistics Period', + type: 'dropdown', + options: [ + { label: 'Today', id: 'today' }, + { label: 'Yesterday', id: 'yesterday' }, + { label: 'Last 7 Days', id: 'last_7_days' }, + { label: 'Last 30 Days', id: 'last_30_days' }, + { label: 'All Time', id: 'all_time' }, + ], + condition: { field: 'operation', value: 'get_analytics' }, + required: true, + value: () => 'last_30_days', + }, + ], + tools: { + access: [ + 'short_io_create_link', + 'short_io_list_domains', + 'short_io_list_links', + 'short_io_delete_link', + 'short_io_get_qr_code', + 'short_io_get_analytics', + ], + config: { + tool: (params) => `short_io_${params.operation}`, + params: (params) => { + const { apiKey, operation, size, domainId, limit, ...rest } = params + const out: Record = { ...rest, apiKey } + if (size !== undefined && size !== '') { + const n = Number(size) + if (typeof n === 'number' && !Number.isNaN(n) && n >= 1 && n <= 99) out.size = n + } + if (operation === 'list_links' && domainId !== undefined && domainId !== '') { + const d = Number(domainId) + if (typeof d === 'number' && !Number.isNaN(d)) out.domainId = d + } + if (operation === 'list_links' && limit !== undefined && limit !== '') { + const l = Number(limit) + if (typeof l === 'number' && !Number.isNaN(l) && l >= 1 && l <= 150) out.limit = l + } + return out + }, + }, + }, + inputs: { + apiKey: { type: 'string', description: 'Secret API Key' }, + operation: { type: 'string', description: 'Short.io operation to perform' }, + domain: { type: 'string', description: 'Your registered Short.io custom domain' }, + originalURL: { type: 'string', description: 'The original long URL to shorten' }, + path: { type: 'string', description: 'Optional custom path for the short link' }, + domainId: { type: 'number', description: 'Domain ID (from List Domains)' }, + limit: { type: 'number', description: 'Max links to return (1–150)' }, + pageToken: { type: 'string', description: 'Pagination token for List Links' }, + linkId: { type: 'string', description: 'The Short.io internal link ID string' }, + type: { type: 'string', description: 'QR output format: png or svg' }, + size: { type: 'number', description: 'QR size 1–99' }, + color: { type: 'string', description: 'QR color hex' }, + backgroundColor: { type: 'string', description: 'QR background color hex' }, + period: { type: 'string', description: 'Statistics period (e.g. today, last_30_days, all_time)' }, + tz: { type: 'string', description: 'Timezone for statistics (e.g. UTC)' }, + }, + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + shortURL: { type: 'string', description: 'The generated short link' }, + idString: { type: 'string', description: 'The Short.io link ID' }, + domains: { type: 'array', description: 'List of domains (from List Domains)' }, + count: { type: 'number', description: 'Number of domains or links returned' }, + links: { type: 'array', description: 'List of links (from List Links)' }, + nextPageToken: { type: 'string', description: 'Pagination token for next page' }, + deleted: { type: 'boolean', description: 'Whether the link was deleted' }, + file: { type: 'file', description: 'Generated QR code image file' }, + clicks: { type: 'number', description: 'Total clicks in period' }, + totalClicks: { type: 'number', description: 'Total clicks' }, + humanClicks: { type: 'number', description: 'Human clicks' }, + totalClicksChange: { type: 'string', description: 'Change in total clicks vs previous period' }, + humanClicksChange: { type: 'string', description: 'Change in human clicks vs previous period' }, + referer: { type: 'array', description: 'Referrer breakdown (referer, score)' }, + country: { type: 'array', description: 'Country breakdown (countryName, country, score)' }, + browser: { type: 'array', description: 'Browser breakdown (browser, score)' }, + os: { type: 'array', description: 'OS breakdown (os, score)' }, + city: { type: 'array', description: 'City breakdown (city, name, countryCode, score)' }, + device: { type: 'array', description: 'Device breakdown' }, + social: { type: 'array', description: 'Social source breakdown (social, score)' }, + utmMedium: { type: 'array', description: 'UTM medium breakdown' }, + utmSource: { type: 'array', description: 'UTM source breakdown' }, + utmCampaign: { type: 'array', description: 'UTM campaign breakdown' }, + clickStatistics: { type: 'object', description: 'Time-series click data (datasets with x/y per interval)' }, + interval: { type: 'object', description: 'Date range (startDate, endDate, prevStartDate, prevEndDate, tz)' }, + error: { type: 'string', description: 'Error message if operation failed' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 03b9827a77..cfbcfb7042 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -130,6 +130,7 @@ import { ServiceNowBlock } from '@/blocks/blocks/servicenow' import { SftpBlock } from '@/blocks/blocks/sftp' import { SharepointBlock } from '@/blocks/blocks/sharepoint' import { ShopifyBlock } from '@/blocks/blocks/shopify' +import { ShortIoBlock } from '@/blocks/blocks/short_io' import { SimilarwebBlock } from '@/blocks/blocks/similarweb' import { SlackBlock } from '@/blocks/blocks/slack' import { SmtpBlock } from '@/blocks/blocks/smtp' @@ -323,6 +324,7 @@ export const registry: Record = { sftp: SftpBlock, sharepoint: SharepointBlock, shopify: ShopifyBlock, + short_io: ShortIoBlock, similarweb: SimilarwebBlock, slack: SlackBlock, smtp: SmtpBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index dcd5741f2b..f79fa028c3 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -76,7 +76,6 @@ export function ApiIcon(props: SVGProps) { ) } - export function ConditionalIcon(props: SVGProps) { return ( ) { ) } + +export function ShortIoIcon(props: SVGProps) { + return ( + + + + + + + ) +} \ No newline at end of file diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 5a2f5787c7..98887695bb 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1609,6 +1609,14 @@ import { shopifyUpdateOrderTool, shopifyUpdateProductTool, } from '@/tools/shopify' +import { + shortIoCreateLinkTool, + shortIoDeleteLinkTool, + shortIoGetAnalyticsTool, + shortIoGetQrCodeTool, + shortIoListDomainsTool, + shortIoListLinksTool, +} from '@/tools/short_io' import { similarwebBounceRateTool, similarwebPagesPerVisitTool, @@ -2353,6 +2361,12 @@ export const tools: Record = { tavily_extract: tavilyExtractTool, tavily_crawl: tavilyCrawlTool, tavily_map: tavilyMapTool, + short_io_create_link: shortIoCreateLinkTool, + short_io_list_domains: shortIoListDomainsTool, + short_io_list_links: shortIoListLinksTool, + short_io_delete_link: shortIoDeleteLinkTool, + short_io_get_qr_code: shortIoGetQrCodeTool, + short_io_get_analytics: shortIoGetAnalyticsTool, supabase_query: supabaseQueryTool, supabase_insert: supabaseInsertTool, supabase_get_row: supabaseGetRowTool, diff --git a/apps/sim/tools/short_io/create_link.ts b/apps/sim/tools/short_io/create_link.ts new file mode 100644 index 0000000000..b4531d8e40 --- /dev/null +++ b/apps/sim/tools/short_io/create_link.ts @@ -0,0 +1,81 @@ +import type { ShortIoCreateLinkParams } from '@/tools/short_io/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export const shortIoCreateLinkTool: ToolConfig = { + id: 'short_io_create_link', + name: 'Short.io Create Link', + description: 'Create a short link using your Short.io custom domain.', + version: '1.0', + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Short.io Secret API Key', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your registered Short.io custom domain', + }, + originalURL: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The long URL to shorten', + }, + path: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional custom path for the short link', + }, + }, + request: { + url: 'https://api.short.io/links', + method: 'POST', + headers: (params) => ({ + Authorization: params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => { + const bodyData: Record = { + domain: params.domain, + originalURL: params.originalURL, + } + if (params.path) { + bodyData.path = params.path + } + return bodyData + }, + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text().catch(() => response.statusText) + return { + success: false, + output: { + success: false, + error: `Failed to create short link: ${errorText}`, + }, + } + } + + const data = await response.json().catch(() => ({})) + return { + success: true, + output: { + success: true, + shortURL: data.shortURL, + idString: data.idString, + }, + } + }, + outputs: { + success: { type: 'boolean', description: 'Whether the link was created successfully' }, + shortURL: { type: 'string', description: 'The generated short link URL' }, + idString: { type: 'string', description: 'The unique Short.io link ID string' }, + error: { type: 'string', description: 'Error message if failed' }, + }, +} diff --git a/apps/sim/tools/short_io/delete_link.ts b/apps/sim/tools/short_io/delete_link.ts new file mode 100644 index 0000000000..b0e2762220 --- /dev/null +++ b/apps/sim/tools/short_io/delete_link.ts @@ -0,0 +1,51 @@ +import type { ShortIoDeleteLinkParams } from '@/tools/short_io/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export const shortIoDeleteLinkTool: ToolConfig = { + id: 'short_io_delete_link', + name: 'Short.io Delete Link', + description: 'Delete a short link by ID (e.g. lnk_abc123_abcdef). Rate limit 20/s.', + version: '1.0', + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Short.io Secret API Key', + }, + linkId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Link ID to delete', + }, + }, + request: { + url: (params) => `https://api.short.io/links/${encodeURIComponent(params.linkId)}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: params.apiKey, + }), + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const err = await response.text().catch(() => response.statusText) + return { success: false, output: { success: false, error: err } } + } + const data = await response.json().catch(() => ({})) + return { + success: true, + output: { + success: true, + deleted: data.success === true, + idString: data.idString ?? undefined, + }, + } + }, + outputs: { + success: { type: 'boolean', description: 'Success status' }, + deleted: { type: 'boolean', description: 'Whether the link was deleted' }, + idString: { type: 'string', description: 'Deleted link ID' }, + error: { type: 'string', description: 'Error message' }, + }, +} diff --git a/apps/sim/tools/short_io/get_analytics.ts b/apps/sim/tools/short_io/get_analytics.ts new file mode 100644 index 0000000000..ea7b0992cd --- /dev/null +++ b/apps/sim/tools/short_io/get_analytics.ts @@ -0,0 +1,115 @@ +import type { ShortIoGetAnalyticsParams } from '@/tools/short_io/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +const STATS_PERIOD_MAP: Record = { + today: 'today', + yesterday: 'yesterday', + last_7_days: 'last7', + last_30_days: 'last30', + all_time: 'total', + week: 'week', + month: 'month', + lastmonth: 'lastmonth', +} + +export const shortIoGetAnalyticsTool: ToolConfig = { + id: 'short_io_get_analytics', + name: 'Short.io Get Link Statistics', + description: + 'Fetch click statistics for a Short.io link (Statistics API: totalClicks, humanClicks, referer, country, etc.).', + version: '1.0', + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Short.io Secret API Key', + }, + linkId: { type: 'string', required: true, visibility: 'user-or-llm', description: 'Link ID' }, + period: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Period: today, yesterday, last7, last30, total, week, month, lastmonth', + }, + tz: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Timezone (default UTC)', + }, + }, + request: { + url: (params) => { + const base = `https://statistics.short.io/statistics/link/${encodeURIComponent(params.linkId)}` + const period = STATS_PERIOD_MAP[params.period] ?? params.period ?? 'last30' + const q = new URLSearchParams({ period }) + if (params.tz) q.set('tz', params.tz) + return `${base}?${q.toString()}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: params.apiKey, + Accept: 'application/json', + }), + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const err = await response.text().catch(() => response.statusText) + return { success: false, output: { success: false, error: err } } + } + const data = await response.json().catch(() => ({})) + const totalClicks = data.totalClicks ?? data.clicks ?? 0 + const humanClicks = data.humanClicks ?? totalClicks + return { + success: true, + output: { + success: true, + clicks: totalClicks, + totalClicks, + humanClicks, + totalClicksChange: data.totalClicksChange, + humanClicksChange: data.humanClicksChange, + referer: data.referer ?? [], + country: data.country ?? [], + browser: data.browser ?? [], + os: data.os ?? [], + city: data.city ?? [], + device: data.device ?? [], + social: data.social ?? [], + utmMedium: data.utm_medium ?? [], + utmSource: data.utm_source ?? [], + utmCampaign: data.utm_campaign ?? [], + clickStatistics: data.clickStatistics, + interval: data.interval, + }, + } + }, + outputs: { + success: { type: 'boolean', description: 'Success status' }, + clicks: { type: 'number', description: 'Total clicks in period' }, + totalClicks: { type: 'number', description: 'Total clicks' }, + humanClicks: { type: 'number', description: 'Human clicks' }, + totalClicksChange: { type: 'string', description: 'Change vs previous period' }, + humanClicksChange: { type: 'string', description: 'Human clicks change' }, + referer: { type: 'array', description: 'Referrer breakdown (referer, score)' }, + country: { type: 'array', description: 'Country breakdown (countryName, country, score)' }, + browser: { type: 'array', description: 'Browser breakdown (browser, score)' }, + os: { type: 'array', description: 'OS breakdown (os, score)' }, + city: { type: 'array', description: 'City breakdown (city, name, countryCode, score)' }, + device: { type: 'array', description: 'Device breakdown' }, + social: { type: 'array', description: 'Social source breakdown (social, score)' }, + utmMedium: { type: 'array', description: 'UTM medium breakdown' }, + utmSource: { type: 'array', description: 'UTM source breakdown' }, + utmCampaign: { type: 'array', description: 'UTM campaign breakdown' }, + clickStatistics: { + type: 'object', + description: 'Time-series click data (datasets with x/y points per interval)', + }, + interval: { + type: 'object', + description: 'Date range (startDate, endDate, prevStartDate, prevEndDate, tz)', + }, + error: { type: 'string', description: 'Error message' }, + }, +} diff --git a/apps/sim/tools/short_io/get_qr_code.ts b/apps/sim/tools/short_io/get_qr_code.ts new file mode 100644 index 0000000000..84bd4adad0 --- /dev/null +++ b/apps/sim/tools/short_io/get_qr_code.ts @@ -0,0 +1,92 @@ +import type { ShortIoGetQrParams } from '@/tools/short_io/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export const shortIoGetQrCodeTool: ToolConfig = { + id: 'short_io_get_qr_code', + name: 'Short.io Generate QR Code', + description: 'Generate a QR code for a Short.io link (POST /links/qr/{linkIdString}).', + version: '1.0', + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Short.io Secret API Key', + }, + linkId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Link ID (e.g. lnk_abc123_abcdef)', + }, + color: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'QR color hex (e.g. 000000)', + }, + backgroundColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Background color hex (e.g. FFFFFF)', + }, + size: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'QR size 1–99', + }, + type: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Output format: png or svg', + }, + useDomainSettings: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Use domain settings (default true)', + }, + }, + request: { + url: '/api/tools/short_io/qr', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + apiKey: params.apiKey, + linkId: params.linkId, + useDomainSettings: params.useDomainSettings ?? true, + } + if (params.color != null && params.color !== '') body.color = params.color + if (params.backgroundColor != null && params.backgroundColor !== '') + body.backgroundColor = params.backgroundColor + if (params.size != null && params.size >= 1 && params.size <= 99) body.size = params.size + if (params.type === 'svg' || params.type === 'png') body.type = params.type + return body + }, + }, + transformResponse: async (response: Response) => { + const data = await response.json().catch(() => ({})) + if (!response.ok || !data.success) { + return { + success: false, + output: { success: false, error: data.error || response.statusText }, + } + } + return { + success: true, + output: data.output, + } + }, + outputs: { + file: { + type: 'file', + description: 'Generated QR code image file', + }, + }, +} diff --git a/apps/sim/tools/short_io/index.ts b/apps/sim/tools/short_io/index.ts new file mode 100644 index 0000000000..6ac95fe227 --- /dev/null +++ b/apps/sim/tools/short_io/index.ts @@ -0,0 +1,6 @@ +export { shortIoCreateLinkTool } from '@/tools/short_io/create_link' +export { shortIoDeleteLinkTool } from '@/tools/short_io/delete_link' +export { shortIoGetAnalyticsTool } from '@/tools/short_io/get_analytics' +export { shortIoGetQrCodeTool } from '@/tools/short_io/get_qr_code' +export { shortIoListDomainsTool } from '@/tools/short_io/list_domains' +export { shortIoListLinksTool } from '@/tools/short_io/list_links' diff --git a/apps/sim/tools/short_io/list_domains.ts b/apps/sim/tools/short_io/list_domains.ts new file mode 100644 index 0000000000..313656daa0 --- /dev/null +++ b/apps/sim/tools/short_io/list_domains.ts @@ -0,0 +1,43 @@ +import type { ShortIoListDomainsParams } from '@/tools/short_io/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export const shortIoListDomainsTool: ToolConfig = { + id: 'short_io_list_domains', + name: 'Short.io List Domains', + description: 'List Short.io domains. Returns domain IDs and details for use in List Links.', + version: '1.0', + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Short.io Secret API Key', + }, + }, + request: { + url: 'https://api.short.io/api/domains', + method: 'GET', + headers: (params) => ({ + Authorization: params.apiKey, + Accept: 'application/json', + }), + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const err = await response.text().catch(() => response.statusText) + return { success: false, output: { success: false, error: err } } + } + const data = await response.json().catch(() => ({})) + const list = Array.isArray(data) ? data : (data.domains ?? data.list ?? []) + return { + success: true, + output: { success: true, domains: list, count: list.length }, + } + }, + outputs: { + success: { type: 'boolean', description: 'Success status' }, + domains: { type: 'array', description: 'List of domain objects (id, hostname, etc.)' }, + count: { type: 'number', description: 'Number of domains' }, + error: { type: 'string', description: 'Error message' }, + }, +} diff --git a/apps/sim/tools/short_io/list_links.ts b/apps/sim/tools/short_io/list_links.ts new file mode 100644 index 0000000000..b32affefa4 --- /dev/null +++ b/apps/sim/tools/short_io/list_links.ts @@ -0,0 +1,89 @@ +import type { ShortIoListLinksParams } from '@/tools/short_io/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export const shortIoListLinksTool: ToolConfig = { + id: 'short_io_list_links', + name: 'Short.io List Links', + description: + 'List short links for a domain. Requires domain_id (from List Domains or dashboard). Max 150 per request.', + version: '1.0', + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Short.io Secret API Key', + }, + domainId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Domain ID (from List Domains)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Max links to return (1–150)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token from previous response', + }, + dateSortOrder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort by date: asc or desc', + }, + }, + request: { + url: (params) => { + const u = new URL('https://api.short.io/api/links') + u.searchParams.set('domain_id', String(params.domainId)) + if (params.limit != null && params.limit >= 1 && params.limit <= 150) { + u.searchParams.set('limit', String(params.limit)) + } + if (params.pageToken) u.searchParams.set('pageToken', params.pageToken) + if (params.dateSortOrder === 'asc' || params.dateSortOrder === 'desc') { + u.searchParams.set('dateSortOrder', params.dateSortOrder) + } + return u.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: params.apiKey, + Accept: 'application/json', + }), + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const err = await response.text().catch(() => response.statusText) + return { success: false, output: { success: false, error: err } } + } + const data = await response.json().catch(() => ({})) + const links = data.links ?? [] + const count = data.count ?? links.length + return { + success: true, + output: { + success: true, + links, + count, + nextPageToken: data.nextPageToken ?? undefined, + }, + } + }, + outputs: { + success: { type: 'boolean', description: 'Success status' }, + links: { + type: 'array', + description: 'List of link objects (idString, shortURL, originalURL, path, etc.)', + }, + count: { type: 'number', description: 'Number of links returned' }, + nextPageToken: { type: 'string', description: 'Token for next page' }, + error: { type: 'string', description: 'Error message' }, + }, +} diff --git a/apps/sim/tools/short_io/types.ts b/apps/sim/tools/short_io/types.ts new file mode 100644 index 0000000000..ee4e9c050d --- /dev/null +++ b/apps/sim/tools/short_io/types.ts @@ -0,0 +1,40 @@ +export interface ShortIoCreateLinkParams { + apiKey: string + domain: string + originalURL: string + path?: string +} + +export interface ShortIoListDomainsParams { + apiKey: string +} + +export interface ShortIoListLinksParams { + apiKey: string + domainId: number + limit?: number + pageToken?: string + dateSortOrder?: 'asc' | 'desc' +} + +export interface ShortIoDeleteLinkParams { + apiKey: string + linkId: string +} + +export interface ShortIoGetQrParams { + apiKey: string + linkId: string + color?: string + backgroundColor?: string + size?: number + type?: 'png' | 'svg' + useDomainSettings?: boolean +} + +export interface ShortIoGetAnalyticsParams { + apiKey: string + linkId: string + period: string + tz?: string +}