From ffdeeac6dcb37a00167a4478f5a30ccb3aa4117c Mon Sep 17 00:00:00 2001 From: William Astorga Date: Fri, 3 Oct 2025 10:23:24 -0600 Subject: [PATCH] feat(webex): added webex block and tools for messaging --- apps/docs/content/docs/en/tools/webex.mdx | 588 ++++++++++++++++++ .../(landing)/components/footer/footer.tsx | 1 + apps/sim/blocks/blocks/webex.ts | 353 +++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 480 ++++++++++++++ apps/sim/lib/auth.ts | 61 ++ apps/sim/lib/env.ts | 2 + apps/sim/lib/oauth/oauth.ts | 41 ++ apps/sim/tools/registry.ts | 10 + apps/sim/tools/webex/create_message.ts | 168 +++++ apps/sim/tools/webex/edit_message.ts | 122 ++++ apps/sim/tools/webex/index.ts | 6 + apps/sim/tools/webex/list_messages.ts | 132 ++++ apps/sim/tools/webex/list_rooms.ts | 130 ++++ apps/sim/tools/webex/types.ts | 117 ++++ 15 files changed, 2213 insertions(+) create mode 100644 apps/docs/content/docs/en/tools/webex.mdx create mode 100644 apps/sim/blocks/blocks/webex.ts create mode 100644 apps/sim/tools/webex/create_message.ts create mode 100644 apps/sim/tools/webex/edit_message.ts create mode 100644 apps/sim/tools/webex/index.ts create mode 100644 apps/sim/tools/webex/list_messages.ts create mode 100644 apps/sim/tools/webex/list_rooms.ts create mode 100644 apps/sim/tools/webex/types.ts diff --git a/apps/docs/content/docs/en/tools/webex.mdx b/apps/docs/content/docs/en/tools/webex.mdx new file mode 100644 index 0000000000..b52aae634a --- /dev/null +++ b/apps/docs/content/docs/en/tools/webex.mdx @@ -0,0 +1,588 @@ +--- +title: Webex +description: Interact with Webex +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `} +/> + +## Usage Instructions + +Integrate Webex into the workflow. Can fetch rooms and messages, create and edit messages. Requires OAuth. + + + +## Tools + +### `webex_list_rooms` + +List rooms + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | No | Team ID | +| `type` | string | No | Room type | +| `from` | string | No | Made public after this timestamp | +| `to` | string | No | Made public before this timestamp | +| `max` | number | No | Maximum number of items to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or status message | +| `results` | array | Array of room objects | + +### `webex_list_messages` + +List messages + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `roomId` | string | Yes | Room ID | +| `mentionedPeople` | string | No | Mentioned People | +| `before` | string | No | List messages sent before a date and time | +| `beforeMessage` | string | No | Made public before this timestamp | +| `max` | number | No | Maximum number of items to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or status message | +| `results` | array | Array of message objects | + +### `webex_create_message` + +Create message + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `markdown` | string | No | Markdown message | +| `parentId` | string | No | Parent message ID to reply to | +| `roomId` | string | Yes | Room ID | +| `text` | string | No | Text message | +| `toPersonEmail` | string | No | The recipient email address | +| `toPersonId` | string | No | The recipient person ID | +| `files` | string | No | Public URLs to binary files \(comma-separated\), currently only one file may be included | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or status message | +| `results` | object | Message object created | +| `createdId` | string | Created ID | + +### `webex_edit_message` + +Edit message + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `roomId` | string | Yes | Room ID | +| `markdown` | string | No | Markdown message | +| `text` | string | No | Text message | +| `messageId` | string | Yes | Message ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or status message | +| `results` | object | Message object modified | + + + +## Notes + +- Category: `tools` +- Type: `webex` diff --git a/apps/sim/app/(landing)/components/footer/footer.tsx b/apps/sim/app/(landing)/components/footer/footer.tsx index 67a45c6283..3a5c86701f 100644 --- a/apps/sim/app/(landing)/components/footer/footer.tsx +++ b/apps/sim/app/(landing)/components/footer/footer.tsx @@ -85,6 +85,7 @@ const tools = [ 'Typeform', 'Vision', 'Wealthbox', + 'Webex', 'Webhook', 'WhatsApp', 'Wikipedia', diff --git a/apps/sim/blocks/blocks/webex.ts b/apps/sim/blocks/blocks/webex.ts new file mode 100644 index 0000000000..2ffbeeb13a --- /dev/null +++ b/apps/sim/blocks/blocks/webex.ts @@ -0,0 +1,353 @@ +import { WebexIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import type { WebexResponse } from '@/tools/webex/types' + +export const WebexBlock: BlockConfig = { + type: 'webex', + name: 'Webex', + description: 'Interact with Webex', + longDescription: + 'Integrate Webex into the workflow. Can fetch rooms and messages, create and edit messages. Requires OAuth.', + docsLink: 'https://docs.sim.ai/tools/webex', + category: 'tools', + bgColor: '#E0E0E0', // Webex's white color + icon: WebexIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'List Rooms', id: 'list_rooms' }, + { label: 'List Messages', id: 'list_messages' }, + { label: 'Create Message', id: 'create_message' }, + { label: 'Edit Message', id: 'edit_message' }, + ], + value: () => 'list_rooms', + }, + { + id: 'credential', + title: 'Webex Account', + type: 'oauth-input', + layout: 'full', + provider: 'webex', + serviceId: 'webex', + requiredScopes: [ + 'spark:people_read', + 'spark:messages_read', + 'spark:messages_write', + 'spark-compliance:rooms_read', + 'spark:rooms_read', + ], + placeholder: 'Select Webex account', + required: true, + }, + { + id: 'teamId', + title: 'Team ID', + type: 'short-input', + layout: 'full', + placeholder: 'Team ID', + condition: { + field: 'operation', + value: ['list_rooms'], + }, + required: false, + mode: 'advanced', + }, + { + id: 'type', + title: 'Type', + type: 'short-input', + layout: 'full', + placeholder: 'Type', + condition: { + field: 'operation', + value: ['list_rooms'], + }, + required: false, + mode: 'advanced', + }, + { + id: 'from', + title: 'From', + type: 'short-input', + layout: 'full', + placeholder: '2022-10-10T17:00:00.000Z', + condition: { + field: 'operation', + value: ['list_rooms'], + }, + required: false, + mode: 'advanced', + }, + { + id: 'to', + title: 'To', + type: 'short-input', + layout: 'full', + placeholder: '2022-10-10T17:00:00.000Z', + condition: { + field: 'operation', + value: ['list_rooms'], + }, + required: false, + mode: 'advanced', + }, + { + id: 'max', + title: 'Max', + type: 'short-input', + layout: 'full', + placeholder: 'Max', + condition: { + field: 'operation', + value: ['list_rooms', 'list_messages'], + }, + required: false, + }, + // + // Operation: For list messages + { + id: 'roomId', + title: 'Room ID', + type: 'short-input', + layout: 'full', + placeholder: 'Room ID', + condition: { + field: 'operation', + // list_messages and edit_message do required it. + value: ['list_messages', 'create_message', 'edit_message'], + }, + required: true, + }, + // mentionedGroups array of string + { + id: 'mentionedGroups', + title: 'Mentioned Groups', + type: 'short-input', + layout: 'full', + placeholder: 'Group names, comma-separated', + condition: { field: 'operation', value: ['list_messages'] }, + required: false, + mode: 'advanced', + }, + // mentionedPeople array of string + { + id: 'mentionedPeople', + title: 'Mentioned People', + type: 'short-input', + layout: 'full', + placeholder: 'Only me or the person ID of the current user may be specified, comma-separated', + condition: { field: 'operation', value: ['list_messages'] }, + required: false, + mode: 'advanced', + }, + // before string + { + id: 'before', + title: 'Before', + type: 'short-input', + layout: 'full', + placeholder: '2016-04-21T19:01:55.966Z', + condition: { + field: 'operation', + value: ['list_messages'], + }, + required: false, + mode: 'advanced', + }, + // beforeMessage string + { + id: 'beforeMessage', + title: 'Before Message', + type: 'short-input', + layout: 'full', + placeholder: 'Message ID', + condition: { + field: 'operation', + value: ['list_messages'], + }, + required: false, + mode: 'advanced', + }, + // + // Operation: for create messages + // markdown string + { + id: 'markdown', + title: 'Markdown', + type: 'short-input', + layout: 'full', + placeholder: 'Markdown message', + condition: { field: 'operation', value: ['create_message', 'edit_message'] }, + required: false, + }, + // parentId string + { + id: 'parentId', + title: 'Parent ID', + type: 'short-input', + layout: 'full', + placeholder: 'Parent message ID to reply to', + condition: { field: 'operation', value: ['create_message'] }, + required: false, + mode: 'advanced', + }, + // text string + { + id: 'text', + title: 'Text', + type: 'short-input', + layout: 'full', + placeholder: 'Text message', + condition: { field: 'operation', value: ['create_message', 'edit_message'] }, + required: false, + }, + // toPersonEmail string + { + id: 'toPersonEmail', + title: 'To Person Email', + type: 'short-input', + layout: 'full', + placeholder: 'The recipient email address', + condition: { field: 'operation', value: ['create_message'] }, + required: false, + mode: 'advanced', + }, + // toPersonId string + { + id: 'toPersonId', + title: 'To Person ID', + type: 'short-input', + layout: 'full', + placeholder: 'The recipient person ID', + condition: { field: 'operation', value: ['create_message'] }, + required: false, + mode: 'advanced', + }, + // files string[] + { + id: 'files', + title: 'Files', + type: 'short-input', + layout: 'full', + placeholder: 'Public URL to a binary file', + condition: { field: 'operation', value: ['create_message'] }, + required: false, + mode: 'advanced', + }, + // attachments Attachment[] + // + // Operation: for edit message + // messageId* string + { + id: 'messageId', + title: 'Message ID', + type: 'short-input', + layout: 'full', + placeholder: 'Message ID', + condition: { field: 'operation', value: 'edit_message' }, + required: true, + }, + ], + tools: { + access: [ + 'webex_list_rooms', + 'webex_list_messages', + 'webex_create_message', + 'webex_edit_message', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'list_rooms': + return 'webex_list_rooms' + case 'list_messages': + return 'webex_list_messages' + case 'create_message': + return 'webex_create_message' + case 'edit_message': + return 'webex_edit_message' + default: + throw new Error(`Invalid Webex operation: ${params.operation}`) + } + }, + params: (params) => { + const { credential, ...rest } = params + + // Convert string values to appropriate types + const parsedParams: Record = { + credential: credential, + } + + // Add other params + Object.keys(rest).forEach((key) => { + const value = rest[key] + // Convert numeric strings to numbers where appropriate + if (key === 'max' && value) { + parsedParams[key] = Number.parseInt(value as string, 10) + } + // Handle mediaIds conversion from comma-separated string to array + else if ( + (key === 'files' || key === 'mentionedPeople' || key === 'mentionedGroups') && + typeof value === 'string' + ) { + parsedParams[key] = value + .split(',') + .map((id) => id.trim()) + .filter((id) => id !== '') + } + // Keep other values as is + else { + parsedParams[key] = value + } + }) + return parsedParams + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Webex access token' }, + // + teamId: { type: 'string', description: 'Team ID' }, + type: { type: 'string', description: 'Room type, "direct" or "group"' }, + from: { type: 'string', description: 'Made public after this timestamp' }, + to: { type: 'string', description: 'Made public before this timestamp' }, + max: { type: 'number', description: 'Maximum number of items to retrieve' }, + // + roomId: { type: 'string', description: 'Room ID' }, + mentionedGroups: { + type: 'string', + description: 'Group names for the groups mentioned in the message.', + }, + mentionedPeople: { + type: 'string', + description: 'People IDs for anyone mentioned in the message', + }, + before: { type: 'string', description: 'List messages sent before a date and time' }, + beforeMessage: { type: 'string', description: 'List messages sent before a message, by ID' }, + // + markdown: { type: 'string', description: 'Markdown message' }, + parentId: { type: 'string', description: 'Parent ID message' }, + text: { type: 'string', description: 'Text message' }, + toPersonEmail: { type: 'string', description: 'The recipient email address' }, + toPersonId: { type: 'string', description: 'The recipient person ID' }, + files: { + type: 'string', + description: + 'Public URLs to binary files (comma-separated), currently only one file may be included', + }, + // + messageId: { type: 'string', description: 'Message ID' }, + }, + outputs: { + // Common outputs + message: { type: 'string', description: 'Response message' }, + results: { type: 'json', description: 'Operation results' }, + // + createdId: { type: 'string', description: 'Created ID' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 1f30aea2c2..3d6e95f34d 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -79,6 +79,7 @@ import { TwilioSMSBlock } from '@/blocks/blocks/twilio' import { TypeformBlock } from '@/blocks/blocks/typeform' import { VisionBlock } from '@/blocks/blocks/vision' import { WealthboxBlock } from '@/blocks/blocks/wealthbox' +import { WebexBlock } from '@/blocks/blocks/webex' import { WebhookBlock } from '@/blocks/blocks/webhook' import { WhatsAppBlock } from '@/blocks/blocks/whatsapp' import { WikipediaBlock } from '@/blocks/blocks/wikipedia' @@ -167,6 +168,7 @@ export const registry: Record = { typeform: TypeformBlock, vision: VisionBlock, wealthbox: WealthboxBlock, + webex: WebexBlock, webhook: WebhookBlock, whatsapp: WhatsAppBlock, wikipedia: WikipediaBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 39f356cc61..eaa3f61d71 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3009,6 +3009,486 @@ export function WealthboxIcon(props: SVGProps) { ) } +export function WebexIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + export function WebhookIcon(props: SVGProps) { return ( { + try { + const response = await fetch('https://webexapis.com/v1/people/me', { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) + + if (!response.ok) { + logger.error('Error fetching Webex user info:', { + status: response.status, + statusText: response.statusText, + }) + return null + } + + const profile = await response.json() + + if (!profile) { + logger.error('Invalid Webex profile response:', profile) + return null + } + + const now = new Date() + + const emails = profile.emails + const email = emails && emails.length > 0 && emails[0] + + return { + id: profile.id, + name: profile.displayName, + email: email, + image: null, + emailVerified: profile.type === 'person' || false, + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in Webex getUserInfo:', { error }) + return null + } + }, + }, // Supabase provider { diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index e3b5c05b1d..1b19c40e13 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -192,6 +192,8 @@ export const env = createEnv({ HUBSPOT_CLIENT_SECRET: z.string().optional(), // HubSpot OAuth client secret WEALTHBOX_CLIENT_ID: z.string().optional(), // WealthBox OAuth client ID WEALTHBOX_CLIENT_SECRET: z.string().optional(), // WealthBox OAuth client secret + WEBEX_CLIENT_ID: z.string().optional(), // Webex OAuth client ID + WEBEX_CLIENT_SECRET: z.string().optional(), // Webex OAuth client secret LINEAR_CLIENT_ID: z.string().optional(), // Linear OAuth client ID LINEAR_CLIENT_SECRET: z.string().optional(), // Linear OAuth client secret SLACK_CLIENT_ID: z.string().optional(), // Slack OAuth client ID diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 803100b752..7d954e4745 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -25,6 +25,7 @@ import { SlackIcon, SupabaseIcon, WealthboxIcon, + WebexIcon, xIcon, } from '@/components/icons' import { env } from '@/lib/env' @@ -47,6 +48,7 @@ export type OAuthProvider = | 'slack' | 'reddit' | 'wealthbox' + | 'webex' | string export type OAuthService = @@ -75,6 +77,7 @@ export type OAuthService = | 'slack' | 'reddit' | 'wealthbox' + | 'webex' | 'onedrive' export interface OAuthProviderConfig { id: OAuthProvider @@ -500,6 +503,29 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'wealthbox', }, + webex: { + id: 'webex', + name: 'Webex', + icon: (props) => WebexIcon(props), + services: { + webex: { + id: 'webex', + name: 'Webex', + description: 'Manage Webex items.', + providerId: 'webex', + icon: (props) => WebexIcon(props), + baseProviderIcon: (props) => WebexIcon(props), + scopes: [ + 'spark:people_read', + 'spark:messages_read', + 'spark:messages_write', + 'spark-compliance:rooms_read', + 'spark:rooms_read', + ], + }, + }, + defaultService: 'webex', + }, } // Helper function to get a service by provider and service ID @@ -584,6 +610,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[] return 'reddit' } else if (provider === 'wealthbox') { return 'wealthbox' + } else if (provider === 'webex') { + return 'webex' } return providerConfig.defaultService @@ -885,6 +913,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: true, } } + case 'webex': { + const { clientId, clientSecret } = getCredentials( + env.WEBEX_CLIENT_ID, + env.WEBEX_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://webexapis.com/v1/access_token', + clientId, + clientSecret, + useBasicAuth: false, + supportsRefreshTokenRotation: true, + } + } default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 9fc727f15a..c35c493b11 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -188,6 +188,12 @@ import { wealthboxWriteNoteTool, wealthboxWriteTaskTool, } from '@/tools/wealthbox' +import { + webexListRoomsTool, + webexListMessagesTool, + webexCreateMessageTool, + webexEditMessageTool, +} from '@/tools/webex' import { whatsappSendMessageTool } from '@/tools/whatsapp' import { wikipediaPageContentTool, @@ -361,6 +367,10 @@ export const tools: Record = { wealthbox_write_task: wealthboxWriteTaskTool, wealthbox_read_note: wealthboxReadNoteTool, wealthbox_write_note: wealthboxWriteNoteTool, + webex_list_rooms: webexListRoomsTool, + webex_list_messages: webexListMessagesTool, + webex_create_message: webexCreateMessageTool, + webex_edit_message: webexEditMessageTool, wikipedia_summary: wikipediaPageSummaryTool, wikipedia_search: wikipediaSearchTool, wikipedia_content: wikipediaPageContentTool, diff --git a/apps/sim/tools/webex/create_message.ts b/apps/sim/tools/webex/create_message.ts new file mode 100644 index 0000000000..53e9232f55 --- /dev/null +++ b/apps/sim/tools/webex/create_message.ts @@ -0,0 +1,168 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { ToolConfig } from '@/tools/types' +import type { + WebexCreateMessageParams, + WebexCreateMessageResponse, + WebexSingleMessage, +} from '@/tools/webex/types' + +const logger = createLogger('WebexCreateMessage') + +export const webexCreateMessageTool: ToolConfig< + WebexCreateMessageParams, + WebexCreateMessageResponse +> = { + id: 'webex_create_message', + name: 'Webex Create Message', + description: 'Create message', + version: '1.0.0', + + oauth: { + required: true, + provider: 'webex', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Webex API', + }, + markdown: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Markdown message', + }, + parentId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Parent message ID to reply to', + }, + roomId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Room ID', + }, + text: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Text message', + }, + toPersonEmail: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The recipient email address', + }, + toPersonId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The recipient person ID', + }, + files: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Public URLs to binary files (comma-separated), currently only one file may be included', + }, + }, + request: { + url: (params) => { + const baseUrl = `https://webexapis.com/v1/messages` + return baseUrl + }, + method: 'POST', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params: WebexCreateMessageParams): Record => { + // Helper function to parse comma-separated files + const parseFile = (fileString?: string) => { + if (!fileString) return undefined + const files = fileString + .split(',') + .map((item) => item.trim()) + .filter((item) => item.length > 0) + if (files.length > 0) { + // It only picks one as only one is supported + return [files[0]] + } + return undefined + } + + const replyBody: Record = {} + // Only include allowed params, handle 'files' separately + const { + accessToken, // omit + files, + ...rest + } = params + + if (files) { + replyBody.files = parseFile(files) + } + + Object.entries(rest).forEach(([key, value]) => { + // Checks for truthiness, excluding parameters when they do not have value, all of them are treated as strings + if (value) { + replyBody[key] = value + } + }) + + return replyBody + }, + }, + transformResponse: async (response: Response) => { + logger.info('Received response status: ', response.status) + + try { + const data: WebexSingleMessage = await response.json() + // API returns messages in 'items' array + const item = data || {} + + if (Object.keys(item).length === 0) { + return { + success: true, + output: { + message: 'No new message created.', + results: {}, + }, + } + } + + return { + success: true, + output: { + message: `Successfully created ${item.id} message`, + createdId: item.id, + results: item, + }, + } + } catch (error) { + logger.error('Error processing response:', { + error, + }) + throw error + } + }, + outputs: { + message: { type: 'string', description: 'Success or status message' }, + results: { type: 'object', description: 'Message object created' }, + createdId: { type: 'string', description: 'Created ID' }, + }, +} diff --git a/apps/sim/tools/webex/edit_message.ts b/apps/sim/tools/webex/edit_message.ts new file mode 100644 index 0000000000..932b7a6769 --- /dev/null +++ b/apps/sim/tools/webex/edit_message.ts @@ -0,0 +1,122 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { ToolConfig } from '@/tools/types' +import type { + WebexEditMessageParams, + WebexEditMessageResponse, + WebexSingleMessage, +} from '@/tools/webex/types' + +const logger = createLogger('WebexEditMessage') + +export const webexEditMessageTool: ToolConfig = { + id: 'webex_edit_message', + name: 'Webex Edit Message', + description: 'Edit message', + version: '1.0.0', + + oauth: { + required: true, + provider: 'webex', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Webex API', + }, + roomId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Room ID', + }, + markdown: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Markdown message', + }, + text: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Text message', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Message ID', + }, + }, + request: { + url: (params) => { + if (!params.messageId) { + throw new Error('Path parameter "messageId" is required') + } + const baseUrl = `https://webexapis.com/v1/messages/${params.messageId}` + return baseUrl + }, + method: 'PUT', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params: WebexEditMessageParams): Record => { + const { accessToken, messageId, ...rest } = params + const replyBody: Record = {} + Object.entries(rest).forEach(([key, value]) => { + // Checks for truthiness, excluding parameters when they do not have value, all of them are treated as strings + if (value) { + replyBody[key] = value + } + }) + return replyBody + }, + }, + transformResponse: async (response: Response) => { + logger.info('Received response status: ', response.status) + + try { + const data: WebexSingleMessage = await response.json() + // API returns messages in 'items' array + const item = data || {} + + if (Object.keys(item).length === 0) { + return { + success: true, + output: { + message: 'No message modified.', + results: {}, + }, + } + } + + return { + success: true, + output: { + message: `Successfully modified ${item.id} message`, + results: item, + }, + } + } catch (error) { + logger.error('Error processing response:', { + error, + }) + throw error + } + }, + outputs: { + message: { type: 'string', description: 'Success or status message' }, + results: { type: 'object', description: 'Message object modified' }, + }, +} diff --git a/apps/sim/tools/webex/index.ts b/apps/sim/tools/webex/index.ts new file mode 100644 index 0000000000..f8180c4172 --- /dev/null +++ b/apps/sim/tools/webex/index.ts @@ -0,0 +1,6 @@ +import { webexListRoomsTool } from '@/tools/webex/list_rooms' +import { webexListMessagesTool } from '@/tools/webex/list_messages' +import { webexCreateMessageTool } from '@/tools/webex/create_message' +import { webexEditMessageTool } from '@/tools/webex/edit_message' + +export { webexListRoomsTool, webexListMessagesTool, webexCreateMessageTool, webexEditMessageTool } diff --git a/apps/sim/tools/webex/list_messages.ts b/apps/sim/tools/webex/list_messages.ts new file mode 100644 index 0000000000..6b5b3075b5 --- /dev/null +++ b/apps/sim/tools/webex/list_messages.ts @@ -0,0 +1,132 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { ToolConfig } from '@/tools/types' +import type { + WebexListMessages, + WebexListMessagesParams, + WebexListMessagesResponse, +} from '@/tools/webex/types' + +const logger = createLogger('WebexListMessages') + +export const webexListMessagesTool: ToolConfig = + { + id: 'webex_list_messages', + name: 'Webex List Messages', + description: 'List messages', + version: '1.0.0', + + oauth: { + required: true, + provider: 'webex', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Webex API', + }, + roomId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Room ID', + }, + mentionedPeople: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Mentioned People', + }, + before: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'List messages sent before a date and time', + }, + beforeMessage: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Made public before this timestamp', + }, + max: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of items to retrieve', + }, + }, + request: { + url: (params: WebexListMessagesParams) => { + let baseUrl = `https://webexapis.com/v1/messages` + const searchParams = new URLSearchParams() + if (!params.roomId) { + throw new Error('RoomId is required') + } + const { accessToken, ...rest } = params + Object.entries(rest).forEach(([key, value]) => { + /** Checks for truthiness, excluding parameters when they do not have value + * many of them are treated as strings + * 'max' is a number but it does not allow 0 as value + **/ + if (value) { + searchParams.set(key, String(value)) + } + }) + const paramsString = searchParams.toString() + if (paramsString) { + baseUrl += `?${paramsString}` + } + return baseUrl + }, + method: 'GET', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + transformResponse: async (response: Response) => { + logger.info('Received response status: ', response.status) + + try { + const data: WebexListMessages = await response.json() + // API returns messages in 'items' array + const items = data.items || [] + + if (items.length === 0) { + return { + success: true, + output: { + message: 'No messages found.', + results: [], + }, + } + } + + return { + success: true, + output: { + message: `Successfully read ${items.length} message(s)`, + results: items, + }, + } + } catch (error) { + logger.error('Error processing response:', { + error, + }) + throw error + } + }, + outputs: { + message: { type: 'string', description: 'Success or status message' }, + results: { type: 'array', description: 'Array of message objects' }, + }, + } diff --git a/apps/sim/tools/webex/list_rooms.ts b/apps/sim/tools/webex/list_rooms.ts new file mode 100644 index 0000000000..8f4bd0949f --- /dev/null +++ b/apps/sim/tools/webex/list_rooms.ts @@ -0,0 +1,130 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { ToolConfig } from '@/tools/types' +import type { + WebexListRooms, + WebexListRoomsParams, + WebexListRoomsResponse, +} from '@/tools/webex/types' + +const logger = createLogger('WebexListRooms') + +export const webexListRoomsTool: ToolConfig = { + id: 'webex_list_rooms', + name: 'Webex List Rooms', + description: 'List rooms', + version: '1.0.0', + + oauth: { + required: true, + provider: 'webex', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Webex API', + }, + teamId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Team ID', + }, + type: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Room type', + }, + from: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Made public after this timestamp', + }, + to: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Made public before this timestamp', + }, + max: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of items to retrieve', + }, + }, + request: { + url: (params: WebexListRoomsParams) => { + let baseUrl = `https://webexapis.com/v1/rooms` + const searchParams = new URLSearchParams() + const { accessToken, ...rest } = params + Object.entries(rest).forEach(([key, value]) => { + /** Checks for truthiness, excluding parameters when they do not have value + * many of them are treated as strings + * 'max' is a number but it does not allow 0 as value + **/ + if (value) { + searchParams.set(key, String(value)) + } + }) + const paramsString = searchParams.toString() + if (paramsString) { + baseUrl += `?${paramsString}` + } + return baseUrl + }, + method: 'GET', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + transformResponse: async (response: Response) => { + logger.info('Received response status: ', response.status) + + try { + const data: WebexListRooms = await response.json() + logger.info('Response parsed successfully') + + // API returns rooms in 'items' array + const items = data.items || [] + + if (items.length === 0) { + return { + success: true, + output: { + message: 'No rooms found.', + results: [], + }, + } + } + + return { + success: true, + output: { + message: `Successfully read ${items.length} room(s)`, + results: items, + }, + } + } catch (error) { + logger.error('Error processing response:', { + error, + }) + throw error + } + }, + outputs: { + message: { type: 'string', description: 'Success or status message' }, + results: { type: 'array', description: 'Array of room objects' }, + }, +} diff --git a/apps/sim/tools/webex/types.ts b/apps/sim/tools/webex/types.ts new file mode 100644 index 0000000000..b040d85045 --- /dev/null +++ b/apps/sim/tools/webex/types.ts @@ -0,0 +1,117 @@ +import type { ToolResponse } from '@/tools/types' + +export interface WebexListRoomsParams { + accessToken: string + teamId?: string + type?: string + from?: string + to?: string + max?: number +} + +export interface WebexListRoomsResponse extends ToolResponse { + output: { + message: string + results: WebexListRoom[] + } +} + +export interface WebexListRoom { + id?: string + classificationId?: string + created?: string + creatorId?: string + description?: string + lastActivity?: string + madePublic?: string + ownerId?: string + teamId?: string + title?: string + type?: string + isAnnouncementOnly?: boolean + isLocked?: boolean + isPublic?: boolean + isReadOnly?: boolean +} + +export interface WebexListRooms { + items: WebexListRoom[] +} + +export interface WebexListMessagesParams { + accessToken: string + roomId: string + max?: number + mentionedPeople?: string + before?: string + beforeMessage?: string +} + +export interface WebexListMessages { + items: WebexSingleMessage[] +} + +export interface WebexListMessagesResponse extends ToolResponse { + output: { + message: string + results: WebexSingleMessage[] + } +} + +export interface WebexSingleMessage { + created?: string + html?: string + id?: string + markdown?: string + parentId?: string + personEmail?: string + personId?: string + roomId?: string + roomType?: string + text?: string + updated?: string + isVoiceClip?: boolean + files?: string[] + mentionedGroups?: string[] + mentionedPeople?: string[] +} + +export interface WebexCreateMessageParams { + accessToken: string + markdown?: string + parentId?: string + roomId?: string + text?: string + toPersonEmail?: string + toPersonId?: string + files?: string +} + +export interface WebexCreateMessageResponse extends ToolResponse { + output: { + message: string + results: WebexSingleMessage + createdId?: string + } +} + +export interface WebexEditMessageParams { + accessToken: string + messageId: string + roomId: string + markdown?: string + text?: string +} + +export interface WebexEditMessageResponse extends ToolResponse { + output: { + message: string + results: WebexSingleMessage + } +} + +export type WebexResponse = + | WebexListMessagesResponse + | WebexListRoomsResponse + | WebexCreateMessageResponse + | WebexEditMessageResponse