mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(twilio-block): added twilio block/tools (#201)
* Add twilio block base * Edit fields * Add twilio tool * Fix Serialization of body in request. Add check for form-url-encoded. * Remove Test file * Remove execute in types
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { TwilioIcon } from '@/components/icons'
|
||||
import { BlockCategory, BlockConfig, BlockIcon } from '../types'
|
||||
import { TwilioSMSBlockOutput } from '@/tools/twilio/types'
|
||||
|
||||
export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
|
||||
type: 'twilio_sms',
|
||||
name: 'Twilio SMS',
|
||||
description: 'Send SMS messages via Twilio',
|
||||
longDescription:
|
||||
'Send text messages to single or multiple recipients using the Twilio API.',
|
||||
category: 'tools',
|
||||
bgColor: '#F22F46', // Twilio brand color
|
||||
icon: TwilioIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'phoneNumbers',
|
||||
title: 'Recipient Phone Numbers',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter phone numbers with country code (one per line, e.g., +1234567890)',
|
||||
},
|
||||
{
|
||||
id: 'message',
|
||||
title: 'Message',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
placeholder: 'e.g. "Hello! This is a test message."',
|
||||
},
|
||||
{
|
||||
id: 'accountSid',
|
||||
title: 'Twilio Account SID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Your Twilio Account SID',
|
||||
},
|
||||
{
|
||||
id: 'authToken',
|
||||
title: 'Auth Token',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Your Twilio Auth Token',
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
id: 'fromNumber',
|
||||
title: 'From Twilio Phone Number',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'e.g. +1234567890',
|
||||
}
|
||||
],
|
||||
tools: {
|
||||
access: ['twilio_send_sms'],
|
||||
config: {
|
||||
tool: () => 'twilio_send_sms'
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
phoneNumbers: { type: 'string', required: true },
|
||||
message: { type: 'string', required: true },
|
||||
accountSid: { type: 'string', required: true },
|
||||
authToken: { type: 'string', required: true },
|
||||
fromNumber: { type: 'string', required: true }
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
success: 'boolean',
|
||||
messageId: 'string',
|
||||
status: 'string',
|
||||
error: 'string'
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import { XBlock } from './blocks/x'
|
||||
import { YouTubeBlock } from './blocks/youtube'
|
||||
import { AirtableBlock } from './blocks/airtable'
|
||||
import { BlockConfig } from './types'
|
||||
import { TwilioSMSBlock } from './blocks/twilio'
|
||||
|
||||
// Export blocks for ease of use
|
||||
export {
|
||||
@@ -71,6 +72,7 @@ export {
|
||||
GoogleSheetsBlock,
|
||||
PerplexityBlock,
|
||||
ConfluenceBlock,
|
||||
TwilioSMSBlock,
|
||||
ImageGeneratorBlock,
|
||||
TypeformBlock,
|
||||
}
|
||||
@@ -107,6 +109,7 @@ const blocks: Record<string, BlockConfig> = {
|
||||
supabase: SupabaseBlock,
|
||||
tavily: TavilyBlock,
|
||||
translate: TranslateBlock,
|
||||
twilio_sms: TwilioSMSBlock,
|
||||
typeform: TypeformBlock,
|
||||
vision: VisionBlock,
|
||||
whatsapp: WhatsAppBlock,
|
||||
|
||||
@@ -1734,6 +1734,26 @@ export function ConfluenceIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function TwilioIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="128" cy="128" r="128" fill="none" stroke="white" strokeWidth="21"/>
|
||||
<circle cx="85" cy="85" r="21" fill="white"/>
|
||||
<circle cx="171" cy="85" r="21" fill="white"/>
|
||||
<circle cx="85" cy="171" r="21" fill="white"/>
|
||||
<circle cx="171" cy="171" r="21" fill="white"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ImageIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -44,6 +44,7 @@ import { visionTool } from './vision/vision'
|
||||
import { whatsappSendMessageTool } from './whatsapp'
|
||||
import { xReadTool, xSearchTool, xUserTool, xWriteTool } from './x'
|
||||
import { youtubeSearchTool } from './youtube/search'
|
||||
import { sendSMSTool } from './twilio/sendSMS'
|
||||
import { airtableReadTool, airtableWriteTool, airtableUpdateTool } from './airtable'
|
||||
|
||||
const logger = createLogger('Tools')
|
||||
@@ -110,6 +111,7 @@ export const tools: Record<string, ToolConfig> = {
|
||||
confluence_retrieve: confluenceRetrieveTool,
|
||||
confluence_list: confluenceListTool,
|
||||
confluence_update: confluenceUpdateTool,
|
||||
twilio_send_sms: sendSMSTool,
|
||||
dalle_generate: dalleTool,
|
||||
airtable_read: airtableReadTool,
|
||||
airtable_write: airtableWriteTool,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { sendSMSTool } from './sendSMS'
|
||||
|
||||
export const twilioSendSMSTool = sendSMSTool
|
||||
@@ -0,0 +1,115 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { ToolConfig } from '../types'
|
||||
import { TwilioSMSBlockOutput, TwilioSendSMSParams } from './types'
|
||||
|
||||
const logger = createLogger('Twilio Send SMS Tool')
|
||||
|
||||
export const sendSMSTool: ToolConfig<TwilioSendSMSParams, TwilioSMSBlockOutput> = {
|
||||
id: 'twilio_send_sms',
|
||||
name: 'Twilio Send SMS',
|
||||
description: 'Send text messages to single or multiple recipients using the Twilio API.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
phoneNumbers: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Phone numbers to send the message to, separated by newlines'
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Message to send'
|
||||
},
|
||||
accountSid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Twilio Account SID',
|
||||
requiredForToolCall: true
|
||||
},
|
||||
authToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Twilio Auth Token',
|
||||
requiredForToolCall: true
|
||||
},
|
||||
fromNumber: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Twilio phone number to send the message from'
|
||||
}
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.accountSid) {
|
||||
throw new Error('Twilio Account SID is required')
|
||||
}
|
||||
const url = `https://api.twilio.com/2010-04-01/Accounts/${params.accountSid}/Messages.json`;
|
||||
return url
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accountSid || !params.authToken) {
|
||||
throw new Error('Twilio credentials are required')
|
||||
}
|
||||
// Use Buffer instead of btoa for Node.js compatibility
|
||||
const authToken = Buffer.from(`${params.accountSid}:${params.authToken}`).toString('base64')
|
||||
const headers = {
|
||||
Authorization: `Basic ${authToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
return headers
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.phoneNumbers) {
|
||||
throw new Error('Phone numbers are required but not provided')
|
||||
}
|
||||
if (!params.message) {
|
||||
throw new Error('Message content is required but not provided')
|
||||
}
|
||||
if (!params.fromNumber) {
|
||||
throw new Error('From number is required but not provided')
|
||||
}
|
||||
|
||||
// Get first phone number if multiple are provided
|
||||
const toNumber = params.phoneNumbers.split('\n')[0].trim()
|
||||
|
||||
// Create a URLSearchParams object and convert to string
|
||||
const formData = new URLSearchParams()
|
||||
formData.append('To', toNumber)
|
||||
formData.append('From', params.fromNumber)
|
||||
formData.append('Body', params.message)
|
||||
|
||||
const formDataString = formData.toString()
|
||||
return { body: formDataString }
|
||||
}
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data.error?.message || data.message || `Failed to send SMS (HTTP ${response.status})`
|
||||
logger.error('Twilio API error:', data)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
logger.info('Twilio Response:', data)
|
||||
logger.info('Twilio Response type:', typeof data)
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
messageId: data.sid,
|
||||
status: data.status
|
||||
},
|
||||
error: undefined
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
logger.error('Twilio tool error:', { error })
|
||||
return `SMS sending failed: ${error.message || 'Unknown error occurred'}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ToolResponse } from '../types'
|
||||
|
||||
export interface TwilioSendSMSParams {
|
||||
phoneNumbers: string
|
||||
message: string
|
||||
accountSid: string
|
||||
authToken: string
|
||||
fromNumber: string
|
||||
}
|
||||
|
||||
export interface TwilioSMSBlockOutput extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
messageId?: string
|
||||
status?: string
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -47,10 +47,10 @@ export function formatRequestParams(tool: ToolConfig, params: Record<string, any
|
||||
const hasBody = method !== 'GET' && method !== 'HEAD' && !!tool.request.body
|
||||
const bodyResult = tool.request.body ? tool.request.body(params) : undefined
|
||||
|
||||
// Special handling for NDJSON content type
|
||||
const isNDJSON = headers['Content-Type'] === 'application/x-ndjson'
|
||||
// Special handling for NDJSON content type or 'application/x-www-form-urlencoded'
|
||||
const isPreformattedContent = headers['Content-Type'] === 'application/x-ndjson' || headers['Content-Type'] === 'application/x-www-form-urlencoded'
|
||||
const body = hasBody
|
||||
? isNDJSON && bodyResult
|
||||
? isPreformattedContent && bodyResult
|
||||
? bodyResult.body
|
||||
: JSON.stringify(bodyResult)
|
||||
: undefined
|
||||
|
||||
Reference in New Issue
Block a user