@openclaw/googlechat 2026.5.2-beta.2 → 2026.5.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/api-C67jv0ow.js +627 -0
  2. package/dist/api.js +3 -0
  3. package/dist/channel-DE2hroMn.js +682 -0
  4. package/dist/channel-config-api.js +5 -0
  5. package/dist/channel-plugin-api.js +2 -0
  6. package/dist/channel.runtime-CoSLc-4R.js +920 -0
  7. package/dist/contract-api.js +3 -0
  8. package/dist/doctor-contract-CG1sLToP.js +151 -0
  9. package/dist/doctor-contract-api.js +2 -0
  10. package/dist/index.js +22 -0
  11. package/dist/runtime-api-wkIdfwqY.js +30 -0
  12. package/dist/runtime-api.js +2 -0
  13. package/dist/secret-contract-DtQ_IO7J.js +99 -0
  14. package/dist/secret-contract-api.js +2 -0
  15. package/dist/setup-entry.js +15 -0
  16. package/dist/setup-plugin-api.js +75 -0
  17. package/dist/setup-surface-CofP-Gg9.js +309 -0
  18. package/dist/test-api.js +3 -0
  19. package/package.json +14 -6
  20. package/api.ts +0 -3
  21. package/channel-config-api.ts +0 -1
  22. package/channel-plugin-api.ts +0 -1
  23. package/contract-api.ts +0 -5
  24. package/doctor-contract-api.ts +0 -1
  25. package/index.ts +0 -20
  26. package/runtime-api.ts +0 -60
  27. package/secret-contract-api.ts +0 -5
  28. package/setup-entry.ts +0 -13
  29. package/setup-plugin-api.ts +0 -3
  30. package/src/accounts.ts +0 -169
  31. package/src/actions.test.ts +0 -265
  32. package/src/actions.ts +0 -227
  33. package/src/api.ts +0 -322
  34. package/src/approval-auth.test.ts +0 -24
  35. package/src/approval-auth.ts +0 -32
  36. package/src/auth.ts +0 -209
  37. package/src/channel-config.test.ts +0 -39
  38. package/src/channel.adapters.ts +0 -293
  39. package/src/channel.deps.runtime.ts +0 -28
  40. package/src/channel.runtime.ts +0 -17
  41. package/src/channel.setup.ts +0 -98
  42. package/src/channel.test.ts +0 -695
  43. package/src/channel.ts +0 -256
  44. package/src/config-schema.test.ts +0 -16
  45. package/src/config-schema.ts +0 -3
  46. package/src/doctor-contract.test.ts +0 -75
  47. package/src/doctor-contract.ts +0 -182
  48. package/src/doctor.ts +0 -57
  49. package/src/gateway.ts +0 -63
  50. package/src/google-auth.runtime.test.ts +0 -462
  51. package/src/google-auth.runtime.ts +0 -539
  52. package/src/group-policy.ts +0 -17
  53. package/src/monitor-access.test.ts +0 -443
  54. package/src/monitor-access.ts +0 -405
  55. package/src/monitor-reply-delivery.ts +0 -156
  56. package/src/monitor-routing.ts +0 -55
  57. package/src/monitor-types.ts +0 -33
  58. package/src/monitor-webhook.test.ts +0 -446
  59. package/src/monitor-webhook.ts +0 -285
  60. package/src/monitor.reply-delivery.test.ts +0 -139
  61. package/src/monitor.ts +0 -428
  62. package/src/monitor.webhook-routing.test.ts +0 -252
  63. package/src/runtime.ts +0 -9
  64. package/src/secret-contract.test.ts +0 -60
  65. package/src/secret-contract.ts +0 -161
  66. package/src/sender-allow.ts +0 -46
  67. package/src/setup-core.ts +0 -40
  68. package/src/setup-surface.ts +0 -236
  69. package/src/setup.test.ts +0 -560
  70. package/src/targets.test.ts +0 -421
  71. package/src/targets.ts +0 -66
  72. package/src/types.config.ts +0 -3
  73. package/src/types.ts +0 -73
  74. package/test-api.ts +0 -2
  75. package/tsconfig.json +0 -16
