diff --git a/bun.lock b/bun.lock index 64fa1f2bfc1..5cc4c0ba7ff 100644 --- a/bun.lock +++ b/bun.lock @@ -2988,7 +2988,7 @@ }, "packages/pieces/community/gmail": { "name": "@activepieces/piece-gmail", - "version": "0.12.2", + "version": "0.12.3", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -3537,6 +3537,17 @@ "tslib": "^2.3.0", }, }, + "packages/pieces/community/iloveapi": { + "name": "@activepieces/piece-iloveapi", + "version": "0.0.1", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "form-data": "4.0.4", + "tslib": "2.6.2", + }, + }, "packages/pieces/community/image-router": { "name": "@activepieces/piece-image-router", "version": "0.1.4", @@ -5612,6 +5623,16 @@ "tslib": "2.6.2", }, }, + "packages/pieces/community/pubrio": { + "name": "@activepieces/piece-pubrio", + "version": "0.0.1", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "tslib": "^2.3.0", + }, + }, "packages/pieces/community/pushbullet": { "name": "@activepieces/piece-pushbullet", "version": "0.1.4", @@ -8288,7 +8309,7 @@ }, "packages/shared": { "name": "@activepieces/shared", - "version": "0.71.4", + "version": "0.71.6", "dependencies": { "dayjs": "1.11.9", "deepmerge-ts": "7.1.0", @@ -9050,6 +9071,8 @@ "@activepieces/piece-ibm-cognose": ["@activepieces/piece-ibm-cognose@workspace:packages/pieces/community/ibm-cognose"], + "@activepieces/piece-iloveapi": ["@activepieces/piece-iloveapi@workspace:packages/pieces/community/iloveapi"], + "@activepieces/piece-image-helper": ["@activepieces/piece-image-helper@workspace:packages/pieces/core/image-helper"], "@activepieces/piece-image-router": ["@activepieces/piece-image-router@workspace:packages/pieces/community/image-router"], @@ -9436,6 +9459,8 @@ "@activepieces/piece-proxycurl": ["@activepieces/piece-proxycurl@workspace:packages/pieces/community/proxycurl"], + "@activepieces/piece-pubrio": ["@activepieces/piece-pubrio@workspace:packages/pieces/community/pubrio"], + "@activepieces/piece-pushbullet": ["@activepieces/piece-pushbullet@workspace:packages/pieces/community/pushbullet"], "@activepieces/piece-pushover": ["@activepieces/piece-pushover@workspace:packages/pieces/community/pushover"], @@ -15548,6 +15573,8 @@ "@activepieces/piece-prompthub/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@activepieces/piece-pubrio/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@activepieces/piece-rabbitmq/@types/amqplib": ["@types/amqplib@0.10.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-IVj3avf9AQd2nXCx0PGk/OYq7VmHiyNxWFSb5HhU9ATh+i+gHWvVcljFTcTWQ/dyHJCTrzCixde+r/asL2ErDA=="], "@activepieces/piece-roe-ai/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="], diff --git a/docs/install/configuration/environment-variables.mdx b/docs/install/configuration/environment-variables.mdx index 0408c9d06f9..05c4ab7be41 100644 --- a/docs/install/configuration/environment-variables.mdx +++ b/docs/install/configuration/environment-variables.mdx @@ -88,7 +88,6 @@ it will produce these values. | `AP_MAX_FIELDS_PER_TABLE` | The maximum allowed number of fields per table | `100` | `100` | `AP_MAX_TABLES_PER_PROJECT` | The maximum allowed number of tables per project | `20` | `20` | `AP_MAX_MCPS_PER_PROJECT` | The maximum allowed number of mcp per project | `20` | `20` -| `AP_MCP_OAUTH_ISSUER_URL` | The issuer URL used for MCP OAuth token generation and metadata discovery. Falls back to `AP_FRONTEND_URL` if not set. | None | `https://mcp.example.com` | `AP_ENABLE_FLOW_ON_PUBLISH` | Whether publishing a new flow version should automatically enable the flow | `true` | `false` | `AP_ISSUE_ARCHIVE_DAYS` | Controls the automatic archival of issues in the system. Issues that have not been updated for this many days will be automatically moved to an archived state.| `14` | `1` | `AP_LOAD_TRANSLATIONS_FOR_DEV_PIECES` | Load translations for dev pieces (configured via `AP_DEV_PIECES`). When disabled, dev pieces are loaded without translations. This only affects development mode.| `false` | `true` diff --git a/packages/pieces/community/iloveapi/.eslintrc.json b/packages/pieces/community/iloveapi/.eslintrc.json new file mode 100644 index 00000000000..359ff63d51d --- /dev/null +++ b/packages/pieces/community/iloveapi/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": [ + "../../../../.eslintrc.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} \ No newline at end of file diff --git a/packages/pieces/community/iloveapi/package.json b/packages/pieces/community/iloveapi/package.json new file mode 100644 index 00000000000..56a18e4bd81 --- /dev/null +++ b/packages/pieces/community/iloveapi/package.json @@ -0,0 +1,18 @@ +{ + "name": "@activepieces/piece-iloveapi", + "version": "0.0.1", + "type": "commonjs", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "form-data": "4.0.4", + "tslib": "2.6.2" + }, + "scripts": { + "build": "tsc -p tsconfig.lib.json && cp package.json dist/", + "lint": "eslint 'src/**/*.ts'" + } +} diff --git a/packages/pieces/community/iloveapi/src/index.ts b/packages/pieces/community/iloveapi/src/index.ts new file mode 100644 index 00000000000..0629a781b3e --- /dev/null +++ b/packages/pieces/community/iloveapi/src/index.ts @@ -0,0 +1,86 @@ +import { createPiece } from '@activepieces/pieces-framework'; +import { PieceCategory } from '@activepieces/shared'; +import { iloveapiAuth } from './lib/common/auth'; +import { compressPdfAction } from './lib/actions/compress-pdf'; +import { mergePdfAction } from './lib/actions/merge-pdf'; +import { splitPdfAction } from './lib/actions/split-pdf'; +import { pdfToJpgAction } from './lib/actions/pdf-to-jpg'; +import { jpgToPdfAction } from './lib/actions/jpg-to-pdf'; +import { officeToPdfAction } from './lib/actions/office-to-pdf'; +import { htmlToPdfAction } from './lib/actions/html-to-pdf'; +import { ocrPdfAction } from './lib/actions/ocr-pdf'; +import { watermarkPdfAction } from './lib/actions/watermark-pdf'; +import { protectPdfAction } from './lib/actions/protect-pdf'; +import { unlockPdfAction } from './lib/actions/unlock-pdf'; +import { pageNumbersPdfAction } from './lib/actions/page-numbers'; +import { rotatePdfAction } from './lib/actions/rotate-pdf'; +import { extractTextPdfAction } from './lib/actions/extract-text'; +import { repairPdfAction } from './lib/actions/repair-pdf'; +import { createSignatureRequestAction } from './lib/actions/create-signature-request'; +import { getSignatureStatusAction } from './lib/actions/get-signature-status'; +import { sendSignerReminderAction } from './lib/actions/send-signer-reminder'; +import { voidSignatureAction } from './lib/actions/void-signature'; +import { increaseExpirationDaysAction } from './lib/actions/increase-expiration-days'; +import { downloadSignedFilesAction } from './lib/actions/download-signed-files'; +import { downloadAuditTrailAction } from './lib/actions/download-audit-trail'; +import { taskCompletedTrigger } from './lib/triggers/task-completed'; +import { taskFailedTrigger } from './lib/triggers/task-failed'; +import { + signatureCompletedTrigger, + signatureCreatedTrigger, + signatureDeclinedTrigger, + signatureExpiredTrigger, + signatureSentTrigger, + signatureVoidedTrigger, + signerDeclinedTrigger, + signerSignedTrigger, + signerViewedTrigger, +} from './lib/triggers/signature-events'; + +export const iloveapi = createPiece({ + displayName: 'iLoveAPI', + description: + 'Compress, merge, split, convert, OCR, watermark, protect and sign PDFs and images using iLovePDF / iLoveIMG / iLoveSign.', + auth: iloveapiAuth, + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/iloveapi.png', + authors: ['sanket-a11y'], + categories: [PieceCategory.CONTENT_AND_FILES], + actions: [ + compressPdfAction, + mergePdfAction, + splitPdfAction, + pdfToJpgAction, + jpgToPdfAction, + officeToPdfAction, + htmlToPdfAction, + ocrPdfAction, + watermarkPdfAction, + protectPdfAction, + unlockPdfAction, + pageNumbersPdfAction, + rotatePdfAction, + extractTextPdfAction, + repairPdfAction, + createSignatureRequestAction, + getSignatureStatusAction, + sendSignerReminderAction, + voidSignatureAction, + increaseExpirationDaysAction, + downloadSignedFilesAction, + downloadAuditTrailAction, + ], + triggers: [ + taskCompletedTrigger, + taskFailedTrigger, + signatureCreatedTrigger, + signatureSentTrigger, + signatureCompletedTrigger, + signatureDeclinedTrigger, + signatureExpiredTrigger, + signatureVoidedTrigger, + signerViewedTrigger, + signerSignedTrigger, + signerDeclinedTrigger, + ], +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/compress-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/compress-pdf.ts new file mode 100644 index 00000000000..9ade321bd9b --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/compress-pdf.ts @@ -0,0 +1,48 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { fileToUploadInput, sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; + +export const compressPdfAction = createAction({ + auth: iloveapiAuth, + name: 'compress_pdf', + displayName: 'Compress PDF', + description: 'Reduce the size of a PDF file using iLoveAPI.', + props: { + file: Property.File({ + displayName: 'PDF File', + description: 'PDF file to compress.', + required: true, + }), + compression_level: Property.StaticDropdown({ + displayName: 'Compression Level', + required: false, + defaultValue: 'recommended', + options: { + disabled: false, + options: [ + { label: 'Extreme (smallest, lower quality)', value: 'extreme' }, + { label: 'Recommended (balanced)', value: 'recommended' }, + { label: 'Low (largest, best quality)', value: 'low' }, + ], + }, + }), + ...sharedProps, + }, + async run(context) { + const { file, compression_level, output_filename, packaged_filename } = + context.propsValue; + + return await runAndStoreResult({ + auth: context.auth.secret_text, + files: context.files, + tool: 'compress', + uploads: [fileToUploadInput(file)], + options: { + compression_level: compression_level ?? 'recommended', + }, + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/create-signature-request.ts b/packages/pieces/community/iloveapi/src/lib/actions/create-signature-request.ts new file mode 100644 index 00000000000..c385e97063e --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/create-signature-request.ts @@ -0,0 +1,342 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { iLoveApi } from '../common/client'; +import { + CreateSignatureRequest, + SignatureElement, + SignatureSigner, +} from '../common/types'; + +type SignerRow = { + name?: string; + email?: string; + phone?: string; + type?: 'signer' | 'validator' | 'viewer'; + force_signature_type?: 'all' | 'text' | 'sign' | 'image'; + access_code?: string; +}; + +type ElementRow = { + signer_email?: string; + type?: 'signature' | 'initials' | 'date' | 'text' | 'input'; + position?: string; + pages?: string; + size?: number; + content?: string; + color?: string; +}; + +export const createSignatureRequestAction = createAction({ + auth: iloveapiAuth, + name: 'create_signature_request', + displayName: 'Create Signature Request', + description: + 'Send one or more PDF documents for electronic signature, with custom signers and signature placeholders.', + props: { + files: Property.Array({ + displayName: 'PDF Files', + description: 'Documents to send for signature.', + required: true, + properties: { + file: Property.File({ + displayName: 'File', + required: true, + }), + }, + }), + signers: Property.Array({ + displayName: 'Signers', + description: 'People who will receive the document.', + required: true, + properties: { + name: Property.ShortText({ + displayName: 'Name', + required: true, + }), + email: Property.ShortText({ + displayName: 'Email', + required: true, + }), + phone: Property.ShortText({ + displayName: 'Phone (optional)', + description: + 'International format including country code, e.g. +14155550100.', + required: false, + }), + type: Property.StaticDropdown({ + displayName: 'Role', + required: false, + defaultValue: 'signer', + options: { + disabled: false, + options: [ + { label: 'Signer (can sign)', value: 'signer' }, + { label: 'Validator (approves)', value: 'validator' }, + { label: 'Viewer (read only)', value: 'viewer' }, + ], + }, + }), + force_signature_type: Property.StaticDropdown({ + displayName: 'Force Signature Type', + required: false, + defaultValue: 'all', + options: { + disabled: false, + options: [ + { label: 'All types allowed', value: 'all' }, + { label: 'Type only', value: 'text' }, + { label: 'Hand-drawn signature', value: 'sign' }, + { label: 'Upload image', value: 'image' }, + ], + }, + }), + access_code: Property.ShortText({ + displayName: 'Access Code (optional)', + description: + 'Extra code the signer must enter before opening the document.', + required: false, + }), + }, + }), + elements: Property.Array({ + displayName: 'Signature Elements', + description: + 'Placeholders to drop on each PDF page. Match them to signers by email. If empty, iLovePDF will let signers place fields freely.', + required: false, + properties: { + signer_email: Property.ShortText({ + displayName: 'Signer Email', + description: 'Email matching one of the signers above.', + required: true, + }), + type: Property.StaticDropdown({ + displayName: 'Element Type', + required: true, + defaultValue: 'signature', + options: { + disabled: false, + options: [ + { label: 'Signature', value: 'signature' }, + { label: 'Initials', value: 'initials' }, + { label: 'Date', value: 'date' }, + { label: 'Static Text', value: 'text' }, + { label: 'Input (text field)', value: 'input' }, + ], + }, + }), + position: Property.ShortText({ + displayName: 'Position', + description: + 'Either a gravity keyword like "bottom right" or coordinates "X Y" (PDF origin is bottom-left; use negative Y to measure from top).', + required: true, + defaultValue: 'bottom right', + }), + pages: Property.ShortText({ + displayName: 'Pages', + description: 'Pages to drop this element on, e.g. "1" or "1-3" or "all".', + required: true, + defaultValue: '1', + }), + size: Property.Number({ + displayName: 'Size', + description: 'Font size or element height. Defaults to 14 for text, 50 for signature.', + required: false, + }), + content: Property.ShortText({ + displayName: 'Content', + description: + 'Static text (for type "text") or default value (for "input" / "date").', + required: false, + }), + color: Property.ShortText({ + displayName: 'Color', + description: 'Hex color for text elements, e.g. #000000.', + required: false, + }), + }, + }), + brand_name: Property.ShortText({ + displayName: 'Brand Name', + description: 'Sender name shown in the email and signing page.', + required: false, + }), + language: Property.StaticDropdown({ + displayName: 'Language', + required: false, + defaultValue: 'en-US', + options: { + disabled: false, + options: [ + { label: 'English (US)', value: 'en-US' }, + { label: 'Spanish', value: 'es' }, + { label: 'French', value: 'fr' }, + { label: 'German', value: 'de' }, + { label: 'Italian', value: 'it' }, + { label: 'Portuguese', value: 'pt' }, + { label: 'Catalan', value: 'ca' }, + { label: 'Dutch', value: 'nl' }, + ], + }, + }), + subject_signer: Property.ShortText({ + displayName: 'Email Subject', + description: 'Subject line for the signature invitation email.', + required: false, + }), + message_signer: Property.LongText({ + displayName: 'Email Message', + description: 'Body for the signature invitation email.', + required: false, + }), + expiration_days: Property.Number({ + displayName: 'Expiration (days)', + description: 'Days until the request expires. 1 to 130. Default 120.', + required: false, + }), + lock_order: Property.Checkbox({ + displayName: 'Sign in Order', + description: + 'When enabled, signers receive the document one at a time in the listed order.', + required: false, + defaultValue: false, + }), + signer_reminders: Property.Checkbox({ + displayName: 'Auto Reminders', + description: 'Send reminder emails to signers who have not yet signed.', + required: false, + defaultValue: true, + }), + signer_reminder_days_cycle: Property.Number({ + displayName: 'Reminder Frequency (days)', + description: 'Days between auto-reminders. Default 1.', + required: false, + }), + certified: Property.Checkbox({ + displayName: 'Certified Signature', + description: 'Apply a certified-signature seal to the final PDF.', + required: false, + defaultValue: true, + }), + }, + async run(context) { + const { + files, + signers, + elements, + brand_name, + language, + subject_signer, + message_signer, + expiration_days, + lock_order, + signer_reminders, + signer_reminder_days_cycle, + certified, + } = context.propsValue; + + if (!Array.isArray(files) || files.length === 0) { + throw new Error('At least one PDF file is required.'); + } + if (!Array.isArray(signers) || signers.length === 0) { + throw new Error('At least one signer is required.'); + } + + const token = await iLoveApi.authenticate({ + publicKey: context.auth.secret_text, + }); + const startResp = await iLoveApi.startTask({ token, tool: 'sign' }); + + const uploadedFiles: Array<{ server_filename: string; filename: string }> = []; + for (const entry of files as Array<{ + file: { base64: string; filename: string }; + }>) { + if (!entry.file?.base64) { + throw new Error('Each PDF row must include a file.'); + } + const buffer = Buffer.from(entry.file.base64, 'base64'); + const serverFilename = await iLoveApi.uploadBuffer({ + token, + server: startResp.server, + task: startResp.task, + buffer, + filename: entry.file.filename, + }); + uploadedFiles.push({ + server_filename: serverFilename, + filename: entry.file.filename, + }); + } + + const signerRows = signers as SignerRow[]; + const elementRows = (elements ?? []) as ElementRow[]; + + const elementsByEmail = new Map(); + for (const row of elementRows) { + if (!row.signer_email || !row.type || !row.position || !row.pages) continue; + const element: SignatureElement = { + type: row.type, + position: row.position, + pages: row.pages, + ...(row.size !== undefined && row.size !== null ? { size: row.size } : {}), + ...(row.content ? { content: row.content } : {}), + ...(row.color ? { color: row.color } : {}), + }; + const list = elementsByEmail.get(row.signer_email) ?? []; + list.push(element); + elementsByEmail.set(row.signer_email, list); + } + + const signatureSigners: SignatureSigner[] = signerRows.map((row) => { + if (!row.name || !row.email) { + throw new Error('Each signer needs both name and email.'); + } + const elementsForSigner = elementsByEmail.get(row.email); + const signerFiles = uploadedFiles.map((file) => ({ + server_filename: file.server_filename, + ...(elementsForSigner && elementsForSigner.length > 0 + ? { elements: elementsForSigner } + : {}), + })); + return { + name: row.name, + email: row.email, + type: row.type ?? 'signer', + force_signature_type: row.force_signature_type ?? 'all', + files: signerFiles, + ...(row.phone ? { phone: row.phone } : {}), + ...(row.access_code ? { access_code: row.access_code } : {}), + }; + }); + + const body: CreateSignatureRequest = { + task: startResp.task, + files: uploadedFiles, + signers: signatureSigners, + ...(brand_name ? { brand_name } : {}), + ...(language ? { language } : {}), + ...(subject_signer ? { subject_signer } : {}), + ...(message_signer ? { message_signer } : {}), + ...(expiration_days !== undefined && expiration_days !== null + ? { expiration_days } + : {}), + ...(lock_order !== undefined ? { lock_order } : {}), + ...(signer_reminders !== undefined ? { signer_reminders } : {}), + ...(signer_reminder_days_cycle !== undefined && + signer_reminder_days_cycle !== null + ? { signer_reminder_days_cycle } + : {}), + ...(certified !== undefined ? { certified } : {}), + }; + + const response = await iLoveApi.createSignature({ + token, + server: startResp.server, + body, + }); + + return { + ...response, + server: startResp.server, + }; + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/download-audit-trail.ts b/packages/pieces/community/iloveapi/src/lib/actions/download-audit-trail.ts new file mode 100644 index 00000000000..a5fb467808d --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/download-audit-trail.ts @@ -0,0 +1,49 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { iLoveApi } from '../common/client'; + +export const downloadAuditTrailAction = createAction({ + auth: iloveapiAuth, + name: 'download_audit_trail', + displayName: 'Download Audit Trail', + description: + 'Download the audit-trail PDF for a completed signature request. Useful for compliance and dispute resolution.', + props: { + token_requester: Property.ShortText({ + displayName: 'Requester Token', + description: 'The "token_requester" of the completed signature request.', + required: true, + }), + file_name: Property.ShortText({ + displayName: 'Output File Name', + description: 'Name to store the audit PDF under. Defaults to "audit-{token}.pdf".', + required: false, + }), + }, + async run(context) { + const { token_requester, file_name } = context.propsValue; + if (!token_requester) { + throw new Error('Requester Token is required.'); + } + + const token = await iLoveApi.authenticate({ + publicKey: context.auth.secret_text, + }); + const buffer = await iLoveApi.downloadAuditTrail({ + token, + tokenRequester: token_requester, + }); + + const finalName = file_name ?? `audit-${token_requester}.pdf`; + const storedFile = await context.files.write({ + fileName: finalName, + data: buffer, + }); + + return { + output_file: storedFile, + file_name: finalName, + size: buffer.length, + }; + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/download-signed-files.ts b/packages/pieces/community/iloveapi/src/lib/actions/download-signed-files.ts new file mode 100644 index 00000000000..880180bdc2e --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/download-signed-files.ts @@ -0,0 +1,49 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { iLoveApi } from '../common/client'; + +export const downloadSignedFilesAction = createAction({ + auth: iloveapiAuth, + name: 'download_signed_files', + displayName: 'Download Signed Files', + description: + 'Download the signed PDF (or ZIP if multiple files). The signature must be in "completed" status.', + props: { + token_requester: Property.ShortText({ + displayName: 'Requester Token', + description: 'The "token_requester" of the completed signature request.', + required: true, + }), + file_name: Property.ShortText({ + displayName: 'Output File Name', + description: 'Name to store the downloaded file under. Defaults to "signed-{token}.pdf".', + required: false, + }), + }, + async run(context) { + const { token_requester, file_name } = context.propsValue; + if (!token_requester) { + throw new Error('Requester Token is required.'); + } + + const token = await iLoveApi.authenticate({ + publicKey: context.auth.secret_text, + }); + const buffer = await iLoveApi.downloadSignedFiles({ + token, + tokenRequester: token_requester, + }); + + const finalName = file_name ?? `signed-${token_requester}.pdf`; + const storedFile = await context.files.write({ + fileName: finalName, + data: buffer, + }); + + return { + output_file: storedFile, + file_name: finalName, + size: buffer.length, + }; + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/extract-text.ts b/packages/pieces/community/iloveapi/src/lib/actions/extract-text.ts new file mode 100644 index 00000000000..54101e318b3 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/extract-text.ts @@ -0,0 +1,73 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { fileToUploadInput, sharedProps } from '../common/props'; +import { iLoveApi } from '../common/client'; + +export const extractTextPdfAction = createAction({ + auth: iloveapiAuth, + name: 'extract_text_pdf', + displayName: 'Extract Text from PDF', + description: + 'Extract plain or detailed text (with positions and font metadata) from a PDF.', + props: { + file: Property.File({ + displayName: 'PDF File', + required: true, + }), + detailed: Property.Checkbox({ + displayName: 'Detailed Output', + description: + 'Include positional and font metadata for each text block. Useful for downstream layout-aware parsing.', + required: false, + defaultValue: false, + }), + ...sharedProps, + }, + async run(context) { + const { file, detailed, output_filename, packaged_filename } = + context.propsValue; + + const result = await iLoveApi.runTask({ + publicKey: context.auth.secret_text, + tool: 'extract', + uploads: [fileToUploadInput(file)], + options: { detailed: detailed ?? false }, + output_filename, + packaged_filename, + }); + + const storedFile = await context.files.write({ + fileName: result.downloadFilename, + data: result.buffer, + }); + + const text = result.buffer.toString('utf-8'); + const detailedJson = parseDetailedJson({ detailed: detailed ?? false, text }); + + return { + output_file: storedFile, + download_filename: result.downloadFilename, + output_filenumber: result.process.output_filenumber, + output_filesize: result.process.output_filesize, + output_extensions: result.process.output_extensions, + status: result.process.status, + text, + detailed: detailedJson, + }; + }, +}); + +function parseDetailedJson({ + detailed, + text, +}: { + detailed: boolean; + text: string; +}): unknown { + if (!detailed) return undefined; + try { + return JSON.parse(text); + } catch { + return undefined; + } +} diff --git a/packages/pieces/community/iloveapi/src/lib/actions/get-signature-status.ts b/packages/pieces/community/iloveapi/src/lib/actions/get-signature-status.ts new file mode 100644 index 00000000000..67ce8f922e8 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/get-signature-status.ts @@ -0,0 +1,33 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { iLoveApi } from '../common/client'; + +export const getSignatureStatusAction = createAction({ + auth: iloveapiAuth, + name: 'get_signature_status', + displayName: 'Get Signature Status', + description: + 'Look up the current status of a signature request, including each signer.', + props: { + token_requester: Property.ShortText({ + displayName: 'Requester Token', + description: + 'The "token_requester" returned when the signature request was created.', + required: true, + }), + }, + async run(context) { + const { token_requester } = context.propsValue; + if (!token_requester) { + throw new Error('Requester Token is required.'); + } + + const token = await iLoveApi.authenticate({ + publicKey: context.auth.secret_text, + }); + return await iLoveApi.getSignatureStatus({ + token, + tokenRequester: token_requester, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/html-to-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/html-to-pdf.ts new file mode 100644 index 00000000000..24f0cfef1c9 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/html-to-pdf.ts @@ -0,0 +1,35 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; + +export const htmlToPdfAction = createAction({ + auth: iloveapiAuth, + name: 'html_to_pdf', + displayName: 'HTML to PDF', + description: 'Convert a publicly accessible web page URL into a PDF.', + props: { + url: Property.ShortText({ + displayName: 'Page URL', + description: 'Public URL of the web page to render as PDF.', + required: true, + }), + ...sharedProps, + }, + async run(context) { + const { url, output_filename, packaged_filename } = context.propsValue; + + if (!url || !/^https?:\/\//i.test(url)) { + throw new Error('A valid http(s) URL is required.'); + } + + return await runAndStoreResult({ + auth:context.auth.secret_text, + files: context.files, + tool: 'htmlpdf', + uploads: [{ kind: 'url', url }], + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/increase-expiration-days.ts b/packages/pieces/community/iloveapi/src/lib/actions/increase-expiration-days.ts new file mode 100644 index 00000000000..4ce803e00b6 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/increase-expiration-days.ts @@ -0,0 +1,41 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { iLoveApi } from '../common/client'; + +export const increaseExpirationDaysAction = createAction({ + auth: iloveapiAuth, + name: 'increase_expiration_days', + displayName: 'Increase Expiration Days', + description: + 'Extend the expiration of a signature request by a number of days. Total cannot exceed 130 days from now.', + props: { + token_requester: Property.ShortText({ + displayName: 'Requester Token', + description: 'The "token_requester" of the signature request.', + required: true, + }), + days: Property.Number({ + displayName: 'Additional Days', + description: 'Number of days to add. Must be between 1 and 130.', + required: true, + }), + }, + async run(context) { + const { token_requester, days } = context.propsValue; + if (!token_requester) { + throw new Error('Requester Token is required.'); + } + if (days === undefined || days === null || days < 1 || days > 130) { + throw new Error('Additional Days must be between 1 and 130.'); + } + + const token = await iLoveApi.authenticate({ + publicKey: context.auth.secret_text, + }); + return await iLoveApi.increaseExpirationDays({ + token, + tokenRequester: token_requester, + days, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/jpg-to-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/jpg-to-pdf.ts new file mode 100644 index 00000000000..d27d6c5dde0 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/jpg-to-pdf.ts @@ -0,0 +1,108 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; +import { UploadInput } from '../common/client'; + +export const jpgToPdfAction = createAction({ + auth: iloveapiAuth, + name: 'jpg_to_pdf', + displayName: 'JPG to PDF', + description: 'Convert one or more images (JPG/PNG) into a PDF document.', + props: { + images: Property.Array({ + displayName: 'Images', + description: 'Images to include in the PDF, in order.', + required: true, + properties: { + file: Property.File({ + displayName: 'Image', + required: true, + }), + }, + }), + orientation: Property.StaticDropdown({ + displayName: 'Orientation', + required: false, + defaultValue: 'portrait', + options: { + disabled: false, + options: [ + { label: 'Portrait', value: 'portrait' }, + { label: 'Landscape', value: 'landscape' }, + ], + }, + }), + margin: Property.Number({ + displayName: 'Margin (px)', + description: 'Page margin in pixels around each image. Default 0.', + required: false, + }), + pagesize: Property.StaticDropdown({ + displayName: 'Page Size', + required: false, + defaultValue: 'fit', + options: { + disabled: false, + options: [ + { label: 'Fit to image', value: 'fit' }, + { label: 'A4', value: 'A4' }, + { label: 'Letter', value: 'letter' }, + ], + }, + }), + merge_after: Property.Checkbox({ + displayName: 'Merge into single PDF', + description: + 'When enabled (default), produces one PDF for all images. When disabled, produces one PDF per image.', + required: false, + defaultValue: true, + }), + ...sharedProps, + }, + async run(context) { + const { + images, + orientation, + margin, + pagesize, + merge_after, + output_filename, + packaged_filename, + } = context.propsValue; + + if (!Array.isArray(images) || images.length === 0) { + throw new Error('At least one image is required.'); + } + + const uploads: UploadInput[] = images.map((entry) => { + const file = (entry as { file: { base64: string; filename: string } }).file; + if (!file?.base64) { + throw new Error('Each row must include an image file.'); + } + return { + kind: 'file', + file: { base64: file.base64, filename: file.filename }, + }; + }); + + const options: Record = { + orientation: orientation ?? 'portrait', + pagesize: pagesize ?? 'fit', + merge_after: merge_after ?? true, + }; + if (margin !== undefined && margin !== null) { + options['margin'] = margin; + } + + return await runAndStoreResult({ + auth: context.auth.secret_text, + files: context.files, + tool: 'imagepdf', + uploads, + options, + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/merge-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/merge-pdf.ts new file mode 100644 index 00000000000..8301b292d61 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/merge-pdf.ts @@ -0,0 +1,55 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; +import { UploadInput } from '../common/client'; + +export const mergePdfAction = createAction({ + auth: iloveapiAuth, + name: 'merge_pdf', + displayName: 'Merge PDF', + description: + 'Combine multiple PDF files into a single document, in the order provided.', + props: { + files: Property.Array({ + displayName: 'PDF Files', + description: + 'List of PDF files to merge. The output preserves the listed order.', + required: true, + properties: { + file: Property.File({ + displayName: 'File', + required: true, + }), + }, + }), + ...sharedProps, + }, + async run(context) { + const { files, output_filename, packaged_filename } = context.propsValue; + + if (!Array.isArray(files) || files.length < 2) { + throw new Error('At least two PDF files are required for merging.'); + } + + const uploads: UploadInput[] = files.map((entry) => { + const file = (entry as { file: { base64: string; filename: string } }).file; + if (!file?.base64) { + throw new Error('Each row must include a file.'); + } + return { + kind: 'file', + file: { base64: file.base64, filename: file.filename }, + }; + }); + + return await runAndStoreResult({ + auth: context.auth.secret_text, + files: context.files, + tool: 'merge', + uploads, + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/ocr-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/ocr-pdf.ts new file mode 100644 index 00000000000..78413f6bc8f --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/ocr-pdf.ts @@ -0,0 +1,45 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { fileToUploadInput, sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; + +export const ocrPdfAction = createAction({ + auth: iloveapiAuth, + name: 'ocr_pdf', + displayName: 'OCR PDF', + description: + 'Run OCR on a PDF to make scanned documents searchable. Supports 100+ languages.', + props: { + file: Property.File({ + displayName: 'PDF File', + required: true, + }), + ocr_languages: Property.Array({ + displayName: 'OCR Languages', + description: + 'ISO 639-2 language codes (e.g. eng, fra, deu, spa, ita, por, rus, chi_sim, jpn, ara). Defaults to English.', + required: false, + properties: {}, + }), + ...sharedProps, + }, + async run(context) { + const { file, ocr_languages, output_filename, packaged_filename } = + context.propsValue; + + const languages = + Array.isArray(ocr_languages) && ocr_languages.length > 0 + ? ocr_languages.map((lang) => String(lang)).filter((l) => l.length > 0) + : ['eng']; + + return await runAndStoreResult({ + auth: context.auth.secret_text, + files: context.files, + tool: 'pdfocr', + uploads: [fileToUploadInput(file)], + options: { ocr_languages: languages }, + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/office-to-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/office-to-pdf.ts new file mode 100644 index 00000000000..d8d2f626643 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/office-to-pdf.ts @@ -0,0 +1,32 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { fileToUploadInput, sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; + +export const officeToPdfAction = createAction({ + auth: iloveapiAuth, + name: 'office_to_pdf', + displayName: 'Office to PDF', + description: + 'Convert Office documents (DOCX, XLSX, PPTX, ODT, etc.) to PDF.', + props: { + file: Property.File({ + displayName: 'Office File', + description: 'Supported formats include DOCX, XLSX, PPTX, ODT, ODS, ODP, RTF, TXT.', + required: true, + }), + ...sharedProps, + }, + async run(context) { + const { file, output_filename, packaged_filename } = context.propsValue; + + return await runAndStoreResult({ + auth: context.auth.secret_text, + files: context.files, + tool: 'officepdf', + uploads: [fileToUploadInput(file)], + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/page-numbers.ts b/packages/pieces/community/iloveapi/src/lib/actions/page-numbers.ts new file mode 100644 index 00000000000..555d80f2222 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/page-numbers.ts @@ -0,0 +1,141 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { fileToUploadInput, sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; + +export const pageNumbersPdfAction = createAction({ + auth: iloveapiAuth, + name: 'page_numbers_pdf', + displayName: 'Add Page Numbers to PDF', + description: 'Stamp page numbers onto a PDF, with control over position and styling.', + props: { + file: Property.File({ + displayName: 'PDF File', + required: true, + }), + pages: Property.ShortText({ + displayName: 'Pages', + description: + 'Pages to number. Use "all" or ranges like "2-5,7". Defaults to all.', + required: false, + defaultValue: 'all', + }), + starting_number: Property.Number({ + displayName: 'Starting Number', + description: 'First page number to print. Defaults to 1.', + required: false, + defaultValue: 1, + }), + first_cover: Property.Checkbox({ + displayName: 'Skip First Page (Cover)', + description: 'When enabled, skips numbering the first page.', + required: false, + defaultValue: false, + }), + facing_pages: Property.Checkbox({ + displayName: 'Facing Pages (book mode)', + description: + 'Alternate horizontal position for left/right pages, like a printed book.', + required: false, + defaultValue: false, + }), + vertical_position: Property.StaticDropdown({ + displayName: 'Vertical Position', + required: false, + defaultValue: 'bottom', + options: { + disabled: false, + options: [ + { label: 'Top', value: 'top' }, + { label: 'Bottom', value: 'bottom' }, + ], + }, + }), + horizontal_position: Property.StaticDropdown({ + displayName: 'Horizontal Position', + required: false, + defaultValue: 'center', + options: { + disabled: false, + options: [ + { label: 'Left', value: 'left' }, + { label: 'Center', value: 'center' }, + { label: 'Right', value: 'right' }, + ], + }, + }), + text: Property.ShortText({ + displayName: 'Number Format', + description: 'Use {n} for current page and {p} for total pages. Example: "{n} of {p}".', + required: false, + defaultValue: '{n}', + }), + font_family: Property.StaticDropdown({ + displayName: 'Font', + required: false, + defaultValue: 'Arial', + options: { + disabled: false, + options: [ + { label: 'Arial', value: 'Arial' }, + { label: 'Verdana', value: 'Verdana' }, + { label: 'Courier', value: 'Courier' }, + { label: 'Times New Roman', value: 'Times New Roman' }, + { label: 'Comic Sans MS', value: 'Comic Sans MS' }, + ], + }, + }), + font_size: Property.Number({ + displayName: 'Font Size', + required: false, + defaultValue: 14, + }), + font_color: Property.ShortText({ + displayName: 'Font Color', + description: 'Hex color, e.g. #000000.', + required: false, + defaultValue: '#000000', + }), + ...sharedProps, + }, + async run(context) { + const { + file, + pages, + starting_number, + first_cover, + facing_pages, + vertical_position, + horizontal_position, + text, + font_family, + font_size, + font_color, + output_filename, + packaged_filename, + } = context.propsValue; + + const options: Record = { + pages: pages ?? 'all', + starting_number: starting_number ?? 1, + first_cover: first_cover ?? false, + facing_pages: facing_pages ?? false, + vertical_position: vertical_position ?? 'bottom', + horizontal_position: horizontal_position ?? 'center', + text: text ?? '{n}', + font_family: font_family ?? 'Arial', + font_size: font_size ?? 14, + font_color: font_color ?? '#000000', + }; + + return await runAndStoreResult({ + auth:context.auth.secret_text, + files: context.files, + tool: 'pagenumber', + uploads: [fileToUploadInput(file)], + options, + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/pdf-to-jpg.ts b/packages/pieces/community/iloveapi/src/lib/actions/pdf-to-jpg.ts new file mode 100644 index 00000000000..8d29f9d1abe --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/pdf-to-jpg.ts @@ -0,0 +1,47 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { fileToUploadInput, sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; + +export const pdfToJpgAction = createAction({ + auth: iloveapiAuth, + name: 'pdf_to_jpg', + displayName: 'PDF to JPG', + description: + 'Convert PDF pages to JPG images, or extract images embedded in the PDF.', + props: { + file: Property.File({ + displayName: 'PDF File', + required: true, + }), + pdfjpg_mode: Property.StaticDropdown({ + displayName: 'Mode', + required: false, + defaultValue: 'pages', + options: { + disabled: false, + options: [ + { label: 'Render each page as a JPG', value: 'pages' }, + { label: 'Extract embedded images', value: 'extract' }, + ], + }, + }), + ...sharedProps, + }, + async run(context) { + const { file, pdfjpg_mode, output_filename, packaged_filename } = + context.propsValue; + + return await runAndStoreResult({ + auth:context.auth.secret_text, + files: context.files, + tool: 'pdfjpg', + uploads: [fileToUploadInput(file)], + options: { + pdfjpg_mode: pdfjpg_mode ?? 'pages', + }, + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/protect-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/protect-pdf.ts new file mode 100644 index 00000000000..ac3db443ea5 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/protect-pdf.ts @@ -0,0 +1,41 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { fileToUploadInput, sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; + +export const protectPdfAction = createAction({ + auth: iloveapiAuth, + name: 'protect_pdf', + displayName: 'Protect PDF', + description: 'Add a password to a PDF document.', + props: { + file: Property.File({ + displayName: 'PDF File', + required: true, + }), + password: Property.ShortText({ + displayName: 'Password', + description: 'Password used to encrypt the PDF.', + required: true, + }), + ...sharedProps, + }, + async run(context) { + const { file, password, output_filename, packaged_filename } = + context.propsValue; + + if (!password) { + throw new Error('Password is required.'); + } + + return await runAndStoreResult({ + auth: context.auth.secret_text, + files: context.files, + tool: 'protect', + uploads: [fileToUploadInput(file)], + options: { password }, + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/repair-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/repair-pdf.ts new file mode 100644 index 00000000000..db349b9c32d --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/repair-pdf.ts @@ -0,0 +1,30 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { fileToUploadInput, sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; + +export const repairPdfAction = createAction({ + auth: iloveapiAuth, + name: 'repair_pdf', + displayName: 'Repair PDF', + description: 'Attempt to fix a damaged or corrupted PDF file.', + props: { + file: Property.File({ + displayName: 'PDF File', + required: true, + }), + ...sharedProps, + }, + async run(context) { + const { file, output_filename, packaged_filename } = context.propsValue; + + return await runAndStoreResult({ + auth: context.auth.secret_text, + files: context.files, + tool: 'repair', + uploads: [fileToUploadInput(file)], + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/rotate-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/rotate-pdf.ts new file mode 100644 index 00000000000..fd74e46ef98 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/rotate-pdf.ts @@ -0,0 +1,47 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { fileToUploadInput, sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; + +export const rotatePdfAction = createAction({ + auth: iloveapiAuth, + name: 'rotate_pdf', + displayName: 'Rotate PDF', + description: 'Rotate every page of a PDF by 90, 180, or 270 degrees.', + props: { + file: Property.File({ + displayName: 'PDF File', + required: true, + }), + rotation: Property.StaticDropdown({ + displayName: 'Rotation', + required: true, + defaultValue: 90, + options: { + disabled: false, + options: [ + { label: '90° clockwise', value: 90 }, + { label: '180°', value: 180 }, + { label: '270° (90° counter-clockwise)', value: 270 }, + ], + }, + }), + ...sharedProps, + }, + async run(context) { + const { file, rotation, output_filename, packaged_filename } = + context.propsValue; + + const rotateValue = rotation as 90 | 180 | 270; + + return await runAndStoreResult({ + auth: context.auth.secret_text, + files: context.files, + tool: 'rotate', + uploads: [fileToUploadInput(file)], + perFileOverrides: [{ rotate: rotateValue }], + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/send-signer-reminder.ts b/packages/pieces/community/iloveapi/src/lib/actions/send-signer-reminder.ts new file mode 100644 index 00000000000..5bd7c47d59e --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/send-signer-reminder.ts @@ -0,0 +1,32 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { iLoveApi } from '../common/client'; + +export const sendSignerReminderAction = createAction({ + auth: iloveapiAuth, + name: 'send_signer_reminder', + displayName: 'Send Signer Reminder', + description: + 'Email a reminder to signers who have not yet completed the signature. Limited to 2 calls per day.', + props: { + token_requester: Property.ShortText({ + displayName: 'Requester Token', + description: 'The "token_requester" of the signature request.', + required: true, + }), + }, + async run(context) { + const { token_requester } = context.propsValue; + if (!token_requester) { + throw new Error('Requester Token is required.'); + } + + const token = await iLoveApi.authenticate({ + publicKey: context.auth.secret_text, + }); + return await iLoveApi.sendSignerReminder({ + token, + tokenRequester: token_requester, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/split-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/split-pdf.ts new file mode 100644 index 00000000000..50ad14d7ec5 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/split-pdf.ts @@ -0,0 +1,119 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { fileToUploadInput, sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; + +export const splitPdfAction = createAction({ + auth: iloveapiAuth, + name: 'split_pdf', + displayName: 'Split PDF', + description: + 'Split a PDF by page ranges, fixed page batches, removed pages, or target file size.', + props: { + file: Property.File({ + displayName: 'PDF File', + required: true, + }), + split_mode: Property.StaticDropdown({ + displayName: 'Split Mode', + required: true, + defaultValue: 'ranges', + options: { + disabled: false, + options: [ + { label: 'Page Ranges', value: 'ranges' }, + { label: 'Fixed Range (every N pages)', value: 'fixed_range' }, + { label: 'Remove Pages', value: 'remove_pages' }, + { label: 'By File Size', value: 'filesize' }, + ], + }, + }), + ranges: Property.ShortText({ + displayName: 'Ranges', + description: + 'Used with mode "Page Ranges". Example: "1-5,7,10-13". Each range becomes a separate PDF.', + required: false, + }), + fixed_range: Property.Number({ + displayName: 'Fixed Range Size', + description: + 'Used with mode "Fixed Range". Number of pages per output PDF (e.g. 1 splits into one PDF per page).', + required: false, + }), + remove_pages: Property.ShortText({ + displayName: 'Remove Pages', + description: + 'Used with mode "Remove Pages". Pages to delete, e.g. "2,4-6". The remaining pages stay in one PDF.', + required: false, + }), + filesize: Property.Number({ + displayName: 'File Size (MB)', + description: + 'Used with mode "By File Size". Maximum size in megabytes per output PDF.', + required: false, + }), + merge_after: Property.Checkbox({ + displayName: 'Merge results back into one PDF', + description: 'When enabled, merges the split outputs into a single PDF.', + required: false, + defaultValue: false, + }), + ...sharedProps, + }, + async run(context) { + const { + file, + split_mode, + ranges, + fixed_range, + remove_pages, + filesize, + merge_after, + output_filename, + packaged_filename, + } = context.propsValue; + + const options: Record = { + split_mode, + merge_after: merge_after ?? false, + }; + + if (split_mode === 'ranges') { + if (!ranges) { + throw new Error('"Ranges" is required when split mode is "Page Ranges".'); + } + options['ranges'] = ranges; + } else if (split_mode === 'fixed_range') { + if (fixed_range === undefined || fixed_range === null) { + throw new Error( + '"Fixed Range Size" is required when split mode is "Fixed Range".' + ); + } + options['fixed_range'] = fixed_range; + } else if (split_mode === 'remove_pages') { + if (!remove_pages) { + throw new Error( + '"Remove Pages" is required when split mode is "Remove Pages".' + ); + } + options['remove_pages'] = remove_pages; + } else if (split_mode === 'filesize') { + if (filesize === undefined || filesize === null) { + throw new Error( + '"File Size (MB)" is required when split mode is "By File Size".' + ); + } + options['filesize'] = filesize; + } + + return await runAndStoreResult({ + auth: context.auth.secret_text, + files: context.files, + tool: 'split', + uploads: [fileToUploadInput(file)], + options, + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/unlock-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/unlock-pdf.ts new file mode 100644 index 00000000000..0f0416d50e6 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/unlock-pdf.ts @@ -0,0 +1,46 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; + +export const unlockPdfAction = createAction({ + auth: iloveapiAuth, + name: 'unlock_pdf', + displayName: 'Unlock PDF', + description: 'Remove the password from a PDF (you must know the current password).', + props: { + file: Property.File({ + displayName: 'PDF File', + required: true, + }), + password: Property.ShortText({ + displayName: 'Current Password', + description: 'The current password protecting the PDF.', + required: true, + }), + ...sharedProps, + }, + async run(context) { + const { file, password, output_filename, packaged_filename } = + context.propsValue; + + if (!password) { + throw new Error('Current password is required.'); + } + + return await runAndStoreResult({ + auth: context.auth.secret_text, + files: context.files, + tool: 'unlock', + uploads: [ + { + kind: 'file', + file: { base64: file.base64, filename: file.filename }, + }, + ], + perFileOverrides: [{ password }], + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/void-signature.ts b/packages/pieces/community/iloveapi/src/lib/actions/void-signature.ts new file mode 100644 index 00000000000..50b5870daf8 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/void-signature.ts @@ -0,0 +1,32 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { iLoveApi } from '../common/client'; + +export const voidSignatureAction = createAction({ + auth: iloveapiAuth, + name: 'void_signature', + displayName: 'Void Signature', + description: + 'Cancel an in-progress signature request. The document becomes inaccessible to all signers.', + props: { + token_requester: Property.ShortText({ + displayName: 'Requester Token', + description: 'The "token_requester" of the signature request to void.', + required: true, + }), + }, + async run(context) { + const { token_requester } = context.propsValue; + if (!token_requester) { + throw new Error('Requester Token is required.'); + } + + const token = await iLoveApi.authenticate({ + publicKey: context.auth.secret_text, + }); + return await iLoveApi.voidSignature({ + token, + tokenRequester: token_requester, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/actions/watermark-pdf.ts b/packages/pieces/community/iloveapi/src/lib/actions/watermark-pdf.ts new file mode 100644 index 00000000000..03080148103 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/actions/watermark-pdf.ts @@ -0,0 +1,223 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { fileToUploadInput, sharedProps } from '../common/props'; +import { runAndStoreResult } from '../common/runner'; +import { UploadInput } from '../common/client'; + +export const watermarkPdfAction = createAction({ + auth: iloveapiAuth, + name: 'watermark_pdf', + displayName: 'Watermark PDF', + description: 'Stamp a text or image watermark across pages of a PDF.', + props: { + file: Property.File({ + displayName: 'PDF File', + required: true, + }), + mode: Property.StaticDropdown({ + displayName: 'Watermark Mode', + required: true, + defaultValue: 'text', + options: { + disabled: false, + options: [ + { label: 'Text', value: 'text' }, + { label: 'Image', value: 'image' }, + ], + }, + }), + text: Property.ShortText({ + displayName: 'Text', + description: 'Required when mode is "Text". The watermark text to stamp.', + required: false, + }), + image: Property.File({ + displayName: 'Watermark Image', + description: + 'Required when mode is "Image". JPG/PNG image used as the watermark.', + required: false, + }), + pages: Property.ShortText({ + displayName: 'Pages', + description: + 'Pages to apply watermark to. Use "all", a single page, or ranges like "1-3,5".', + required: false, + defaultValue: 'all', + }), + vertical_position: Property.StaticDropdown({ + displayName: 'Vertical Position', + required: false, + defaultValue: 'middle', + options: { + disabled: false, + options: [ + { label: 'Top', value: 'top' }, + { label: 'Middle', value: 'middle' }, + { label: 'Bottom', value: 'bottom' }, + ], + }, + }), + horizontal_position: Property.StaticDropdown({ + displayName: 'Horizontal Position', + required: false, + defaultValue: 'center', + options: { + disabled: false, + options: [ + { label: 'Left', value: 'left' }, + { label: 'Center', value: 'center' }, + { label: 'Right', value: 'right' }, + ], + }, + }), + rotation: Property.Number({ + displayName: 'Rotation (degrees)', + description: 'Rotation between 0 and 360 degrees.', + required: false, + defaultValue: 0, + }), + transparency: Property.Number({ + displayName: 'Transparency (%)', + description: '0 = solid, 100 = fully transparent.', + required: false, + defaultValue: 0, + }), + mosaic: Property.Checkbox({ + displayName: 'Mosaic', + description: 'Repeat the watermark across the entire page.', + required: false, + defaultValue: false, + }), + layer: Property.StaticDropdown({ + displayName: 'Layer', + description: 'Place the watermark above or below the page content.', + required: false, + defaultValue: 'above', + options: { + disabled: false, + options: [ + { label: 'Above content', value: 'above' }, + { label: 'Below content', value: 'below' }, + ], + }, + }), + font_family: Property.StaticDropdown({ + displayName: 'Font (text mode)', + required: false, + defaultValue: 'Arial', + options: { + disabled: false, + options: [ + { label: 'Arial', value: 'Arial' }, + { label: 'Arial Unicode MS', value: 'Arial Unicode MS' }, + { label: 'Verdana', value: 'Verdana' }, + { label: 'Courier', value: 'Courier' }, + { label: 'Times New Roman', value: 'Times New Roman' }, + { label: 'Comic Sans MS', value: 'Comic Sans MS' }, + { label: 'WenQuanYi Zen Hei', value: 'WenQuanYi Zen Hei' }, + { label: 'Lohit Marathi', value: 'Lohit Marathi' }, + ], + }, + }), + font_size: Property.Number({ + displayName: 'Font Size (text mode)', + required: false, + defaultValue: 14, + }), + font_color: Property.ShortText({ + displayName: 'Font Color (text mode)', + description: 'Hex color, e.g. #000000.', + required: false, + defaultValue: '#000000', + }), + font_style: Property.StaticDropdown({ + displayName: 'Font Style (text mode)', + required: false, + defaultValue: '', + options: { + disabled: false, + options: [ + { label: 'Regular', value: '' }, + { label: 'Bold', value: 'Bold' }, + { label: 'Italic', value: 'Italic' }, + ], + }, + }), + ...sharedProps, + }, + async run(context) { + const { + file, + mode, + text, + image, + pages, + vertical_position, + horizontal_position, + rotation, + transparency, + mosaic, + layer, + font_family, + font_size, + font_color, + font_style, + output_filename, + packaged_filename, + } = context.propsValue; + + if (mode === 'text' && !text) { + throw new Error('"Text" is required when watermark mode is "Text".'); + } + if (mode === 'image' && !image?.base64) { + throw new Error( + 'A watermark image is required when watermark mode is "Image".' + ); + } + + const baseOptions: Record = { + mode, + pages: pages ?? 'all', + vertical_position: vertical_position ?? 'middle', + horizontal_position: horizontal_position ?? 'center', + rotation: rotation ?? 0, + transparency: transparency ?? 0, + mosaic: mosaic ?? false, + layer: layer ?? 'above', + }; + + if (mode === 'text') { + baseOptions['text'] = text; + baseOptions['font_family'] = font_family ?? 'Arial'; + baseOptions['font_size'] = font_size ?? 14; + baseOptions['font_color'] = font_color ?? '#000000'; + if (font_style) baseOptions['font_style'] = font_style; + } + + const extraUploads: UploadInput[] | undefined = + mode === 'image' && image?.base64 + ? [ + { + kind: 'file', + file: { base64: image.base64, filename: image.filename }, + }, + ] + : undefined; + + return await runAndStoreResult({ + auth: context.auth.secret_text, + files: context.files, + tool: 'watermark', + uploads: [fileToUploadInput(file)], + extraUploads, + options: (extras) => { + if (mode === 'image') { + return { ...baseOptions, image: extras[0] }; + } + return baseOptions; + }, + output_filename, + packaged_filename, + }); + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/common/auth.ts b/packages/pieces/community/iloveapi/src/lib/common/auth.ts new file mode 100644 index 00000000000..1f965bc79a4 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/common/auth.ts @@ -0,0 +1,28 @@ +import { PieceAuth } from '@activepieces/pieces-framework'; +import { iLoveApi } from './client'; + +export const iloveapiAuth = PieceAuth.SecretText({ + displayName: 'Project Public Key', + required: true, + description: ` +To get your project public key: + +1. Sign up or log in at https://developer.ilovepdf.com/ +2. Open your project on the developer dashboard +3. Copy the **Project public key** (starts with \`project_public_\`) +4. Paste it here + +Only the public key is required. Tokens are issued automatically per request. + `, + validate: async ({ auth }) => { + try { + await iLoveApi.authenticate({ publicKey: auth }); + return { valid: true }; + } catch { + return { + valid: false, + error: 'Invalid iLoveAPI project public key.', + }; + } + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/common/client.ts b/packages/pieces/community/iloveapi/src/lib/common/client.ts new file mode 100644 index 00000000000..e013fdc12ce --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/common/client.ts @@ -0,0 +1,410 @@ +import { + AuthenticationType, + HttpMethod, + HttpRequest, + httpClient, +} from '@activepieces/pieces-common'; +import FormData from 'form-data'; +import { + AuthResponse, + CreateSignatureRequest, + CreateSignatureResponse, + ILoveApiTool, + ProcessFile, + ProcessRequest, + ProcessResponse, + StartTaskResponse, + UploadResponse, +} from './types'; + +const ROOT_API = 'https://api.ilovepdf.com'; + +async function authenticate({ publicKey }: { publicKey: string }): Promise { + const response = await httpClient.sendRequest({ + method: HttpMethod.POST, + url: `${ROOT_API}/v1/auth`, + body: { public_key: publicKey }, + }); + return response.body.token; +} + +async function startTask({ + token, + tool, +}: { + token: string; + tool: ILoveApiTool; +}): Promise { + const response = await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `${ROOT_API}/v1/start/${tool}`, + authentication: { type: AuthenticationType.BEARER_TOKEN, token }, + }); + return response.body; +} + +async function uploadBuffer({ + token, + server, + task, + buffer, + filename, +}: { + token: string; + server: string; + task: string; + buffer: Buffer; + filename: string; +}): Promise { + const formData = new FormData(); + formData.append('task', task); + formData.append('file', buffer, filename); + + const request: HttpRequest = { + method: HttpMethod.POST, + url: `https://${server}/v1/upload`, + headers: { + ...formData.getHeaders(), + Authorization: `Bearer ${token}`, + }, + body: formData, + }; + const response = await httpClient.sendRequest(request); + return response.body.server_filename; +} + +async function uploadCloudUrl({ + token, + server, + task, + url, +}: { + token: string; + server: string; + task: string; + url: string; +}): Promise { + const response = await httpClient.sendRequest({ + method: HttpMethod.POST, + url: `https://${server}/v1/upload`, + authentication: { type: AuthenticationType.BEARER_TOKEN, token }, + body: { task, cloud_file: url }, + }); + return response.body.server_filename; +} + +async function processTask({ + token, + server, + body, +}: { + token: string; + server: string; + body: ProcessRequest; +}): Promise { + const response = await httpClient.sendRequest({ + method: HttpMethod.POST, + url: `https://${server}/v1/process`, + authentication: { type: AuthenticationType.BEARER_TOKEN, token }, + body, + }); + return response.body; +} + +async function downloadResult({ + token, + server, + task, +}: { + token: string; + server: string; + task: string; +}): Promise { + const response = await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `https://${server}/v1/download/${task}`, + authentication: { type: AuthenticationType.BEARER_TOKEN, token }, + responseType: 'arraybuffer', + }); + if (typeof response.body === 'string') { + return Buffer.from(response.body, 'binary'); + } + return Buffer.from(response.body as ArrayBuffer); +} + +async function createSignature({ + token, + server, + body, +}: { + token: string; + server: string; + body: CreateSignatureRequest; +}): Promise { + const response = await httpClient.sendRequest({ + method: HttpMethod.POST, + url: `https://${server}/v1/signature`, + authentication: { type: AuthenticationType.BEARER_TOKEN, token }, + body, + }); + return response.body; +} + +async function getSignatureStatus({ + token, + tokenRequester, +}: { + token: string; + tokenRequester: string; +}): Promise> { + const response = await httpClient.sendRequest>({ + method: HttpMethod.GET, + url: `${ROOT_API}/v1/signature/requesterview/${tokenRequester}`, + authentication: { type: AuthenticationType.BEARER_TOKEN, token }, + }); + return response.body; +} + +async function sendSignerReminder({ + token, + tokenRequester, +}: { + token: string; + tokenRequester: string; +}): Promise> { + const response = await httpClient.sendRequest>({ + method: HttpMethod.POST, + url: `${ROOT_API}/v1/signature/sendReminder/${tokenRequester}`, + authentication: { type: AuthenticationType.BEARER_TOKEN, token }, + }); + return response.body; +} + +async function voidSignature({ + token, + tokenRequester, +}: { + token: string; + tokenRequester: string; +}): Promise> { + const response = await httpClient.sendRequest>({ + method: HttpMethod.POST, + url: `${ROOT_API}/v1/signature/void/${tokenRequester}`, + authentication: { type: AuthenticationType.BEARER_TOKEN, token }, + }); + return response.body; +} + +async function increaseExpirationDays({ + token, + tokenRequester, + days, +}: { + token: string; + tokenRequester: string; + days: number; +}): Promise> { + const response = await httpClient.sendRequest>({ + method: HttpMethod.POST, + url: `${ROOT_API}/v1/signature/increase-expiration-days/${tokenRequester}`, + authentication: { type: AuthenticationType.BEARER_TOKEN, token }, + body: { days }, + }); + return response.body; +} + +async function downloadSignedFiles({ + token, + tokenRequester, +}: { + token: string; + tokenRequester: string; +}): Promise { + const response = await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `${ROOT_API}/v1/signature/${tokenRequester}/download-signed`, + authentication: { type: AuthenticationType.BEARER_TOKEN, token }, + responseType: 'arraybuffer', + }); + if (typeof response.body === 'string') { + return Buffer.from(response.body, 'binary'); + } + return Buffer.from(response.body as ArrayBuffer); +} + +async function downloadAuditTrail({ + token, + tokenRequester, +}: { + token: string; + tokenRequester: string; +}): Promise { + const response = await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `${ROOT_API}/v1/signature/${tokenRequester}/download-audit`, + authentication: { type: AuthenticationType.BEARER_TOKEN, token }, + responseType: 'arraybuffer', + }); + if (typeof response.body === 'string') { + return Buffer.from(response.body, 'binary'); + } + return Buffer.from(response.body as ArrayBuffer); +} + +export const iLoveApiClient = { + authenticate, + startTask, + uploadBuffer, + uploadCloudUrl, + processTask, + downloadResult, + createSignature, + getSignatureStatus, + sendSignerReminder, + voidSignature, + increaseExpirationDays, + downloadSignedFiles, + downloadAuditTrail, +}; + +export type UploadInput = + | { kind: 'file'; file: { base64: string; filename: string } } + | { kind: 'url'; url: string; filename?: string }; + +export type RunTaskInput = { + publicKey: string; + tool: ILoveApiTool; + uploads: UploadInput[]; + options?: + | Record + | ((extraServerFilenames: string[]) => Record); + output_filename?: string; + packaged_filename?: string; + perFileOverrides?: Array>>; + extraUploads?: UploadInput[]; +}; + +export type RunTaskResult = { + buffer: Buffer; + downloadFilename: string; + process: ProcessResponse; + uploadedExtraFilenames: string[]; +}; + +async function runTask({ + publicKey, + tool, + uploads, + options, + output_filename, + packaged_filename, + perFileOverrides, + extraUploads, +}: RunTaskInput): Promise { + if (!uploads || uploads.length === 0) { + throw new Error('At least one input file or URL is required'); + } + + const token = await iLoveApiClient.authenticate({ publicKey }); + const { server, task } = await iLoveApiClient.startTask({ token, tool }); + + const files: ProcessFile[] = []; + for (let i = 0; i < uploads.length; i++) { + const upload = uploads[i]; + if (upload.kind === 'file') { + const buffer = Buffer.from(upload.file.base64, 'base64'); + const serverFilename = await iLoveApiClient.uploadBuffer({ + token, + server, + task, + buffer, + filename: upload.file.filename, + }); + files.push({ + server_filename: serverFilename, + filename: upload.file.filename, + ...perFileOverrides?.[i], + }); + } else { + const serverFilename = await iLoveApiClient.uploadCloudUrl({ + token, + server, + task, + url: upload.url, + }); + files.push({ + server_filename: serverFilename, + filename: upload.filename ?? deriveFilenameFromUrl(upload.url), + ...perFileOverrides?.[i], + }); + } + } + + const uploadedExtraFilenames: string[] = []; + if (extraUploads && extraUploads.length > 0) { + for (const extra of extraUploads) { + if (extra.kind === 'file') { + const buffer = Buffer.from(extra.file.base64, 'base64'); + const serverFilename = await iLoveApiClient.uploadBuffer({ + token, + server, + task, + buffer, + filename: extra.file.filename, + }); + uploadedExtraFilenames.push(serverFilename); + } else { + const serverFilename = await iLoveApiClient.uploadCloudUrl({ + token, + server, + task, + url: extra.url, + }); + uploadedExtraFilenames.push(serverFilename); + } + } + } + + const resolvedOptions = + typeof options === 'function' ? options(uploadedExtraFilenames) : options ?? {}; + + const processBody: ProcessRequest = { + task, + tool, + files, + ...(output_filename ? { output_filename } : {}), + ...(packaged_filename ? { packaged_filename } : {}), + ...resolvedOptions, + }; + + const processResponse = await iLoveApiClient.processTask({ + token, + server, + body: processBody, + }); + + const buffer = await iLoveApiClient.downloadResult({ token, server, task }); + + return { + buffer, + downloadFilename: processResponse.download_filename, + process: processResponse, + uploadedExtraFilenames, + }; +} + +function deriveFilenameFromUrl(url: string): string { + try { + const parsed = new URL(url); + const last = parsed.pathname.split('/').filter(Boolean).pop(); + if (last && last.length > 0) return last; + } catch { + // fall through + } + return 'remote-file'; +} + +export const iLoveApi = { + ...iLoveApiClient, + runTask, +}; diff --git a/packages/pieces/community/iloveapi/src/lib/common/props.ts b/packages/pieces/community/iloveapi/src/lib/common/props.ts new file mode 100644 index 00000000000..4358ec8858b --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/common/props.ts @@ -0,0 +1,23 @@ +import { Property } from '@activepieces/pieces-framework'; +import { UploadInput } from './client'; + +export const sharedProps = { + output_filename: Property.ShortText({ + displayName: 'Output Filename', + description: + 'Output file name without extension. Use {date}, {n} or {filename} placeholders.', + required: false, + }), + packaged_filename: Property.ShortText({ + displayName: 'Packaged ZIP Filename', + description: + 'Name of the ZIP archive when the result contains multiple files. No extension.', + required: false, + }), +}; + +type ApFile = { base64: string; filename: string }; + +export function fileToUploadInput(file: ApFile): UploadInput { + return { kind: 'file', file: { base64: file.base64, filename: file.filename } }; +} diff --git a/packages/pieces/community/iloveapi/src/lib/common/runner.ts b/packages/pieces/community/iloveapi/src/lib/common/runner.ts new file mode 100644 index 00000000000..1080630ea15 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/common/runner.ts @@ -0,0 +1,59 @@ +import { FilesService } from '@activepieces/pieces-framework'; +import { ILoveApiTool, ProcessResponse } from './types'; +import { RunTaskInput, UploadInput, iLoveApi } from './client'; + +export type RunAndStoreInput = Omit & { + auth: string; + files: FilesService; +}; + +export type RunAndStoreResult = { + output_file: string; + download_filename: string; + output_filenumber?: number; + output_filesize?: number; + output_extensions?: string[]; + status?: string; + process: ProcessResponse; +}; + +export async function runAndStoreResult({ + auth, + files, + tool, + uploads, + options, + output_filename, + packaged_filename, + perFileOverrides, + extraUploads, +}: RunAndStoreInput): Promise { + const result = await iLoveApi.runTask({ + publicKey: auth, + tool, + uploads, + options, + output_filename, + packaged_filename, + perFileOverrides, + extraUploads, + }); + + const storedFile = await files.write({ + fileName: result.downloadFilename, + data: result.buffer, + }); + + return { + output_file: storedFile, + download_filename: result.downloadFilename, + output_filenumber: result.process.output_filenumber, + output_filesize: result.process.output_filesize, + output_extensions: result.process.output_extensions, + status: result.process.status, + process: result.process, + }; +} + +export type ToolUploadInput = UploadInput; +export type ToolName = ILoveApiTool; diff --git a/packages/pieces/community/iloveapi/src/lib/common/types.ts b/packages/pieces/community/iloveapi/src/lib/common/types.ts new file mode 100644 index 00000000000..63f92ee8556 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/common/types.ts @@ -0,0 +1,123 @@ +export type ILoveApiTool = + | 'compress' + | 'merge' + | 'split' + | 'pdfjpg' + | 'imagepdf' + | 'officepdf' + | 'htmlpdf' + | 'pdfocr' + | 'watermark' + | 'protect' + | 'unlock' + | 'pagenumber' + | 'rotate' + | 'extract' + | 'repair' + | 'sign'; + +export type SignatureSigner = { + name: string; + email: string; + phone?: string; + type?: 'signer' | 'validator' | 'viewer'; + force_signature_type?: 'all' | 'text' | 'sign' | 'image'; + files?: Array<{ server_filename: string; elements?: SignatureElement[] }>; + access_code?: string; + notes?: string; +}; + +export type SignatureElement = { + type: 'signature' | 'initials' | 'date' | 'text' | 'input'; + position: string; + pages: string; + size?: number; + content?: string; + color?: string; + font?: string; +}; + +export type CreateSignatureRequest = { + task: string; + files: Array<{ + server_filename: string; + filename: string; + }>; + signers: SignatureSigner[]; + brand_name?: string; + brand_logo?: string; + language?: string; + subject_signer?: string; + message_signer?: string; + subject_completed?: string; + message_completed?: string; + lock_order?: boolean; + expiration_days?: number; + signer_reminders?: boolean; + signer_reminder_days_cycle?: number; + uuid_visible?: boolean; + verify_enabled?: boolean; + certified?: boolean; + mode?: 'single' | 'multiple' | 'batch'; +}; + +export type CreateSignatureResponse = { + uuid: string; + token_requester: string; + status: string; + created?: string; + expires?: string; + certified?: boolean; + signers?: Array<{ + uuid: string; + name: string; + email: string; + type: string; + status: string; + token_requester?: string; + }>; + files?: Array<{ filename: string; pages?: number }>; +} & Record; + +export type StartTaskResponse = { + server: string; + task: string; + remaining_files?: number; +}; + +export type AuthResponse = { + token: string; +}; + +export type UploadResponse = { + server_filename: string; +}; + +export type ProcessFile = { + server_filename: string; + filename: string; + rotate?: 0 | 90 | 180 | 270; + password?: string; +}; + +export type ProcessRequest = { + task: string; + tool: ILoveApiTool; + files: ProcessFile[]; + output_filename?: string; + packaged_filename?: string; + ignore_errors?: boolean; + ignore_password?: boolean; + try_pdf_repair?: boolean; +} & Record; + +export type ProcessResponse = { + download_filename: string; + filesize?: number; + output_filesize?: number; + output_filenumber?: number; + output_extensions?: string[]; + status?: string; + task?: string; + timer?: string; +} & Record; diff --git a/packages/pieces/community/iloveapi/src/lib/triggers/common.ts b/packages/pieces/community/iloveapi/src/lib/triggers/common.ts new file mode 100644 index 00000000000..f2c96584646 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/triggers/common.ts @@ -0,0 +1,87 @@ +import { Property } from '@activepieces/pieces-framework'; + +export const webhookInstructions = (eventLabel: string): string => ` +**How to connect** + +1. Sign in to https://developer.ilovepdf.com/ and open your project. +2. Go to **Webhooks** and click **Add Webhook**. +3. Paste the URL below into **Endpoint URL**: + +\`\`\`text +{{webhookUrl}} +\`\`\` + +4. In **Events**, select **${eventLabel}**. +5. Save the webhook. The trigger will fire on every matching event. +`; + +export const toolFilterProperty = Property.StaticMultiSelectDropdown({ + displayName: 'Filter by Tool', + description: + 'Optional. When set, only fires for tasks using one of the selected tools. Leave empty to fire for every tool.', + required: false, + options: { + disabled: false, + options: [ + { label: 'Compress PDF', value: 'compress' }, + { label: 'Merge PDF', value: 'merge' }, + { label: 'Split PDF', value: 'split' }, + { label: 'PDF to JPG', value: 'pdfjpg' }, + { label: 'JPG/Image to PDF', value: 'imagepdf' }, + { label: 'Office to PDF', value: 'officepdf' }, + { label: 'HTML to PDF', value: 'htmlpdf' }, + { label: 'OCR PDF', value: 'pdfocr' }, + { label: 'Watermark PDF', value: 'watermark' }, + { label: 'Protect PDF', value: 'protect' }, + { label: 'Unlock PDF', value: 'unlock' }, + { label: 'Page Numbers', value: 'pagenumber' }, + { label: 'Rotate PDF', value: 'rotate' }, + { label: 'Extract Text', value: 'extract' }, + { label: 'Repair PDF', value: 'repair' }, + { label: 'PDF/A', value: 'pdfa' }, + { label: 'Validate PDF/A', value: 'validatepdfa' }, + { label: 'Edit PDF', value: 'editpdf' }, + { label: 'Sign PDF', value: 'sign' }, + ], + }, +}); + +type WebhookEnvelope = { + event?: string; + data?: { + task?: { tool?: string } & Record; + signature?: Record; + signer?: Record; + } & Record; +} & Record; + +export function extractEnvelope(body: unknown): WebhookEnvelope | null { + if (!body || typeof body !== 'object') return null; + return body as WebhookEnvelope; +} + +export function matchesEvent({ + body, + expectedEvent, +}: { + body: unknown; + expectedEvent: string; +}): WebhookEnvelope | null { + const envelope = extractEnvelope(body); + if (!envelope) return null; + if (envelope.event !== expectedEvent) return null; + return envelope; +} + +export function matchesToolFilter({ + envelope, + tools, +}: { + envelope: WebhookEnvelope; + tools?: string[] | null; +}): boolean { + if (!tools || tools.length === 0) return true; + const tool = envelope.data?.task?.tool; + if (!tool) return false; + return tools.includes(tool); +} diff --git a/packages/pieces/community/iloveapi/src/lib/triggers/sample-payloads.ts b/packages/pieces/community/iloveapi/src/lib/triggers/sample-payloads.ts new file mode 100644 index 00000000000..63a7e547532 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/triggers/sample-payloads.ts @@ -0,0 +1,127 @@ +export const taskCompletedSample = { + event: 'task.completed', + data: { + task: { + task: 'b3fd95c0c3b8c6f3a7e7b5b7a09a6d2a', + tool: 'compress', + status: 'TaskSuccess', + process_start: '2026-05-07 10:00:00', + timer: '1.32', + filesize: 1048576, + output_filesize: 524288, + output_filenumber: 1, + output_extensions: ['pdf'], + server: 'api1g.ilovepdf.com', + custom_int: null, + custom_string: null, + }, + }, +}; + +export const taskFailedSample = { + event: 'task.failed', + data: { + task: { + task: 'a1b2c3d4e5f60718293a4b5c6d7e8f90', + tool: 'pdfocr', + status: 'TaskError', + process_start: '2026-05-07 10:00:00', + message: 'DamagedFile: PDF could not be processed', + server: 'api1g.ilovepdf.com', + }, + }, +}; + +const baseSignature = { + signature_id: 'sig_01HV1Y8FZQ3XW3Y8RPVYT6WBNH', + brand_name: 'Acme Corp', + language: 'en-US', + status: 'created', + uuid: '5f7d2a3b-abc-1234', + mode: 'multiple', + subject_signer: 'Please sign this document', + message_signer: 'Sign the attached contract', + certified: true, + files: [ + { + server_filename: 'contract.pdf', + filename: 'contract.pdf', + }, + ], + signers: [ + { + uuid: 'rcv_01HV1Y8FZQ3XW3Y8RPVYT6SIGN', + email: 'john@example.com', + name: 'John Doe', + type: 'signer', + status: 'waiting', + }, + ], + created: '2026-05-07T10:00:00Z', +}; + +export const signatureCreatedSample = { + event: 'signature.created', + data: { signature: { ...baseSignature, status: 'created' } }, +}; + +export const signatureSentSample = { + event: 'signature.sent', + data: { signature: { ...baseSignature, status: 'sent' } }, +}; + +export const signatureCompletedSample = { + event: 'signature.completed', + data: { signature: { ...baseSignature, status: 'completed' } }, +}; + +export const signatureDeclinedSample = { + event: 'signature.declined', + data: { signature: { ...baseSignature, status: 'declined' } }, +}; + +export const signatureExpiredSample = { + event: 'signature.expired', + data: { signature: { ...baseSignature, status: 'expired' } }, +}; + +export const signatureVoidedSample = { + event: 'signature.voided', + data: { signature: { ...baseSignature, status: 'voided' } }, +}; + +const baseSigner = { + uuid: 'rcv_01HV1Y8FZQ3XW3Y8RPVYT6SIGN', + email: 'john@example.com', + name: 'John Doe', + type: 'signer', +}; + +export const signerViewedSample = { + event: 'signature.signer.viewed', + data: { + signature: { ...baseSignature }, + signer: { ...baseSigner, status: 'viewed', viewed: '2026-05-07T10:05:00Z' }, + }, +}; + +export const signerSignedSample = { + event: 'signature.signer.completed', + data: { + signature: { ...baseSignature }, + signer: { ...baseSigner, status: 'signed', signed: '2026-05-07T10:10:00Z' }, + }, +}; + +export const signerDeclinedSample = { + event: 'signature.signer.declined', + data: { + signature: { ...baseSignature }, + signer: { + ...baseSigner, + status: 'declined', + declined: '2026-05-07T10:15:00Z', + reason: 'Not the right document', + }, + }, +}; diff --git a/packages/pieces/community/iloveapi/src/lib/triggers/signature-builder.ts b/packages/pieces/community/iloveapi/src/lib/triggers/signature-builder.ts new file mode 100644 index 00000000000..c20c21cd293 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/triggers/signature-builder.ts @@ -0,0 +1,41 @@ +import { Property, TriggerStrategy, createTrigger } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { matchesEvent, webhookInstructions } from './common'; + +export type SignatureTriggerSpec = { + name: string; + displayName: string; + description: string; + event: string; + sampleData: unknown; +}; + +export function buildSignatureTrigger(spec: SignatureTriggerSpec) { + return createTrigger({ + auth: iloveapiAuth, + name: spec.name, + displayName: spec.displayName, + description: spec.description, + type: TriggerStrategy.WEBHOOK, + props: { + instructions: Property.MarkDown({ + value: webhookInstructions(`${spec.displayName} (${spec.event})`), + }), + }, + sampleData: spec.sampleData, + async onEnable() { + return; + }, + async onDisable() { + return; + }, + async run(context) { + const envelope = matchesEvent({ + body: context.payload.body, + expectedEvent: spec.event, + }); + if (!envelope) return []; + return [envelope]; + }, + }); +} diff --git a/packages/pieces/community/iloveapi/src/lib/triggers/signature-events.ts b/packages/pieces/community/iloveapi/src/lib/triggers/signature-events.ts new file mode 100644 index 00000000000..75fdd3286a5 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/triggers/signature-events.ts @@ -0,0 +1,86 @@ +import { buildSignatureTrigger } from './signature-builder'; +import { + signatureCompletedSample, + signatureCreatedSample, + signatureDeclinedSample, + signatureExpiredSample, + signatureSentSample, + signatureVoidedSample, + signerDeclinedSample, + signerSignedSample, + signerViewedSample, +} from './sample-payloads'; + +export const signatureCreatedTrigger = buildSignatureTrigger({ + name: 'signature_created', + displayName: 'Signature Created', + description: 'Fires when a new signature request is created.', + event: 'signature.created', + sampleData: signatureCreatedSample, +}); + +export const signatureSentTrigger = buildSignatureTrigger({ + name: 'signature_sent', + displayName: 'Signature Sent', + description: 'Fires when a signature request is sent to all receivers.', + event: 'signature.sent', + sampleData: signatureSentSample, +}); + +export const signatureCompletedTrigger = buildSignatureTrigger({ + name: 'signature_completed', + displayName: 'Signature Completed', + description: + 'Fires when every signer has finished. Use this to drive contract-completion automations.', + event: 'signature.completed', + sampleData: signatureCompletedSample, +}); + +export const signatureDeclinedTrigger = buildSignatureTrigger({ + name: 'signature_declined', + displayName: 'Signature Declined', + description: + 'Fires when a signer or validator declines the signature request.', + event: 'signature.declined', + sampleData: signatureDeclinedSample, +}); + +export const signatureExpiredTrigger = buildSignatureTrigger({ + name: 'signature_expired', + displayName: 'Signature Expired', + description: 'Fires when a signature request reaches its expiration date.', + event: 'signature.expired', + sampleData: signatureExpiredSample, +}); + +export const signatureVoidedTrigger = buildSignatureTrigger({ + name: 'signature_voided', + displayName: 'Signature Voided', + description: 'Fires when the requester cancels an in-progress signature.', + event: 'signature.voided', + sampleData: signatureVoidedSample, +}); + +export const signerViewedTrigger = buildSignatureTrigger({ + name: 'signer_viewed', + displayName: 'Signer Viewed Document', + description: 'Fires when an individual receiver opens the document.', + event: 'signature.signer.viewed', + sampleData: signerViewedSample, +}); + +export const signerSignedTrigger = buildSignatureTrigger({ + name: 'signer_signed', + displayName: 'Signer Signed', + description: 'Fires when an individual signer completes their signature.', + event: 'signature.signer.completed', + sampleData: signerSignedSample, +}); + +export const signerDeclinedTrigger = buildSignatureTrigger({ + name: 'signer_declined', + displayName: 'Signer Declined', + description: 'Fires when an individual signer declines to sign.', + event: 'signature.signer.declined', + sampleData: signerDeclinedSample, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/triggers/task-completed.ts b/packages/pieces/community/iloveapi/src/lib/triggers/task-completed.ts new file mode 100644 index 00000000000..5b77d1d32c5 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/triggers/task-completed.ts @@ -0,0 +1,43 @@ +import { Property, TriggerStrategy, createTrigger } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { + matchesEvent, + matchesToolFilter, + toolFilterProperty, + webhookInstructions, +} from './common'; +import { taskCompletedSample } from './sample-payloads'; + +export const taskCompletedTrigger = createTrigger({ + auth: iloveapiAuth, + name: 'task_completed', + displayName: 'Task Completed', + description: + 'Fires when a processing task finishes successfully. Optionally filter by tool.', + type: TriggerStrategy.WEBHOOK, + props: { + instructions: Property.MarkDown({ + value: webhookInstructions('Task Completed (task.completed)'), + }), + tools: toolFilterProperty, + }, + sampleData: taskCompletedSample, + async onEnable() { + return; + }, + async onDisable() { + return; + }, + async run(context) { + const envelope = matchesEvent({ + body: context.payload.body, + expectedEvent: 'task.completed', + }); + if (!envelope) return []; + + const tools = (context.propsValue.tools ?? []) as string[] | undefined; + if (!matchesToolFilter({ envelope, tools })) return []; + + return [envelope]; + }, +}); diff --git a/packages/pieces/community/iloveapi/src/lib/triggers/task-failed.ts b/packages/pieces/community/iloveapi/src/lib/triggers/task-failed.ts new file mode 100644 index 00000000000..463f74c9a61 --- /dev/null +++ b/packages/pieces/community/iloveapi/src/lib/triggers/task-failed.ts @@ -0,0 +1,43 @@ +import { Property, TriggerStrategy, createTrigger } from '@activepieces/pieces-framework'; +import { iloveapiAuth } from '../common/auth'; +import { + matchesEvent, + matchesToolFilter, + toolFilterProperty, + webhookInstructions, +} from './common'; +import { taskFailedSample } from './sample-payloads'; + +export const taskFailedTrigger = createTrigger({ + auth: iloveapiAuth, + name: 'task_failed', + displayName: 'Task Failed', + description: + 'Fires when a processing task fails. Optionally filter by tool.', + type: TriggerStrategy.WEBHOOK, + props: { + instructions: Property.MarkDown({ + value: webhookInstructions('Task Failed (task.failed)'), + }), + tools: toolFilterProperty, + }, + sampleData: taskFailedSample, + async onEnable() { + return; + }, + async onDisable() { + return; + }, + async run(context) { + const envelope = matchesEvent({ + body: context.payload.body, + expectedEvent: 'task.failed', + }); + if (!envelope) return []; + + const tools = (context.propsValue.tools ?? []) as string[] | undefined; + if (!matchesToolFilter({ envelope, tools })) return []; + + return [envelope]; + }, +}); diff --git a/packages/pieces/community/iloveapi/tsconfig.json b/packages/pieces/community/iloveapi/tsconfig.json new file mode 100644 index 00000000000..b512ca3708c --- /dev/null +++ b/packages/pieces/community/iloveapi/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} \ No newline at end of file diff --git a/packages/pieces/community/iloveapi/tsconfig.lib.json b/packages/pieces/community/iloveapi/tsconfig.lib.json new file mode 100644 index 00000000000..8b4345e5114 --- /dev/null +++ b/packages/pieces/community/iloveapi/tsconfig.lib.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "paths": {}, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts" + ] +} \ No newline at end of file diff --git a/packages/pieces/community/pubrio/.eslintrc.json b/packages/pieces/community/pubrio/.eslintrc.json new file mode 100644 index 00000000000..6aa5514eb37 --- /dev/null +++ b/packages/pieces/community/pubrio/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "rules": {} + } + ] +} diff --git a/packages/pieces/community/pubrio/README.md b/packages/pieces/community/pubrio/README.md new file mode 100644 index 00000000000..39fcb3b7b76 --- /dev/null +++ b/packages/pieces/community/pubrio/README.md @@ -0,0 +1,100 @@ +# @activepieces/piece-pubrio + +Activepieces piece for [Pubrio](https://pubrio.com) — the glocalized business data layer for AI agents and revenue teams. Search the whole market, not just the 30% in mainstream datasets. + +## Actions (51) + +### Company (6) + +| Action | Description | +|--------|-------------| +| Search Companies | Search companies by name, domain, location, industry, technology, headcount, and more | +| Lookup Company | Look up a company by domain, LinkedIn URL, or ID | +| Enrich Company | Enrich company with full firmographic data (uses credits) | +| LinkedIn Company Lookup | Real-time LinkedIn company lookup | +| Find Similar Companies | Find lookalike companies with filters for location, industry, technology, headcount, and more | +| Lookup Technology | Look up technologies used by a company | + +### Signals (7) + +| Action | Description | +|--------|-------------| +| Search Jobs | Search job postings across companies by title, location, keyword, and date | +| Lookup Job | Look up detailed information about a specific job posting | +| Search News | Search company news and press releases by category, language, gallery, and date | +| Lookup News | Look up detailed information about a specific news article | +| Search Ads | Search company advertisements by keyword, headline, target location, and date range | +| Lookup Advertisement | Look up detailed information about a specific advertisement | +| Lookup Lookalike | Look up a similar/lookalike company result | + +### People (7) + +| Action | Description | +|--------|-------------| +| Search People | Search people by name, title, department, seniority, location, company, and more | +| Lookup Person | Look up a person by LinkedIn URL or people_search_id | +| LinkedIn Person Lookup | Real-time LinkedIn person lookup | +| Enrich Person | Enrich person with full professional details (uses credits) | +| Reveal Contact | Reveal email (work/personal) or phone for a person (uses credits) | +| Batch Redeem Contacts | Reveal contact details for multiple people at once (uses credits) | +| Query Batch Redeem | Check the status and results of a batch contact redeem operation | + +### Reference Data (14) + +| Action | Description | +|--------|-------------| +| Get Locations | Get all available location codes for search filters | +| Get Departments | Get all department title codes for people search filters | +| Get Department Functions | Get all department function codes for people search filters | +| Get Management Levels | Get all management/seniority level codes for people search filters | +| Get Company Sizes | Get all company size range codes for search filters | +| Get Timezones | Get all available timezone codes | +| Get News Categories | Get all news category codes for news search filters | +| Get News Galleries | Get all news gallery codes for news search filters | +| Get News Languages | Get all news language codes for news search filters | +| Search Technologies | Search for technology names by keyword | +| Search Technology Categories | Search for technology category names by keyword | +| Search Verticals | Search for industry vertical names by keyword | +| Search Vertical Categories | Search for vertical category names by keyword | +| Search Vertical Sub-Categories | Search for vertical sub-category names by keyword | + +### Monitors (14) + +| Action | Description | +|--------|-------------| +| Create Monitor | Create a new signal monitor for jobs, news, or advertisements | +| Update Monitor | Update an existing signal monitor configuration | +| Get Monitor | Get detailed information about a specific monitor | +| List Monitors | List all signal monitors with pagination | +| Delete Monitor | Permanently delete a signal monitor | +| Duplicate Monitor | Create a copy of an existing monitor | +| Test Run Monitor | Execute a test run of a monitor to preview triggers | +| Retry Monitor | Retry a failed monitor trigger by log ID | +| Validate Webhook | Test a webhook destination configuration | +| Get Monitor Stats | Get aggregate statistics across all monitors | +| Get Monitor Chart | Get daily trigger statistics for a monitor over a date range | +| Get Monitor Logs | Get trigger logs for a specific monitor | +| Get Monitor Log Detail | Look up detailed information about a specific monitor log entry | +| Reveal Monitor Signature | Reveal the webhook signature secret for a monitor | + +### Profile (3) + +| Action | Description | +|--------|-------------| +| Get Profile | Get full profile information for the authenticated account | +| Get Usage | Get credit usage and subscription information | +| Get User | Get current authenticated user details | + +## Triggers + +| Trigger | Description | +|---------|-------------| +| Monitor Event | Webhook trigger for Pubrio monitor events | + +## Authentication + +Get your API key from [dashboard.pubrio.com](https://dashboard.pubrio.com) under Settings. + +## License + +MIT diff --git a/packages/pieces/community/pubrio/package.json b/packages/pieces/community/pubrio/package.json new file mode 100644 index 00000000000..c3619efbf97 --- /dev/null +++ b/packages/pieces/community/pubrio/package.json @@ -0,0 +1,17 @@ +{ + "name": "@activepieces/piece-pubrio", + "version": "0.0.1", + "type": "commonjs", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "tslib": "^2.3.0" + }, + "scripts": { + "build": "tsc -p tsconfig.lib.json && cp package.json dist/", + "lint": "eslint 'src/**/*.ts'" + } +} diff --git a/packages/pieces/community/pubrio/src/index.ts b/packages/pieces/community/pubrio/src/index.ts new file mode 100644 index 00000000000..955d226ba19 --- /dev/null +++ b/packages/pieces/community/pubrio/src/index.ts @@ -0,0 +1,98 @@ +import { createPiece, PieceAuth } from '@activepieces/pieces-framework'; +import { searchCompanies } from './lib/actions/search-companies'; +import { lookupCompany } from './lib/actions/lookup-company'; +import { enrichCompany } from './lib/actions/enrich-company'; +import { searchPeople } from './lib/actions/search-people'; +import { lookupPerson } from './lib/actions/lookup-person'; +import { revealContact } from './lib/actions/reveal-contact'; +import { searchJobs } from './lib/actions/search-jobs'; +import { searchNews } from './lib/actions/search-news'; +import { findSimilarCompanies } from './lib/actions/find-similar-companies'; +import { lookupTechnology } from './lib/actions/lookup-technology'; +import { linkedinPersonLookup } from './lib/actions/linkedin-person-lookup'; +import { searchAds } from './lib/actions/search-ads'; +import { linkedinCompanyLookup } from './lib/actions/linkedin-company-lookup'; +import { lookupJob } from './lib/actions/lookup-job'; +import { lookupNews } from './lib/actions/lookup-news'; +import { lookupAdvertisement } from './lib/actions/lookup-advertisement'; +import { lookupLookalike } from './lib/actions/lookup-lookalike'; +import { batchRedeemContacts } from './lib/actions/batch-redeem-contacts'; +import { queryBatchRedeem } from './lib/actions/query-batch-redeem'; +import { createMonitor } from './lib/actions/create-monitor'; +import { updateMonitor } from './lib/actions/update-monitor'; +import { getMonitor } from './lib/actions/get-monitor'; +import { listMonitors } from './lib/actions/list-monitors'; +import { deleteMonitor } from './lib/actions/delete-monitor'; +import { duplicateMonitor } from './lib/actions/duplicate-monitor'; +import { testRunMonitor } from './lib/actions/test-run-monitor'; +import { revealMonitorSignature } from './lib/actions/reveal-monitor-signature'; +import { pubrioWebhookTrigger } from './lib/triggers/webhook'; +import { + createCustomApiCallAction, + HttpMethod, +} from '@activepieces/pieces-common'; +import { pubrioRequest } from './lib/common'; + +export const pubrioAuth = PieceAuth.SecretText({ + displayName: 'API Key', + description: + 'Go to dashboard.pubrio.com → Settings → API Keys → Create New Key → copy the key here.', + required: true, + validate: async ({ auth }) => { + try { + await pubrioRequest(auth, HttpMethod.GET, '/user'); + return { + valid: true, + }; + } catch (error) { + return { valid: false, error: 'Error validating API key' }; + } + }, +}); + +export const pubrio = createPiece({ + displayName: 'Pubrio', + description: + 'Enrich company and people data, monitor changes, and get notified with Pubrio.', + auth: pubrioAuth, + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/pubrio.png', + authors: ['pubrio', 'sanket-a11y'], + actions: [ + searchCompanies, + lookupCompany, + enrichCompany, + searchPeople, + lookupPerson, + revealContact, + searchJobs, + searchNews, + findSimilarCompanies, + lookupTechnology, + linkedinPersonLookup, + searchAds, + linkedinCompanyLookup, + lookupJob, + lookupNews, + lookupAdvertisement, + lookupLookalike, + batchRedeemContacts, + queryBatchRedeem, + createMonitor, + updateMonitor, + getMonitor, + listMonitors, + deleteMonitor, + duplicateMonitor, + testRunMonitor, + revealMonitorSignature, + createCustomApiCallAction({ + baseUrl: () => 'https://api.pubrio.com', + auth: pubrioAuth, + authMapping: async (auth) => ({ + 'pubrio-api-key': auth.secret_text, + }), + }), + ], + triggers: [pubrioWebhookTrigger], +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/batch-redeem-contacts.ts b/packages/pieces/community/pubrio/src/lib/actions/batch-redeem-contacts.ts new file mode 100644 index 00000000000..2f681f77242 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/batch-redeem-contacts.ts @@ -0,0 +1,47 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const batchRedeemContacts = createAction({ + auth: pubrioAuth, + name: 'batch_redeem_contacts', + displayName: 'Batch Redeem Contacts', + description: + 'Reveal email or phone numbers for multiple people at once (uses credits)', + props: { + peoples: Property.Array({ + displayName: 'People Search IDs', + required: true, + description: 'People search IDs', + }), + people_contact_types: Property.StaticMultiSelectDropdown({ + displayName: 'Contact Types', + required: false, + options: { + options: [ + { label: 'Work Email', value: 'email-work' }, + { label: 'Personal Email', value: 'email-personal' }, + { label: 'Phone', value: 'phone' }, + ], + }, + }), + }, + async run(context) { + const body: Record = { + peoples: context.propsValue.peoples, + }; + if ( + context.propsValue.people_contact_types && + context.propsValue.people_contact_types.length > 0 + ) { + body['people_contact_types'] = context.propsValue.people_contact_types; + } + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/redeem/people/batch', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/create-monitor.ts b/packages/pieces/community/pubrio/src/lib/actions/create-monitor.ts new file mode 100644 index 00000000000..87ff08fa9b0 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/create-monitor.ts @@ -0,0 +1,204 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const createMonitor = createAction({ + auth: pubrioAuth, + name: 'create_monitor', + displayName: 'Create Monitor', + description: 'Create a new signal monitor for jobs, news, or advertisements', + props: { + name: Property.ShortText({ + displayName: 'Name', + required: true, + description: 'Monitor name', + }), + detection_mode: Property.StaticDropdown({ + displayName: 'Detection Mode', + required: true, + options: { + options: [ + { label: 'Company First', value: 'company_first' }, + { label: 'Signal First', value: 'signal_first' }, + ], + }, + }), + signal_types: Property.StaticMultiSelectDropdown({ + displayName: 'Signal Types', + required: true, + options: { + options: [ + { label: 'Jobs', value: 'jobs' }, + { label: 'News', value: 'news' }, + { label: 'Advertisements', value: 'advertisements' }, + ], + }, + }), + destination_type: Property.StaticDropdown({ + displayName: 'Destination Type', + required: true, + options: { + options: [ + { label: 'Webhook', value: 'webhook' }, + { label: 'Email', value: 'email' }, + { label: 'Sequences', value: 'sequences' }, + ], + }, + }), + webhook_url: Property.ShortText({ + displayName: 'Webhook URL', + required: false, + description: 'Required when destination type is webhook', + }), + email: Property.ShortText({ + displayName: 'Email', + required: false, + description: 'Required when destination type is email', + }), + sequence_identifier: Property.ShortText({ + displayName: 'Sequence Identifier', + required: false, + description: 'Required when destination type is sequences', + }), + record_type: Property.ShortText({ + displayName: 'Record Type', + required: false, + description: 'Required when destination type is sequences', + }), + description: Property.LongText({ + displayName: 'Description', + required: false, + }), + frequency_minute: Property.Number({ + displayName: 'Frequency (minutes)', + required: false, + }), + max_daily_trigger: Property.Number({ + displayName: 'Max Daily Triggers', + required: false, + }), + max_records_per_trigger: Property.Number({ + displayName: 'Max Records Per Trigger', + required: false, + }), + companies: Property.Array({ + displayName: 'Companies', + description: 'Company names', + required: false, + }), + domains: Property.Array({ + displayName: 'Domains', + required: false, + }), + linkedin_urls: Property.Array({ + displayName: 'LinkedIn URLs', + required: false, + }), + company_filters: Property.LongText({ + displayName: 'Company Filters', + description: 'Advanced company filters as JSON string', + required: false, + }), + signal_filters: Property.LongText({ + displayName: 'Signal Filters', + description: 'Signal-specific filters as JSON array string', + required: false, + }), + people_enrichment_configs: Property.LongText({ + displayName: 'People Enrichment Configs', + description: 'People enrichment configs as JSON array string', + required: false, + }), + is_company_enrichment: Property.Checkbox({ + displayName: 'Company Enrichment', + required: false, + defaultValue: false, + }), + is_people_enrichment: Property.Checkbox({ + displayName: 'People Enrichment', + required: false, + defaultValue: false, + }), + max_failure_trigger: Property.Number({ + displayName: 'Max Failure Trigger', + description: '1-10', + required: false, + }), + max_retry_per_trigger: Property.Number({ + displayName: 'Max Retry Per Trigger', + description: '0-3', + required: false, + }), + retry_delay_second: Property.Number({ + displayName: 'Retry Delay (seconds)', + description: '1-5', + required: false, + }), + notification_email: Property.ShortText({ + displayName: 'Notification Email', + required: false, + }), + }, + async run(context) { + const body: Record = { + name: context.propsValue.name, + detection_mode: context.propsValue.detection_mode, + signal_types: context.propsValue.signal_types, + destination_type: context.propsValue.destination_type, + }; + const destConfig: Record = {}; + if (context.propsValue.webhook_url) + destConfig['webhook_url'] = context.propsValue.webhook_url; + if (context.propsValue.email) + destConfig['email'] = context.propsValue.email; + if (context.propsValue.sequence_identifier) + destConfig['sequence_identifier'] = + context.propsValue.sequence_identifier; + if (context.propsValue.record_type) + destConfig['record_type'] = context.propsValue.record_type; + if (Object.keys(destConfig).length > 0) + body['destination_config'] = destConfig; + if (context.propsValue.description) + body['description'] = context.propsValue.description; + if (context.propsValue.frequency_minute != null) + body['frequency_minute'] = context.propsValue.frequency_minute; + if (context.propsValue.max_daily_trigger != null) + body['max_daily_trigger'] = context.propsValue.max_daily_trigger; + if (context.propsValue.max_records_per_trigger != null) + body['max_records_per_trigger'] = + context.propsValue.max_records_per_trigger; + if (context.propsValue.companies) + body['companies'] = context.propsValue.companies; + if (context.propsValue.domains) + body['domains'] = context.propsValue.domains; + if (context.propsValue.linkedin_urls) + body['linkedin_urls'] = context.propsValue.linkedin_urls; + if (context.propsValue.company_filters) + body['company_filters'] = JSON.parse(context.propsValue.company_filters); + if (context.propsValue.signal_filters) + body['signal_filters'] = JSON.parse(context.propsValue.signal_filters); + if (context.propsValue.people_enrichment_configs) + body['people_enrichment_configs'] = JSON.parse( + context.propsValue.people_enrichment_configs + ); + if (context.propsValue.is_company_enrichment) + body['is_company_enrichment'] = context.propsValue.is_company_enrichment; + if (context.propsValue.is_people_enrichment) + body['is_people_enrichment'] = context.propsValue.is_people_enrichment; + if (context.propsValue.max_failure_trigger != null) + body['max_failure_trigger'] = context.propsValue.max_failure_trigger; + if (context.propsValue.max_retry_per_trigger != null) + body['max_retry_per_trigger'] = context.propsValue.max_retry_per_trigger; + if (context.propsValue.retry_delay_second != null) + body['retry_delay_second'] = context.propsValue.retry_delay_second; + if (context.propsValue.notification_email) + body['notification_email'] = context.propsValue.notification_email; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/monitors/create', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/delete-monitor.ts b/packages/pieces/community/pubrio/src/lib/actions/delete-monitor.ts new file mode 100644 index 00000000000..b0e1ffe61b9 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/delete-monitor.ts @@ -0,0 +1,28 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const deleteMonitor = createAction({ + auth: pubrioAuth, + name: 'delete_monitor', + displayName: 'Delete Monitor', + description: 'Delete a monitor by ID', + props: { + monitor_id: Property.ShortText({ + displayName: 'Monitor ID', + required: true, + }), + }, + async run(context) { + const body: Record = { + monitor_id: context.propsValue.monitor_id, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/monitors/delete', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/duplicate-monitor.ts b/packages/pieces/community/pubrio/src/lib/actions/duplicate-monitor.ts new file mode 100644 index 00000000000..2234915d86f --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/duplicate-monitor.ts @@ -0,0 +1,34 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const duplicateMonitor = createAction({ + auth: pubrioAuth, + name: 'duplicate_monitor', + displayName: 'Duplicate Monitor', + description: 'Duplicate an existing monitor', + props: { + monitor_id: Property.ShortText({ + displayName: 'Monitor ID', + required: true, + }), + name: Property.ShortText({ + displayName: 'New Name', + required: false, + description: 'Name for the duplicated monitor', + }), + }, + async run(context) { + const body: Record = { + monitor_id: context.propsValue.monitor_id, + }; + if (context.propsValue.name) body['name'] = context.propsValue.name; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/monitors/duplicate', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/enrich-company.ts b/packages/pieces/community/pubrio/src/lib/actions/enrich-company.ts new file mode 100644 index 00000000000..d5f55867da4 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/enrich-company.ts @@ -0,0 +1,55 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const enrichCompany = createAction({ + auth: pubrioAuth, + name: 'enrich_company', + displayName: 'Enrich Company', + description: + 'Get enriched company data with full firmographic details (uses credits)', + props: { + lookup_type: Property.StaticDropdown({ + displayName: 'Lookup Type', + required: true, + options: { + options: [ + { label: 'Domain', value: 'domain' }, + { label: 'LinkedIn URL', value: 'linkedin_url' }, + { label: 'Domain Search ID', value: 'domain_search_id' }, + { label: 'Domain ID', value: 'domain_id' }, + ], + }, + }), + value: Property.ShortText({ + displayName: 'Value', + required: true, + description: + 'Domain, LinkedIn URL, Domain Search ID, or Domain ID (integer)', + }), + }, + async run(context) { + const lookupType = context.propsValue.lookup_type; + const rawValue = context.propsValue.value; + let lookupValue: string | number = rawValue; + if (lookupType === 'domain_id') { + const parsed = parseInt(rawValue, 10); + if (isNaN(parsed)) { + throw new Error( + `domain_id must be a valid integer, got: "${rawValue}"` + ); + } + lookupValue = parsed; + } + const body: Record = { + [lookupType]: lookupValue, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/companies/lookup/enrich', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/find-similar-companies.ts b/packages/pieces/community/pubrio/src/lib/actions/find-similar-companies.ts new file mode 100644 index 00000000000..a96a378d428 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/find-similar-companies.ts @@ -0,0 +1,49 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const findSimilarCompanies = createAction({ + auth: pubrioAuth, + name: 'find_similar_companies', + displayName: 'Find Similar Companies', + description: + 'Find companies similar to a given company by domain or LinkedIn URL', + props: { + lookup_type: Property.StaticDropdown({ + displayName: 'Lookup Type', + required: true, + options: { + options: [ + { label: 'Domain', value: 'domain' }, + { label: 'LinkedIn URL', value: 'linkedin_url' }, + ], + }, + }), + value: Property.ShortText({ displayName: 'Value', required: true }), + page: Property.Number({ + displayName: 'Page', + required: false, + defaultValue: 1, + }), + per_page: Property.Number({ + displayName: 'Per Page', + description: 'Max 25', + required: false, + defaultValue: 25, + }), + }, + async run(context) { + const body: Record = { + [context.propsValue.lookup_type]: context.propsValue.value, + page: context.propsValue.page ?? 1, + per_page: context.propsValue.per_page ?? 25, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/companies/lookalikes/search', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/get-monitor.ts b/packages/pieces/community/pubrio/src/lib/actions/get-monitor.ts new file mode 100644 index 00000000000..523005c61c7 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/get-monitor.ts @@ -0,0 +1,35 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const getMonitor = createAction({ + auth: pubrioAuth, + name: 'get_monitor', + displayName: 'Get Monitor', + description: 'Look up a monitor by ID', + props: { + monitor_id: Property.ShortText({ + displayName: 'Monitor ID', + required: true, + }), + is_signature_reveal: Property.Checkbox({ + displayName: 'Include Signature Reveal', + required: false, + defaultValue: false, + }), + }, + async run(context) { + const body: Record = { + monitor_id: context.propsValue.monitor_id, + }; + if (context.propsValue.is_signature_reveal) + body['is_signature_reveal'] = context.propsValue.is_signature_reveal; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/monitors/lookup', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/linkedin-company-lookup.ts b/packages/pieces/community/pubrio/src/lib/actions/linkedin-company-lookup.ts new file mode 100644 index 00000000000..87e24472828 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/linkedin-company-lookup.ts @@ -0,0 +1,29 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const linkedinCompanyLookup = createAction({ + auth: pubrioAuth, + name: 'linkedin_company_lookup', + displayName: 'Company LinkedIn Lookup', + description: 'Real-time LinkedIn company lookup by LinkedIn URL', + props: { + linkedin_url: Property.ShortText({ + displayName: 'LinkedIn URL', + required: true, + description: 'Company LinkedIn profile URL', + }), + }, + async run(context) { + const body: Record = { + linkedin_url: context.propsValue.linkedin_url, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/companies/linkedin/lookup', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/linkedin-person-lookup.ts b/packages/pieces/community/pubrio/src/lib/actions/linkedin-person-lookup.ts new file mode 100644 index 00000000000..8480cc34dd8 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/linkedin-person-lookup.ts @@ -0,0 +1,29 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const linkedinPersonLookup = createAction({ + auth: pubrioAuth, + name: 'linkedin_person_lookup', + displayName: 'People LinkedIn Lookup', + description: 'Real-time LinkedIn person lookup by LinkedIn URL', + props: { + people_linkedin_url: Property.ShortText({ + displayName: 'LinkedIn URL', + required: true, + description: 'Person LinkedIn profile URL', + }), + }, + async run(context) { + const body: Record = { + people_linkedin_url: context.propsValue.people_linkedin_url, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/people/linkedin/lookup', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/list-monitors.ts b/packages/pieces/community/pubrio/src/lib/actions/list-monitors.ts new file mode 100644 index 00000000000..1e60d4c78cc --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/list-monitors.ts @@ -0,0 +1,55 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const listMonitors = createAction({ + auth: pubrioAuth, + name: 'list_monitors', + displayName: 'List Monitors', + description: 'List all monitors with pagination', + props: { + page: Property.Number({ + displayName: 'Page', + required: false, + defaultValue: 1, + }), + per_page: Property.Number({ + displayName: 'Per Page', + required: false, + defaultValue: 25, + }), + order_by: Property.StaticDropdown({ + displayName: 'Order By', + required: false, + options: { + options: [ + { label: 'Created At', value: 'created_at' }, + { label: 'Updated At', value: 'updated_at' }, + { label: 'Name', value: 'name' }, + ], + }, + }), + is_ascending_order: Property.Checkbox({ + displayName: 'Ascending Order', + required: false, + defaultValue: false, + }), + }, + async run(context) { + const body: Record = { + page: context.propsValue.page ?? 1, + per_page: context.propsValue.per_page ?? 25, + }; + if (context.propsValue.order_by) + body['order_by'] = context.propsValue.order_by; + if (context.propsValue.is_ascending_order) + body['is_ascending_order'] = context.propsValue.is_ascending_order; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/monitors', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/lookup-advertisement.ts b/packages/pieces/community/pubrio/src/lib/actions/lookup-advertisement.ts new file mode 100644 index 00000000000..aaf46570eb9 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/lookup-advertisement.ts @@ -0,0 +1,30 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const lookupAdvertisement = createAction({ + auth: pubrioAuth, + name: 'lookup_advertisement', + displayName: 'Lookup Advertisement', + description: + 'Look up detailed advertisement information by advertisement search ID', + props: { + advertisement_search_id: Property.ShortText({ + displayName: 'Advertisement Search ID', + required: true, + description: 'The advertisement search ID to look up', + }), + }, + async run(context) { + const body: Record = { + advertisement_search_id: context.propsValue.advertisement_search_id, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/companies/advertisements/lookup', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/lookup-company.ts b/packages/pieces/community/pubrio/src/lib/actions/lookup-company.ts new file mode 100644 index 00000000000..f290b667bca --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/lookup-company.ts @@ -0,0 +1,54 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const lookupCompany = createAction({ + auth: pubrioAuth, + name: 'lookup_company', + displayName: 'Lookup Company', + description: 'Look up detailed company information by domain or LinkedIn URL', + props: { + lookup_type: Property.StaticDropdown({ + displayName: 'Lookup Type', + required: true, + options: { + options: [ + { label: 'Domain', value: 'domain' }, + { label: 'LinkedIn URL', value: 'linkedin_url' }, + { label: 'Domain Search ID', value: 'domain_search_id' }, + { label: 'Domain ID', value: 'domain_id' }, + ], + }, + }), + value: Property.ShortText({ + displayName: 'Value', + required: true, + description: + 'Domain, LinkedIn URL, Domain Search ID, or Domain ID (integer)', + }), + }, + async run(context) { + const lookupType = context.propsValue.lookup_type; + const rawValue = context.propsValue.value; + let lookupValue: string | number = rawValue; + if (lookupType === 'domain_id') { + const parsed = parseInt(rawValue, 10); + if (isNaN(parsed)) { + throw new Error( + `domain_id must be a valid integer, got: "${rawValue}"` + ); + } + lookupValue = parsed; + } + const body: Record = { + [lookupType]: lookupValue, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/companies/lookup', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/lookup-job.ts b/packages/pieces/community/pubrio/src/lib/actions/lookup-job.ts new file mode 100644 index 00000000000..469494ce065 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/lookup-job.ts @@ -0,0 +1,29 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const lookupJob = createAction({ + auth: pubrioAuth, + name: 'lookup_job', + displayName: 'Lookup Job', + description: 'Look up detailed job information by job search ID', + props: { + job_search_id: Property.ShortText({ + displayName: 'Job Search ID', + required: true, + description: 'The job search ID to look up', + }), + }, + async run(context) { + const body: Record = { + job_search_id: context.propsValue.job_search_id, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/companies/jobs/lookup', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/lookup-lookalike.ts b/packages/pieces/community/pubrio/src/lib/actions/lookup-lookalike.ts new file mode 100644 index 00000000000..1ec58737071 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/lookup-lookalike.ts @@ -0,0 +1,41 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const lookupLookalike = createAction({ + auth: pubrioAuth, + name: 'lookup_lookalike', + displayName: 'Lookup Lookalike Company', + description: + 'Look up a similar company result by domain, LinkedIn URL, or domain search ID', + props: { + lookup_type: Property.StaticDropdown({ + displayName: 'Lookup Type', + required: true, + options: { + options: [ + { label: 'Domain', value: 'domain' }, + { label: 'LinkedIn URL', value: 'linkedin_url' }, + { label: 'Domain Search ID', value: 'domain_search_id' }, + ], + }, + }), + value: Property.ShortText({ + displayName: 'Value', + required: true, + description: 'Domain, LinkedIn URL, or Domain Search ID', + }), + }, + async run(context) { + const body: Record = { + [context.propsValue.lookup_type]: context.propsValue.value, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/companies/lookalikes/lookup', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/lookup-news.ts b/packages/pieces/community/pubrio/src/lib/actions/lookup-news.ts new file mode 100644 index 00000000000..fa118f5c4bb --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/lookup-news.ts @@ -0,0 +1,29 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const lookupNews = createAction({ + auth: pubrioAuth, + name: 'lookup_news', + displayName: 'Lookup News', + description: 'Look up detailed news information by news search ID', + props: { + news_search_id: Property.ShortText({ + displayName: 'News Search ID', + required: true, + description: 'The news search ID to look up', + }), + }, + async run(context) { + const body: Record = { + news_search_id: context.propsValue.news_search_id, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/companies/news/lookup', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/lookup-person.ts b/packages/pieces/community/pubrio/src/lib/actions/lookup-person.ts new file mode 100644 index 00000000000..4094352eaed --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/lookup-person.ts @@ -0,0 +1,36 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const lookupPerson = createAction({ + auth: pubrioAuth, + name: 'lookup_person', + displayName: 'Lookup Person', + description: + "Look up a person's professional profile by LinkedIn URL or Pubrio ID", + props: { + lookup_type: Property.StaticDropdown({ + displayName: 'Lookup Type', + required: true, + options: { + options: [ + { label: 'LinkedIn URL', value: 'linkedin_url' }, + { label: 'People Search ID', value: 'people_search_id' }, + ], + }, + }), + value: Property.ShortText({ displayName: 'Value', required: true }), + }, + async run(context) { + const body: Record = { + [context.propsValue.lookup_type]: context.propsValue.value, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/people/lookup', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/lookup-technology.ts b/packages/pieces/community/pubrio/src/lib/actions/lookup-technology.ts new file mode 100644 index 00000000000..dad2e4de1a3 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/lookup-technology.ts @@ -0,0 +1,51 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const lookupTechnology = createAction({ + auth: pubrioAuth, + name: 'lookup_technology', + displayName: 'Lookup Technology', + description: 'Look up technologies used by a company', + props: { + lookup_type: Property.StaticDropdown({ + displayName: 'Lookup Type', + required: true, + options: { + options: [ + { label: 'Domain', value: 'domain' }, + { label: 'LinkedIn URL', value: 'linkedin_url' }, + { label: 'Domain Search ID', value: 'domain_search_id' }, + { label: 'Domain ID', value: 'domain_id' }, + ], + }, + }), + value: Property.ShortText({ + displayName: 'Value', + required: true, + description: + 'Domain, LinkedIn URL, Domain Search ID, or Domain ID (integer)', + }), + }, + async run(context) { + const lookupType = context.propsValue.lookup_type; + const rawValue = context.propsValue.value; + const body: Record = {}; + if (lookupType === 'domain_id') { + const parsed = parseInt(rawValue, 10); + if (isNaN(parsed)) { + throw new Error(`domain_id must be a valid integer, got: "${rawValue}"`); + } + body[lookupType] = parsed; + } else { + body[lookupType] = rawValue; + } + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/technologies/lookup', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/query-batch-redeem.ts b/packages/pieces/community/pubrio/src/lib/actions/query-batch-redeem.ts new file mode 100644 index 00000000000..612807028d6 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/query-batch-redeem.ts @@ -0,0 +1,29 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const queryBatchRedeem = createAction({ + auth: pubrioAuth, + name: 'query_batch_redeem', + displayName: 'Query Batch Redeem', + description: 'Query the status and results of a batch redeem operation', + props: { + redeem_query_id: Property.ShortText({ + displayName: 'Redeem Query ID', + required: true, + description: 'The batch redeem query ID', + }), + }, + async run(context) { + const body: Record = { + redeem_query_id: context.propsValue.redeem_query_id, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/redeem/people/batch/query', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/reveal-contact.ts b/packages/pieces/community/pubrio/src/lib/actions/reveal-contact.ts new file mode 100644 index 00000000000..7e1c8442c16 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/reveal-contact.ts @@ -0,0 +1,56 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const revealContact = createAction({ + auth: pubrioAuth, + name: 'reveal_contact', + displayName: 'Reveal Contact', + description: 'Reveal email or phone number for a person (uses credits)', + props: { + lookup_type: Property.StaticDropdown({ + displayName: 'Lookup Type', + required: true, + options: { + options: [ + { label: 'People Search ID', value: 'people_search_id' }, + { label: 'LinkedIn URL', value: 'linkedin_url' }, + ], + }, + }), + value: Property.ShortText({ + displayName: 'Value', + required: true, + description: 'People Search ID or LinkedIn URL', + }), + people_contact_types: Property.StaticMultiSelectDropdown({ + displayName: 'Contact Types', + required: false, + options: { + options: [ + { label: 'Work Email', value: 'email-work' }, + { label: 'Personal Email', value: 'email-personal' }, + { label: 'Phone', value: 'phone' }, + ], + }, + }), + }, + async run(context) { + const body: Record = { + [context.propsValue.lookup_type]: context.propsValue.value, + }; + if ( + context.propsValue.people_contact_types && + context.propsValue.people_contact_types.length > 0 + ) { + body['people_contact_types'] = context.propsValue.people_contact_types; + } + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/redeem/people', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/reveal-monitor-signature.ts b/packages/pieces/community/pubrio/src/lib/actions/reveal-monitor-signature.ts new file mode 100644 index 00000000000..462bed5a750 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/reveal-monitor-signature.ts @@ -0,0 +1,28 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const revealMonitorSignature = createAction({ + auth: pubrioAuth, + name: 'reveal_monitor_signature', + displayName: 'Reveal Monitor Signature', + description: 'Reveal the signature for a monitor (uses credits)', + props: { + monitor_id: Property.ShortText({ + displayName: 'Monitor ID', + required: true, + }), + }, + async run(context) { + const body: Record = { + monitor_id: context.propsValue.monitor_id, + }; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/monitors/signature/reveal', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/search-ads.ts b/packages/pieces/community/pubrio/src/lib/actions/search-ads.ts new file mode 100644 index 00000000000..804530f2ab5 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/search-ads.ts @@ -0,0 +1,101 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const searchAds = createAction({ + auth: pubrioAuth, + name: 'search_ads', + displayName: 'Search Advertisements', + description: 'Search company advertisements and ad campaigns', + props: { + search_terms: Property.Array({ + displayName: 'Search Terms', + required: false, + }), + target_locations: Property.Array({ + displayName: 'Target Locations', + required: false, + }), + exclude_target_locations: Property.Array({ + displayName: 'Exclude Target Locations', + description: 'Locations to exclude', + required: false, + }), + headlines: Property.Array({ + displayName: 'Headlines', + description: 'Headline keywords', + required: false, + }), + start_dates: Property.Array({ + displayName: 'Start Dates', + description: 'Start date filters', + required: false, + }), + end_dates: Property.Array({ + displayName: 'End Dates', + description: 'End date filters', + required: false, + }), + company_locations: Property.Array({ + displayName: 'Company Locations', + required: false, + }), + domains: Property.Array({ + displayName: 'Company Domains', + required: false, + }), + linkedin_urls: Property.Array({ + displayName: 'LinkedIn URLs', + required: false, + }), + companies: Property.Array({ + displayName: 'Companies', + description: 'Company domain_search_id UUIDs', + required: false, + }), + page: Property.Number({ + displayName: 'Page', + required: false, + defaultValue: 1, + }), + per_page: Property.Number({ + displayName: 'Per Page', + description: 'Max 25', + required: false, + defaultValue: 25, + }), + }, + async run(context) { + const body: any = { + page: context.propsValue.page ?? 1, + per_page: context.propsValue.per_page ?? 25, + }; + if (context.propsValue.search_terms) + body.search_terms = context.propsValue.search_terms; + if (context.propsValue.target_locations) + body.target_locations = context.propsValue.target_locations; + if (context.propsValue.exclude_target_locations) + body.exclude_target_locations = context.propsValue.exclude_target_locations; + if (context.propsValue.headlines) + body.headlines = context.propsValue.headlines; + if (context.propsValue.start_dates) + body.start_dates = context.propsValue.start_dates; + if (context.propsValue.end_dates) + body.end_dates = context.propsValue.end_dates; + if (context.propsValue.company_locations) + body.company_locations = context.propsValue.company_locations; + if (context.propsValue.domains) + body.domains = context.propsValue.domains; + if (context.propsValue.linkedin_urls) + body.linkedin_urls = context.propsValue.linkedin_urls; + if (context.propsValue.companies) + body.companies = context.propsValue.companies; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/companies/advertisements/search', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/search-companies.ts b/packages/pieces/community/pubrio/src/lib/actions/search-companies.ts new file mode 100644 index 00000000000..5d35e510d15 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/search-companies.ts @@ -0,0 +1,296 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const searchCompanies = createAction({ + auth: pubrioAuth, + name: 'search_companies', + displayName: 'Search Companies', + description: + 'Search B2B companies by name, domain, location, industry, technology, or headcount', + props: { + company_name: Property.ShortText({ + displayName: 'Company Name', + required: false, + }), + domains: Property.Array({ + displayName: 'Domains', + required: false, + }), + linkedin_urls: Property.Array({ + displayName: 'LinkedIn URLs', + description: 'LinkedIn company URLs', + required: false, + }), + locations: Property.Array({ + displayName: 'Locations', + description: 'Location codes', + required: false, + }), + exclude_locations: Property.Array({ + displayName: 'Exclude Locations', + description: 'ISO country codes to exclude', + required: false, + }), + places: Property.Array({ + displayName: 'Places', + description: 'Place names', + required: false, + }), + exclude_places: Property.Array({ + displayName: 'Exclude Places', + description: 'Place names to exclude', + required: false, + }), + keywords: Property.Array({ + displayName: 'Keywords', + required: false, + }), + verticals: Property.Array({ + displayName: 'Verticals', + description: 'Industry verticals', + required: false, + }), + vertical_categories: Property.Array({ + displayName: 'Vertical Categories', + description: 'Vertical category IDs', + required: false, + }), + vertical_sub_categories: Property.Array({ + displayName: 'Vertical Sub-Categories', + description: 'Vertical sub-category IDs', + required: false, + }), + technologies: Property.Array({ + displayName: 'Technologies', + required: false, + }), + categories: Property.Array({ + displayName: 'Technology Categories', + description: 'Technology category IDs', + required: false, + }), + companies: Property.Array({ + displayName: 'Companies', + description: 'Company domain_search_id UUIDs', + required: false, + }), + employees_min: Property.Number({ + displayName: 'Min Employees', + required: false, + }), + employees_max: Property.Number({ + displayName: 'Max Employees', + required: false, + }), + revenue_min: Property.Number({ + displayName: 'Min Revenue', + required: false, + }), + revenue_max: Property.Number({ + displayName: 'Max Revenue', + required: false, + }), + founded_year_start: Property.Number({ + displayName: 'Founded Year Start', + description: 'Minimum founding year', + required: false, + }), + founded_year_end: Property.Number({ + displayName: 'Founded Year End', + description: 'Maximum founding year', + required: false, + }), + is_enable_similarity_search: Property.Checkbox({ + displayName: 'Enable Similarity Search', + required: false, + defaultValue: false, + }), + similarity_score: Property.Number({ + displayName: 'Similarity Score', + description: '0-1', + required: false, + }), + exclude_fields: Property.Array({ + displayName: 'Exclude Fields', + description: 'Field names to exclude', + required: false, + }), + job_titles: Property.Array({ + displayName: 'Job Titles', + required: false, + }), + job_locations: Property.Array({ + displayName: 'Job Locations', + description: 'Country codes for job locations', + required: false, + }), + job_exclude_locations: Property.Array({ + displayName: 'Job Exclude Locations', + description: 'Country codes to exclude', + required: false, + }), + job_posted_dates: Property.Array({ + displayName: 'Job Posted Dates', + description: 'Date range YYYY-MM-DD', + required: false, + }), + news_categories: Property.Array({ + displayName: 'News Categories', + description: 'News category slugs', + required: false, + }), + news_published_dates: Property.Array({ + displayName: 'News Published Dates', + description: 'Date range YYYY-MM-DD', + required: false, + }), + news_galleries: Property.Array({ + displayName: 'News Galleries', + description: 'News gallery slugs', + required: false, + }), + news_gallery_ids: Property.Array({ + displayName: 'News Gallery IDs', + description: 'News gallery UUIDs', + required: false, + }), + advertisement_search_terms: Property.Array({ + displayName: 'Advertisement Search Terms', + description: 'Keywords', + required: false, + }), + advertisement_target_locations: Property.Array({ + displayName: 'Advertisement Target Locations', + description: 'Country codes', + required: false, + }), + advertisement_exclude_target_locations: Property.Array({ + displayName: 'Advertisement Exclude Target Locations', + description: 'Country codes', + required: false, + }), + advertisement_start_dates: Property.Array({ + displayName: 'Advertisement Start Dates', + description: 'Date range YYYY-MM-DD', + required: false, + }), + advertisement_end_dates: Property.Array({ + displayName: 'Advertisement End Dates', + description: 'Date range YYYY-MM-DD', + required: false, + }), + page: Property.Number({ + displayName: 'Page', + required: false, + defaultValue: 1, + }), + per_page: Property.Number({ + displayName: 'Per Page', + description: 'Max 25', + required: false, + defaultValue: 25, + }), + }, + async run(context) { + const body: Record = { + page: context.propsValue.page ?? 1, + per_page: context.propsValue.per_page ?? 25, + }; + if (context.propsValue.company_name) + body['company_name'] = context.propsValue.company_name; + if (context.propsValue.domains) + body['domains'] = context.propsValue.domains; + if (context.propsValue.linkedin_urls) + body['linkedin_urls'] = context.propsValue.linkedin_urls; + if (context.propsValue.locations) + body['locations'] = context.propsValue.locations; + if (context.propsValue.exclude_locations) + body['exclude_locations'] = context.propsValue.exclude_locations; + if (context.propsValue.places) + body['places'] = context.propsValue.places; + if (context.propsValue.exclude_places) + body['exclude_places'] = context.propsValue.exclude_places; + if (context.propsValue.keywords) + body['keywords'] = context.propsValue.keywords; + if (context.propsValue.verticals) + body['verticals'] = context.propsValue.verticals; + if (context.propsValue.vertical_categories) + body['vertical_categories'] = context.propsValue.vertical_categories; + if (context.propsValue.vertical_sub_categories) + body['vertical_sub_categories'] = context.propsValue.vertical_sub_categories; + if (context.propsValue.technologies) + body['technologies'] = context.propsValue.technologies; + if (context.propsValue.categories) + body['categories'] = context.propsValue.categories; + if (context.propsValue.companies) + body['companies'] = context.propsValue.companies; + if ( + context.propsValue.employees_min != null || + context.propsValue.employees_max != null + ) { + body['employees'] = [ + context.propsValue.employees_min ?? 1, + context.propsValue.employees_max ?? 1000000, + ]; + } + if ( + context.propsValue.revenue_min != null || + context.propsValue.revenue_max != null + ) { + body['revenues'] = [ + context.propsValue.revenue_min ?? 0, + context.propsValue.revenue_max ?? 999999999, + ]; + } + if ( + context.propsValue.founded_year_start != null || + context.propsValue.founded_year_end != null + ) { + body['founded_dates'] = [ + context.propsValue.founded_year_start ?? 1800, + context.propsValue.founded_year_end ?? new Date().getFullYear(), + ]; + } + if (context.propsValue.is_enable_similarity_search) + body['is_enable_similarity_search'] = + context.propsValue.is_enable_similarity_search; + if (context.propsValue.similarity_score != null) + body['similarity_score'] = context.propsValue.similarity_score; + if (context.propsValue.exclude_fields) + body['exclude_fields'] = context.propsValue.exclude_fields; + if (context.propsValue.job_titles) + body['job_titles'] = context.propsValue.job_titles; + if (context.propsValue.job_locations) + body['job_locations'] = context.propsValue.job_locations; + if (context.propsValue.job_exclude_locations) + body['job_exclude_locations'] = context.propsValue.job_exclude_locations; + if (context.propsValue.job_posted_dates) + body['job_posted_dates'] = context.propsValue.job_posted_dates; + if (context.propsValue.news_categories) + body['news_categories'] = context.propsValue.news_categories; + if (context.propsValue.news_published_dates) + body['news_published_dates'] = context.propsValue.news_published_dates; + if (context.propsValue.news_galleries) + body['news_galleries'] = context.propsValue.news_galleries; + if (context.propsValue.news_gallery_ids) + body['news_gallery_ids'] = context.propsValue.news_gallery_ids; + if (context.propsValue.advertisement_search_terms) + body['advertisement_search_terms'] = context.propsValue.advertisement_search_terms; + if (context.propsValue.advertisement_target_locations) + body['advertisement_target_locations'] = context.propsValue.advertisement_target_locations; + if (context.propsValue.advertisement_exclude_target_locations) + body['advertisement_exclude_target_locations'] = context.propsValue.advertisement_exclude_target_locations; + if (context.propsValue.advertisement_start_dates) + body['advertisement_start_dates'] = context.propsValue.advertisement_start_dates; + if (context.propsValue.advertisement_end_dates) + body['advertisement_end_dates'] = context.propsValue.advertisement_end_dates; + return await pubrioRequest( + context.auth.secret_text , + HttpMethod.POST, + '/companies/search', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/search-jobs.ts b/packages/pieces/community/pubrio/src/lib/actions/search-jobs.ts new file mode 100644 index 00000000000..2f2a437f607 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/search-jobs.ts @@ -0,0 +1,99 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const searchJobs = createAction({ + auth: pubrioAuth, + name: 'search_jobs', + displayName: 'Search Jobs', + description: 'Search job postings across companies', + props: { + search_term: Property.ShortText({ + displayName: 'Search Term', + required: false, + }), + search_terms: Property.Array({ + displayName: 'Search Terms', + required: false, + }), + titles: Property.Array({ + displayName: 'Job Titles', + required: false, + }), + posted_dates: Property.Array({ + displayName: 'Posted Dates', + description: 'Date filters', + required: false, + }), + locations: Property.Array({ + displayName: 'Locations', + required: false, + }), + exclude_locations: Property.Array({ + displayName: 'Exclude Locations', + description: 'Locations to exclude', + required: false, + }), + company_locations: Property.Array({ + displayName: 'Company Locations', + required: false, + }), + companies: Property.Array({ + displayName: 'Companies', + description: 'Company UUIDs', + required: false, + }), + domains: Property.Array({ + displayName: 'Company Domains', + required: false, + }), + linkedin_urls: Property.Array({ + displayName: 'LinkedIn URLs', + required: false, + }), + page: Property.Number({ + displayName: 'Page', + required: false, + defaultValue: 1, + }), + per_page: Property.Number({ + displayName: 'Per Page', + description: 'Max 25', + required: false, + defaultValue: 25, + }), + }, + async run(context) { + const body: Record = { + page: context.propsValue.page ?? 1, + per_page: context.propsValue.per_page ?? 25, + }; + if (context.propsValue.search_term) + body['search_term'] = context.propsValue.search_term; + if (context.propsValue.search_terms) + body['search_terms'] = context.propsValue.search_terms; + if (context.propsValue.titles) + body['titles'] = context.propsValue.titles; + if (context.propsValue.posted_dates) + body['posted_dates'] = context.propsValue.posted_dates; + if (context.propsValue.locations) + body['locations'] = context.propsValue.locations; + if (context.propsValue.exclude_locations) + body['exclude_locations'] = context.propsValue.exclude_locations; + if (context.propsValue.company_locations) + body['company_locations'] = context.propsValue.company_locations; + if (context.propsValue.companies) + body['companies'] = context.propsValue.companies; + if (context.propsValue.domains) + body['domains'] = context.propsValue.domains; + if (context.propsValue.linkedin_urls) + body['linkedin_urls'] = context.propsValue.linkedin_urls; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/companies/jobs/search', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/search-news.ts b/packages/pieces/community/pubrio/src/lib/actions/search-news.ts new file mode 100644 index 00000000000..2387bcdbec9 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/search-news.ts @@ -0,0 +1,108 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const searchNews = createAction({ + auth: pubrioAuth, + name: 'search_news', + displayName: 'Search News', + description: 'Search company news and press releases', + props: { + search_term: Property.ShortText({ + displayName: 'Search Term', + required: false, + }), + search_terms: Property.Array({ + displayName: 'Search Terms', + description: 'Keywords', + required: false, + }), + domains: Property.Array({ + displayName: 'Company Domains', + required: false, + }), + companies: Property.Array({ + displayName: 'Companies', + description: 'Company domain_search_id UUIDs', + required: false, + }), + categories: Property.Array({ + displayName: 'News Categories', + required: false, + }), + published_dates: Property.Array({ + displayName: 'Published Dates', + description: 'Date filters', + required: false, + }), + locations: Property.Array({ + displayName: 'Locations', + required: false, + }), + company_locations: Property.Array({ + displayName: 'Company Locations', + required: false, + }), + news_gallery_ids: Property.Array({ + displayName: 'News Gallery IDs', + description: 'Gallery IDs', + required: false, + }), + news_galleries: Property.Array({ + displayName: 'News Galleries', + description: 'Gallery names', + required: false, + }), + news_languages: Property.Array({ + displayName: 'News Languages', + description: 'Language codes', + required: false, + }), + page: Property.Number({ + displayName: 'Page', + required: false, + defaultValue: 1, + }), + per_page: Property.Number({ + displayName: 'Per Page', + description: 'Max 25', + required: false, + defaultValue: 25, + }), + }, + async run(context) { + const body: Record = { + page: context.propsValue.page ?? 1, + per_page: context.propsValue.per_page ?? 25, + }; + if (context.propsValue.search_term) + body['search_term'] = context.propsValue.search_term; + if (context.propsValue.search_terms) + body['search_terms'] = context.propsValue.search_terms; + if (context.propsValue.domains) + body['domains'] = context.propsValue.domains; + if (context.propsValue.companies) + body['companies'] = context.propsValue.companies; + if (context.propsValue.categories) + body['categories'] = context.propsValue.categories; + if (context.propsValue.published_dates) + body['published_dates'] = context.propsValue.published_dates; + if (context.propsValue.locations) + body['locations'] = context.propsValue.locations; + if (context.propsValue.company_locations) + body['company_locations'] = context.propsValue.company_locations; + if (context.propsValue.news_gallery_ids) + body['news_gallery_ids'] = context.propsValue.news_gallery_ids; + if (context.propsValue.news_galleries) + body['news_galleries'] = context.propsValue.news_galleries; + if (context.propsValue.news_languages) + body['news_languages'] = context.propsValue.news_languages; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/companies/news/search', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/search-people.ts b/packages/pieces/community/pubrio/src/lib/actions/search-people.ts new file mode 100644 index 00000000000..7eb50455e32 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/search-people.ts @@ -0,0 +1,125 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const searchPeople = createAction({ + auth: pubrioAuth, + name: 'search_people', + displayName: 'Search People', + description: + 'Search business professionals by name, title, department, seniority, or company', + props: { + search_term: Property.ShortText({ + displayName: 'Search Term', + required: false, + }), + people_name: Property.ShortText({ + displayName: 'Person Name', + required: false, + }), + people_titles: Property.Array({ + displayName: 'Job Titles', + description: '', + required: false, + }), + peoples: Property.Array({ + displayName: 'People IDs', + description: 'People UUIDs', + required: false, + }), + management_levels: Property.Array({ + displayName: 'Management Levels', + required: false, + }), + departments: Property.Array({ + displayName: 'Departments', + required: false, + }), + department_functions: Property.Array({ + displayName: 'Department Functions', + required: false, + }), + employees: Property.Array({ + displayName: 'Employee Ranges', + description: 'Employee count ranges', + required: false, + }), + people_locations: Property.Array({ + displayName: 'People Locations', + required: false, + }), + company_locations: Property.Array({ + displayName: 'Company Locations', + required: false, + }), + company_linkedin_urls: Property.Array({ + displayName: 'Company LinkedIn URLs', + required: false, + }), + linkedin_urls: Property.Array({ + displayName: 'People LinkedIn URLs', + required: false, + }), + companies: Property.Array({ + displayName: 'Companies', + description: 'Company UUIDs', + required: false, + }), + domains: Property.Array({ + displayName: 'Company Domains', + required: false, + }), + page: Property.Number({ + displayName: 'Page', + required: false, + defaultValue: 1, + }), + per_page: Property.Number({ + displayName: 'Per Page', + description: 'Max 25', + required: false, + defaultValue: 25, + }), + }, + async run(context) { + const body: Record = { + page: context.propsValue.page ?? 1, + per_page: context.propsValue.per_page ?? 25, + }; + if (context.propsValue.search_term) + body['search_term'] = context.propsValue.search_term; + if (context.propsValue.people_name) + body['people_name'] = context.propsValue.people_name; + if (context.propsValue.people_titles) + body['people_titles'] = context.propsValue.people_titles; + if (context.propsValue.peoples) + body['peoples'] = context.propsValue.peoples; + if (context.propsValue.management_levels) + body['management_levels'] = context.propsValue.management_levels; + if (context.propsValue.departments) + body['departments'] = context.propsValue.departments; + if (context.propsValue.department_functions) + body['department_functions'] = context.propsValue.department_functions; + if (context.propsValue.employees) + body['employees'] = context.propsValue.employees; + if (context.propsValue.people_locations) + body['people_locations'] = context.propsValue.people_locations; + if (context.propsValue.company_locations) + body['company_locations'] = context.propsValue.company_locations; + if (context.propsValue.company_linkedin_urls) + body['company_linkedin_urls'] = context.propsValue.company_linkedin_urls; + if (context.propsValue.linkedin_urls) + body['linkedin_urls'] = context.propsValue.linkedin_urls; + if (context.propsValue.companies) + body['companies'] = context.propsValue.companies; + if (context.propsValue.domains) + body['domains'] = context.propsValue.domains; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/people/search', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/test-run-monitor.ts b/packages/pieces/community/pubrio/src/lib/actions/test-run-monitor.ts new file mode 100644 index 00000000000..9f538c1f69d --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/test-run-monitor.ts @@ -0,0 +1,35 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const testRunMonitor = createAction({ + auth: pubrioAuth, + name: 'test_run_monitor', + displayName: 'Test Run Monitor', + description: 'Perform a test run of a monitor', + props: { + monitor_id: Property.ShortText({ + displayName: 'Monitor ID', + required: true, + }), + tried_at: Property.ShortText({ + displayName: 'Tried At', + required: false, + description: 'Timestamp for the test run (ISO 8601)', + }), + }, + async run(context) { + const body: Record = { + monitor_id: context.propsValue.monitor_id, + }; + if (context.propsValue.tried_at) + body['tried_at'] = context.propsValue.tried_at; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/monitors/process/try', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/actions/update-monitor.ts b/packages/pieces/community/pubrio/src/lib/actions/update-monitor.ts new file mode 100644 index 00000000000..aac9b9dce63 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/actions/update-monitor.ts @@ -0,0 +1,216 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { pubrioAuth } from '../../index'; +import { pubrioRequest } from '../common'; + +export const updateMonitor = createAction({ + auth: pubrioAuth, + name: 'update_monitor', + displayName: 'Update Monitor', + description: 'Update an existing signal monitor', + props: { + monitor_id: Property.ShortText({ + displayName: 'Monitor ID', + required: true, + }), + name: Property.ShortText({ displayName: 'Name', required: false }), + detection_mode: Property.StaticDropdown({ + displayName: 'Detection Mode', + required: false, + options: { + options: [ + { label: 'Company First', value: 'company_first' }, + { label: 'Signal First', value: 'signal_first' }, + ], + }, + }), + signal_types: Property.StaticMultiSelectDropdown({ + displayName: 'Signal Types', + required: false, + options: { + options: [ + { label: 'Jobs', value: 'jobs' }, + { label: 'News', value: 'news' }, + { label: 'Advertisements', value: 'advertisements' }, + ], + }, + }), + destination_type: Property.StaticDropdown({ + displayName: 'Destination Type', + required: false, + options: { + options: [ + { label: 'Webhook', value: 'webhook' }, + { label: 'Email', value: 'email' }, + { label: 'Sequences', value: 'sequences' }, + ], + }, + }), + webhook_url: Property.ShortText({ + displayName: 'Webhook URL', + required: false, + }), + email: Property.ShortText({ displayName: 'Email', required: false }), + sequence_identifier: Property.ShortText({ + displayName: 'Sequence Identifier', + required: false, + }), + record_type: Property.ShortText({ + displayName: 'Record Type', + required: false, + description: 'Required when destination type is sequences', + }), + description: Property.LongText({ + displayName: 'Description', + required: false, + }), + frequency_minute: Property.Number({ + displayName: 'Frequency (minutes)', + required: false, + }), + max_daily_trigger: Property.Number({ + displayName: 'Max Daily Triggers', + required: false, + }), + max_records_per_trigger: Property.Number({ + displayName: 'Max Records Per Trigger', + required: false, + }), + companies: Property.Array({ + displayName: 'Companies', + description: 'Company names', + required: false, + }), + domains: Property.Array({ + displayName: 'Domains', + required: false, + }), + linkedin_urls: Property.Array({ + displayName: 'LinkedIn URLs', + required: false, + }), + company_filters: Property.LongText({ + displayName: 'Company Filters', + description: 'Advanced company filters as JSON string', + required: false, + }), + signal_filters: Property.LongText({ + displayName: 'Signal Filters', + description: 'Signal-specific filters as JSON array string', + required: false, + }), + people_enrichment_configs: Property.LongText({ + displayName: 'People Enrichment Configs', + description: 'People enrichment configs as JSON array string', + required: false, + }), + is_company_enrichment: Property.Checkbox({ + displayName: 'Company Enrichment', + required: false, + defaultValue: false, + }), + is_people_enrichment: Property.Checkbox({ + displayName: 'People Enrichment', + required: false, + defaultValue: false, + }), + is_active: Property.Checkbox({ + displayName: 'Is Active', + required: false, + defaultValue: false, + }), + is_paused: Property.Checkbox({ + displayName: 'Is Paused', + required: false, + defaultValue: false, + }), + max_failure_trigger: Property.Number({ + displayName: 'Max Failure Trigger', + required: false, + }), + max_retry_per_trigger: Property.Number({ + displayName: 'Max Retry Per Trigger', + required: false, + }), + retry_delay_second: Property.Number({ + displayName: 'Retry Delay (seconds)', + required: false, + }), + notification_email: Property.ShortText({ + displayName: 'Notification Email', + required: false, + }), + }, + async run(context) { + const body: Record = { + monitor_id: context.propsValue.monitor_id, + }; + if (context.propsValue.name) body['name'] = context.propsValue.name; + if (context.propsValue.detection_mode) + body['detection_mode'] = context.propsValue.detection_mode; + if ( + context.propsValue.signal_types && + context.propsValue.signal_types.length > 0 + ) + body['signal_types'] = context.propsValue.signal_types; + if (context.propsValue.destination_type) + body['destination_type'] = context.propsValue.destination_type; + const destConfig: Record = {}; + if (context.propsValue.webhook_url) + destConfig['webhook_url'] = context.propsValue.webhook_url; + if (context.propsValue.email) + destConfig['email'] = context.propsValue.email; + if (context.propsValue.sequence_identifier) + destConfig['sequence_identifier'] = + context.propsValue.sequence_identifier; + if (context.propsValue.record_type) + destConfig['record_type'] = context.propsValue.record_type; + if (Object.keys(destConfig).length > 0) + body['destination_config'] = destConfig; + if (context.propsValue.description) + body['description'] = context.propsValue.description; + if (context.propsValue.frequency_minute != null) + body['frequency_minute'] = context.propsValue.frequency_minute; + if (context.propsValue.max_daily_trigger != null) + body['max_daily_trigger'] = context.propsValue.max_daily_trigger; + if (context.propsValue.max_records_per_trigger != null) + body['max_records_per_trigger'] = + context.propsValue.max_records_per_trigger; + if (context.propsValue.companies) + body['companies'] = context.propsValue.companies; + if (context.propsValue.domains) + body['domains'] = context.propsValue.domains; + if (context.propsValue.linkedin_urls) + body['linkedin_urls'] = context.propsValue.linkedin_urls; + if (context.propsValue.company_filters) + body['company_filters'] = JSON.parse(context.propsValue.company_filters); + if (context.propsValue.signal_filters) + body['signal_filters'] = JSON.parse(context.propsValue.signal_filters); + if (context.propsValue.people_enrichment_configs) + body['people_enrichment_configs'] = JSON.parse( + context.propsValue.people_enrichment_configs + ); + if (context.propsValue.is_company_enrichment) + body['is_company_enrichment'] = context.propsValue.is_company_enrichment; + if (context.propsValue.is_people_enrichment) + body['is_people_enrichment'] = context.propsValue.is_people_enrichment; + if (context.propsValue.is_active) + body['is_active'] = context.propsValue.is_active; + if (context.propsValue.is_paused) + body['is_paused'] = context.propsValue.is_paused; + if (context.propsValue.max_failure_trigger != null) + body['max_failure_trigger'] = context.propsValue.max_failure_trigger; + if (context.propsValue.max_retry_per_trigger != null) + body['max_retry_per_trigger'] = context.propsValue.max_retry_per_trigger; + if (context.propsValue.retry_delay_second != null) + body['retry_delay_second'] = context.propsValue.retry_delay_second; + if (context.propsValue.notification_email) + body['notification_email'] = context.propsValue.notification_email; + return await pubrioRequest( + context.auth.secret_text, + HttpMethod.POST, + '/monitors/update', + body + ); + }, +}); diff --git a/packages/pieces/community/pubrio/src/lib/common.ts b/packages/pieces/community/pubrio/src/lib/common.ts new file mode 100644 index 00000000000..742e110a0f4 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/common.ts @@ -0,0 +1,25 @@ +import { HttpMethod, httpClient } from '@activepieces/pieces-common'; + +const BASE_URL = 'https://api.pubrio.com'; + +export async function pubrioRequest( + apiKey: string, + method: HttpMethod, + endpoint: string, + body?: any +): Promise { + const key = String(apiKey); + const response = await httpClient.sendRequest({ + method, + url: `${BASE_URL}${endpoint}`, + headers: { + 'pubrio-api-key': key, + 'Content-Type': 'application/json', + }, + body: body && Object.keys(body).length > 0 ? body : undefined, + }); + + return response.body; +} + + diff --git a/packages/pieces/community/pubrio/src/lib/triggers/webhook.ts b/packages/pieces/community/pubrio/src/lib/triggers/webhook.ts new file mode 100644 index 00000000000..47cd77f3528 --- /dev/null +++ b/packages/pieces/community/pubrio/src/lib/triggers/webhook.ts @@ -0,0 +1,89 @@ +import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework'; +import { pubrioAuth } from '../../index'; + +export const pubrioWebhookTrigger = createTrigger({ + auth: pubrioAuth, + name: 'pubrio_monitor_event', + displayName: 'Monitor Event (Webhook)', + description: + 'Triggers once per signal (signal_first mode) or once per company (company_first mode) when a Pubrio monitor fires. Copy the webhook URL into your Pubrio monitor destination configuration.', + type: TriggerStrategy.WEBHOOK, + props: {}, + async onEnable() {}, + async onDisable() {}, + async run(context) { + const body = context.payload.body as Record; + if (!body) return []; + + const monitor = body['monitor'] as Record | undefined; + const triggeredAt = body['triggered_at']; + const detectionMode = monitor?.['detection_mode']; + + const base = { monitor, triggered_at: triggeredAt }; + + if (detectionMode === 'signal_first') { + const signals = body['signals']; + if (Array.isArray(signals) && signals.length > 0) { + return signals.map((signal: unknown) => ({ ...(signal as object), ...base })); + } + } + + if (detectionMode === 'company_first') { + const companies = body['companies']; + if (Array.isArray(companies) && companies.length > 0) { + return companies.map((company: unknown) => ({ ...(company as object), ...base })); + } + } + + return [body]; + }, + sampleData: { + signal_type: 'jobs', + signal: { + signal_type: 'jobs', + job_search_id: 'job-search-001', + companies: [ + { + domain_search_id: 'domain-search-001', + company_name: 'Acme Corp', + domain: 'acme.com', + }, + ], + }, + companies: [ + { + domain_search_id: 'domain-search-001', + company_name: 'Acme Corp', + domain: 'acme.com', + logo_url: 'https://logo.clearbit.com/acme.com', + country_code: 'US', + company_size: 5000, + industry: 'Software', + estimated_revenue: 50000000, + founded_year: 2015, + linkedin_url: 'https://linkedin.com/company/acme-corp', + people: [ + { + people_search_id: 'person-001', + name: 'Jane Smith', + title: 'VP of Engineering', + linkedin_url: 'https://linkedin.com/in/janesmith', + email: 'j.smith@acme.com', + phone: '+14155551234', + }, + ], + emails: ['info@acme.com'], + phones: ['+14155550000'], + }, + ], + monitor: { + monitor_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + name: 'My Signal Monitor', + detection_mode: 'signal_first', + signal_types: ['jobs', 'news'], + is_company_enrichment: true, + is_people_enrichment: true, + }, + triggered_at: '2026-04-05T20:29:43.832Z', + }, +}); diff --git a/packages/pieces/community/pubrio/tsconfig.json b/packages/pieces/community/pubrio/tsconfig.json new file mode 100644 index 00000000000..eff240ac143 --- /dev/null +++ b/packages/pieces/community/pubrio/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/pubrio/tsconfig.lib.json b/packages/pieces/community/pubrio/tsconfig.lib.json new file mode 100644 index 00000000000..812842bd452 --- /dev/null +++ b/packages/pieces/community/pubrio/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "paths": {}, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/pieces/community/supabase/package.json b/packages/pieces/community/supabase/package.json index bd479d55adf..ccc335b9dc4 100644 --- a/packages/pieces/community/supabase/package.json +++ b/packages/pieces/community/supabase/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-supabase", - "version": "0.1.4", + "version": "0.1.5", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { diff --git a/packages/pieces/community/supabase/src/index.ts b/packages/pieces/community/supabase/src/index.ts index 45b993a93aa..bcc8a71468e 100644 --- a/packages/pieces/community/supabase/src/index.ts +++ b/packages/pieces/community/supabase/src/index.ts @@ -12,6 +12,8 @@ import { deleteRows } from './lib/actions/delete-rows'; import { updateRow } from './lib/actions/update-row'; import { upsertRow } from './lib/actions/upsert-row'; import { searchRows } from './lib/actions/search-rows'; +import { listTables } from './lib/actions/list-tables'; +import { getTableSchema } from './lib/actions/get-table-schema'; import { newRow } from './lib/triggers/new-row'; import { supabaseAuth } from './lib/auth'; @@ -55,6 +57,8 @@ export const supabase = createPiece({ upsertRow, deleteRows, searchRows, + listTables, + getTableSchema, createCustomApiCallAction({ baseUrl: (auth) => auth?.props?.url || '', auth: supabaseAuth, diff --git a/packages/pieces/community/supabase/src/lib/actions/get-table-schema.ts b/packages/pieces/community/supabase/src/lib/actions/get-table-schema.ts new file mode 100644 index 00000000000..8075adabc9b --- /dev/null +++ b/packages/pieces/community/supabase/src/lib/actions/get-table-schema.ts @@ -0,0 +1,73 @@ +import { createAction } from '@activepieces/pieces-framework'; +import { createClient } from '@supabase/supabase-js'; +import { supabaseAuth } from '../auth'; +import { supabaseCommon } from '../common/props'; + +export const getTableSchema = createAction({ + name: 'get_table_schema', + displayName: 'Get Table Schema', + description: 'Returns the column definitions for a specific table', + auth: supabaseAuth, + props: { + table_name: supabaseCommon.table_name, + }, + async run(context) { + const { table_name } = context.propsValue; + const { url, apiKey } = context.auth.props; + const supabase = createClient(url, apiKey); + + const { data: rpcColumns, error } = await supabase.rpc('get_table_columns', { + p_table_name: table_name as string, + }); + + if (!error && Array.isArray(rpcColumns) && rpcColumns.length > 0) { + return rpcColumns.map((column: { + column_name: string; + data_type?: string; + format?: string | null; + description?: string | null; + }) => ({ + column_name: column.column_name, + data_type: column.data_type ?? 'unknown', + format: column.format ?? null, + description: column.description ?? null, + })); + } + + const response = await fetch(`${url}/rest/v1/`, { + method: 'GET', + headers: { + apikey: apiKey, + Authorization: `Bearer ${apiKey}`, + Accept: 'application/openapi+json', + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch database schema. Status: ${response.status}`, + ); + } + + const openApiSpec = await response.json(); + const definitions = + openApiSpec.definitions ?? openApiSpec.components?.schemas ?? {}; + const tableDefinition = definitions[table_name as string]; + + if (!tableDefinition || !tableDefinition.properties) { + throw new Error(`Table '${table_name}' not found in database schema.`); + } + + return Object.entries( + tableDefinition.properties as Record< + string, + { type?: string; format?: string | null; description?: string | null } + >, + ).map(([columnName, columnDef]) => ({ + column_name: columnName, + data_type: columnDef.type ?? 'unknown', + format: columnDef.format ?? null, + description: columnDef.description ?? null, + })); + }, +}); diff --git a/packages/pieces/community/supabase/src/lib/actions/list-tables.ts b/packages/pieces/community/supabase/src/lib/actions/list-tables.ts new file mode 100644 index 00000000000..4152b4c4052 --- /dev/null +++ b/packages/pieces/community/supabase/src/lib/actions/list-tables.ts @@ -0,0 +1,49 @@ +import { createAction } from '@activepieces/pieces-framework'; +import { createClient } from '@supabase/supabase-js'; +import { supabaseAuth } from '../auth'; + +export const listTables = createAction({ + name: 'list_tables', + displayName: 'List Tables', + description: 'Returns a list of all public tables and views in the database', + auth: supabaseAuth, + props: {}, + async run(context) { + const { url, apiKey } = context.auth.props; + const supabase = createClient(url, apiKey); + + const { data: tables, error } = await supabase.rpc('get_public_tables'); + + if (!error && Array.isArray(tables) && tables.length > 0) { + return tables.map((table: { table_name?: string; name?: string } | string) => ({ + name: + typeof table === 'string' + ? table + : table.table_name ?? table.name, + })); + } + + const response = await fetch(`${url}/rest/v1/`, { + method: 'GET', + headers: { + apikey: apiKey, + Authorization: `Bearer ${apiKey}`, + Accept: 'application/openapi+json', + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch database schema. Status: ${response.status}`, + ); + } + + const openApiSpec = await response.json(); + const definitions = + openApiSpec.definitions ?? openApiSpec.components?.schemas ?? {}; + + return Object.keys(definitions).map((tableName) => ({ + name: tableName, + })); + }, +}); diff --git a/packages/server/api/src/app/ee/authentication/saml-authn/authn-sso-saml-controller.ts b/packages/server/api/src/app/ee/authentication/saml-authn/authn-sso-saml-controller.ts index 77cc909cb80..5e0818150aa 100644 --- a/packages/server/api/src/app/ee/authentication/saml-authn/authn-sso-saml-controller.ts +++ b/packages/server/api/src/app/ee/authentication/saml-authn/authn-sso-saml-controller.ts @@ -25,7 +25,7 @@ export const authnSsoSamlController: FastifyPluginAsyncZod = async (app) => { body: req.body, query: req.query, }) - const url = new URL('/authenticate', `${req.protocol}://${req.hostname}`) + const url = new URL('/authenticate', networkUtils.getRequestBaseUrl(req)) url.searchParams.append('response', JSON.stringify(response)) applicationEvents(req.log).sendUserEvent({ platformId, diff --git a/packages/server/api/src/app/helper/network-utils.ts b/packages/server/api/src/app/helper/network-utils.ts index 14e98edd862..170c58faaea 100644 --- a/packages/server/api/src/app/helper/network-utils.ts +++ b/packages/server/api/src/app/helper/network-utils.ts @@ -6,7 +6,6 @@ import { FastifyRequest } from 'fastify' const GOOGLE_DNS = '216.239.32.10' const PUBLIC_IP_ADDRESS_QUERY = 'o-o.myaddr.l.google.com' - type IpMetadata = { ip: string } @@ -63,10 +62,17 @@ const extractClientRealIp = (request: FastifyRequest, clientIpHeader: string | u return request.headers[clientIpHeader] as string } +const getRequestBaseUrl = (req: FastifyRequest): string => { + const forwardedProto = req.headers['x-forwarded-proto'] as string | undefined + const protocol = forwardedProto?.split(',')[0]?.trim() ?? req.protocol + const host = req.headers['x-forwarded-host'] as string ?? req.hostname + return `${protocol}://${host}` +} export const networkUtils = { extractClientRealIp, getPublicIp, + getRequestBaseUrl, combineUrl(url: string, path: string) { const cleanedUrl = cleanTrailingSlash(url) const cleanedPath = cleanLeadingSlash(path) @@ -81,5 +87,3 @@ function cleanLeadingSlash(url: string): string { function cleanTrailingSlash(url: string): string { return url.endsWith('/') ? url.slice(0, -1) : url } - - diff --git a/packages/server/api/src/app/helper/system-validator.ts b/packages/server/api/src/app/helper/system-validator.ts index ed7686307fc..4742f5299a0 100644 --- a/packages/server/api/src/app/helper/system-validator.ts +++ b/packages/server/api/src/app/helper/system-validator.ts @@ -167,8 +167,6 @@ const systemPropValidators: { [AppSystemProp.MAX_RECORDS_PER_TABLE]: numberValidator, [AppSystemProp.MAX_FIELDS_PER_TABLE]: numberValidator, - // MCP - [AppSystemProp.MCP_OAUTH_ISSUER_URL]: urlValidator, [AppSystemProp.ENABLE_FLOW_ON_PUBLISH]: booleanValidator, [AppSystemProp.ISSUE_ARCHIVE_DAYS]: (value: string) => { const days = parseInt(value) diff --git a/packages/server/api/src/app/helper/system/system-props.ts b/packages/server/api/src/app/helper/system/system-props.ts index 106a2dffd66..638c53981aa 100644 --- a/packages/server/api/src/app/helper/system/system-props.ts +++ b/packages/server/api/src/app/helper/system/system-props.ts @@ -121,7 +121,6 @@ export enum AppSystemProp { CANARY_APP_URL = 'CANARY_APP_URL', SSRF_ALLOW_LIST = 'SSRF_ALLOW_LIST', NETWORK_MODE = 'NETWORK_MODE', - MCP_OAUTH_ISSUER_URL = 'MCP_OAUTH_ISSUER_URL', CONTAINER_TYPE = 'CONTAINER_TYPE', FRONTEND_URL = 'FRONTEND_URL', PORT = 'PORT', diff --git a/packages/server/api/src/app/mcp/oauth/code/mcp-oauth-authorize.controller.ts b/packages/server/api/src/app/mcp/oauth/code/mcp-oauth-authorize.controller.ts index d70de583e48..0c3b94894d5 100644 --- a/packages/server/api/src/app/mcp/oauth/code/mcp-oauth-authorize.controller.ts +++ b/packages/server/api/src/app/mcp/oauth/code/mcp-oauth-authorize.controller.ts @@ -3,8 +3,7 @@ import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod' import { z } from 'zod' import { securityAccess } from '../../../core/security/authorization/fastify-security' import { JwtAudience, jwtUtils } from '../../../helper/jwt-utils' -import { system } from '../../../helper/system/system' -import { AppSystemProp } from '../../../helper/system/system-props' +import { networkUtils } from '../../../helper/network-utils' import { mcpOAuthClientService } from '../client/mcp-oauth-client.service' const AUTH_REQUEST_TTL_10_MINUTES_SECONDS = 10 * 60 @@ -49,8 +48,7 @@ export const mcpOAuthAuthorizeController: FastifyPluginAsyncZod = async (app) => audience: JwtAudience.MCP_OAUTH_AUTH_REQUEST, }) - const frontendUrl = system.getOrThrow(AppSystemProp.FRONTEND_URL) - const authorizePageUrl = new URL('/mcp-authorize', frontendUrl) + const authorizePageUrl = new URL('/mcp-authorize', networkUtils.getRequestBaseUrl(req)) authorizePageUrl.searchParams.set('authRequestId', authRequestToken) return reply.redirect(authorizePageUrl.toString()) diff --git a/packages/server/api/src/app/mcp/oauth/metadata/mcp-oauth-metadata.controller.ts b/packages/server/api/src/app/mcp/oauth/metadata/mcp-oauth-metadata.controller.ts index 89e4ba7914a..84c765d2baa 100644 --- a/packages/server/api/src/app/mcp/oauth/metadata/mcp-oauth-metadata.controller.ts +++ b/packages/server/api/src/app/mcp/oauth/metadata/mcp-oauth-metadata.controller.ts @@ -1,11 +1,11 @@ import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod' import { securityAccess } from '../../../core/security/authorization/fastify-security' -import { mcpOAuthTokenService } from '../token/mcp-oauth-token.service' +import { networkUtils } from '../../../helper/network-utils' export const mcpOAuthMetadataController: FastifyPluginAsyncZod = async (app) => { - app.get('/.well-known/oauth-authorization-server', AuthorizationServerMetadataRequest, async (_req, reply) => { - const issuer = mcpOAuthTokenService.getIssuerUrl() + app.get('/.well-known/oauth-authorization-server', PublicMetadataRequest, async (req, reply) => { + const issuer = networkUtils.getRequestBaseUrl(req) return reply.status(200).header('Access-Control-Allow-Origin', '*').send({ issuer, authorization_endpoint: `${issuer}/authorize`, @@ -20,16 +20,16 @@ export const mcpOAuthMetadataController: FastifyPluginAsyncZod = async (app) => }) }) - app.get('/.well-known/oauth-protected-resource/mcp', ProtectedResourceMetadataRequest, async (_req, reply) => { - const issuer = mcpOAuthTokenService.getIssuerUrl() + app.get('/.well-known/oauth-protected-resource/mcp', PublicMetadataRequest, async (req, reply) => { + const issuer = networkUtils.getRequestBaseUrl(req) return reply.status(200).header('Access-Control-Allow-Origin', '*').send({ resource: `${issuer}/mcp`, authorization_servers: [issuer], }) }) - app.get('/.well-known/oauth-protected-resource/mcp/platform', ProtectedResourceMetadataRequest, async (_req, reply) => { - const issuer = mcpOAuthTokenService.getIssuerUrl() + app.get('/.well-known/oauth-protected-resource/mcp/platform', PublicMetadataRequest, async (req, reply) => { + const issuer = networkUtils.getRequestBaseUrl(req) return reply.status(200).header('Access-Control-Allow-Origin', '*').send({ resource: `${issuer}/mcp/platform`, authorization_servers: [issuer], @@ -37,12 +37,7 @@ export const mcpOAuthMetadataController: FastifyPluginAsyncZod = async (app) => }) } -const AuthorizationServerMetadataRequest = { - config: { security: securityAccess.public() }, - schema: { hide: true }, -} - -const ProtectedResourceMetadataRequest = { +const PublicMetadataRequest = { config: { security: securityAccess.public() }, schema: { hide: true }, } diff --git a/packages/server/api/src/app/mcp/oauth/token/mcp-oauth-token.service.ts b/packages/server/api/src/app/mcp/oauth/token/mcp-oauth-token.service.ts index 2597dddaa7c..49ca2440228 100644 --- a/packages/server/api/src/app/mcp/oauth/token/mcp-oauth-token.service.ts +++ b/packages/server/api/src/app/mcp/oauth/token/mcp-oauth-token.service.ts @@ -3,8 +3,6 @@ import { cryptoUtils } from '@activepieces/server-utils' import { apId, McpOAuthToken } from '@activepieces/shared' import { repoFactory } from '../../../core/db/repo-factory' import { JwtAudience, jwtUtils } from '../../../helper/jwt-utils' -import { system } from '../../../helper/system/system' -import { AppSystemProp } from '../../../helper/system/system-props' import { mcpOAuthPkce } from '../mcp-oauth.pkce' import { McpOAuthTokenEntity } from './mcp-oauth-token.entity' @@ -129,11 +127,6 @@ export const mcpOAuthTokenService = { async issueInternalAccessToken({ userId, platformId, projectId }: { userId: string, platformId: string, projectId: string | null }): Promise { return issueAccessToken({ userId, platformId, projectId, clientId: INTERNAL_CHAT_CLIENT_ID, scopes: ['mcp'] }) }, - - getIssuerUrl(): string { - return system.get(AppSystemProp.MCP_OAUTH_ISSUER_URL) - ?? system.getOrThrow(AppSystemProp.FRONTEND_URL) - }, } export class OAuthTokenError extends Error { diff --git a/packages/web/vite.config.mts b/packages/web/vite.config.mts index 7ae490d353f..4b75e22731b 100644 --- a/packages/web/vite.config.mts +++ b/packages/web/vite.config.mts @@ -33,32 +33,50 @@ export default defineConfig(({ command, mode }) => { target: 'http://127.0.0.1:3000', secure: false, changeOrigin: true, + headers: { + 'X-Forwarded-Host': 'localhost:4200', + }, rewrite: (p: string) => p, }, '/.well-known': { target: 'http://127.0.0.1:3000', secure: false, changeOrigin: true, + headers: { + 'X-Forwarded-Host': 'localhost:4200', + }, }, '/register': { target: 'http://127.0.0.1:3000', secure: false, changeOrigin: true, + headers: { + 'X-Forwarded-Host': 'localhost:4200', + }, }, '/authorize': { target: 'http://127.0.0.1:3000', secure: false, changeOrigin: true, + headers: { + 'X-Forwarded-Host': 'localhost:4200', + }, }, '/token': { target: 'http://127.0.0.1:3000', secure: false, changeOrigin: true, + headers: { + 'X-Forwarded-Host': 'localhost:4200', + }, }, '/revoke': { target: 'http://127.0.0.1:3000', secure: false, changeOrigin: true, + headers: { + 'X-Forwarded-Host': 'localhost:4200', + }, }, }, port: 4200, diff --git a/tsconfig.base.json b/tsconfig.base.json index f99b7a642e3..6384209357c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -606,6 +606,9 @@ "@activepieces/piece-hunter": [ "packages/pieces/community/hunter/src/index.ts" ], + "@activepieces/piece-iloveapi": [ + "packages/pieces/community/iloveapi/src/index.ts" + ], "@activepieces/piece-image-ai": [ "packages/pieces/community/image-ai/src/index.ts" ], @@ -954,6 +957,9 @@ "@activepieces/piece-prompthub": [ "packages/pieces/community/prompthub/src/index.ts" ], + "@activepieces/piece-pubrio": [ + "packages/pieces/community/pubrio/src/index.ts" + ], "@activepieces/piece-pushover": [ "packages/pieces/community/pushover/src/index.ts" ],