diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 900ee4e13b..df56c4f0f8 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1346,6 +1346,37 @@ export function GoogleCalendarIcon(props: SVGProps) { ) } +export function GoogleChatIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function GoogleTasksIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index a69b0d90f5..8764115058 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -49,6 +49,7 @@ import { GoogleBigQueryIcon, GoogleBooksIcon, GoogleCalendarIcon, + GoogleChatIcon, GoogleContactsIcon, GoogleDocsIcon, GoogleDriveIcon, @@ -205,6 +206,7 @@ export const blockTypeToIconMap: Record = { google_bigquery: GoogleBigQueryIcon, google_books: GoogleBooksIcon, google_calendar_v2: GoogleCalendarIcon, + google_chat: GoogleChatIcon, google_contacts: GoogleContactsIcon, google_docs: GoogleDocsIcon, google_drive: GoogleDriveIcon, diff --git a/apps/docs/content/docs/en/tools/google_chat.mdx b/apps/docs/content/docs/en/tools/google_chat.mdx new file mode 100644 index 0000000000..ed9c49ab07 --- /dev/null +++ b/apps/docs/content/docs/en/tools/google_chat.mdx @@ -0,0 +1,62 @@ +--- +title: Google Chat +description: Send messages and manage Google Chat spaces +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate with Google Chat to send messages to spaces and list available spaces using OAuth. + + + +## Tools + +### `google_chat_send_message` + +Send a message to a Google Chat space + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spaceId` | string | Yes | The Google Chat space ID \(e.g., spaces/AAAA1234\) | +| `message` | string | Yes | Message text to send | +| `threadKey` | string | No | Thread key for sending a threaded reply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `messageName` | string | Google Chat message resource name | +| `spaceName` | string | Space the message was sent to | +| `threadName` | string | Thread resource name | +| `text` | string | Message text that was sent | +| `createTime` | string | Timestamp when the message was created | + +### `google_chat_list_spaces` + +List Google Chat spaces the user is a member of + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `pageSize` | number | No | Maximum number of spaces to return \(default 100, max 1000\) | +| `pageToken` | string | No | Token for fetching the next page of results | +| `filter` | string | No | Filter by space type \(e.g., spaceType = "SPACE", spaceType = "GROUP_CHAT" OR spaceType = "DIRECT_MESSAGE"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `spaces` | json | Array of Google Chat space objects | +| `nextPageToken` | string | Token for fetching the next page of results | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 3a1917fc19..fb3e6d60d7 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -44,6 +44,7 @@ "google_bigquery", "google_books", "google_calendar", + "google_chat", "google_contacts", "google_docs", "google_drive", diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index fb340a877b..28b847fa2d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -55,6 +55,8 @@ const SCOPE_DESCRIPTIONS: Record = { 'https://www.googleapis.com/auth/admin.directory.group.readonly': 'View Google Workspace groups', 'https://www.googleapis.com/auth/admin.directory.group.member.readonly': 'View Google Workspace group memberships', + 'https://www.googleapis.com/auth/chat.spaces.readonly': 'View Google Chat spaces', + 'https://www.googleapis.com/auth/chat.messages.create': 'Send messages in Google Chat', 'https://www.googleapis.com/auth/cloud-platform': 'Full access to Google Cloud resources for Vertex AI', 'read:confluence-content.all': 'Read all Confluence content', diff --git a/apps/sim/blocks/blocks/google_chat.ts b/apps/sim/blocks/blocks/google_chat.ts new file mode 100644 index 0000000000..41ea1bdad6 --- /dev/null +++ b/apps/sim/blocks/blocks/google_chat.ts @@ -0,0 +1,150 @@ +import { GoogleChatIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { GoogleChatResponse } from '@/tools/google_chat/types' + +export const GoogleChatBlock: BlockConfig = { + type: 'google_chat', + name: 'Google Chat', + description: 'Send messages and manage Google Chat spaces', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate with Google Chat to send messages to spaces and list available spaces using OAuth.', + docsLink: 'https://docs.sim.ai/tools/google_chat', + category: 'tools', + bgColor: '#E0E0E0', + icon: GoogleChatIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Send Message', id: 'send_message' }, + { label: 'List Spaces', id: 'list_spaces' }, + ], + value: () => 'send_message', + }, + { + id: 'credential', + title: 'Google Chat Account', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'basic', + required: true, + serviceId: 'google-chat', + requiredScopes: [ + 'https://www.googleapis.com/auth/chat.spaces.readonly', + 'https://www.googleapis.com/auth/chat.messages.create', + ], + placeholder: 'Select Google account', + }, + { + id: 'manualCredential', + title: 'Google Chat Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + }, + { + id: 'spaceId', + title: 'Space ID', + type: 'short-input', + placeholder: 'e.g., spaces/AAAA1234 or AAAA1234', + required: { field: 'operation', value: 'send_message' }, + condition: { field: 'operation', value: 'send_message' }, + }, + { + id: 'message', + title: 'Message', + type: 'long-input', + placeholder: 'Enter your message', + required: { field: 'operation', value: 'send_message' }, + condition: { field: 'operation', value: 'send_message' }, + }, + { + id: 'threadKey', + title: 'Thread Key', + type: 'short-input', + placeholder: 'Optional thread key for threaded replies', + condition: { field: 'operation', value: 'send_message' }, + mode: 'advanced', + }, + { + id: 'filter', + title: 'Filter', + type: 'short-input', + placeholder: 'e.g., spaceType = "SPACE"', + condition: { field: 'operation', value: 'list_spaces' }, + mode: 'advanced', + }, + { + id: 'pageSize', + title: 'Max Results', + type: 'short-input', + placeholder: 'Maximum spaces to return (default 100)', + condition: { field: 'operation', value: 'list_spaces' }, + mode: 'advanced', + }, + ], + tools: { + access: ['google_chat_send_message', 'google_chat_list_spaces'], + config: { + tool: (params) => { + switch (params.operation) { + case 'send_message': + return 'google_chat_send_message' + case 'list_spaces': + return 'google_chat_list_spaces' + default: + throw new Error(`Invalid Google Chat operation: ${params.operation}`) + } + }, + params: (params) => { + const { oauthCredential, operation, ...rest } = params + + switch (operation) { + case 'send_message': + return { + oauthCredential, + spaceId: rest.spaceId, + message: rest.message, + threadKey: rest.threadKey, + } + case 'list_spaces': + return { + oauthCredential, + pageSize: rest.pageSize ? Number(rest.pageSize) : undefined, + filter: rest.filter, + } + default: + return { oauthCredential, ...rest } + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + oauthCredential: { type: 'string', description: 'Google Chat OAuth credential' }, + spaceId: { type: 'string', description: 'Google Chat space ID' }, + message: { type: 'string', description: 'Message text to send' }, + threadKey: { type: 'string', description: 'Thread key for threaded replies' }, + filter: { type: 'string', description: 'Filter by space type' }, + pageSize: { type: 'number', description: 'Maximum number of spaces to return' }, + }, + outputs: { + messageName: { type: 'string', description: 'Message resource name' }, + spaceName: { type: 'string', description: 'Space resource name' }, + threadName: { type: 'string', description: 'Thread resource name' }, + text: { type: 'string', description: 'Message text that was sent' }, + createTime: { type: 'string', description: 'Message creation timestamp' }, + spaces: { + type: 'json', + description: + 'Array of Google Chat space objects (name, displayName, spaceType, singleUserBotDm, threaded, type)', + }, + nextPageToken: { type: 'string', description: 'Token for next page of results' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 3b423aad70..e5eb2b6c91 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -50,6 +50,7 @@ import { GoogleSearchBlock } from '@/blocks/blocks/google' import { GoogleBigQueryBlock } from '@/blocks/blocks/google_bigquery' import { GoogleBooksBlock } from '@/blocks/blocks/google_books' import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar' +import { GoogleChatBlock } from '@/blocks/blocks/google_chat' import { GoogleContactsBlock } from '@/blocks/blocks/google_contacts' import { GoogleDocsBlock } from '@/blocks/blocks/google_docs' import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' @@ -242,6 +243,7 @@ export const registry: Record = { gmail: GmailBlock, gmail_v2: GmailV2Block, google_calendar: GoogleCalendarBlock, + google_chat: GoogleChatBlock, google_calendar_v2: GoogleCalendarV2Block, google_books: GoogleBooksBlock, google_contacts: GoogleContactsBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 900ee4e13b..df56c4f0f8 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1346,6 +1346,37 @@ export function GoogleCalendarIcon(props: SVGProps) { ) } +export function GoogleChatIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function GoogleTasksIcon(props: SVGProps) { return ( diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index b0f498b0b4..53818730a6 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -488,6 +488,7 @@ export const auth = betterAuth({ 'google-bigquery', 'google-vault', 'google-groups', + 'google-chat', 'google-tasks', 'vertex-ai', 'github-repo', @@ -1232,6 +1233,47 @@ export const auth = betterAuth({ }, }, + { + providerId: 'google-chat', + clientId: env.GOOGLE_CLIENT_ID as string, + clientSecret: env.GOOGLE_CLIENT_SECRET as string, + discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration', + accessType: 'offline', + scopes: [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/chat.spaces.readonly', + 'https://www.googleapis.com/auth/chat.messages.create', + ], + prompt: 'consent', + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-chat`, + getUserInfo: async (tokens) => { + try { + const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', { + headers: { Authorization: `Bearer ${tokens.accessToken}` }, + }) + if (!response.ok) { + logger.error('Failed to fetch Google user info', { status: response.status }) + throw new Error(`Failed to fetch Google user info: ${response.statusText}`) + } + const profile = await response.json() + const now = new Date() + return { + id: `${profile.sub}-${crypto.randomUUID()}`, + name: profile.name || 'Google User', + email: profile.email, + image: profile.picture || undefined, + emailVerified: profile.email_verified || false, + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in Google getUserInfo', { error }) + throw error + } + }, + }, + { providerId: 'google-tasks', clientId: env.GOOGLE_CLIENT_ID as string, diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index e1d3f862aa..9240e820e1 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -10,6 +10,7 @@ import { GmailIcon, GoogleBigQueryIcon, GoogleCalendarIcon, + GoogleChatIcon, GoogleContactsIcon, GoogleDocsIcon, GoogleDriveIcon, @@ -168,6 +169,17 @@ export const OAUTH_PROVIDERS: Record = { 'https://www.googleapis.com/auth/admin.directory.group.member', ], }, + 'google-chat': { + name: 'Google Chat', + description: 'Send messages and manage Google Chat spaces.', + providerId: 'google-chat', + icon: GoogleChatIcon, + baseProviderIcon: GoogleIcon, + scopes: [ + 'https://www.googleapis.com/auth/chat.spaces.readonly', + 'https://www.googleapis.com/auth/chat.messages.create', + ], + }, 'vertex-ai': { name: 'Vertex AI', description: 'Access Google Cloud Vertex AI for Gemini models with OAuth.', diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index 2297c5df30..756b7b9533 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -13,6 +13,7 @@ export type OAuthProvider = | 'google-vault' | 'google-forms' | 'google-groups' + | 'google-chat' | 'vertex-ai' | 'github' | 'github-repo' @@ -61,6 +62,7 @@ export type OAuthService = | 'google-vault' | 'google-forms' | 'google-groups' + | 'google-chat' | 'vertex-ai' | 'github' | 'x' diff --git a/apps/sim/tools/google_chat/index.ts b/apps/sim/tools/google_chat/index.ts new file mode 100644 index 0000000000..16a69be66d --- /dev/null +++ b/apps/sim/tools/google_chat/index.ts @@ -0,0 +1,6 @@ +import { listSpacesTool } from './list_spaces' +import { sendMessageTool } from './send_message' + +export const googleChatSendMessageTool = sendMessageTool +export const googleChatListSpacesTool = listSpacesTool +export * from './types' diff --git a/apps/sim/tools/google_chat/list_spaces.ts b/apps/sim/tools/google_chat/list_spaces.ts new file mode 100644 index 0000000000..801f4a733e --- /dev/null +++ b/apps/sim/tools/google_chat/list_spaces.ts @@ -0,0 +1,89 @@ +import type { GoogleChatListSpacesParams, GoogleChatResponse } from '@/tools/google_chat/types' +import type { ToolConfig } from '@/tools/types' + +export const listSpacesTool: ToolConfig = { + id: 'google_chat_list_spaces', + name: 'Google Chat List Spaces', + description: 'List Google Chat spaces the user is a member of', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-chat', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of spaces to return (default 100, max 1000)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for fetching the next page of results', + }, + filter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Filter by space type (e.g., spaceType = "SPACE", spaceType = "GROUP_CHAT" OR spaceType = "DIRECT_MESSAGE")', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://chat.googleapis.com/v1/spaces') + if (params.pageSize) { + url.searchParams.set('pageSize', String(params.pageSize)) + } + if (params.pageToken) { + url.searchParams.set('pageToken', params.pageToken) + } + if (params.filter) { + url.searchParams.set('filter', params.filter) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to list spaces') + } + return { + success: true, + output: { + spaces: data.spaces ?? [], + nextPageToken: data.nextPageToken ?? null, + }, + } + }, + + outputs: { + spaces: { + type: 'json', + description: + 'Array of Google Chat space objects (name, displayName, spaceType, singleUserBotDm, threaded, type)', + }, + nextPageToken: { + type: 'string', + description: 'Token for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_chat/send_message.ts b/apps/sim/tools/google_chat/send_message.ts new file mode 100644 index 0000000000..504e6fee8f --- /dev/null +++ b/apps/sim/tools/google_chat/send_message.ts @@ -0,0 +1,95 @@ +import type { GoogleChatResponse, GoogleChatSendMessageParams } from '@/tools/google_chat/types' +import type { ToolConfig } from '@/tools/types' + +export const sendMessageTool: ToolConfig = { + id: 'google_chat_send_message', + name: 'Google Chat Send Message', + description: 'Send a message to a Google Chat space', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-chat', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Google Chat space ID (e.g., spaces/AAAA1234)', + }, + message: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Message text to send', + }, + threadKey: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Thread key for sending a threaded reply', + }, + }, + + request: { + url: (params) => { + const spaceId = params.spaceId?.trim() + if (!spaceId) { + throw new Error('Space ID is required') + } + const spaceName = spaceId.startsWith('spaces/') ? spaceId : `spaces/${spaceId}` + const url = new URL(`https://chat.googleapis.com/v1/${spaceName}/messages`) + if (params.threadKey) { + url.searchParams.set('messageReplyOption', 'REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD') + } + return url.toString() + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + text: params.message, + } + if (params.threadKey) { + body.thread = { threadKey: params.threadKey } + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to send message') + } + return { + success: true, + output: { + messageName: data.name ?? null, + spaceName: data.space?.name ?? null, + threadName: data.thread?.name ?? null, + text: data.text ?? null, + createTime: data.createTime ?? null, + }, + } + }, + + outputs: { + messageName: { type: 'string', description: 'Google Chat message resource name' }, + spaceName: { type: 'string', description: 'Space the message was sent to' }, + threadName: { type: 'string', description: 'Thread resource name', optional: true }, + text: { type: 'string', description: 'Message text that was sent' }, + createTime: { type: 'string', description: 'Timestamp when the message was created' }, + }, +} diff --git a/apps/sim/tools/google_chat/types.ts b/apps/sim/tools/google_chat/types.ts new file mode 100644 index 0000000000..f41c339090 --- /dev/null +++ b/apps/sim/tools/google_chat/types.ts @@ -0,0 +1,33 @@ +import type { ToolResponse } from '@/tools/types' + +/** + * Common parameters for Google Chat API calls + */ +export interface GoogleChatCommonParams { + accessToken: string +} + +/** + * Parameters for sending a message to a Google Chat space + */ +export interface GoogleChatSendMessageParams extends GoogleChatCommonParams { + spaceId: string + message: string + threadKey?: string +} + +/** + * Parameters for listing Google Chat spaces + */ +export interface GoogleChatListSpacesParams extends GoogleChatCommonParams { + pageSize?: number + pageToken?: string + filter?: string +} + +/** + * Standard response for Google Chat operations + */ +export interface GoogleChatResponse extends ToolResponse { + output: Record +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index a0ac0eda4f..737e17f1a2 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -714,6 +714,7 @@ import { googleCalendarUpdateTool, googleCalendarUpdateV2Tool, } from '@/tools/google_calendar' +import { googleChatListSpacesTool, googleChatSendMessageTool } from '@/tools/google_chat' import { googleContactsCreateTool, googleContactsDeleteTool, @@ -3150,6 +3151,8 @@ export const tools: Record = { google_docs_create: googleDocsCreateTool, google_books_volume_search: googleBooksVolumeSearchTool, google_books_volume_details: googleBooksVolumeDetailsTool, + google_chat_list_spaces: googleChatListSpacesTool, + google_chat_send_message: googleChatSendMessageTool, google_maps_air_quality: googleMapsAirQualityTool, google_maps_directions: googleMapsDirectionsTool, google_maps_distance_matrix: googleMapsDistanceMatrixTool,