@@ -1,405 +0,0 @@
1
- import { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
2
- import { expandAllowFromWithAccessGroups } from "openclaw/plugin-sdk/security-runtime";
3
- import {
4
- normalizeLowercaseStringOrEmpty,
5
- normalizeOptionalString,
6
- } from "openclaw/plugin-sdk/text-runtime";
7
- import {
8
- GROUP_POLICY_BLOCKED_LABEL,
9
- createChannelPairingController,
10
- evaluateGroupRouteAccessForPolicy,
11
- isDangerousNameMatchingEnabled,
12
- resolveAllowlistProviderRuntimeGroupPolicy,
13
- resolveDefaultGroupPolicy,
14
- resolveDmGroupAccessWithLists,
15
- resolveSenderScopedGroupPolicy,
16
- warnMissingProviderGroupPolicyFallbackOnce,
17
- type OpenClawConfig,
18
- } from "../runtime-api.js";
19
- import type { ResolvedGoogleChatAccount } from "./accounts.js";
20
- import { sendGoogleChatMessage } from "./api.js";
21
- import type { GoogleChatCoreRuntime } from "./monitor-types.js";
22
- import { isSenderAllowed } from "./sender-allow.js";
23
- import type { GoogleChatAnnotation, GoogleChatMessage, GoogleChatSpace } from "./types.js";
24
-
25
- function normalizeUserId(raw?: string | null): string {
26
- const trimmed = normalizeOptionalString(raw) ?? "";
27
- if (!trimmed) {
28
- return "";
29
- }
30
- return normalizeLowercaseStringOrEmpty(trimmed.replace(/^users\//i, ""));
31
- }
32
-
33
- export { isSenderAllowed } from "./sender-allow.js";
34
-
35
- type GoogleChatGroupEntry = {
36
- requireMention?: boolean;
37
- enabled?: boolean;
38
- users?: Array<string | number>;
39
- systemPrompt?: string;
40
- };
41
-
42
- function resolveGroupConfig(params: {
43
- groupId: string;
44
- groupName?: string | null;
45
- groups?: Record<string, GoogleChatGroupEntry>;
46
- }) {
47
- const { groupId, groupName, groups } = params;
48
- const entries = groups ?? {};
49
- const keys = Object.keys(entries);
50
- if (keys.length === 0) {
51
- return { entry: undefined, allowlistConfigured: false, deprecatedNameMatch: false };
52
- }
53
- const entry = entries[groupId];
54
- const normalizedGroupName = normalizeLowercaseStringOrEmpty(groupName ?? "");
55
- const deprecatedNameMatch =
56
- !entry &&
57
- Boolean(
58
- groupName &&
59
- keys.some((key) => {
60
- const trimmed = key.trim();
61
- if (!trimmed || trimmed === "*" || /^spaces\//i.test(trimmed)) {
62
- return false;
63
- }
64
- return (
65
- trimmed === groupName || normalizeLowercaseStringOrEmpty(trimmed) === normalizedGroupName
66
- );
67
- }),
68
- );
69
- const fallback = entries["*"];
70
- return {
71
- entry: deprecatedNameMatch ? undefined : (entry ?? fallback),
72
- allowlistConfigured: true,
73
- fallback,
74
- deprecatedNameMatch,
75
- };
76
- }
77
-
78
- function extractMentionInfo(annotations: GoogleChatAnnotation[], botUser?: string | null) {
79
- const mentionAnnotations = annotations.filter((entry) => entry.type === "USER_MENTION");
80
- const hasAnyMention = mentionAnnotations.length > 0;
81
- const botTargets = new Set(["users/app", botUser?.trim()].filter(Boolean) as string[]);
82
- const wasMentioned = mentionAnnotations.some((entry) => {
83
- const userName = entry.userMention?.user?.name;
84
- if (!userName) {
85
- return false;
86
- }
87
- if (botTargets.has(userName)) {
88
- return true;
89
- }
90
- return normalizeUserId(userName) === "app";
91
- });
92
- return { hasAnyMention, wasMentioned };
93
- }
94
-
95
- const warnedDeprecatedUsersEmailAllowFrom = new Set<string>();
96
- const warnedMutableGroupKeys = new Set<string>();
97
-
98
- function warnDeprecatedUsersEmailEntries(logVerbose: (message: string) => void, entries: string[]) {
99
- const deprecated = entries
100
- .map((v) => normalizeOptionalString(v))
101
- .filter((v): v is string => Boolean(v))
102
- .filter((v) => /^users\/.+@.+/i.test(v));
103
- if (deprecated.length === 0) {
104
- return;
105
- }
106
- const key = deprecated
107
- .map((v) => normalizeLowercaseStringOrEmpty(v))
108
- .toSorted((a, b) => a.localeCompare(b))
109
- .join(",");
110
- if (warnedDeprecatedUsersEmailAllowFrom.has(key)) {
111
- return;
112
- }
113
- warnedDeprecatedUsersEmailAllowFrom.add(key);
114
- logVerbose(
115
- `Deprecated allowFrom entry detected: "users/<email>" is no longer treated as an email allowlist. Use raw email (alice@example.com) or immutable user id (users/<id>). entries=${deprecated.join(", ")}`,
116
- );
117
- }
118
-
119
- function warnMutableGroupKeysConfigured(
120
- logVerbose: (message: string) => void,
121
- groups?: Record<string, GoogleChatGroupEntry>,
122
- ) {
123
- const mutableKeys = Object.keys(groups ?? {})
124
- .map((key) => key.trim())
125
- .filter((key) => key && key !== "*" && !/^spaces\//i.test(key));
126
- if (mutableKeys.length === 0) {
127
- return;
128
- }
129
- const warningKey = mutableKeys
130
- .map((key) => normalizeLowercaseStringOrEmpty(key))
131
- .toSorted((a, b) => a.localeCompare(b))
132
- .join(",");
133
- if (warnedMutableGroupKeys.has(warningKey)) {
134
- return;
135
- }
136
- warnedMutableGroupKeys.add(warningKey);
137
- logVerbose(
138
- `Deprecated Google Chat group key detected: group routing now requires stable space ids (spaces/<spaceId>). Update channels.googlechat.groups keys: ${mutableKeys.join(", ")}`,
139
- );
140
- }
141
-
142
- export async function applyGoogleChatInboundAccessPolicy(params: {
143
- account: ResolvedGoogleChatAccount;
144
- config: OpenClawConfig;
145
- core: GoogleChatCoreRuntime;
146
- space: GoogleChatSpace;
147
- message: GoogleChatMessage;
148
- isGroup: boolean;
149
- senderId: string;
150
- senderName: string;
151
- senderEmail?: string;
152
- rawBody: string;
153
- statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
154
- logVerbose: (message: string) => void;
155
- }): Promise<
156
- | {
157
- ok: true;
158
- commandAuthorized: boolean | undefined;
159
- effectiveWasMentioned: boolean | undefined;
160
- groupSystemPrompt: string | undefined;
161
- }
162
- | { ok: false }
163
- > {
164
- const {
165
- account,
166
- config,
167
- core,
168
- space,
169
- message,
170
- isGroup,
171
- senderId,
172
- senderName,
173
- senderEmail,
174
- rawBody,
175
- statusSink,
176
- logVerbose,
177
- } = params;
178
- const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
179
- const spaceId = space.name ?? "";
180
- const pairing = createChannelPairingController({
181
- core,
182
- channel: "googlechat",
183
- accountId: account.accountId,
184
- });
185
-
186
- const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
187
- const { groupPolicy, providerMissingFallbackApplied } =
188
- resolveAllowlistProviderRuntimeGroupPolicy({
189
- providerConfigPresent: config.channels?.googlechat !== undefined,
190
- groupPolicy: account.config.groupPolicy,
191
- defaultGroupPolicy,
192
- });
193
- warnMissingProviderGroupPolicyFallbackOnce({
194
- providerMissingFallbackApplied,
195
- providerKey: "googlechat",
196
- accountId: account.accountId,
197
- blockedLabel: GROUP_POLICY_BLOCKED_LABEL.space,
198
- log: logVerbose,
199
- });
200
- warnMutableGroupKeysConfigured(logVerbose, account.config.groups ?? undefined);
201
- const groupConfigResolved = resolveGroupConfig({
202
- groupId: spaceId,
203
- groupName: space.displayName ?? null,
204
- groups: account.config.groups ?? undefined,
205
- });
206
- const groupEntry = groupConfigResolved.entry;
207
- const groupUsers = groupEntry?.users ?? account.config.groupAllowFrom ?? [];
208
- const isGoogleChatSenderAllowed = (_senderId: string, allowFrom: string[]) =>
209
- isSenderAllowed(senderId, senderEmail, allowFrom, allowNameMatching);
210
- const expandedGroupUsers = await expandAllowFromWithAccessGroups({
211
- cfg: config,
212
- allowFrom: groupUsers,
213
- channel: "googlechat",
214
- accountId: account.accountId,
215
- senderId,
216
- isSenderAllowed: isGoogleChatSenderAllowed,
217
- });
218
- let effectiveWasMentioned: boolean | undefined;
219
-
220
- if (isGroup) {
221
- if (groupConfigResolved.deprecatedNameMatch) {
222
- logVerbose(`drop group message (deprecated mutable group key matched, space=${spaceId})`);
223
- return { ok: false };
224
- }
225
- const groupAllowlistConfigured = groupConfigResolved.allowlistConfigured;
226
- const routeAccess = evaluateGroupRouteAccessForPolicy({
227
- groupPolicy,
228
- routeAllowlistConfigured: groupAllowlistConfigured,
229
- routeMatched: Boolean(groupEntry),
230
- routeEnabled: groupEntry?.enabled !== false,
231
- });
232
- if (!routeAccess.allowed) {
233
- if (routeAccess.reason === "disabled") {
234
- logVerbose(`drop group message (groupPolicy=disabled, space=${spaceId})`);
235
- } else if (routeAccess.reason === "empty_allowlist") {
236
- logVerbose(`drop group message (groupPolicy=allowlist, no allowlist, space=${spaceId})`);
237
- } else if (routeAccess.reason === "route_not_allowlisted") {
238
- logVerbose(`drop group message (not allowlisted, space=${spaceId})`);
239
- } else if (routeAccess.reason === "route_disabled") {
240
- logVerbose(`drop group message (space disabled, space=${spaceId})`);
241
- }
242
- return { ok: false };
243
- }
244
-
245
- if (expandedGroupUsers.length > 0) {
246
- warnDeprecatedUsersEmailEntries(logVerbose, expandedGroupUsers);
247
- const ok = isSenderAllowed(senderId, senderEmail, expandedGroupUsers, allowNameMatching);
248
- if (!ok) {
249
- logVerbose(`drop group message (sender not allowed, ${senderId})`);
250
- return { ok: false };
251
- }
252
- }
253
- }
254
-
255
- const dmPolicy = account.config.dm?.policy ?? "pairing";
256
- const rawConfigAllowFrom = (account.config.dm?.allowFrom ?? []).map((v) => String(v));
257
- const normalizedGroupUsers = expandedGroupUsers;
258
- const senderGroupPolicy =
259
- groupConfigResolved.allowlistConfigured && normalizedGroupUsers.length === 0
260
- ? groupPolicy
261
- : resolveSenderScopedGroupPolicy({
262
- groupPolicy,
263
- groupAllowFrom: normalizedGroupUsers,
264
- });
265
- const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config);
266
- const storeAllowFrom =
267
- !isGroup && dmPolicy !== "allowlist" && dmPolicy !== "open"
268
- ? await pairing.readAllowFromStore().catch(() => [])
269
- : [];
270
- const [configAllowFrom, effectiveStoreAllowFrom] = await Promise.all([
271
- expandAllowFromWithAccessGroups({
272
- cfg: config,
273
- allowFrom: rawConfigAllowFrom,
274
- channel: "googlechat",
275
- accountId: account.accountId,
276
- senderId,
277
- isSenderAllowed: isGoogleChatSenderAllowed,
278
- }),
279
- expandAllowFromWithAccessGroups({
280
- cfg: config,
281
- allowFrom: storeAllowFrom,
282
- channel: "googlechat",
283
- accountId: account.accountId,
284
- senderId,
285
- isSenderAllowed: isGoogleChatSenderAllowed,
286
- }),
287
- ]);
288
- const access = resolveDmGroupAccessWithLists({
289
- isGroup,
290
- dmPolicy,
291
- groupPolicy: senderGroupPolicy,
292
- allowFrom: configAllowFrom,
293
- groupAllowFrom: normalizedGroupUsers,
294
- storeAllowFrom: effectiveStoreAllowFrom,
295
- groupAllowFromFallbackToAllowFrom: false,
296
- isSenderAllowed: (allowFrom) =>
297
- isSenderAllowed(senderId, senderEmail, allowFrom, allowNameMatching),
298
- });
299
- const effectiveAllowFrom = access.effectiveAllowFrom;
300
- const effectiveGroupAllowFrom = access.effectiveGroupAllowFrom;
301
- warnDeprecatedUsersEmailEntries(logVerbose, effectiveAllowFrom);
302
- const commandAllowFrom = isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom;
303
- const useAccessGroups = config.commands?.useAccessGroups !== false;
304
- const senderAllowedForCommands = isSenderAllowed(
305
- senderId,
306
- senderEmail,
307
- commandAllowFrom,
308
- allowNameMatching,
309
- );
310
- const commandAuthorized = shouldComputeAuth
311
- ? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
312
- useAccessGroups,
313
- authorizers: [
314
- { configured: commandAllowFrom.length > 0, allowed: senderAllowedForCommands },
315
- ],
316
- })
317
- : undefined;
318
-
319
- if (isGroup) {
320
- const requireMention = groupEntry?.requireMention ?? account.config.requireMention ?? true;
321
- const annotations = message.annotations ?? [];
322
- const mentionInfo = extractMentionInfo(annotations, account.config.botUser);
323
- const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
324
- cfg: config,
325
- surface: "googlechat",
326
- });
327
- const mentionDecision = resolveInboundMentionDecision({
328
- facts: {
329
- canDetectMention: true,
330
- wasMentioned: mentionInfo.wasMentioned,
331
- hasAnyMention: mentionInfo.hasAnyMention,
332
- implicitMentionKinds: [],
333
- },
334
- policy: {
335
- isGroup: true,
336
- requireMention,
337
- allowTextCommands,
338
- hasControlCommand: core.channel.text.hasControlCommand(rawBody, config),
339
- commandAuthorized: commandAuthorized === true,
340
- },
341
- });
342
- effectiveWasMentioned = mentionDecision.effectiveWasMentioned;
343
- if (mentionDecision.shouldSkip) {
344
- logVerbose(`drop group message (mention required, space=${spaceId})`);
345
- return { ok: false };
346
- }
347
- }
348
-
349
- if (isGroup && access.decision !== "allow") {
350
- logVerbose(
351
- `drop group message (sender policy blocked, reason=${access.reason}, space=${spaceId})`,
352
- );
353
- return { ok: false };
354
- }
355
-
356
- if (!isGroup) {
357
- if (account.config.dm?.enabled === false) {
358
- logVerbose(`Blocked Google Chat DM from ${senderId} (dmPolicy=disabled)`);
359
- return { ok: false };
360
- }
361
-
362
- if (access.decision !== "allow") {
363
- if (access.decision === "pairing") {
364
- await pairing.issueChallenge({
365
- senderId,
366
- senderIdLine: `Your Google Chat user id: ${senderId}`,
367
- meta: { name: senderName || undefined, email: senderEmail },
368
- onCreated: () => {
369
- logVerbose(`googlechat pairing request sender=${senderId}`);
370
- },
371
- sendPairingReply: async (text) => {
372
- await sendGoogleChatMessage({
373
- account,
374
- space: spaceId,
375
- text,
376
- });
377
- statusSink?.({ lastOutboundAt: Date.now() });
378
- },
379
- onReplyError: (err) => {
380
- logVerbose(`pairing reply failed for ${senderId}: ${String(err)}`);
381
- },
382
- });
383
- } else {
384
- logVerbose(`Blocked unauthorized Google Chat sender ${senderId} (dmPolicy=${dmPolicy})`);
385
- }
386
- return { ok: false };
387
- }
388
- }
389
-
390
- if (
391
- isGroup &&
392
- core.channel.commands.isControlCommandMessage(rawBody, config) &&
393
- commandAuthorized !== true
394
- ) {
395
- logVerbose(`googlechat: drop control command from ${senderId}`);
396
- return { ok: false };
397
- }
398
-
399
- return {
400
- ok: true,
401
- commandAuthorized,
402
- effectiveWasMentioned,
403
- groupSystemPrompt: normalizeOptionalString(groupEntry?.systemPrompt),
404
- };
405
- }
@@ -1,156 +0,0 @@
1
- import {
2
- deliverTextOrMediaReply,
3
- resolveSendableOutboundReplyParts,
4
- } from "openclaw/plugin-sdk/reply-payload";
5
- import type { OpenClawConfig } from "../runtime-api.js";
6
- import type { ResolvedGoogleChatAccount } from "./accounts.js";
7
- import {
8
- deleteGoogleChatMessage,
9
- sendGoogleChatMessage,
10
- updateGoogleChatMessage,
11
- uploadGoogleChatAttachment,
12
- } from "./api.js";
13
- import type { GoogleChatCoreRuntime, GoogleChatRuntimeEnv } from "./monitor-types.js";
14
-
15
- export async function deliverGoogleChatReply(params: {
16
- payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string; replyToId?: string };
17
- account: ResolvedGoogleChatAccount;
18
- spaceId: string;
19
- runtime: GoogleChatRuntimeEnv;
20
- core: GoogleChatCoreRuntime;
21
- config: OpenClawConfig;
22
- statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
23
- typingMessageName?: string;
24
- }): Promise<void> {
25
- const { payload, account, spaceId, runtime, core, config, statusSink } = params;
26
- // Clear this whenever the typing message is deleted or unavailable; otherwise
27
- // text delivery can keep retrying a dead message and drop content.
28
- let typingMessageName = params.typingMessageName;
29
- const reply = resolveSendableOutboundReplyParts(payload);
30
- const mediaCount = reply.mediaCount;
31
- const hasMedia = reply.hasMedia;
32
- const text = reply.text;
33
- let firstTextChunk = true;
34
- let suppressCaption = false;
35
-
36
- if (hasMedia && typingMessageName) {
37
- try {
38
- await deleteGoogleChatMessage({
39
- account,
40
- messageName: typingMessageName,
41
- });
42
- typingMessageName = undefined;
43
- } catch (err) {
44
- runtime.error?.(`Google Chat typing cleanup failed: ${String(err)}`);
45
- if (typingMessageName) {
46
- const fallbackText = reply.hasText
47
- ? text
48
- : mediaCount > 1
49
- ? "Sent attachments."
50
- : "Sent attachment.";
51
- try {
52
- await updateGoogleChatMessage({
53
- account,
54
- messageName: typingMessageName,
55
- text: fallbackText,
56
- });
57
- suppressCaption = Boolean(text.trim());
58
- } catch (updateErr) {
59
- runtime.error?.(`Google Chat typing update failed: ${String(updateErr)}`);
60
- typingMessageName = undefined;
61
- }
62
- }
63
- }
64
- }
65
-
66
- const chunkLimit = account.config.textChunkLimit ?? 4000;
67
- const chunkMode = core.channel.text.resolveChunkMode(config, "googlechat", account.accountId);
68
- const sendTextMessage = async (chunk: string) => {
69
- await sendGoogleChatMessage({
70
- account,
71
- space: spaceId,
72
- text: chunk,
73
- thread: payload.replyToId,
74
- });
75
- };
76
- await deliverTextOrMediaReply({
77
- payload,
78
- text: suppressCaption ? "" : reply.text,
79
- chunkText: (value) => core.channel.text.chunkMarkdownTextWithMode(value, chunkLimit, chunkMode),
80
- sendText: async (chunk) => {
81
- try {
82
- if (firstTextChunk && typingMessageName) {
83
- await updateGoogleChatMessage({
84
- account,
85
- messageName: typingMessageName,
86
- text: chunk,
87
- });
88
- } else {
89
- await sendTextMessage(chunk);
90
- }
91
- firstTextChunk = false;
92
- statusSink?.({ lastOutboundAt: Date.now() });
93
- } catch (err) {
94
- runtime.error?.(`Google Chat message send failed: ${String(err)}`);
95
- if (firstTextChunk && typingMessageName) {
96
- typingMessageName = undefined;
97
- try {
98
- await sendTextMessage(chunk);
99
- statusSink?.({ lastOutboundAt: Date.now() });
100
- } catch (fallbackErr) {
101
- runtime.error?.(`Google Chat message fallback send failed: ${String(fallbackErr)}`);
102
- } finally {
103
- firstTextChunk = false;
104
- }
105
- }
106
- }
107
- },
108
- sendMedia: async ({ mediaUrl, caption }) => {
109
- try {
110
- const loaded = await core.channel.media.fetchRemoteMedia({
111
- url: mediaUrl,
112
- maxBytes: (account.config.mediaMaxMb ?? 20) * 1024 * 1024,
113
- });
114
- const upload = await uploadAttachmentForReply({
115
- account,
116
- spaceId,
117
- buffer: loaded.buffer,
118
- contentType: loaded.contentType,
119
- filename: loaded.fileName ?? "attachment",
120
- });
121
- if (!upload.attachmentUploadToken) {
122
- throw new Error("missing attachment upload token");
123
- }
124
- await sendGoogleChatMessage({
125
- account,
126
- space: spaceId,
127
- text: caption,
128
- thread: payload.replyToId,
129
- attachments: [
130
- { attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.fileName },
131
- ],
132
- });
133
- statusSink?.({ lastOutboundAt: Date.now() });
134
- } catch (err) {
135
- runtime.error?.(`Google Chat attachment send failed: ${String(err)}`);
136
- }
137
- },
138
- });
139
- }
140
-
141
- async function uploadAttachmentForReply(params: {
142
- account: ResolvedGoogleChatAccount;
143
- spaceId: string;
144
- buffer: Buffer;
145
- contentType?: string;
146
- filename: string;
147
- }) {
148
- const { account, spaceId, buffer, contentType, filename } = params;
149
- return await uploadGoogleChatAttachment({
150
- account,
151
- space: spaceId,
152
- filename,
153
- buffer,
154
- contentType,
155
- });
156
- }
@@ -1,55 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- import { createWebhookInFlightLimiter } from "openclaw/plugin-sdk/webhook-request-guards";
3
- import { registerWebhookTargetWithPluginRoute } from "openclaw/plugin-sdk/webhook-targets";
4
- import type { WebhookTarget } from "./monitor-types.js";
5
- import { createGoogleChatWebhookRequestHandler } from "./monitor-webhook.js";
6
- import type { GoogleChatEvent } from "./types.js";
7
-
8
- type ProcessGoogleChatEvent = (event: GoogleChatEvent, target: WebhookTarget) => Promise<void>;
9
-
10
- const webhookTargets = new Map<string, WebhookTarget[]>();
11
- const webhookInFlightLimiter = createWebhookInFlightLimiter();
12
-
13
- let processGoogleChatEvent: ProcessGoogleChatEvent = async () => {};
14
-
15
- export function setGoogleChatWebhookEventProcessor(processEvent: ProcessGoogleChatEvent): void {
16
- processGoogleChatEvent = processEvent;
17
- }
18
-
19
- const googleChatWebhookRequestHandler = createGoogleChatWebhookRequestHandler({
20
- webhookTargets,
21
- webhookInFlightLimiter,
22
- processEvent: async (event, target) => {
23
- await processGoogleChatEvent(event, target);
24
- },
25
- });
26
-
27
- export function registerGoogleChatWebhookTarget(target: WebhookTarget): () => void {
28
- return registerWebhookTargetWithPluginRoute({
29
- targetsByPath: webhookTargets,
30
- target,
31
- route: {
32
- auth: "plugin",
33
- match: "exact",
34
- pluginId: "googlechat",
35
- source: "googlechat-webhook",
36
- accountId: target.account.accountId,
37
- log: target.runtime.log,
38
- handler: async (req, res) => {
39
- const handled = await handleGoogleChatWebhookRequest(req, res);
40
- if (!handled && !res.headersSent) {
41
- res.statusCode = 404;
42
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
43
- res.end("Not Found");
44
- }
45
- },
46
- },
47
- }).unregister;
48
- }
49
-
50
- export async function handleGoogleChatWebhookRequest(
51
- req: IncomingMessage,
52
- res: ServerResponse,
53
- ): Promise<boolean> {
54
- return await googleChatWebhookRequestHandler(req, res);
55
- }
@@ -1,33 +0,0 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
2
- import type { ResolvedGoogleChatAccount } from "./accounts.js";
3
- import type { GoogleChatAudienceType } from "./auth.js";
4
- import type { getGoogleChatRuntime } from "./runtime.js";
5
-
6
- export type GoogleChatRuntimeEnv = {
7
- log?: (message: string) => void;
8
- error?: (message: string) => void;
9
- };
10
-
11
- export type GoogleChatMonitorOptions = {
12
- account: ResolvedGoogleChatAccount;
13
- config: OpenClawConfig;
14
- runtime: GoogleChatRuntimeEnv;
15
- abortSignal: AbortSignal;
16
- webhookPath?: string;
17
- webhookUrl?: string;
18
- statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
19
- };
20
-
21
- export type GoogleChatCoreRuntime = ReturnType<typeof getGoogleChatRuntime>;
22
-
23
- export type WebhookTarget = {
24
- account: ResolvedGoogleChatAccount;
25
- config: OpenClawConfig;
26
- runtime: GoogleChatRuntimeEnv;
27
- core: GoogleChatCoreRuntime;
28
- path: string;
29
- audienceType?: GoogleChatAudienceType;
30
- audience?: string;
31
- statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
32
- mediaMaxMb: number;
33
- };