diff --git a/apps/docs/content/docs/en/integrations/linq.mdx b/apps/docs/content/docs/en/integrations/linq.mdx index 82ad460cfe1..dad202058e7 100644 --- a/apps/docs/content/docs/en/integrations/linq.mdx +++ b/apps/docs/content/docs/en/integrations/linq.mdx @@ -88,7 +88,7 @@ Check whether an address (phone number or email) supports RCS | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Linq API key | -| `address` | string | Yes | Phone number \(E.164 format\) or email address to check | +| `address` | string | Yes | Phone number \(E.164 format\) to check | | `from` | string | No | Sender phone number to check from \(defaults to an available number\) | #### Output @@ -277,8 +277,9 @@ Edit the text of a sent message (up to 5 times, within 15 minutes of sending; iM | `id` | string | Message ID | | `chatId` | string | ID of the chat the message belongs to | | `isFromMe` | boolean | Whether the message was sent by you | -| `isDelivered` | boolean | Whether the message was delivered | -| `isRead` | boolean | Whether the message was read | +| `deliveryStatus` | string | Delivery status \(pending, queued, sent, delivered, received, read, failed\) | +| `isDelivered` | boolean | Whether the message was delivered \(deprecated; use deliveryStatus\) | +| `isRead` | boolean | Whether the message was read \(deprecated; use deliveryStatus\) | | `service` | string | Delivery service \(iMessage, SMS, RCS\) | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | @@ -374,8 +375,9 @@ Retrieve a single message by ID, including parts, reactions, and delivery status | `id` | string | Message ID | | `chatId` | string | ID of the chat the message belongs to | | `isFromMe` | boolean | Whether the message was sent by you | -| `isDelivered` | boolean | Whether the message was delivered | -| `isRead` | boolean | Whether the message was read | +| `deliveryStatus` | string | Delivery status \(pending, queued, sent, delivered, received, read, failed\) | +| `isDelivered` | boolean | Whether the message was delivered \(deprecated; use deliveryStatus\) | +| `isRead` | boolean | Whether the message was read \(deprecated; use deliveryStatus\) | | `service` | string | Delivery service \(iMessage, SMS, RCS\) | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | @@ -483,7 +485,8 @@ List all phone numbers assigned to your partner account, with line health | `phoneNumbers` | array | Phone numbers assigned to the account | | ↳ `id` | string | Phone number ID | | ↳ `phoneNumber` | string | Phone number in E.164 format | -| ↳ `healthStatus` | json | Line health status \(status, doc_url\) | +| ↳ `forwardingNumber` | string | Forwarding number in E.164 format, or null | +| ↳ `healthStatus` | json | Line reputation/health status \(status, doc_url\) | ### `linq_list_thread` @@ -548,7 +551,7 @@ List all webhook subscriptions on your account ### `linq_mark_chat_read` -Mark all messages in a chat as read +Mark messages in a chat as read (only applies to 1:1 iMessage/RCS; no effect on group chats) #### Input @@ -633,7 +636,7 @@ Send a message to an existing chat, with optional media, link, effect, or reply | --------- | ---- | ----------- | | `chatId` | string | ID of the chat the message was sent to | | `messageId` | string | ID of the sent message | -| `deliveryStatus` | string | Delivery status \(pending, queued, sent, delivered, failed\) | +| `deliveryStatus` | string | Delivery status \(pending, queued, sent, delivered, received, read, failed\) | | `sentAt` | string | ISO 8601 timestamp the message was sent | | `service` | string | Delivery service \(iMessage, SMS, RCS\) | | `message` | json | The full sent message object with parts | @@ -785,3 +788,149 @@ Update a webhook subscription (target URL, events, phone filter, or active state | `updatedAt` | string | ISO 8601 update timestamp | + +## Triggers + +A **Trigger** is a block that starts a workflow when an event happens in this service. + +### Linq Message Delivered + +Trigger workflow when a message is delivered + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + + +--- + +### Linq Message Failed + +Trigger workflow when a message fails to deliver + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + + +--- + +### Linq Message Read + +Trigger workflow when a message is read + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + + +--- + +### Linq Message Received + +Trigger workflow when an inbound message is received + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + + +--- + +### Linq Reaction Added + +Trigger workflow when a reaction is added to a message + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + + +--- + +### Linq Webhook (All Events) + +Trigger on any Linq webhook event (messages, reactions, chats, and more) + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + diff --git a/apps/sim/blocks/blocks/linq.ts b/apps/sim/blocks/blocks/linq.ts index 01dddba3942..e2ae7eaed95 100644 --- a/apps/sim/blocks/blocks/linq.ts +++ b/apps/sim/blocks/blocks/linq.ts @@ -2,6 +2,7 @@ import { LinqIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' import { normalizeFileInput } from '@/blocks/utils' +import { getTrigger } from '@/triggers' const CHAT_ID_OPS = [ 'get_chat', @@ -559,6 +560,12 @@ export const LinqBlock: BlockConfig = { condition: { field: 'operation', value: [...PAGINATION_OPS] }, mode: 'advanced', }, + ...getTrigger('linq_message_received').subBlocks, + ...getTrigger('linq_message_delivered').subBlocks, + ...getTrigger('linq_message_failed').subBlocks, + ...getTrigger('linq_message_read').subBlocks, + ...getTrigger('linq_reaction_added').subBlocks, + ...getTrigger('linq_webhook').subBlocks, ], tools: { @@ -863,6 +870,18 @@ export const LinqBlock: BlockConfig = { events: { type: 'json', description: 'Available webhook event types' }, docUrl: { type: 'string', description: 'Documentation URL' }, }, + + triggers: { + enabled: true, + available: [ + 'linq_message_received', + 'linq_message_delivered', + 'linq_message_failed', + 'linq_message_read', + 'linq_reaction_added', + 'linq_webhook', + ], + }, } export const LinqBlockMeta = { diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 917a746c666..62ca382d8dc 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -10003,7 +10003,7 @@ }, { "name": "Mark Chat as Read", - "description": "Mark all messages in a chat as read" + "description": "Mark messages in a chat as read (only applies to 1:1 iMessage/RCS; no effect on group chats)" }, { "name": "Leave Chat", @@ -10119,8 +10119,39 @@ } ], "operationCount": 34, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "linq_message_received", + "name": "Linq Message Received", + "description": "Trigger workflow when an inbound message is received" + }, + { + "id": "linq_message_delivered", + "name": "Linq Message Delivered", + "description": "Trigger workflow when a message is delivered" + }, + { + "id": "linq_message_failed", + "name": "Linq Message Failed", + "description": "Trigger workflow when a message fails to deliver" + }, + { + "id": "linq_message_read", + "name": "Linq Message Read", + "description": "Trigger workflow when a message is read" + }, + { + "id": "linq_reaction_added", + "name": "Linq Reaction Added", + "description": "Trigger workflow when a reaction is added to a message" + }, + { + "id": "linq_webhook", + "name": "Linq Webhook (All Events)", + "description": "Trigger on any Linq webhook event (messages, reactions, chats, and more)" + } + ], + "triggerCount": 6, "authType": "api-key", "category": "tools", "integrationType": "communication", @@ -15649,11 +15680,11 @@ "landingContent": { "install": { "heading": "Add Sim to your Slack workspace", - "intro": "Sim connects to Slack through Slack’s official OAuth flow. The “Add to Slack” button lives inside your Sim account (after sign-in) — connect from there and the Sim bot is installed in your Slack workspace. The steps below show exactly how to reach it.", + "intro": "Sim connects to Slack through Slack’s official OAuth flow. The “Add to Slack” button lives inside your Sim account (after sign-in). Connect from there and the Sim bot is installed in your Slack workspace. The steps below show exactly how to reach it.", "steps": [ { "title": "Create your free Sim account", - "body": "Sign up at sim.ai — no credit card required." + "body": "Sign up at sim.ai. No credit card required." }, { "title": "Add a Slack block", @@ -15673,7 +15704,7 @@ "body": "Sim requests only the Slack permissions its actions and triggers need, and never shows private channel names or messages to people who are not members of those channels in Slack.", "href": "/privacy" }, - "aiDisclaimer": "Sim agents use AI models to generate messages and responses sent to Slack. AI-generated content can be inaccurate or incomplete — review automated outputs before relying on them, especially for important communications." + "aiDisclaimer": "Sim agents use AI models to generate messages and responses sent to Slack. AI-generated content can be inaccurate or incomplete, so review automated outputs before relying on them, especially for important communications." } }, { diff --git a/apps/sim/lib/webhooks/providers/linq.ts b/apps/sim/lib/webhooks/providers/linq.ts new file mode 100644 index 00000000000..819b87ce0c7 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/linq.ts @@ -0,0 +1,266 @@ +import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Base64 } from '@sim/security/hmac' +import { NextResponse } from 'next/server' +import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import type { + AuthContext, + DeleteSubscriptionContext, + EventMatchContext, + FormatInputContext, + FormatInputResult, + SubscriptionContext, + SubscriptionResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' +import { LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import { LINQ_ALL_WEBHOOK_EVENT_TYPES, LINQ_TRIGGER_TO_EVENT_TYPE } from '@/triggers/linq/utils' + +const logger = createLogger('WebhookProvider:Linq') + +/** Max clock skew tolerated between the webhook timestamp and now (seconds). */ +const MAX_TIMESTAMP_SKEW_SECONDS = 5 * 60 + +/** + * Verify a Linq webhook signature using the Standard Webhooks scheme. + * Linq signs `${webhook-id}.${webhook-timestamp}.${rawBody}` with HMAC-SHA256 using + * the base64-decoded `whsec_...` signing secret, and delivers the result as one or + * more space-separated `v1,` signatures in the `webhook-signature` header. + */ +function verifyLinqSignature( + secret: string, + msgId: string, + timestamp: string, + signatures: string, + rawBody: string +): boolean { + try { + const ts = Number.parseInt(timestamp, 10) + const now = Math.floor(Date.now() / 1000) + if (Number.isNaN(ts) || Math.abs(now - ts) > MAX_TIMESTAMP_SKEW_SECONDS) { + return false + } + + const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64') + const toSign = `${msgId}.${timestamp}.${rawBody}` + const expectedSignature = hmacSha256Base64(toSign, secretBytes) + + for (const versionedSig of signatures.split(' ')) { + const parts = versionedSig.split(',') + if (parts.length !== 2) continue + if (safeCompare(parts[1], expectedSignature)) { + return true + } + } + return false + } catch (error) { + logger.error('Error verifying Linq webhook signature:', error) + return false + } +} + +/** Parse a comma/whitespace-separated list of phone numbers into a clean array. */ +function parsePhoneNumbers(value: unknown): string[] { + if (typeof value !== 'string') return [] + return value + .split(/[\n,]/) + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0) +} + +export const linqHandler: WebhookProviderHandler = { + async verifyAuth({ + request, + rawBody, + requestId, + providerConfig, + }: AuthContext): Promise { + const signingSecret = providerConfig.signingSecret as string | undefined + if (!signingSecret?.trim()) { + logger.warn(`[${requestId}] Linq webhook missing signing secret in provider configuration`) + return new NextResponse('Unauthorized - Linq signing secret is required', { status: 401 }) + } + + const webhookId = request.headers.get('webhook-id') + const webhookTimestamp = request.headers.get('webhook-timestamp') + const webhookSignature = request.headers.get('webhook-signature') + + if (!webhookId || !webhookTimestamp || !webhookSignature) { + logger.warn(`[${requestId}] Linq webhook missing Standard Webhooks signature headers`) + return new NextResponse('Unauthorized - Missing Linq signature headers', { status: 401 }) + } + + if ( + !verifyLinqSignature(signingSecret, webhookId, webhookTimestamp, webhookSignature, rawBody) + ) { + logger.warn(`[${requestId}] Linq webhook signature verification failed`) + return new NextResponse('Unauthorized - Invalid Linq signature', { status: 401 }) + } + + return null + }, + + matchEvent({ body, providerConfig, requestId }: EventMatchContext): boolean { + const triggerId = providerConfig.triggerId as string | undefined + if (!triggerId || triggerId === 'linq_webhook') { + return true + } + + const expectedType = LINQ_TRIGGER_TO_EVENT_TYPE[triggerId] + if (!expectedType) { + logger.debug(`[${requestId}] Unknown Linq triggerId ${triggerId}, skipping.`) + return false + } + + const actualType = (body as Record)?.event_type as string | undefined + if (actualType !== expectedType) { + logger.debug( + `[${requestId}] Linq event type mismatch: expected ${expectedType}, got ${actualType}. Skipping.` + ) + return false + } + + return true + }, + + async formatInput({ body }: FormatInputContext): Promise { + const payload = body as Record + return { + input: { + eventType: payload.event_type ?? null, + eventId: payload.event_id ?? null, + createdAt: payload.created_at ?? null, + webhookVersion: payload.webhook_version ?? null, + data: payload.data ?? null, + }, + } + }, + + extractIdempotencyId(body: unknown): string | null { + const eventId = (body as Record)?.event_id + return typeof eventId === 'string' && eventId.length > 0 ? eventId : null + }, + + async createSubscription(ctx: SubscriptionContext): Promise { + const { webhook, requestId } = ctx + const providerConfig = getProviderConfig(webhook) + const apiKey = providerConfig.triggerApiKey as string | undefined + const triggerId = providerConfig.triggerId as string | undefined + + if (!apiKey) { + logger.warn(`[${requestId}] Missing apiKey for Linq webhook creation.`, { + webhookId: webhook.id, + }) + throw new Error( + 'Linq API Key is required. Please provide your Linq API Key in the trigger configuration.' + ) + } + + const events = + triggerId === 'linq_webhook' + ? LINQ_ALL_WEBHOOK_EVENT_TYPES + : triggerId && LINQ_TRIGGER_TO_EVENT_TYPE[triggerId] + ? [LINQ_TRIGGER_TO_EVENT_TYPE[triggerId]] + : null + + if (!events?.length) { + throw new Error(`Unknown or unsupported Linq trigger type: ${triggerId ?? '(missing)'}`) + } + + const phoneNumbers = parsePhoneNumbers(providerConfig.triggerPhoneNumbers) + const requestBody: Record = { + target_url: getNotificationUrl(webhook), + subscribed_events: events, + } + if (phoneNumbers.length > 0) { + requestBody.phone_numbers = phoneNumbers + } + + logger.info(`[${requestId}] Creating Linq webhook subscription`, { + triggerId, + events, + webhookId: webhook.id, + }) + + const response = await fetch(`${LINQ_API_BASE}/webhook-subscriptions`, { + method: 'POST', + headers: linqHeaders(apiKey), + body: JSON.stringify(requestBody), + }) + + const responseBody = (await response.json().catch(() => ({}))) as Record + + if (!response.ok) { + const errorMessage = + ((responseBody.error as Record)?.message as string) || + (responseBody.message as string) || + 'Unknown Linq API error' + logger.error( + `[${requestId}] Failed to create Linq webhook subscription for webhook ${webhook.id}. Status: ${response.status}`, + { message: errorMessage } + ) + + if (response.status === 401 || response.status === 403) { + throw new Error('Invalid Linq API Key. Please verify your API Key is correct.') + } + throw new Error(`Linq error: ${errorMessage}`) + } + + const externalId = responseBody.id + const signingSecret = responseBody.signing_secret + + if (typeof externalId !== 'string' || !externalId.trim()) { + throw new Error('Linq webhook was created but the API response did not include a webhook id.') + } + if (typeof signingSecret !== 'string' || !signingSecret.trim()) { + throw new Error( + 'Linq webhook was created but the API response did not include a signing secret.' + ) + } + + logger.info(`[${requestId}] Successfully created Linq webhook subscription ${externalId}.`) + + return { + providerConfigUpdates: { + externalId, + signingSecret, + }, + } + }, + + async deleteSubscription(ctx: DeleteSubscriptionContext): Promise { + const { webhook, requestId } = ctx + try { + const config = getProviderConfig(webhook) + const apiKey = config.triggerApiKey as string | undefined + const externalId = config.externalId as string | undefined + + if (!apiKey || !externalId) { + logger.warn( + `[${requestId}] Missing apiKey or externalId for Linq webhook deletion ${webhook.id}, skipping cleanup` + ) + if (ctx.strict) throw new Error('Missing Linq webhook deletion credentials') + return + } + + const response = await fetch(`${LINQ_API_BASE}/webhook-subscriptions/${externalId}`, { + method: 'DELETE', + headers: linqHeaders(apiKey), + }) + + if (!response.ok && response.status !== 404) { + logger.warn( + `[${requestId}] Failed to delete Linq webhook subscription (non-fatal): ${response.status}` + ) + if (ctx.strict) { + throw new Error(`Failed to delete Linq webhook subscription: ${response.status}`) + } + } else { + logger.info(`[${requestId}] Successfully deleted Linq webhook subscription ${externalId}`) + } + } catch (error) { + logger.warn(`[${requestId}] Error deleting Linq webhook subscription (non-fatal)`, error) + if (ctx.strict) throw error + } + }, +} diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 93a96a6fc4e..1965a9663c7 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -28,6 +28,7 @@ import { jiraHandler } from '@/lib/webhooks/providers/jira' import { jsmHandler } from '@/lib/webhooks/providers/jsm' import { lemlistHandler } from '@/lib/webhooks/providers/lemlist' import { linearHandler } from '@/lib/webhooks/providers/linear' +import { linqHandler } from '@/lib/webhooks/providers/linq' import { loopsHandler } from '@/lib/webhooks/providers/loops' import { microsoftTeamsHandler } from '@/lib/webhooks/providers/microsoft-teams' import { mondayHandler } from '@/lib/webhooks/providers/monday' @@ -88,6 +89,7 @@ const PROVIDER_HANDLERS: Record = { jsm: jsmHandler, lemlist: lemlistHandler, linear: linearHandler, + linq: linqHandler, loops: loopsHandler, monday: mondayHandler, resend: resendHandler, diff --git a/apps/sim/tools/linq/check_imessage.ts b/apps/sim/tools/linq/check_imessage.ts index cc44b5047b2..4718ce16490 100644 --- a/apps/sim/tools/linq/check_imessage.ts +++ b/apps/sim/tools/linq/check_imessage.ts @@ -44,7 +44,7 @@ export const linqCheckImessageTool: ToolConfig< }, transformResponse: async (response): Promise => { - const data = await response.json() + const data = await response.json().catch(() => null) if (!response.ok) { return { @@ -57,8 +57,8 @@ export const linqCheckImessageTool: ToolConfig< return { success: true, output: { - address: data.address ?? '', - available: data.available ?? false, + address: data?.address ?? '', + available: data?.available ?? false, }, } }, diff --git a/apps/sim/tools/linq/check_rcs.ts b/apps/sim/tools/linq/check_rcs.ts index 12c3808f871..84993b2ae1f 100644 --- a/apps/sim/tools/linq/check_rcs.ts +++ b/apps/sim/tools/linq/check_rcs.ts @@ -19,7 +19,7 @@ export const linqCheckRcsTool: ToolConfig => { - const data = await response.json() + const data = await response.json().catch(() => null) if (!response.ok) { return { @@ -54,8 +54,8 @@ export const linqCheckRcsTool: ToolConfig => { - const data = await response.json() + // Linq returns 200 with the updated Message, or 204 No Content when the edit + // is accepted for an already-deleted message — guard the empty-body case. + const data = await response.json().catch(() => null) if (!response.ok) { return { @@ -58,6 +60,7 @@ export const linqEditMessageTool: ToolConfig => { - const data = await response.json() + const data = await response.json().catch(() => null) if (!response.ok) { return { @@ -44,10 +44,12 @@ export const linqListPhoneNumbersTool: ToolConfig< return { success: true, output: { - phoneNumbers: (data.phone_numbers ?? []).map((num: Record) => ({ + phoneNumbers: (data?.phone_numbers ?? []).map((num: Record) => ({ id: (num.id as string) ?? '', phoneNumber: (num.phone_number as string) ?? '', - healthStatus: (num.health_status as LinqHealthStatus | undefined) ?? null, + forwardingNumber: (num.forwarding_number as string | null) ?? null, + healthStatus: + ((num.reputation ?? num.health_status) as LinqHealthStatus | undefined) ?? null, })), }, } @@ -62,7 +64,16 @@ export const linqListPhoneNumbersTool: ToolConfig< properties: { id: { type: 'string', description: 'Phone number ID' }, phoneNumber: { type: 'string', description: 'Phone number in E.164 format' }, - healthStatus: { type: 'json', description: 'Line health status (status, doc_url)' }, + forwardingNumber: { + type: 'string', + description: 'Forwarding number in E.164 format, or null', + nullable: true, + }, + healthStatus: { + type: 'json', + description: 'Line reputation/health status (status, doc_url)', + nullable: true, + }, }, }, }, diff --git a/apps/sim/tools/linq/list_webhook_subscriptions.ts b/apps/sim/tools/linq/list_webhook_subscriptions.ts index 391c6680e73..c9c7ec4bd3a 100644 --- a/apps/sim/tools/linq/list_webhook_subscriptions.ts +++ b/apps/sim/tools/linq/list_webhook_subscriptions.ts @@ -63,10 +63,22 @@ export const linqListWebhookSubscriptionsTool: ToolConfig< id: { type: 'string', description: 'Subscription ID' }, targetUrl: { type: 'string', description: 'Endpoint that receives events' }, subscribedEvents: { type: 'json', description: 'Subscribed event types' }, - phoneNumbers: { type: 'json', description: 'Filtered phone numbers (null = all)' }, + phoneNumbers: { + type: 'json', + description: 'Filtered phone numbers (null = all)', + nullable: true, + }, isActive: { type: 'boolean', description: 'Whether the subscription is active' }, - createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' }, - updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' }, + createdAt: { + type: 'string', + description: 'ISO 8601 creation timestamp', + nullable: true, + }, + updatedAt: { + type: 'string', + description: 'ISO 8601 update timestamp', + nullable: true, + }, }, }, }, diff --git a/apps/sim/tools/linq/mark_chat_read.ts b/apps/sim/tools/linq/mark_chat_read.ts index 25ea3e16c74..6ab72f68a39 100644 --- a/apps/sim/tools/linq/mark_chat_read.ts +++ b/apps/sim/tools/linq/mark_chat_read.ts @@ -5,7 +5,8 @@ import type { ToolConfig } from '@/tools/types' export const linqMarkChatReadTool: ToolConfig = { id: 'linq_mark_chat_read', name: 'Mark Chat as Read', - description: 'Mark all messages in a chat as read', + description: + 'Mark messages in a chat as read (only applies to 1:1 iMessage/RCS; no effect on group chats)', version: '1.0.0', params: { diff --git a/apps/sim/tools/linq/send_message.ts b/apps/sim/tools/linq/send_message.ts index f7448ece0d7..cf857331b71 100644 --- a/apps/sim/tools/linq/send_message.ts +++ b/apps/sim/tools/linq/send_message.ts @@ -123,7 +123,8 @@ export const linqSendMessageTool: ToolConfig } diff --git a/apps/sim/triggers/linq/index.ts b/apps/sim/triggers/linq/index.ts new file mode 100644 index 00000000000..5ea641b6c0f --- /dev/null +++ b/apps/sim/triggers/linq/index.ts @@ -0,0 +1,6 @@ +export { linqMessageDeliveredTrigger } from './message_delivered' +export { linqMessageFailedTrigger } from './message_failed' +export { linqMessageReadTrigger } from './message_read' +export { linqMessageReceivedTrigger } from './message_received' +export { linqReactionAddedTrigger } from './reaction_added' +export { linqWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/linq/message_delivered.ts b/apps/sim/triggers/linq/message_delivered.ts new file mode 100644 index 00000000000..9328d818232 --- /dev/null +++ b/apps/sim/triggers/linq/message_delivered.ts @@ -0,0 +1,38 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Linq Message Delivered Trigger + * Fires when a sent message is confirmed delivered to the recipient. + */ +export const linqMessageDeliveredTrigger: TriggerConfig = { + id: 'linq_message_delivered', + name: 'Linq Message Delivered', + provider: 'linq', + description: 'Trigger workflow when a message is delivered', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_message_delivered', + triggerOptions: linqTriggerOptions, + setupInstructions: linqSetupInstructions('message.delivered'), + extraFields: buildLinqExtraFields('linq_message_delivered'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/linq/message_failed.ts b/apps/sim/triggers/linq/message_failed.ts new file mode 100644 index 00000000000..e5566e1edaa --- /dev/null +++ b/apps/sim/triggers/linq/message_failed.ts @@ -0,0 +1,38 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Linq Message Failed Trigger + * Fires when a sent message fails to deliver. + */ +export const linqMessageFailedTrigger: TriggerConfig = { + id: 'linq_message_failed', + name: 'Linq Message Failed', + provider: 'linq', + description: 'Trigger workflow when a message fails to deliver', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_message_failed', + triggerOptions: linqTriggerOptions, + setupInstructions: linqSetupInstructions('message.failed'), + extraFields: buildLinqExtraFields('linq_message_failed'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/linq/message_read.ts b/apps/sim/triggers/linq/message_read.ts new file mode 100644 index 00000000000..7280f945b3b --- /dev/null +++ b/apps/sim/triggers/linq/message_read.ts @@ -0,0 +1,38 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Linq Message Read Trigger + * Fires when a recipient reads a sent message (1:1 iMessage/RCS). + */ +export const linqMessageReadTrigger: TriggerConfig = { + id: 'linq_message_read', + name: 'Linq Message Read', + provider: 'linq', + description: 'Trigger workflow when a message is read', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_message_read', + triggerOptions: linqTriggerOptions, + setupInstructions: linqSetupInstructions('message.read'), + extraFields: buildLinqExtraFields('linq_message_read'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/linq/message_received.ts b/apps/sim/triggers/linq/message_received.ts new file mode 100644 index 00000000000..92a4edb156a --- /dev/null +++ b/apps/sim/triggers/linq/message_received.ts @@ -0,0 +1,39 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Linq Message Received Trigger + * Fires when an inbound iMessage, SMS, or RCS message arrives. + */ +export const linqMessageReceivedTrigger: TriggerConfig = { + id: 'linq_message_received', + name: 'Linq Message Received', + provider: 'linq', + description: 'Trigger workflow when an inbound message is received', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_message_received', + triggerOptions: linqTriggerOptions, + includeDropdown: true, + setupInstructions: linqSetupInstructions('message.received'), + extraFields: buildLinqExtraFields('linq_message_received'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/linq/reaction_added.ts b/apps/sim/triggers/linq/reaction_added.ts new file mode 100644 index 00000000000..5d9cb55e0f0 --- /dev/null +++ b/apps/sim/triggers/linq/reaction_added.ts @@ -0,0 +1,38 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Linq Reaction Added Trigger + * Fires when a tapback or custom reaction is added to a message. + */ +export const linqReactionAddedTrigger: TriggerConfig = { + id: 'linq_reaction_added', + name: 'Linq Reaction Added', + provider: 'linq', + description: 'Trigger workflow when a reaction is added to a message', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_reaction_added', + triggerOptions: linqTriggerOptions, + setupInstructions: linqSetupInstructions('reaction.added'), + extraFields: buildLinqExtraFields('linq_reaction_added'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/linq/utils.ts b/apps/sim/triggers/linq/utils.ts new file mode 100644 index 00000000000..5ec40192481 --- /dev/null +++ b/apps/sim/triggers/linq/utils.ts @@ -0,0 +1,143 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +/** + * Maps Sim Linq trigger IDs to a single Linq webhook event type. + * Kept in sync with subscription registration in the `linq` webhook provider. + */ +export const LINQ_TRIGGER_TO_EVENT_TYPE: Record = { + linq_message_received: 'message.received', + linq_message_delivered: 'message.delivered', + linq_message_failed: 'message.failed', + linq_message_read: 'message.read', + linq_reaction_added: 'reaction.added', +} + +/** + * Every Linq webhook event type, registered for the catch-all `linq_webhook` trigger. + * Mirrors the Linq OpenAPI `WebhookEventType` enum verbatim (all 27 values). + */ +export const LINQ_ALL_WEBHOOK_EVENT_TYPES: string[] = [ + 'message.sent', + 'message.received', + 'message.read', + 'message.delivered', + 'message.failed', + 'message.edited', + 'reaction.added', + 'reaction.removed', + 'participant.added', + 'participant.removed', + 'chat.created', + 'chat.group_name_updated', + 'chat.group_icon_updated', + 'chat.group_name_update_failed', + 'chat.group_icon_update_failed', + 'chat.typing_indicator.started', + 'chat.typing_indicator.stopped', + 'phone_number.status_updated', + 'call.initiated', + 'call.ringing', + 'call.answered', + 'call.ended', + 'call.failed', + 'call.declined', + 'call.no_answer', + 'location.sharing.started', + 'location.sharing.stopped', +] + +/** Shared trigger dropdown options for all Linq triggers. */ +export const linqTriggerOptions = [ + { label: 'Message Received', id: 'linq_message_received' }, + { label: 'Message Delivered', id: 'linq_message_delivered' }, + { label: 'Message Failed', id: 'linq_message_failed' }, + { label: 'Message Read', id: 'linq_message_read' }, + { label: 'Reaction Added', id: 'linq_reaction_added' }, + { label: 'Webhook (All Events)', id: 'linq_webhook' }, +] + +/** + * Generates setup instructions for Linq webhooks. + * The subscription is created and deleted automatically via the Linq API. + */ +export function linqSetupInstructions(eventType: string): string { + const instructions = [ + 'Enter your Linq API Key above.', + 'You can find your API key in the Linq partner dashboard.', + 'Optionally restrict delivery to specific phone numbers (E.164, comma-separated). Leave empty to receive events from all numbers.', + `Click "Save Configuration" to automatically create the webhook subscription in Linq for ${eventType}.`, + 'The subscription is automatically deleted when you remove this trigger.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +/** + * Builds Linq-specific extra fields for the trigger UI. + * Includes the required API key and an optional phone-number filter. + * Use with the generic `buildTriggerSubBlocks` from `@/triggers`. + */ +export function buildLinqExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'triggerApiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Linq API key', + description: 'Required to create the webhook subscription in Linq.', + password: true, + paramVisibility: 'user-only', + required: true, + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'triggerPhoneNumbers', + title: 'Phone Numbers (Optional)', + type: 'short-input', + placeholder: 'Leave empty for all numbers (e.g. +15551234567, +15557654321)', + description: 'Comma-separated E.164 numbers to restrict which numbers deliver events.', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +/** + * Outputs exposed by every Linq trigger. + * + * Only the delivery-envelope fields are documented in the Linq OpenAPI spec; the + * per-event `data` shape is not enumerated, so it is surfaced as a JSON passthrough + * rather than fabricating typed sub-fields. + */ +export function buildLinqOutputs(): Record { + return { + eventType: { + type: 'string', + description: 'Event type (e.g. message.received, message.delivered, reaction.added)', + }, + eventId: { + type: 'string', + description: 'Unique event identifier used for deduplication', + }, + createdAt: { + type: 'string', + description: 'ISO 8601 timestamp of when the event occurred', + }, + webhookVersion: { + type: 'string', + description: 'Payload schema version of the delivered event', + }, + data: { + type: 'json', + description: + 'Full event payload (shape varies by event type — message, reaction, chat, etc.)', + }, + } +} diff --git a/apps/sim/triggers/linq/webhook.ts b/apps/sim/triggers/linq/webhook.ts new file mode 100644 index 00000000000..d7334d3c349 --- /dev/null +++ b/apps/sim/triggers/linq/webhook.ts @@ -0,0 +1,40 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Generic Linq Webhook Trigger + * Subscribes to every Linq webhook event type (messages, reactions, participants, + * chats, typing, phone number status, location sharing). Use the data + * output for the full payload, which varies by eventType. + */ +export const linqWebhookTrigger: TriggerConfig = { + id: 'linq_webhook', + name: 'Linq Webhook (All Events)', + provider: 'linq', + description: 'Trigger on any Linq webhook event (messages, reactions, chats, and more)', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_webhook', + triggerOptions: linqTriggerOptions, + setupInstructions: linqSetupInstructions('All Events'), + extraFields: buildLinqExtraFields('linq_webhook'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 3869d8a66bc..c13704ed89f 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -267,6 +267,14 @@ import { linearWebhookTrigger, linearWebhookV2Trigger, } from '@/triggers/linear' +import { + linqMessageDeliveredTrigger, + linqMessageFailedTrigger, + linqMessageReadTrigger, + linqMessageReceivedTrigger, + linqReactionAddedTrigger, + linqWebhookTrigger, +} from '@/triggers/linq' import { loopsCampaignEmailSentTrigger, loopsEmailClickedTrigger, @@ -598,6 +606,12 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { linear_project_update_created_v2: linearProjectUpdateCreatedV2Trigger, linear_customer_request_created_v2: linearCustomerRequestCreatedV2Trigger, linear_customer_request_updated_v2: linearCustomerRequestUpdatedV2Trigger, + linq_message_received: linqMessageReceivedTrigger, + linq_message_delivered: linqMessageDeliveredTrigger, + linq_message_failed: linqMessageFailedTrigger, + linq_message_read: linqMessageReadTrigger, + linq_reaction_added: linqReactionAddedTrigger, + linq_webhook: linqWebhookTrigger, monday_item_created: mondayItemCreatedTrigger, monday_column_changed: mondayColumnChangedTrigger, monday_status_changed: mondayStatusChangedTrigger,