diff --git a/docs/channels/googlechat.md b/docs/channels/googlechat.md index b77c66ffe..41618a521 100644 --- a/docs/channels/googlechat.md +++ b/docs/channels/googlechat.md @@ -49,7 +49,41 @@ Status: ready for DMs + spaces via Google Chat API webhooks (HTTP only). ## User OAuth (optional, enables reactions) Service accounts cover most bot workflows, but **reactions and user-attributed actions require user OAuth**. -1) Configure OAuth consent + create OAuth client credentials in your Google Cloud project. +### Option A: Use gog OAuth (recommended if you already use `gog`) +If you already use `gog` for Google Workspace, you can reuse its OAuth client + refresh token. +`gog` stores the OAuth client credentials JSON in your config directory and the refresh token in your system keyring. citeturn9view0turn6view0 + +1) Ensure `gog` is already authorized: + ```bash + gog auth credentials /path/to/client_secret.json + gog auth add you@example.com --services gmail,calendar,drive,contacts,docs,sheets + ``` +2) Configure Google Chat to reuse `gog`: + ```json5 + { + channels: { + googlechat: { + oauthFromGog: true, + // Optional when multiple gog clients or accounts are configured: + gogAccount: "you@example.com", + gogClient: "work" + } + } + } + ``` +3) Ensure `gog` can access its keyring on the gateway host. + - `gog` stores refresh tokens in the system keychain by default. citeturn6view0 + - For headless systems, switch to file keyring + password (see `gog` docs). citeturn6view0 + +Clawdbot reads `gog` OAuth client files from: +- `~/.config/gogcli/credentials.json` +- `~/.config/gogcli/credentials-.json` +- `~/.config/gogcli/credentials-.json` (or macOS equivalent) citeturn9view0 + +Clawdbot queries `gog auth tokens --json` to reuse the stored refresh token. If this fails, set `oauthRefreshToken` manually. + +### Option B: Manual OAuth +1) Configure OAuth consent + create OAuth client credentials in your Google Cloud project (desktop app recommended). citeturn6view0 2) Use an OAuth 2.0 flow to request **offline** access and collect a refresh token. - Required scopes for reactions include: - `https://www.googleapis.com/auth/chat.messages.reactions.create` @@ -160,6 +194,10 @@ Use these identifiers for delivery and allowlists: // Optional: user OAuth for reactions + user-attributed actions oauthClientFile: "/path/to/oauth-client.json", oauthRefreshToken: "1//0g...", + // Or reuse gog: + // oauthFromGog: true, + // gogAccount: "you@example.com", + // gogClient: "work", // Or explicit fields: // oauthClientId: "123456.apps.googleusercontent.com", // oauthClientSecret: "GOCSPX-...", @@ -195,6 +233,7 @@ Notes: - Env options (default account): `GOOGLE_CHAT_OAUTH_CLIENT_ID`, `GOOGLE_CHAT_OAUTH_CLIENT_SECRET`, `GOOGLE_CHAT_OAUTH_REDIRECT_URI`, `GOOGLE_CHAT_OAUTH_CLIENT_FILE`, `GOOGLE_CHAT_OAUTH_REFRESH_TOKEN`, `GOOGLE_CHAT_OAUTH_REFRESH_TOKEN_FILE`. +- `oauthFromGog` reuses the `gog` keyring. Use `gogAccount`/`gogClient` (or `GOG_ACCOUNT`/`GOG_CLIENT`) when multiple accounts or clients exist. - Default webhook path is `/googlechat` if `webhookPath` isn’t set. - Reactions are available via the `reactions` tool and `channels action` when `actions.reactions` is enabled. - `typingIndicator` supports `none`, `message` (default), and `reaction` (reaction requires user OAuth). diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index f93d7f927..3f567c07f 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -1137,6 +1137,10 @@ Multi-account support lives under `channels.googlechat.accounts` (see the multi- serviceAccountFile: "/path/to/service-account.json", oauthClientFile: "/path/to/oauth-client.json", oauthRefreshToken: "1//0g...", + // Or reuse gog OAuth: + // oauthFromGog: true, + // gogAccount: "you@example.com", + // gogClient: "work", audienceType: "app-url", // app-url | project-number audience: "https://gateway.example.com/googlechat", webhookPath: "/googlechat", @@ -1166,6 +1170,7 @@ Notes: `GOOGLE_CHAT_OAUTH_CLIENT_SECRET`, `GOOGLE_CHAT_OAUTH_REDIRECT_URI`, `GOOGLE_CHAT_OAUTH_CLIENT_FILE`, `GOOGLE_CHAT_OAUTH_REFRESH_TOKEN`, `GOOGLE_CHAT_OAUTH_REFRESH_TOKEN_FILE`. +- `oauthFromGog` reuses `gog` OAuth credentials; `gogAccount`/`gogClient` (or `GOG_ACCOUNT`/`GOG_CLIENT`) select the account/client. - `audienceType` + `audience` must match the Chat app’s webhook auth config. - Use `spaces/` or `users/` when setting delivery targets. diff --git a/extensions/googlechat/src/accounts.ts b/extensions/googlechat/src/accounts.ts index 1216ab666..6f72509b7 100644 --- a/extensions/googlechat/src/accounts.ts +++ b/extensions/googlechat/src/accounts.ts @@ -2,6 +2,7 @@ import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; import type { GoogleChatAccountConfig, GoogleChatConfig } from "./types.config.js"; +import { readGogRefreshTokenSync, resolveGogCredentialsFile } from "./gog.js"; export type GoogleChatAppCredentialSource = "file" | "inline" | "env" | "none"; export type GoogleChatUserCredentialSource = "file" | "inline" | "env" | "none"; @@ -21,6 +22,8 @@ export type ResolvedGoogleChatAccount = { const ENV_SERVICE_ACCOUNT = "GOOGLE_CHAT_SERVICE_ACCOUNT"; const ENV_SERVICE_ACCOUNT_FILE = "GOOGLE_CHAT_SERVICE_ACCOUNT_FILE"; +const ENV_GOG_ACCOUNT = "GOG_ACCOUNT"; +const ENV_GOG_CLIENT = "GOG_CLIENT"; const ENV_OAUTH_CLIENT_ID = "GOOGLE_CHAT_OAUTH_CLIENT_ID"; const ENV_OAUTH_CLIENT_SECRET = "GOOGLE_CHAT_OAUTH_CLIENT_SECRET"; const ENV_OAUTH_CLIENT_FILE = "GOOGLE_CHAT_OAUTH_CLIENT_FILE"; @@ -121,6 +124,8 @@ function resolveUserAuthSource(params: { account: GoogleChatAccountConfig; }): GoogleChatUserCredentialSource { const { account, accountId } = params; + const gogAccount = account.gogAccount?.trim() || process.env[ENV_GOG_ACCOUNT]?.trim() || undefined; + const gogClient = account.gogClient?.trim() || process.env[ENV_GOG_CLIENT]?.trim() || undefined; const clientId = account.oauthClientId?.trim(); const clientSecret = account.oauthClientSecret?.trim(); const clientFile = account.oauthClientFile?.trim(); @@ -131,6 +136,12 @@ function resolveUserAuthSource(params: { const hasFileClient = hasNonEmptyString(clientFile); const hasInlineRefresh = hasNonEmptyString(refreshToken); const hasFileRefresh = hasNonEmptyString(refreshTokenFile); + const hasGogClient = account.oauthFromGog + ? Boolean(resolveGogCredentialsFile({ gogClient, gogAccount })) + : false; + const hasGogRefresh = account.oauthFromGog + ? Boolean(readGogRefreshTokenSync({ gogAccount, gogClient })) + : false; const hasEnvClient = accountId === DEFAULT_ACCOUNT_ID && @@ -144,8 +155,10 @@ function resolveUserAuthSource(params: { accountId === DEFAULT_ACCOUNT_ID && hasNonEmptyString(process.env[ENV_OAUTH_REFRESH_TOKEN_FILE]); - const hasClient = hasInlineClient || hasFileClient || hasEnvClient || hasEnvClientFile; - const hasRefresh = hasInlineRefresh || hasFileRefresh || hasEnvRefresh || hasEnvRefreshFile; + const hasClient = + hasInlineClient || hasFileClient || hasEnvClient || hasEnvClientFile || hasGogClient; + const hasRefresh = + hasInlineRefresh || hasFileRefresh || hasEnvRefresh || hasEnvRefreshFile || hasGogRefresh; if (!hasClient || !hasRefresh) return "none"; if (hasEnvClient || hasEnvClientFile || hasEnvRefresh || hasEnvRefreshFile) return "env"; diff --git a/extensions/googlechat/src/auth.ts b/extensions/googlechat/src/auth.ts index 915b9ecd0..3c8a31519 100644 --- a/extensions/googlechat/src/auth.ts +++ b/extensions/googlechat/src/auth.ts @@ -4,6 +4,7 @@ import { GoogleAuth, OAuth2Client } from "google-auth-library"; import { DEFAULT_ACCOUNT_ID } from "clawdbot/plugin-sdk"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; +import { readGogRefreshTokenSync, resolveGogCredentialsFile } from "./gog.js"; const CHAT_SCOPE = "https://www.googleapis.com/auth/chat.bot"; const CHAT_ISSUER = "chat@system.gserviceaccount.com"; @@ -65,6 +66,8 @@ const ENV_OAUTH_REDIRECT_URI = "GOOGLE_CHAT_OAUTH_REDIRECT_URI"; const ENV_OAUTH_CLIENT_FILE = "GOOGLE_CHAT_OAUTH_CLIENT_FILE"; const ENV_OAUTH_REFRESH_TOKEN = "GOOGLE_CHAT_OAUTH_REFRESH_TOKEN"; const ENV_OAUTH_REFRESH_TOKEN_FILE = "GOOGLE_CHAT_OAUTH_REFRESH_TOKEN_FILE"; +const ENV_GOG_ACCOUNT = "GOG_ACCOUNT"; +const ENV_GOG_CLIENT = "GOG_CLIENT"; type OAuthClientConfig = { clientId: string; @@ -102,6 +105,8 @@ function readJsonFile(path: string): unknown | null { function resolveOAuthClientConfig(account: ResolvedGoogleChatAccount): OAuthClientConfig | null { const cfg = account.config; + const gogAccount = cfg.gogAccount?.trim() || process.env[ENV_GOG_ACCOUNT]?.trim() || undefined; + const gogClient = cfg.gogClient?.trim() || process.env[ENV_GOG_CLIENT]?.trim() || undefined; const inlineId = cfg.oauthClientId?.trim(); const inlineSecret = cfg.oauthClientSecret?.trim(); const inlineRedirect = cfg.oauthRedirectUri?.trim(); @@ -119,6 +124,14 @@ function resolveOAuthClientConfig(account: ResolvedGoogleChatAccount): OAuthClie if (parsed) return parsed; } + if (cfg.oauthFromGog) { + const gogCredentials = resolveGogCredentialsFile({ gogClient, gogAccount }); + if (gogCredentials) { + const parsed = parseOAuthClientJson(readJsonFile(gogCredentials)); + if (parsed) return parsed; + } + } + if (account.accountId === DEFAULT_ACCOUNT_ID) { const envId = process.env[ENV_OAUTH_CLIENT_ID]?.trim(); const envSecret = process.env[ENV_OAUTH_CLIENT_SECRET]?.trim(); @@ -138,6 +151,8 @@ function resolveOAuthClientConfig(account: ResolvedGoogleChatAccount): OAuthClie function resolveOAuthRefreshToken(account: ResolvedGoogleChatAccount): string | null { const cfg = account.config; + const gogAccount = cfg.gogAccount?.trim() || process.env[ENV_GOG_ACCOUNT]?.trim() || undefined; + const gogClient = cfg.gogClient?.trim() || process.env[ENV_GOG_CLIENT]?.trim() || undefined; if (cfg.oauthRefreshToken?.trim()) return cfg.oauthRefreshToken.trim(); const tokenFile = cfg.oauthRefreshTokenFile?.trim(); if (tokenFile) { @@ -154,6 +169,10 @@ function resolveOAuthRefreshToken(account: ResolvedGoogleChatAccount): string | if (token) return token; } } + if (cfg.oauthFromGog) { + const token = readGogRefreshTokenSync({ gogAccount, gogClient }); + if (token) return token; + } if (account.accountId === DEFAULT_ACCOUNT_ID) { const envToken = process.env[ENV_OAUTH_REFRESH_TOKEN]?.trim(); if (envToken) return envToken; diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index 3d96d905f..436960620 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -155,6 +155,9 @@ export const googlechatPlugin: ChannelPlugin = { "oauthClientFile", "oauthRefreshToken", "oauthRefreshTokenFile", + "oauthFromGog", + "gogAccount", + "gogClient", "audienceType", "audience", "webhookPath", @@ -308,7 +311,8 @@ export const googlechatPlugin: ChannelPlugin = { } const hasServiceAccount = Boolean(input.token || input.tokenFile); const hasOauthInput = Boolean( - input.oauthClientId || + input.oauthFromGog || + input.oauthClientId || input.oauthClientSecret || input.oauthRedirectUri || input.oauthClientFile || @@ -318,7 +322,7 @@ export const googlechatPlugin: ChannelPlugin = { if (!input.useEnv && !hasServiceAccount && !hasOauthInput) { return "Google Chat requires service account JSON or OAuth credentials."; } - if (hasOauthInput) { + if (hasOauthInput && !input.oauthFromGog) { const hasClient = Boolean(input.oauthClientFile) || (Boolean(input.oauthClientId) && Boolean(input.oauthClientSecret)); @@ -356,6 +360,9 @@ export const googlechatPlugin: ChannelPlugin = { const oauthClientFile = input.oauthClientFile?.trim(); const oauthRefreshToken = input.oauthRefreshToken?.trim(); const oauthRefreshTokenFile = input.oauthRefreshTokenFile?.trim(); + const oauthFromGog = input.oauthFromGog === true ? true : undefined; + const gogAccount = input.gogAccount?.trim(); + const gogClient = input.gogClient?.trim(); const audienceType = input.audienceType?.trim(); const audience = input.audience?.trim(); const webhookPath = input.webhookPath?.trim(); @@ -368,6 +375,9 @@ export const googlechatPlugin: ChannelPlugin = { ...(oauthClientFile ? { oauthClientFile } : {}), ...(oauthRefreshToken ? { oauthRefreshToken } : {}), ...(oauthRefreshTokenFile ? { oauthRefreshTokenFile } : {}), + ...(oauthFromGog ? { oauthFromGog } : {}), + ...(gogAccount ? { gogAccount } : {}), + ...(gogClient ? { gogClient } : {}), ...(audienceType ? { audienceType } : {}), ...(audience ? { audience } : {}), ...(webhookPath ? { webhookPath } : {}), diff --git a/extensions/googlechat/src/gog.ts b/extensions/googlechat/src/gog.ts new file mode 100644 index 000000000..67851d03a --- /dev/null +++ b/extensions/googlechat/src/gog.ts @@ -0,0 +1,153 @@ +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +type GogTokenEntry = { + account?: string; + refreshToken: string; +}; + +const tokenCache = new Map(); + +function resolveConfigDirs(): string[] { + const dirs: string[] = []; + const xdg = process.env.XDG_CONFIG_HOME; + if (xdg) dirs.push(path.join(xdg, "gogcli")); + const home = os.homedir(); + if (home) dirs.push(path.join(home, ".config", "gogcli")); + if (process.platform === "darwin" && home) { + dirs.push(path.join(home, "Library", "Application Support", "gogcli")); + } + if (process.platform === "win32") { + const appData = process.env.APPDATA; + if (appData) dirs.push(path.join(appData, "gogcli")); + } + return Array.from(new Set(dirs)); +} + +function extractDomain(account?: string | null): string | null { + const value = account?.trim(); + if (!value) return null; + const at = value.lastIndexOf("@"); + if (at === -1) return null; + return value.slice(at + 1).toLowerCase(); +} + +export function resolveGogCredentialsFile(params: { + gogClient?: string | null; + gogAccount?: string | null; +}): string | null { + const client = params.gogClient?.trim(); + const account = params.gogAccount?.trim(); + const domain = extractDomain(account); + const dirs = resolveConfigDirs(); + const candidates: string[] = []; + + if (client) { + for (const dir of dirs) { + candidates.push(path.join(dir, `credentials-${client}.json`)); + } + } + if (domain) { + for (const dir of dirs) { + candidates.push(path.join(dir, `credentials-${domain}.json`)); + } + } + for (const dir of dirs) { + candidates.push(path.join(dir, "credentials.json")); + } + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) return candidate; + } + return null; +} + +function looksLikeRefreshToken(token: string): boolean { + const trimmed = token.trim(); + if (!trimmed) return false; + if (trimmed.startsWith("ya29.")) return false; + if (trimmed.startsWith("1//")) return true; + return trimmed.length > 30; +} + +function collectTokens(value: unknown, out: GogTokenEntry[]) { + if (!value || typeof value !== "object") return; + if (Array.isArray(value)) { + for (const entry of value) collectTokens(entry, out); + return; + } + const record = value as Record; + const refreshToken = + typeof record.refresh_token === "string" + ? record.refresh_token + : typeof record.refreshToken === "string" + ? record.refreshToken + : undefined; + if (refreshToken && looksLikeRefreshToken(refreshToken)) { + const account = + typeof record.email === "string" + ? record.email + : typeof record.account === "string" + ? record.account + : typeof record.user === "string" + ? record.user + : undefined; + out.push({ account, refreshToken }); + } + for (const entry of Object.values(record)) { + collectTokens(entry, out); + } +} + +export function readGogRefreshTokenSync(params: { + gogAccount?: string | null; + gogClient?: string | null; +}): string | null { + const cacheKey = `${params.gogClient ?? ""}:${params.gogAccount ?? ""}`; + const cached = tokenCache.get(cacheKey); + if (cached) return cached; + + let stdout = ""; + try { + stdout = execFileSync("gog", ["auth", "tokens", "--json"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + } catch { + return null; + } + + let parsed: unknown; + try { + parsed = JSON.parse(stdout); + } catch { + return null; + } + + const tokens: GogTokenEntry[] = []; + collectTokens(parsed, tokens); + if (tokens.length === 0) return null; + + const target = params.gogAccount?.trim().toLowerCase(); + if (target) { + const match = tokens.find( + (entry) => entry.account?.trim().toLowerCase() === target, + ); + if (match?.refreshToken) { + tokenCache.set(cacheKey, match.refreshToken); + return match.refreshToken; + } + } + + if (tokens.length === 1) { + const only = tokens[0]?.refreshToken; + if (only) { + tokenCache.set(cacheKey, only); + return only; + } + } + + return null; +} diff --git a/extensions/googlechat/src/onboarding.ts b/extensions/googlechat/src/onboarding.ts index 33ae5e808..2a5cbaf32 100644 --- a/extensions/googlechat/src/onboarding.ts +++ b/extensions/googlechat/src/onboarding.ts @@ -218,14 +218,29 @@ async function promptOAuthCredentials(params: { const method = await prompter.select({ message: "OAuth client source", options: [ + { value: "gog", label: "Reuse gog OAuth (recommended if already set up)" }, { value: "file", label: "OAuth client JSON file" }, { value: "manual", label: "OAuth client id + secret" }, ], - initialValue: "file", + initialValue: "gog", }); let patch: Record = {}; - if (method === "file") { + if (method === "gog") { + const gogAccount = await prompter.text({ + message: "gog account email (optional)", + placeholder: "you@example.com", + }); + const gogClient = await prompter.text({ + message: "gog client name (optional)", + placeholder: "work", + }); + patch = { + oauthFromGog: true, + ...(String(gogAccount ?? "").trim() ? { gogAccount: String(gogAccount).trim() } : {}), + ...(String(gogClient ?? "").trim() ? { gogClient: String(gogClient).trim() } : {}), + }; + } else if (method === "file") { const path = await prompter.text({ message: "OAuth client JSON path", placeholder: "/path/to/oauth-client.json", @@ -254,18 +269,21 @@ async function promptOAuthCredentials(params: { }; } - const refreshToken = await prompter.text({ - message: "OAuth refresh token", - placeholder: "1//0g...", - validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), - }); + const refreshToken = + method === "gog" + ? undefined + : await prompter.text({ + message: "OAuth refresh token", + placeholder: "1//0g...", + validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), + }); return applyAccountConfig({ cfg, accountId, patch: { ...patch, - oauthRefreshToken: String(refreshToken).trim(), + ...(refreshToken ? { oauthRefreshToken: String(refreshToken).trim() } : {}), }, }); } @@ -308,6 +326,7 @@ async function noteGoogleChatSetup(prompter: WizardPrompter) { "Google Chat apps use service-account auth or user OAuth plus an HTTPS webhook.", "Set the Chat API scopes in your service account and configure the Chat app URL.", "User OAuth enables reactions and other user-level APIs.", + "If gog is configured, you can reuse its OAuth credentials for Chat.", "Webhook verification requires audience type + audience value.", `Docs: ${formatDocsLink("/channels/googlechat", "channels/googlechat")}`, ].join("\n"), diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index f1410ba94..4f6a69fbc 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -41,6 +41,9 @@ export type ChannelSetupInput = { oauthClientFile?: string; oauthRefreshToken?: string; oauthRefreshTokenFile?: string; + oauthFromGog?: boolean; + gogAccount?: string; + gogClient?: string; useEnv?: boolean; homeserver?: string; userId?: string; diff --git a/src/cli/channels-cli.ts b/src/cli/channels-cli.ts index 91a33d45a..86badefba 100644 --- a/src/cli/channels-cli.ts +++ b/src/cli/channels-cli.ts @@ -44,6 +44,9 @@ const optionNamesAdd = [ "oauthClientFile", "oauthRefreshToken", "oauthRefreshTokenFile", + "oauthFromGog", + "gogAccount", + "gogClient", "useEnv", "homeserver", "userId", @@ -187,6 +190,9 @@ export function registerChannelsCli(program: Command) { .option("--oauth-client-file ", "Google Chat OAuth client JSON file") .option("--oauth-refresh-token ", "Google Chat OAuth refresh token") .option("--oauth-refresh-token-file ", "Google Chat OAuth refresh token file") + .option("--oauth-from-gog", "Reuse gog OAuth credentials for Google Chat", false) + .option("--gog-account ", "gog account email to match refresh token") + .option("--gog-client ", "gog client name to match credentials file") .option("--homeserver ", "Matrix homeserver URL") .option("--user-id ", "Matrix user ID") .option("--access-token ", "Matrix access token") diff --git a/src/commands/channels/add-mutators.ts b/src/commands/channels/add-mutators.ts index 3a49de525..847e5a860 100644 --- a/src/commands/channels/add-mutators.ts +++ b/src/commands/channels/add-mutators.ts @@ -45,6 +45,9 @@ export function applyChannelAccountConfig(params: { oauthClientFile?: string; oauthRefreshToken?: string; oauthRefreshTokenFile?: string; + oauthFromGog?: boolean; + gogAccount?: string; + gogClient?: string; useEnv?: boolean; homeserver?: string; userId?: string; @@ -88,6 +91,9 @@ export function applyChannelAccountConfig(params: { oauthClientFile: params.oauthClientFile, oauthRefreshToken: params.oauthRefreshToken, oauthRefreshTokenFile: params.oauthRefreshTokenFile, + oauthFromGog: params.oauthFromGog, + gogAccount: params.gogAccount, + gogClient: params.gogClient, useEnv: params.useEnv, homeserver: params.homeserver, userId: params.userId, diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index f0e582840..b6bc95cd1 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -42,6 +42,9 @@ export type ChannelsAddOptions = { oauthClientFile?: string; oauthRefreshToken?: string; oauthRefreshTokenFile?: string; + oauthFromGog?: boolean; + gogAccount?: string; + gogClient?: string; useEnv?: boolean; homeserver?: string; userId?: string; @@ -216,6 +219,9 @@ export async function channelsAddCommand( oauthClientFile: opts.oauthClientFile, oauthRefreshToken: opts.oauthRefreshToken, oauthRefreshTokenFile: opts.oauthRefreshTokenFile, + oauthFromGog: opts.oauthFromGog, + gogAccount: opts.gogAccount, + gogClient: opts.gogClient, homeserver: opts.homeserver, userId: opts.userId, accessToken: opts.accessToken, @@ -265,6 +271,9 @@ export async function channelsAddCommand( oauthClientFile: opts.oauthClientFile, oauthRefreshToken: opts.oauthRefreshToken, oauthRefreshTokenFile: opts.oauthRefreshTokenFile, + oauthFromGog: opts.oauthFromGog, + gogAccount: opts.gogAccount, + gogClient: opts.gogClient, homeserver: opts.homeserver, userId: opts.userId, accessToken: opts.accessToken, diff --git a/src/config/types.googlechat.ts b/src/config/types.googlechat.ts index 668a0a733..169e70fe9 100644 --- a/src/config/types.googlechat.ts +++ b/src/config/types.googlechat.ts @@ -72,6 +72,12 @@ export type GoogleChatAccountConfig = { oauthRefreshToken?: string; /** OAuth refresh token file path (user auth). */ oauthRefreshTokenFile?: string; + /** Reuse gog OAuth credentials (user auth). */ + oauthFromGog?: boolean; + /** gog account email to match refresh token (optional). */ + gogAccount?: string; + /** gog client name to match credentials file (optional). */ + gogClient?: string; /** Webhook audience type (app-url or project-number). */ audienceType?: "app-url" | "project-number"; /** Audience value (app URL or project number). */ diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index a2f8205b7..c8d6d7a83 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -317,6 +317,9 @@ export const GoogleChatAccountSchema = z oauthClientFile: z.string().optional(), oauthRefreshToken: z.string().optional(), oauthRefreshTokenFile: z.string().optional(), + oauthFromGog: z.boolean().optional(), + gogAccount: z.string().optional(), + gogClient: z.string().optional(), audienceType: z.enum(["app-url", "project-number"]).optional(), audience: z.string().optional(), webhookPath: z.string().optional(),