feat: add gog oauth reuse for google chat
This commit is contained in:
parent
e83e4bf28c
commit
e4591ca120
@ -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. citeturn9view0turn6view0
|
||||
|
||||
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. citeturn6view0
|
||||
- For headless systems, switch to file keyring + password (see `gog` docs). citeturn6view0
|
||||
|
||||
Clawdbot reads `gog` OAuth client files from:
|
||||
- `~/.config/gogcli/credentials.json`
|
||||
- `~/.config/gogcli/credentials-<client>.json`
|
||||
- `~/.config/gogcli/credentials-<domain>.json` (or macOS equivalent) citeturn9view0
|
||||
|
||||
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). citeturn6view0
|
||||
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).
|
||||
|
||||
@ -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/<spaceId>` or `users/<userId|email>` when setting delivery targets.
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -155,6 +155,9 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
|
||||
"oauthClientFile",
|
||||
"oauthRefreshToken",
|
||||
"oauthRefreshTokenFile",
|
||||
"oauthFromGog",
|
||||
"gogAccount",
|
||||
"gogClient",
|
||||
"audienceType",
|
||||
"audience",
|
||||
"webhookPath",
|
||||
@ -308,7 +311,8 @@ export const googlechatPlugin: ChannelPlugin<ResolvedGoogleChatAccount> = {
|
||||
}
|
||||
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<ResolvedGoogleChatAccount> = {
|
||||
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<ResolvedGoogleChatAccount> = {
|
||||
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<ResolvedGoogleChatAccount> = {
|
||||
...(oauthClientFile ? { oauthClientFile } : {}),
|
||||
...(oauthRefreshToken ? { oauthRefreshToken } : {}),
|
||||
...(oauthRefreshTokenFile ? { oauthRefreshTokenFile } : {}),
|
||||
...(oauthFromGog ? { oauthFromGog } : {}),
|
||||
...(gogAccount ? { gogAccount } : {}),
|
||||
...(gogClient ? { gogClient } : {}),
|
||||
...(audienceType ? { audienceType } : {}),
|
||||
...(audience ? { audience } : {}),
|
||||
...(webhookPath ? { webhookPath } : {}),
|
||||
|
||||
153
extensions/googlechat/src/gog.ts
Normal file
153
extensions/googlechat/src/gog.ts
Normal file
@ -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<string, string>();
|
||||
|
||||
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<string, unknown>;
|
||||
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;
|
||||
}
|
||||
@ -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<string, unknown> = {};
|
||||
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"),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 <path>", "Google Chat OAuth client JSON file")
|
||||
.option("--oauth-refresh-token <token>", "Google Chat OAuth refresh token")
|
||||
.option("--oauth-refresh-token-file <path>", "Google Chat OAuth refresh token file")
|
||||
.option("--oauth-from-gog", "Reuse gog OAuth credentials for Google Chat", false)
|
||||
.option("--gog-account <email>", "gog account email to match refresh token")
|
||||
.option("--gog-client <client>", "gog client name to match credentials file")
|
||||
.option("--homeserver <url>", "Matrix homeserver URL")
|
||||
.option("--user-id <id>", "Matrix user ID")
|
||||
.option("--access-token <token>", "Matrix access token")
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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). */
|
||||
|
||||
@ -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(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user