fix: polish Google Chat plugin (#1635) (thanks @iHildy)
Co-authored-by: Ian Hildebrand <ian@jedi.net>
This commit is contained in:
@@ -25,6 +25,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.
|
- Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.
|
||||||
- macOS: default direct-transport `ws://` URLs to port 18789; document `gateway.remote.transport`. (#1603) Thanks @ngutman.
|
- macOS: default direct-transport `ws://` URLs to port 18789; document `gateway.remote.transport`. (#1603) Thanks @ngutman.
|
||||||
- Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634)
|
- Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634)
|
||||||
|
- Google Chat: tighten email allowlist matching, typing cleanup, media caps, and onboarding/docs/tests. (#1635) Thanks @iHildy.
|
||||||
|
|
||||||
## 2026.1.23-1
|
## 2026.1.23-1
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ Configure your tunnel's ingress rules to only route the webhook path:
|
|||||||
|
|
||||||
## Targets
|
## Targets
|
||||||
Use these identifiers for delivery and allowlists:
|
Use these identifiers for delivery and allowlists:
|
||||||
- Direct messages: `users/<userId>` (Clawdbot resolves to a DM space automatically).
|
- Direct messages: `users/<userId>` or `users/<email>` (email addresses are accepted).
|
||||||
- Spaces: `spaces/<spaceId>`.
|
- Spaces: `spaces/<spaceId>`.
|
||||||
|
|
||||||
## Config highlights
|
## Config highlights
|
||||||
@@ -162,6 +162,7 @@ Use these identifiers for delivery and allowlists:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: { reactions: true },
|
actions: { reactions: true },
|
||||||
|
typingIndicator: "message",
|
||||||
mediaMaxMb: 20
|
mediaMaxMb: 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,6 +173,7 @@ Notes:
|
|||||||
- Service account credentials can also be passed inline with `serviceAccount` (JSON string).
|
- Service account credentials can also be passed inline with `serviceAccount` (JSON string).
|
||||||
- Default webhook path is `/googlechat` if `webhookPath` isn’t set.
|
- 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.
|
- 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).
|
||||||
- Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`).
|
- Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`).
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|||||||
@@ -1145,6 +1145,7 @@ Multi-account support lives under `channels.googlechat.accounts` (see the multi-
|
|||||||
"spaces/AAAA": { allow: true, requireMention: true }
|
"spaces/AAAA": { allow: true, requireMention: true }
|
||||||
},
|
},
|
||||||
actions: { reactions: true },
|
actions: { reactions: true },
|
||||||
|
typingIndicator: "message",
|
||||||
mediaMaxMb: 20
|
mediaMaxMb: 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1155,7 +1156,7 @@ Notes:
|
|||||||
- Service account JSON can be inline (`serviceAccount`) or file-based (`serviceAccountFile`).
|
- Service account JSON can be inline (`serviceAccount`) or file-based (`serviceAccountFile`).
|
||||||
- Env fallbacks for the default account: `GOOGLE_CHAT_SERVICE_ACCOUNT` or `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE`.
|
- Env fallbacks for the default account: `GOOGLE_CHAT_SERVICE_ACCOUNT` or `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE`.
|
||||||
- `audienceType` + `audience` must match the Chat app’s webhook auth config.
|
- `audienceType` + `audience` must match the Chat app’s webhook auth config.
|
||||||
- Use `spaces/<spaceId>` or `users/<userId>` when setting delivery targets.
|
- Use `spaces/<spaceId>` or `users/<userId|email>` when setting delivery targets.
|
||||||
|
|
||||||
### `channels.slack` (socket mode)
|
### `channels.slack` (socket mode)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clawdbot": "workspace:*",
|
|
||||||
"google-auth-library": "^10.5.0"
|
"google-auth-library": "^10.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"clawdbot": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"clawdbot": ">=2026.1.24-0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
extensions/googlechat/src/api.test.ts
Normal file
62
extensions/googlechat/src/api.test.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { ResolvedGoogleChatAccount } from "./accounts.js";
|
||||||
|
import { downloadGoogleChatMedia } from "./api.js";
|
||||||
|
|
||||||
|
vi.mock("./auth.js", () => ({
|
||||||
|
getGoogleChatAccessToken: vi.fn().mockResolvedValue("token"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const account = {
|
||||||
|
accountId: "default",
|
||||||
|
enabled: true,
|
||||||
|
credentialSource: "inline",
|
||||||
|
config: {},
|
||||||
|
} as ResolvedGoogleChatAccount;
|
||||||
|
|
||||||
|
describe("downloadGoogleChatMedia", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects when content-length exceeds max bytes", async () => {
|
||||||
|
const body = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue(new Uint8Array([1, 2, 3]));
|
||||||
|
controller.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const response = new Response(body, {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-length": "50", "content-type": "application/octet-stream" },
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(response));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadGoogleChatMedia({ account, resourceName: "media/123", maxBytes: 10 }),
|
||||||
|
).rejects.toThrow(/max bytes/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects when streamed payload exceeds max bytes", async () => {
|
||||||
|
const chunks = [new Uint8Array(6), new Uint8Array(6)];
|
||||||
|
let index = 0;
|
||||||
|
const body = new ReadableStream({
|
||||||
|
pull(controller) {
|
||||||
|
if (index < chunks.length) {
|
||||||
|
controller.enqueue(chunks[index++]);
|
||||||
|
} else {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const response = new Response(body, {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "application/octet-stream" },
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(response));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadGoogleChatMedia({ account, resourceName: "media/123", maxBytes: 10 }),
|
||||||
|
).rejects.toThrow(/max bytes/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -51,6 +51,7 @@ async function fetchBuffer(
|
|||||||
account: ResolvedGoogleChatAccount,
|
account: ResolvedGoogleChatAccount,
|
||||||
url: string,
|
url: string,
|
||||||
init?: RequestInit,
|
init?: RequestInit,
|
||||||
|
options?: { maxBytes?: number },
|
||||||
): Promise<{ buffer: Buffer; contentType?: string }> {
|
): Promise<{ buffer: Buffer; contentType?: string }> {
|
||||||
const token = await getGoogleChatAccessToken(account);
|
const token = await getGoogleChatAccessToken(account);
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
@@ -64,7 +65,34 @@ async function fetchBuffer(
|
|||||||
const text = await res.text().catch(() => "");
|
const text = await res.text().catch(() => "");
|
||||||
throw new Error(`Google Chat API ${res.status}: ${text || res.statusText}`);
|
throw new Error(`Google Chat API ${res.status}: ${text || res.statusText}`);
|
||||||
}
|
}
|
||||||
const buffer = Buffer.from(await res.arrayBuffer());
|
const maxBytes = options?.maxBytes;
|
||||||
|
const lengthHeader = res.headers.get("content-length");
|
||||||
|
if (maxBytes && lengthHeader) {
|
||||||
|
const length = Number(lengthHeader);
|
||||||
|
if (Number.isFinite(length) && length > maxBytes) {
|
||||||
|
throw new Error(`Google Chat media exceeds max bytes (${maxBytes})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!maxBytes || !res.body) {
|
||||||
|
const buffer = Buffer.from(await res.arrayBuffer());
|
||||||
|
const contentType = res.headers.get("content-type") ?? undefined;
|
||||||
|
return { buffer, contentType };
|
||||||
|
}
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let total = 0;
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
if (!value) continue;
|
||||||
|
total += value.length;
|
||||||
|
if (total > maxBytes) {
|
||||||
|
await reader.cancel();
|
||||||
|
throw new Error(`Google Chat media exceeds max bytes (${maxBytes})`);
|
||||||
|
}
|
||||||
|
chunks.push(Buffer.from(value));
|
||||||
|
}
|
||||||
|
const buffer = Buffer.concat(chunks, total);
|
||||||
const contentType = res.headers.get("content-type") ?? undefined;
|
const contentType = res.headers.get("content-type") ?? undefined;
|
||||||
return { buffer, contentType };
|
return { buffer, contentType };
|
||||||
}
|
}
|
||||||
@@ -108,6 +136,15 @@ export async function updateGoogleChatMessage(params: {
|
|||||||
return { messageName: result.name };
|
return { messageName: result.name };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteGoogleChatMessage(params: {
|
||||||
|
account: ResolvedGoogleChatAccount;
|
||||||
|
messageName: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { account, messageName } = params;
|
||||||
|
const url = `${CHAT_API_BASE}/${messageName}`;
|
||||||
|
await fetchOk(account, url, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
export async function uploadGoogleChatAttachment(params: {
|
export async function uploadGoogleChatAttachment(params: {
|
||||||
account: ResolvedGoogleChatAccount;
|
account: ResolvedGoogleChatAccount;
|
||||||
space: string;
|
space: string;
|
||||||
@@ -151,10 +188,11 @@ export async function uploadGoogleChatAttachment(params: {
|
|||||||
export async function downloadGoogleChatMedia(params: {
|
export async function downloadGoogleChatMedia(params: {
|
||||||
account: ResolvedGoogleChatAccount;
|
account: ResolvedGoogleChatAccount;
|
||||||
resourceName: string;
|
resourceName: string;
|
||||||
|
maxBytes?: number;
|
||||||
}): Promise<{ buffer: Buffer; contentType?: string }> {
|
}): Promise<{ buffer: Buffer; contentType?: string }> {
|
||||||
const { account, resourceName } = params;
|
const { account, resourceName, maxBytes } = params;
|
||||||
const url = `${CHAT_API_BASE}/media/${resourceName}?alt=media`;
|
const url = `${CHAT_API_BASE}/media/${resourceName}?alt=media`;
|
||||||
return await fetchBuffer(account, url);
|
return await fetchBuffer(account, url, undefined, { maxBytes });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createGoogleChatReaction(params: {
|
export async function createGoogleChatReaction(params: {
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ const meta = getChatChannelMeta("googlechat");
|
|||||||
const formatAllowFromEntry = (entry: string) =>
|
const formatAllowFromEntry = (entry: string) =>
|
||||||
entry
|
entry
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/^(googlechat|gchat):/i, "")
|
.replace(/^(googlechat|google-chat|gchat):/i, "")
|
||||||
|
.replace(/^user:/i, "")
|
||||||
.replace(/^users\//i, "")
|
.replace(/^users\//i, "")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
||||||
|
|||||||
27
extensions/googlechat/src/monitor.test.ts
Normal file
27
extensions/googlechat/src/monitor.test.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { isSenderAllowed } from "./monitor.js";
|
||||||
|
|
||||||
|
describe("isSenderAllowed", () => {
|
||||||
|
it("matches allowlist entries with users/<email>", () => {
|
||||||
|
expect(
|
||||||
|
isSenderAllowed("users/123", "Jane@Example.com", ["users/jane@example.com"]),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches allowlist entries with raw email", () => {
|
||||||
|
expect(isSenderAllowed("users/123", "Jane@Example.com", ["jane@example.com"])).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still matches user id entries", () => {
|
||||||
|
expect(isSenderAllowed("users/abc", "jane@example.com", ["users/abc"])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects non-matching emails", () => {
|
||||||
|
expect(isSenderAllowed("users/123", "jane@example.com", ["users/other@example.com"])).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "./accounts.js";
|
} from "./accounts.js";
|
||||||
import {
|
import {
|
||||||
downloadGoogleChatMedia,
|
downloadGoogleChatMedia,
|
||||||
|
deleteGoogleChatMessage,
|
||||||
sendGoogleChatMessage,
|
sendGoogleChatMessage,
|
||||||
updateGoogleChatMessage,
|
updateGoogleChatMessage,
|
||||||
} from "./api.js";
|
} from "./api.js";
|
||||||
@@ -296,7 +297,11 @@ function normalizeUserId(raw?: string | null): string {
|
|||||||
return trimmed.replace(/^users\//i, "").toLowerCase();
|
return trimmed.replace(/^users\//i, "").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSenderAllowed(senderId: string, senderEmail: string | undefined, allowFrom: string[]) {
|
export function isSenderAllowed(
|
||||||
|
senderId: string,
|
||||||
|
senderEmail: string | undefined,
|
||||||
|
allowFrom: string[],
|
||||||
|
) {
|
||||||
if (allowFrom.includes("*")) return true;
|
if (allowFrom.includes("*")) return true;
|
||||||
const normalizedSenderId = normalizeUserId(senderId);
|
const normalizedSenderId = normalizeUserId(senderId);
|
||||||
const normalizedEmail = senderEmail?.trim().toLowerCase() ?? "";
|
const normalizedEmail = senderEmail?.trim().toLowerCase() ?? "";
|
||||||
@@ -305,8 +310,11 @@ function isSenderAllowed(senderId: string, senderEmail: string | undefined, allo
|
|||||||
if (!normalized) return false;
|
if (!normalized) return false;
|
||||||
if (normalized === normalizedSenderId) return true;
|
if (normalized === normalizedSenderId) return true;
|
||||||
if (normalizedEmail && normalized === normalizedEmail) return true;
|
if (normalizedEmail && normalized === normalizedEmail) return true;
|
||||||
|
if (normalizedEmail && normalized.replace(/^users\//i, "") === normalizedEmail) return true;
|
||||||
if (normalized.replace(/^users\//i, "") === normalizedSenderId) return true;
|
if (normalized.replace(/^users\//i, "") === normalizedSenderId) return true;
|
||||||
if (normalized.replace(/^(googlechat|gchat):/i, "") === normalizedSenderId) return true;
|
if (normalized.replace(/^(googlechat|google-chat|gchat):/i, "") === normalizedSenderId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -700,7 +708,7 @@ async function downloadAttachment(
|
|||||||
const resourceName = attachment.attachmentDataRef?.resourceName;
|
const resourceName = attachment.attachmentDataRef?.resourceName;
|
||||||
if (!resourceName) return null;
|
if (!resourceName) return null;
|
||||||
const maxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
|
const maxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
|
||||||
const downloaded = await downloadGoogleChatMedia({ account, resourceName });
|
const downloaded = await downloadGoogleChatMedia({ account, resourceName, maxBytes });
|
||||||
const saved = await core.channel.media.saveMediaBuffer(
|
const saved = await core.channel.media.saveMediaBuffer(
|
||||||
downloaded.buffer,
|
downloaded.buffer,
|
||||||
downloaded.contentType ?? attachment.contentType,
|
downloaded.contentType ?? attachment.contentType,
|
||||||
@@ -728,9 +736,35 @@ async function deliverGoogleChatReply(params: {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
if (mediaList.length > 0) {
|
if (mediaList.length > 0) {
|
||||||
|
let suppressCaption = false;
|
||||||
|
if (typingMessageName) {
|
||||||
|
try {
|
||||||
|
await deleteGoogleChatMessage({
|
||||||
|
account,
|
||||||
|
messageName: typingMessageName,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
runtime.error?.(`Google Chat typing cleanup failed: ${String(err)}`);
|
||||||
|
const fallbackText = payload.text?.trim()
|
||||||
|
? payload.text
|
||||||
|
: mediaList.length > 1
|
||||||
|
? "Sent attachments."
|
||||||
|
: "Sent attachment.";
|
||||||
|
try {
|
||||||
|
await updateGoogleChatMessage({
|
||||||
|
account,
|
||||||
|
messageName: typingMessageName,
|
||||||
|
text: fallbackText,
|
||||||
|
});
|
||||||
|
suppressCaption = Boolean(payload.text?.trim());
|
||||||
|
} catch (updateErr) {
|
||||||
|
runtime.error?.(`Google Chat typing update failed: ${String(updateErr)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let first = true;
|
let first = true;
|
||||||
for (const mediaUrl of mediaList) {
|
for (const mediaUrl of mediaList) {
|
||||||
const caption = first ? payload.text : undefined;
|
const caption = first && !suppressCaption ? payload.text : undefined;
|
||||||
first = false;
|
first = false;
|
||||||
try {
|
try {
|
||||||
const loaded = await core.channel.media.fetchRemoteMedia(mediaUrl, {
|
const loaded = await core.channel.media.fetchRemoteMedia(mediaUrl, {
|
||||||
|
|||||||
@@ -136,7 +136,9 @@ async function promptCredentials(params: {
|
|||||||
}): Promise<ClawdbotConfig> {
|
}): Promise<ClawdbotConfig> {
|
||||||
const { cfg, prompter, accountId } = params;
|
const { cfg, prompter, accountId } = params;
|
||||||
const envReady =
|
const envReady =
|
||||||
Boolean(process.env[ENV_SERVICE_ACCOUNT]) || Boolean(process.env[ENV_SERVICE_ACCOUNT_FILE]);
|
accountId === DEFAULT_ACCOUNT_ID &&
|
||||||
|
(Boolean(process.env[ENV_SERVICE_ACCOUNT]) ||
|
||||||
|
Boolean(process.env[ENV_SERVICE_ACCOUNT_FILE]));
|
||||||
if (envReady) {
|
if (envReady) {
|
||||||
const useEnv = await prompter.confirm({
|
const useEnv = await prompter.confirm({
|
||||||
message: "Use GOOGLE_CHAT_SERVICE_ACCOUNT env vars?",
|
message: "Use GOOGLE_CHAT_SERVICE_ACCOUNT env vars?",
|
||||||
|
|||||||
35
extensions/googlechat/src/targets.test.ts
Normal file
35
extensions/googlechat/src/targets.test.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isGoogleChatSpaceTarget,
|
||||||
|
isGoogleChatUserTarget,
|
||||||
|
normalizeGoogleChatTarget,
|
||||||
|
} from "./targets.js";
|
||||||
|
|
||||||
|
describe("normalizeGoogleChatTarget", () => {
|
||||||
|
it("normalizes provider prefixes", () => {
|
||||||
|
expect(normalizeGoogleChatTarget("googlechat:users/123")).toBe("users/123");
|
||||||
|
expect(normalizeGoogleChatTarget("google-chat:spaces/AAA")).toBe("spaces/AAA");
|
||||||
|
expect(normalizeGoogleChatTarget("gchat:user:User@Example.com")).toBe(
|
||||||
|
"users/user@example.com",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes email targets to users/<email>", () => {
|
||||||
|
expect(normalizeGoogleChatTarget("User@Example.com")).toBe("users/user@example.com");
|
||||||
|
expect(normalizeGoogleChatTarget("users/User@Example.com")).toBe("users/user@example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves space targets", () => {
|
||||||
|
expect(normalizeGoogleChatTarget("space:spaces/BBB")).toBe("spaces/BBB");
|
||||||
|
expect(normalizeGoogleChatTarget("spaces/CCC")).toBe("spaces/CCC");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("target helpers", () => {
|
||||||
|
it("detects user and space targets", () => {
|
||||||
|
expect(isGoogleChatUserTarget("users/abc")).toBe(true);
|
||||||
|
expect(isGoogleChatSpaceTarget("spaces/abc")).toBe(true);
|
||||||
|
expect(isGoogleChatUserTarget("spaces/abc")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,10 +4,16 @@ import { findGoogleChatDirectMessage } from "./api.js";
|
|||||||
export function normalizeGoogleChatTarget(raw?: string | null): string | undefined {
|
export function normalizeGoogleChatTarget(raw?: string | null): string | undefined {
|
||||||
const trimmed = raw?.trim();
|
const trimmed = raw?.trim();
|
||||||
if (!trimmed) return undefined;
|
if (!trimmed) return undefined;
|
||||||
const withoutPrefix = trimmed.replace(/^(googlechat|gchat):/i, "");
|
const withoutPrefix = trimmed.replace(/^(googlechat|google-chat|gchat):/i, "");
|
||||||
const normalized = withoutPrefix
|
const normalized = withoutPrefix
|
||||||
.replace(/^user:/i, "users/")
|
.replace(/^user:/i, "users/")
|
||||||
.replace(/^space:/i, "spaces/");
|
.replace(/^space:/i, "spaces/");
|
||||||
|
if (isGoogleChatUserTarget(normalized)) {
|
||||||
|
const suffix = normalized.slice("users/".length);
|
||||||
|
return suffix.includes("@") ? `users/${suffix.toLowerCase()}` : normalized;
|
||||||
|
}
|
||||||
|
if (isGoogleChatSpaceTarget(normalized)) return normalized;
|
||||||
|
if (normalized.includes("@")) return `users/${normalized.toLowerCase()}`;
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -304,12 +304,13 @@ importers:
|
|||||||
|
|
||||||
extensions/googlechat:
|
extensions/googlechat:
|
||||||
dependencies:
|
dependencies:
|
||||||
clawdbot:
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../..
|
|
||||||
google-auth-library:
|
google-auth-library:
|
||||||
specifier: ^10.5.0
|
specifier: ^10.5.0
|
||||||
version: 10.5.0
|
version: 10.5.0
|
||||||
|
devDependencies:
|
||||||
|
clawdbot:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../..
|
||||||
|
|
||||||
extensions/imessage: {}
|
extensions/imessage: {}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ function detectAutoKind(input: string): ChannelResolveKind {
|
|||||||
if (!trimmed) return "group";
|
if (!trimmed) return "group";
|
||||||
if (trimmed.startsWith("@")) return "user";
|
if (trimmed.startsWith("@")) return "user";
|
||||||
if (/^<@!?/.test(trimmed)) return "user";
|
if (/^<@!?/.test(trimmed)) return "user";
|
||||||
|
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) return "user";
|
||||||
if (
|
if (
|
||||||
/^(user|discord|slack|matrix|msteams|teams|zalo|zalouser|googlechat|google-chat|gchat):/i.test(
|
/^(user|discord|slack|matrix|msteams|teams|zalo|zalouser|googlechat|google-chat|gchat):/i.test(
|
||||||
trimmed,
|
trimmed,
|
||||||
|
|||||||
Reference in New Issue
Block a user