diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index dcd5741f2b..c6217e64ac 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1302,6 +1302,37 @@ export function GoogleCalendarIcon(props: SVGProps) { ) } +export function GoogleChatIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function SupabaseIcon(props: SVGProps) { const id = useId() const gradient0 = `supabase_paint0_${id}` diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 5121253240..1cb91c66bc 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -44,6 +44,7 @@ import { GongIcon, GoogleBooksIcon, GoogleCalendarIcon, + GoogleChatIcon, GoogleDocsIcon, GoogleDriveIcon, GoogleFormsIcon, @@ -189,6 +190,7 @@ export const blockTypeToIconMap: Record = { gong: GongIcon, google_books: GoogleBooksIcon, google_calendar_v2: GoogleCalendarIcon, + google_chat: GoogleChatIcon, google_docs: GoogleDocsIcon, google_drive: GoogleDriveIcon, google_forms: GoogleFormsIcon, 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 9fc1cc577e..8a943b0b32 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -39,6 +39,7 @@ "gong", "google_books", "google_calendar", + "google_chat", "google_docs", "google_drive", "google_forms", 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 6cac32e626..874fd95e8b 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 @@ -52,6 +52,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..b5bd75c8bf --- /dev/null +++ b/apps/sim/blocks/blocks/google_chat.ts @@ -0,0 +1,143 @@ +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' }, + }, + { + id: 'filter', + title: 'Filter', + type: 'short-input', + placeholder: 'e.g., spaceType = "SPACE"', + condition: { field: 'operation', value: 'list_spaces' }, + }, + { + id: 'pageSize', + title: 'Max Results', + type: 'short-input', + placeholder: 'Maximum spaces to return (default 100)', + condition: { field: 'operation', value: 'list_spaces' }, + }, + ], + 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' }, + 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 03b9827a77..42a55a773d 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -45,6 +45,7 @@ import { GongBlock } from '@/blocks/blocks/gong' import { GoogleSearchBlock } from '@/blocks/blocks/google' import { GoogleBooksBlock } from '@/blocks/blocks/google_books' import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar' +import { GoogleChatBlock } from '@/blocks/blocks/google_chat' import { GoogleDocsBlock } from '@/blocks/blocks/google_docs' import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' import { GoogleFormsBlock } from '@/blocks/blocks/google_forms' @@ -227,6 +228,7 @@ export const registry: Record = { gmail: GmailBlock, gmail_v2: GmailV2Block, google_calendar: GoogleCalendarBlock, + google_chat: GoogleChatBlock, google_calendar_v2: GoogleCalendarV2Block, google_books: GoogleBooksBlock, google_docs: GoogleDocsBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index dcd5741f2b..c6217e64ac 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1302,6 +1302,37 @@ export function GoogleCalendarIcon(props: SVGProps) { ) } +export function GoogleChatIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function SupabaseIcon(props: SVGProps) { const id = useId() const gradient0 = `supabase_paint0_${id}` diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 91eb88b1d3..92ca8d2760 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -486,6 +486,7 @@ export const auth = betterAuth({ 'google-forms', 'google-vault', 'google-groups', + 'google-chat', 'vertex-ai', 'github-repo', 'microsoft-dataverse', @@ -1150,6 +1151,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: 'vertex-ai', clientId: env.GOOGLE_CLIENT_ID as string, diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index b890566334..02dc76608c 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -9,6 +9,7 @@ import { GithubIcon, GmailIcon, GoogleCalendarIcon, + GoogleChatIcon, GoogleDocsIcon, GoogleDriveIcon, GoogleFormsIcon, @@ -141,6 +142,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 d5114a38bc..639e88cf48 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -10,6 +10,7 @@ export type OAuthProvider = | 'google-vault' | 'google-forms' | 'google-groups' + | 'google-chat' | 'vertex-ai' | 'github' | 'github-repo' @@ -55,6 +56,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..db7844b43c --- /dev/null +++ b/apps/sim/tools/google_chat/index.ts @@ -0,0 +1,5 @@ +import { listSpacesTool } from './list_spaces' +import { sendMessageTool } from './send_message' + +export const googleChatSendMessageTool = sendMessageTool +export const googleChatListSpacesTool = listSpacesTool 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..5d18268704 --- /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}`, + 'Content-Type': 'application/json', + }), + }, + + 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', + }, + 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 5a2f5787c7..9aebdfe546 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -643,6 +643,7 @@ import { googleCalendarUpdateTool, googleCalendarUpdateV2Tool, } from '@/tools/google_calendar' +import { googleChatListSpacesTool, googleChatSendMessageTool } from '@/tools/google_chat' import { googleDocsCreateTool, googleDocsReadTool, googleDocsWriteTool } from '@/tools/google_docs' import { googleDriveCopyTool, @@ -2878,6 +2879,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,