@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.
- package/dist/api-C67jv0ow.js +627 -0
- package/dist/api.js +3 -0
- package/dist/channel-DE2hroMn.js +682 -0
- package/dist/channel-config-api.js +5 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-CoSLc-4R.js +920 -0
- package/dist/contract-api.js +3 -0
- package/dist/doctor-contract-CG1sLToP.js +151 -0
- package/dist/doctor-contract-api.js +2 -0
- package/dist/index.js +22 -0
- package/dist/runtime-api-wkIdfwqY.js +30 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-DtQ_IO7J.js +99 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +75 -0
- package/dist/setup-surface-CofP-Gg9.js +309 -0
- package/dist/test-api.js +3 -0
- package/package.json +14 -6
- package/api.ts +0 -3
- package/channel-config-api.ts +0 -1
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -5
- package/doctor-contract-api.ts +0 -1
- package/index.ts +0 -20
- package/runtime-api.ts +0 -60
- package/secret-contract-api.ts +0 -5
- package/setup-entry.ts +0 -13
- package/setup-plugin-api.ts +0 -3
- package/src/accounts.ts +0 -169
- package/src/actions.test.ts +0 -265
- package/src/actions.ts +0 -227
- package/src/api.ts +0 -322
- package/src/approval-auth.test.ts +0 -24
- package/src/approval-auth.ts +0 -32
- package/src/auth.ts +0 -209
- package/src/channel-config.test.ts +0 -39
- package/src/channel.adapters.ts +0 -293
- package/src/channel.deps.runtime.ts +0 -28
- package/src/channel.runtime.ts +0 -17
- package/src/channel.setup.ts +0 -98
- package/src/channel.test.ts +0 -695
- package/src/channel.ts +0 -256
- package/src/config-schema.test.ts +0 -16
- package/src/config-schema.ts +0 -3
- package/src/doctor-contract.test.ts +0 -75
- package/src/doctor-contract.ts +0 -182
- package/src/doctor.ts +0 -57
- package/src/gateway.ts +0 -63
- package/src/google-auth.runtime.test.ts +0 -462
- package/src/google-auth.runtime.ts +0 -539
- package/src/group-policy.ts +0 -17
- package/src/monitor-access.test.ts +0 -443
- package/src/monitor-access.ts +0 -405
- package/src/monitor-reply-delivery.ts +0 -156
- package/src/monitor-routing.ts +0 -55
- package/src/monitor-types.ts +0 -33
- package/src/monitor-webhook.test.ts +0 -446
- package/src/monitor-webhook.ts +0 -285
- package/src/monitor.reply-delivery.test.ts +0 -139
- package/src/monitor.ts +0 -428
- package/src/monitor.webhook-routing.test.ts +0 -252
- package/src/runtime.ts +0 -9
- package/src/secret-contract.test.ts +0 -60
- package/src/secret-contract.ts +0 -161
- package/src/sender-allow.ts +0 -46
- package/src/setup-core.ts +0 -40
- package/src/setup-surface.ts +0 -236
- package/src/setup.test.ts +0 -560
- package/src/targets.test.ts +0 -421
- package/src/targets.ts +0 -66
- package/src/types.config.ts +0 -3
- package/src/types.ts +0 -73
- package/test-api.ts +0 -2
- package/tsconfig.json +0 -16
package/src/monitor-access.ts
DELETED
|
@@ -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
|
-
}
|
package/src/monitor-routing.ts
DELETED
|
@@ -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
|
-
}
|
package/src/monitor-types.ts
DELETED
|
@@ -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
|
-
};
|