@kodelyth/googlechat 2026.5.39 → 2026.5.42
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/api.ts +3 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +1 -0
- package/config-api.ts +2 -0
- package/contract-api.ts +5 -0
- package/dist/actions-YK1wn4ed.js +160 -0
- package/dist/api-BkZX4VNX.js +633 -0
- package/dist/api.js +3 -0
- package/dist/channel-DFZdjXD6.js +584 -0
- package/dist/channel-config-api.js +6 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-en3RNg9S.js +998 -0
- package/dist/contract-api.js +3 -0
- package/dist/doctor-contract-8SF6XoKj.js +151 -0
- package/dist/doctor-contract-api.js +2 -0
- package/dist/index.js +22 -0
- package/dist/runtime-api-DUH2Cg-0.js +29 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-DWX4ikgT.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-B3Fa7XRx.js +321 -0
- package/dist/test-api.js +3 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +20 -0
- package/klaw.plugin.json +2 -967
- package/package.json +4 -4
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/accounts.ts +181 -0
- package/src/actions.test.ts +289 -0
- package/src/actions.ts +227 -0
- package/src/api.ts +316 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +32 -0
- package/src/auth.ts +218 -0
- package/src/channel-config.test.ts +39 -0
- package/src/channel.adapters.ts +340 -0
- package/src/channel.deps.runtime.ts +29 -0
- package/src/channel.runtime.ts +17 -0
- package/src/channel.setup.ts +98 -0
- package/src/channel.test.ts +784 -0
- package/src/channel.ts +277 -0
- package/src/config-schema.test.ts +31 -0
- package/src/config-schema.ts +3 -0
- package/src/doctor-contract.test.ts +75 -0
- package/src/doctor-contract.ts +182 -0
- package/src/doctor.ts +57 -0
- package/src/gateway.ts +63 -0
- package/src/google-auth.runtime.test.ts +543 -0
- package/src/google-auth.runtime.ts +568 -0
- package/src/group-policy.ts +17 -0
- package/src/monitor-access.test.ts +491 -0
- package/src/monitor-access.ts +465 -0
- package/src/monitor-durable.test.ts +39 -0
- package/src/monitor-durable.ts +23 -0
- package/src/monitor-reply-delivery.ts +156 -0
- package/src/monitor-routing.ts +65 -0
- package/src/monitor-types.ts +33 -0
- package/src/monitor-webhook.test.ts +587 -0
- package/src/monitor-webhook.ts +303 -0
- package/src/monitor.reply-delivery.test.ts +144 -0
- package/src/monitor.test.ts +159 -0
- package/src/monitor.ts +527 -0
- package/src/monitor.webhook-routing.test.ts +257 -0
- package/src/runtime.ts +9 -0
- package/src/secret-contract.test.ts +60 -0
- package/src/secret-contract.ts +161 -0
- package/src/setup-core.ts +40 -0
- package/src/setup-surface.ts +243 -0
- package/src/setup.test.ts +619 -0
- package/src/targets.test.ts +453 -0
- package/src/targets.ts +66 -0
- package/src/types.config.ts +3 -0
- package/src/types.ts +73 -0
- package/test-api.ts +2 -0
- package/tsconfig.json +16 -0
- package/api.js +0 -7
- package/channel-config-api.js +0 -7
- package/channel-plugin-api.js +0 -7
- package/contract-api.js +0 -7
- package/doctor-contract-api.js +0 -7
- package/index.js +0 -7
- package/runtime-api.js +0 -7
- package/secret-contract-api.js +0 -7
- package/setup-entry.js +0 -7
- package/setup-plugin-api.js +0 -7
- package/test-api.js +0 -7
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
import { E as resolveDefaultGroupPolicy, M as warnMissingProviderGroupPolicyFallbackOnce, O as resolveInboundRouteEnvelopeBuilderWithRuntime, P as getGoogleChatRuntime, k as resolveWebhookPath, m as isDangerousNameMatchingEnabled, n as GROUP_POLICY_BLOCKED_LABEL, u as createChannelPairingController, w as resolveAllowlistProviderRuntimeGroupPolicy } from "./runtime-api-DUH2Cg-0.js";
|
|
2
|
+
import { c as sendGoogleChatMessage, d as verifyGoogleChatRequest, i as downloadGoogleChatMedia, l as updateGoogleChatMessage, n as deleteGoogleChatMessage, s as probeGoogleChat, u as uploadGoogleChatAttachment } from "./api-BkZX4VNX.js";
|
|
3
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "klaw/plugin-sdk/string-coerce-runtime";
|
|
4
|
+
import { mergePairLoopGuardConfig } from "klaw/plugin-sdk/pair-loop-guard-runtime";
|
|
5
|
+
import { WEBHOOK_RATE_LIMIT_DEFAULTS, createFixedWindowRateLimiter, normalizeWebhookPath, resolveRequestClientIp } from "klaw/plugin-sdk/webhook-ingress";
|
|
6
|
+
import { registerWebhookTargetWithPluginRoute, resolveWebhookTargetWithAuthOrReject, withResolvedWebhookRequestPipeline } from "klaw/plugin-sdk/webhook-targets";
|
|
7
|
+
import { createWebhookInFlightLimiter, readJsonWebhookBodyOrReject } from "klaw/plugin-sdk/webhook-request-guards";
|
|
8
|
+
import { recordChannelBotPairLoopAndCheckSuppression } from "klaw/plugin-sdk/inbound-reply-dispatch";
|
|
9
|
+
import { channelIngressRoutes, createChannelIngressResolver, defineStableChannelIngressIdentity } from "klaw/plugin-sdk/channel-ingress-runtime";
|
|
10
|
+
import { deliverTextOrMediaReply, resolveSendableOutboundReplyParts } from "klaw/plugin-sdk/reply-payload";
|
|
11
|
+
//#region extensions/googlechat/src/monitor-access.ts
|
|
12
|
+
function normalizeUserId(raw) {
|
|
13
|
+
const trimmed = normalizeOptionalString(raw) ?? "";
|
|
14
|
+
if (!trimmed) return "";
|
|
15
|
+
return normalizeLowercaseStringOrEmpty(trimmed.replace(/^users\//i, ""));
|
|
16
|
+
}
|
|
17
|
+
const GOOGLECHAT_EMAIL_KIND = "plugin:googlechat-email";
|
|
18
|
+
function normalizeEntryValue(raw) {
|
|
19
|
+
return normalizeLowercaseStringOrEmpty(raw ?? "");
|
|
20
|
+
}
|
|
21
|
+
function normalizeGoogleChatStableEntry(entry) {
|
|
22
|
+
const withoutProvider = normalizeEntryValue(entry).replace(/^(googlechat|google-chat|gchat):/i, "");
|
|
23
|
+
if (!withoutProvider) return null;
|
|
24
|
+
return withoutProvider.startsWith("users/") ? normalizeUserId(withoutProvider) : withoutProvider;
|
|
25
|
+
}
|
|
26
|
+
function normalizeGoogleChatEmailEntry(entry) {
|
|
27
|
+
if (normalizeEntryValue(entry).replace(/^(googlechat|google-chat|gchat):/i, "").startsWith("users/")) return null;
|
|
28
|
+
const stable = normalizeGoogleChatStableEntry(entry);
|
|
29
|
+
return stable?.includes("@") ? stable : null;
|
|
30
|
+
}
|
|
31
|
+
const googleChatIngressIdentity = defineStableChannelIngressIdentity({
|
|
32
|
+
key: "sender-id",
|
|
33
|
+
normalizeEntry: normalizeGoogleChatStableEntry,
|
|
34
|
+
normalizeSubject: normalizeUserId,
|
|
35
|
+
aliases: [{
|
|
36
|
+
key: "email",
|
|
37
|
+
kind: GOOGLECHAT_EMAIL_KIND,
|
|
38
|
+
normalizeEntry: normalizeGoogleChatEmailEntry,
|
|
39
|
+
normalizeSubject: normalizeEntryValue,
|
|
40
|
+
dangerous: true
|
|
41
|
+
}],
|
|
42
|
+
isWildcardEntry: (entry) => normalizeEntryValue(entry) === "*",
|
|
43
|
+
resolveEntryId: ({ entryIndex, fieldKey }) => fieldKey === "stableId" ? `entry-${entryIndex + 1}:user` : `entry-${entryIndex + 1}:${fieldKey}`
|
|
44
|
+
});
|
|
45
|
+
function resolveGroupConfig(params) {
|
|
46
|
+
const { groupId, groupName, groups } = params;
|
|
47
|
+
const entries = groups ?? {};
|
|
48
|
+
const keys = Object.keys(entries);
|
|
49
|
+
if (keys.length === 0) return {
|
|
50
|
+
entry: void 0,
|
|
51
|
+
allowlistConfigured: false,
|
|
52
|
+
deprecatedNameMatch: false
|
|
53
|
+
};
|
|
54
|
+
const entry = entries[groupId];
|
|
55
|
+
const normalizedGroupName = normalizeLowercaseStringOrEmpty(groupName ?? "");
|
|
56
|
+
const deprecatedNameMatch = !entry && Boolean(groupName && keys.some((key) => {
|
|
57
|
+
const trimmed = key.trim();
|
|
58
|
+
if (!trimmed || trimmed === "*" || /^spaces\//i.test(trimmed)) return false;
|
|
59
|
+
return trimmed === groupName || normalizeLowercaseStringOrEmpty(trimmed) === normalizedGroupName;
|
|
60
|
+
}));
|
|
61
|
+
const fallback = entries["*"];
|
|
62
|
+
return {
|
|
63
|
+
entry: deprecatedNameMatch ? void 0 : entry ?? fallback,
|
|
64
|
+
allowlistConfigured: true,
|
|
65
|
+
fallback,
|
|
66
|
+
deprecatedNameMatch
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function extractMentionInfo(annotations, botUser) {
|
|
70
|
+
const mentionAnnotations = annotations.filter((entry) => entry.type === "USER_MENTION");
|
|
71
|
+
const hasAnyMention = mentionAnnotations.length > 0;
|
|
72
|
+
const botTargets = new Set(["users/app", botUser?.trim()].filter(Boolean));
|
|
73
|
+
return {
|
|
74
|
+
hasAnyMention,
|
|
75
|
+
wasMentioned: mentionAnnotations.some((entry) => {
|
|
76
|
+
const userName = entry.userMention?.user?.name;
|
|
77
|
+
if (!userName) return false;
|
|
78
|
+
if (botTargets.has(userName)) return true;
|
|
79
|
+
return normalizeUserId(userName) === "app";
|
|
80
|
+
})
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const warnedDeprecatedUsersEmailAllowFrom = /* @__PURE__ */ new Set();
|
|
84
|
+
const warnedMutableGroupKeys = /* @__PURE__ */ new Set();
|
|
85
|
+
function warnDeprecatedUsersEmailEntries(logVerbose, entries) {
|
|
86
|
+
const deprecated = entries.map((v) => normalizeOptionalString(v)).filter((v) => Boolean(v)).filter((v) => /^users\/.+@.+/i.test(v));
|
|
87
|
+
if (deprecated.length === 0) return;
|
|
88
|
+
const key = deprecated.map((v) => normalizeLowercaseStringOrEmpty(v)).toSorted((a, b) => a.localeCompare(b)).join(",");
|
|
89
|
+
if (warnedDeprecatedUsersEmailAllowFrom.has(key)) return;
|
|
90
|
+
warnedDeprecatedUsersEmailAllowFrom.add(key);
|
|
91
|
+
logVerbose(`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(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
function warnMutableGroupKeysConfigured(logVerbose, groups) {
|
|
94
|
+
const mutableKeys = Object.keys(groups ?? {}).map((key) => key.trim()).filter((key) => key && key !== "*" && !/^spaces\//i.test(key));
|
|
95
|
+
if (mutableKeys.length === 0) return;
|
|
96
|
+
const warningKey = mutableKeys.map((key) => normalizeLowercaseStringOrEmpty(key)).toSorted((a, b) => a.localeCompare(b)).join(",");
|
|
97
|
+
if (warnedMutableGroupKeys.has(warningKey)) return;
|
|
98
|
+
warnedMutableGroupKeys.add(warningKey);
|
|
99
|
+
logVerbose(`Deprecated Google Chat group key detected: group routing now requires stable space ids (spaces/<spaceId>). Update channels.googlechat.groups keys: ${mutableKeys.join(", ")}`);
|
|
100
|
+
}
|
|
101
|
+
async function applyGoogleChatInboundAccessPolicy(params) {
|
|
102
|
+
const { account, config, core, space, message, isGroup, senderId, senderName, senderEmail, rawBody, statusSink, logVerbose } = params;
|
|
103
|
+
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
|
104
|
+
const spaceId = space.name ?? "";
|
|
105
|
+
const pairing = createChannelPairingController({
|
|
106
|
+
core,
|
|
107
|
+
channel: "googlechat",
|
|
108
|
+
accountId: account.accountId
|
|
109
|
+
});
|
|
110
|
+
const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
|
|
111
|
+
const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
|
|
112
|
+
providerConfigPresent: config.channels?.googlechat !== void 0,
|
|
113
|
+
groupPolicy: account.config.groupPolicy,
|
|
114
|
+
defaultGroupPolicy
|
|
115
|
+
});
|
|
116
|
+
warnMissingProviderGroupPolicyFallbackOnce({
|
|
117
|
+
providerMissingFallbackApplied,
|
|
118
|
+
providerKey: "googlechat",
|
|
119
|
+
accountId: account.accountId,
|
|
120
|
+
blockedLabel: GROUP_POLICY_BLOCKED_LABEL.space,
|
|
121
|
+
log: logVerbose
|
|
122
|
+
});
|
|
123
|
+
warnMutableGroupKeysConfigured(logVerbose, account.config.groups ?? void 0);
|
|
124
|
+
const groupConfigResolved = resolveGroupConfig({
|
|
125
|
+
groupId: spaceId,
|
|
126
|
+
groupName: space.displayName ?? null,
|
|
127
|
+
groups: account.config.groups ?? void 0
|
|
128
|
+
});
|
|
129
|
+
const groupEntry = groupConfigResolved.entry;
|
|
130
|
+
const groupUsers = groupEntry?.users ?? account.config.groupAllowFrom ?? [];
|
|
131
|
+
let effectiveWasMentioned;
|
|
132
|
+
const dmPolicy = account.config.dm?.policy ?? "pairing";
|
|
133
|
+
const rawConfigAllowFrom = normalizeStringEntries(account.config.dm?.allowFrom);
|
|
134
|
+
const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config);
|
|
135
|
+
const groupActivation = (() => {
|
|
136
|
+
if (!isGroup) return;
|
|
137
|
+
const requireMention = groupEntry?.requireMention ?? account.config.requireMention ?? true;
|
|
138
|
+
const mentionInfo = extractMentionInfo(message.annotations ?? [], account.config.botUser);
|
|
139
|
+
return {
|
|
140
|
+
requireMention,
|
|
141
|
+
allowTextCommands: core.channel.commands.shouldHandleTextCommands({
|
|
142
|
+
cfg: config,
|
|
143
|
+
surface: "googlechat"
|
|
144
|
+
}),
|
|
145
|
+
hasControlCommand: core.channel.text.hasControlCommand(rawBody, config),
|
|
146
|
+
wasMentioned: mentionInfo.wasMentioned,
|
|
147
|
+
hasAnyMention: mentionInfo.hasAnyMention
|
|
148
|
+
};
|
|
149
|
+
})();
|
|
150
|
+
const command = {
|
|
151
|
+
hasControlCommand: groupActivation?.hasControlCommand ?? shouldComputeAuth,
|
|
152
|
+
groupOwnerAllowFrom: "none"
|
|
153
|
+
};
|
|
154
|
+
const groupAllowFrom = normalizeStringEntries(groupUsers);
|
|
155
|
+
const senderGroupPolicy = groupConfigResolved.allowlistConfigured && groupAllowFrom.length === 0 ? groupPolicy : groupPolicy === "disabled" ? "disabled" : groupAllowFrom.length > 0 ? "allowlist" : "open";
|
|
156
|
+
const route = channelIngressRoutes(isGroup && groupPolicy !== "disabled" && groupEntry?.enabled === false && {
|
|
157
|
+
id: "googlechat:space",
|
|
158
|
+
enabled: false,
|
|
159
|
+
matched: true,
|
|
160
|
+
matchId: "googlechat-space",
|
|
161
|
+
blockReason: "route_disabled"
|
|
162
|
+
}, isGroup && groupPolicy === "allowlist" && groupEntry?.enabled !== false && !groupConfigResolved.allowlistConfigured && {
|
|
163
|
+
id: "googlechat:space",
|
|
164
|
+
allowed: false,
|
|
165
|
+
blockReason: "empty_allowlist"
|
|
166
|
+
}, isGroup && groupPolicy === "allowlist" && groupEntry?.enabled !== false && groupConfigResolved.allowlistConfigured && {
|
|
167
|
+
id: "googlechat:space",
|
|
168
|
+
senderPolicy: "deny-when-empty",
|
|
169
|
+
...groupEntry ? { senderAllowFromSource: "effective-group" } : {},
|
|
170
|
+
allowed: Boolean(groupEntry),
|
|
171
|
+
matchId: "googlechat-space",
|
|
172
|
+
blockReason: groupEntry ? "sender_empty_allowlist" : "route_not_allowlisted"
|
|
173
|
+
});
|
|
174
|
+
const resolvedAccess = await createChannelIngressResolver({
|
|
175
|
+
channelId: "googlechat",
|
|
176
|
+
accountId: account.accountId,
|
|
177
|
+
identity: googleChatIngressIdentity,
|
|
178
|
+
cfg: config,
|
|
179
|
+
readStoreAllowFrom: pairing.readAllowFromStore
|
|
180
|
+
}).message({
|
|
181
|
+
subject: {
|
|
182
|
+
stableId: senderId,
|
|
183
|
+
aliases: { email: senderEmail }
|
|
184
|
+
},
|
|
185
|
+
conversation: {
|
|
186
|
+
kind: isGroup ? "group" : "direct",
|
|
187
|
+
id: spaceId
|
|
188
|
+
},
|
|
189
|
+
route,
|
|
190
|
+
allowFrom: rawConfigAllowFrom,
|
|
191
|
+
groupAllowFrom,
|
|
192
|
+
dmPolicy,
|
|
193
|
+
groupPolicy: senderGroupPolicy,
|
|
194
|
+
policy: {
|
|
195
|
+
groupAllowFromFallbackToAllowFrom: false,
|
|
196
|
+
mutableIdentifierMatching: allowNameMatching ? "enabled" : "disabled",
|
|
197
|
+
...groupActivation ? { activation: {
|
|
198
|
+
requireMention: groupActivation.requireMention,
|
|
199
|
+
allowTextCommands: groupActivation.allowTextCommands
|
|
200
|
+
} } : {}
|
|
201
|
+
},
|
|
202
|
+
...groupActivation == null ? {} : { mentionFacts: {
|
|
203
|
+
canDetectMention: true,
|
|
204
|
+
wasMentioned: groupActivation.wasMentioned,
|
|
205
|
+
hasAnyMention: groupActivation.hasAnyMention,
|
|
206
|
+
implicitMentionKinds: []
|
|
207
|
+
} },
|
|
208
|
+
command
|
|
209
|
+
});
|
|
210
|
+
const senderAccess = resolvedAccess.senderAccess;
|
|
211
|
+
const commandAuthorized = resolvedAccess.commandAccess.requested ? resolvedAccess.commandAccess.authorized : void 0;
|
|
212
|
+
if (isGroup) {
|
|
213
|
+
if (groupConfigResolved.deprecatedNameMatch) {
|
|
214
|
+
logVerbose(`drop group message (deprecated mutable group key matched, space=${spaceId})`);
|
|
215
|
+
return { ok: false };
|
|
216
|
+
}
|
|
217
|
+
const routeBlockReason = resolvedAccess.routeAccess.reason;
|
|
218
|
+
if (routeBlockReason && routeBlockReason !== "sender_empty_allowlist") {
|
|
219
|
+
if (routeBlockReason === "empty_allowlist") logVerbose(`drop group message (groupPolicy=allowlist, no allowlist, space=${spaceId})`);
|
|
220
|
+
else if (routeBlockReason === "route_not_allowlisted") logVerbose(`drop group message (not allowlisted, space=${spaceId})`);
|
|
221
|
+
else if (routeBlockReason === "route_disabled") logVerbose(`drop group message (space disabled, space=${spaceId})`);
|
|
222
|
+
return { ok: false };
|
|
223
|
+
}
|
|
224
|
+
if (senderAccess.effectiveGroupAllowFrom.length > 0 && senderAccess.decision !== "allow") {
|
|
225
|
+
warnDeprecatedUsersEmailEntries(logVerbose, senderAccess.effectiveGroupAllowFrom);
|
|
226
|
+
logVerbose(`drop group message (sender not allowed, ${senderId})`);
|
|
227
|
+
return { ok: false };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const effectiveAllowFrom = senderAccess.effectiveAllowFrom;
|
|
231
|
+
warnDeprecatedUsersEmailEntries(logVerbose, effectiveAllowFrom);
|
|
232
|
+
if (isGroup && resolvedAccess.activationAccess.ran) {
|
|
233
|
+
effectiveWasMentioned = resolvedAccess.activationAccess.effectiveWasMentioned;
|
|
234
|
+
if (resolvedAccess.activationAccess.shouldSkip) {
|
|
235
|
+
logVerbose(`drop group message (mention required, space=${spaceId})`);
|
|
236
|
+
return { ok: false };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (isGroup && senderAccess.decision !== "allow") {
|
|
240
|
+
logVerbose(`drop group message (sender policy blocked, reason=${resolvedAccess.ingress.reasonCode === "route_sender_empty" ? "groupPolicy=allowlist (empty allowlist)" : senderAccess.reasonCode}, space=${spaceId})`);
|
|
241
|
+
return { ok: false };
|
|
242
|
+
}
|
|
243
|
+
if (!isGroup) {
|
|
244
|
+
if (account.config.dm?.enabled === false) {
|
|
245
|
+
logVerbose(`Blocked Google Chat DM from ${senderId} (dmPolicy=disabled)`);
|
|
246
|
+
return { ok: false };
|
|
247
|
+
}
|
|
248
|
+
if (senderAccess.decision !== "allow") {
|
|
249
|
+
if (senderAccess.decision === "pairing") await pairing.issueChallenge({
|
|
250
|
+
senderId,
|
|
251
|
+
senderIdLine: `Your Google Chat user id: ${senderId}`,
|
|
252
|
+
meta: {
|
|
253
|
+
name: senderName || void 0,
|
|
254
|
+
email: senderEmail
|
|
255
|
+
},
|
|
256
|
+
onCreated: () => {
|
|
257
|
+
logVerbose(`googlechat pairing request sender=${senderId}`);
|
|
258
|
+
},
|
|
259
|
+
sendPairingReply: async (text) => {
|
|
260
|
+
await sendGoogleChatMessage({
|
|
261
|
+
account,
|
|
262
|
+
space: spaceId,
|
|
263
|
+
text
|
|
264
|
+
});
|
|
265
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
266
|
+
},
|
|
267
|
+
onReplyError: (err) => {
|
|
268
|
+
logVerbose(`pairing reply failed for ${senderId}: ${String(err)}`);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
else logVerbose(`Blocked unauthorized Google Chat sender ${senderId} (dmPolicy=${dmPolicy})`);
|
|
272
|
+
return { ok: false };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (isGroup && core.channel.commands.isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
|
|
276
|
+
logVerbose(`googlechat: drop control command from ${senderId}`);
|
|
277
|
+
return { ok: false };
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
ok: true,
|
|
281
|
+
commandAuthorized,
|
|
282
|
+
effectiveWasMentioned,
|
|
283
|
+
groupBotLoopProtection: groupEntry?.botLoopProtection,
|
|
284
|
+
groupSystemPrompt: normalizeOptionalString(groupEntry?.systemPrompt)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region extensions/googlechat/src/monitor-durable.ts
|
|
289
|
+
function resolveGoogleChatDurableReplyOptions(params) {
|
|
290
|
+
if (params.infoKind !== "final" || params.typingMessageName) return false;
|
|
291
|
+
const threadId = params.payload.replyToId?.trim() || void 0;
|
|
292
|
+
return {
|
|
293
|
+
to: params.spaceId,
|
|
294
|
+
...threadId ? {
|
|
295
|
+
replyToId: threadId,
|
|
296
|
+
threadId
|
|
297
|
+
} : {}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region extensions/googlechat/src/monitor-reply-delivery.ts
|
|
302
|
+
async function deliverGoogleChatReply(params) {
|
|
303
|
+
const { payload, account, spaceId, runtime, core, config, statusSink } = params;
|
|
304
|
+
let typingMessageName = params.typingMessageName;
|
|
305
|
+
const reply = resolveSendableOutboundReplyParts(payload);
|
|
306
|
+
const mediaCount = reply.mediaCount;
|
|
307
|
+
const hasMedia = reply.hasMedia;
|
|
308
|
+
const text = reply.text;
|
|
309
|
+
let firstTextChunk = true;
|
|
310
|
+
let suppressCaption = false;
|
|
311
|
+
if (hasMedia && typingMessageName) try {
|
|
312
|
+
await deleteGoogleChatMessage({
|
|
313
|
+
account,
|
|
314
|
+
messageName: typingMessageName
|
|
315
|
+
});
|
|
316
|
+
typingMessageName = void 0;
|
|
317
|
+
} catch (err) {
|
|
318
|
+
runtime.error?.(`Google Chat typing cleanup failed: ${String(err)}`);
|
|
319
|
+
if (typingMessageName) {
|
|
320
|
+
const fallbackText = reply.hasText ? text : mediaCount > 1 ? "Sent attachments." : "Sent attachment.";
|
|
321
|
+
try {
|
|
322
|
+
await updateGoogleChatMessage({
|
|
323
|
+
account,
|
|
324
|
+
messageName: typingMessageName,
|
|
325
|
+
text: fallbackText
|
|
326
|
+
});
|
|
327
|
+
suppressCaption = Boolean(text.trim());
|
|
328
|
+
} catch (updateErr) {
|
|
329
|
+
runtime.error?.(`Google Chat typing update failed: ${String(updateErr)}`);
|
|
330
|
+
typingMessageName = void 0;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const chunkLimit = account.config.textChunkLimit ?? 4e3;
|
|
335
|
+
const chunkMode = core.channel.text.resolveChunkMode(config, "googlechat", account.accountId);
|
|
336
|
+
const sendTextMessage = async (chunk) => {
|
|
337
|
+
await sendGoogleChatMessage({
|
|
338
|
+
account,
|
|
339
|
+
space: spaceId,
|
|
340
|
+
text: chunk,
|
|
341
|
+
thread: payload.replyToId
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
await deliverTextOrMediaReply({
|
|
345
|
+
payload,
|
|
346
|
+
text: suppressCaption ? "" : reply.text,
|
|
347
|
+
chunkText: (value) => core.channel.text.chunkMarkdownTextWithMode(value, chunkLimit, chunkMode),
|
|
348
|
+
sendText: async (chunk) => {
|
|
349
|
+
try {
|
|
350
|
+
if (firstTextChunk && typingMessageName) await updateGoogleChatMessage({
|
|
351
|
+
account,
|
|
352
|
+
messageName: typingMessageName,
|
|
353
|
+
text: chunk
|
|
354
|
+
});
|
|
355
|
+
else await sendTextMessage(chunk);
|
|
356
|
+
firstTextChunk = false;
|
|
357
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
358
|
+
} catch (err) {
|
|
359
|
+
runtime.error?.(`Google Chat message send failed: ${String(err)}`);
|
|
360
|
+
if (firstTextChunk && typingMessageName) {
|
|
361
|
+
typingMessageName = void 0;
|
|
362
|
+
try {
|
|
363
|
+
await sendTextMessage(chunk);
|
|
364
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
365
|
+
} catch (fallbackErr) {
|
|
366
|
+
runtime.error?.(`Google Chat message fallback send failed: ${String(fallbackErr)}`);
|
|
367
|
+
} finally {
|
|
368
|
+
firstTextChunk = false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
sendMedia: async ({ mediaUrl, caption }) => {
|
|
374
|
+
try {
|
|
375
|
+
const loaded = await core.channel.media.readRemoteMediaBuffer({
|
|
376
|
+
url: mediaUrl,
|
|
377
|
+
maxBytes: (account.config.mediaMaxMb ?? 20) * 1024 * 1024
|
|
378
|
+
});
|
|
379
|
+
const upload = await uploadAttachmentForReply({
|
|
380
|
+
account,
|
|
381
|
+
spaceId,
|
|
382
|
+
buffer: loaded.buffer,
|
|
383
|
+
contentType: loaded.contentType,
|
|
384
|
+
filename: loaded.fileName ?? "attachment"
|
|
385
|
+
});
|
|
386
|
+
if (!upload.attachmentUploadToken) throw new Error("missing attachment upload token");
|
|
387
|
+
await sendGoogleChatMessage({
|
|
388
|
+
account,
|
|
389
|
+
space: spaceId,
|
|
390
|
+
text: caption,
|
|
391
|
+
thread: payload.replyToId,
|
|
392
|
+
attachments: [{
|
|
393
|
+
attachmentUploadToken: upload.attachmentUploadToken,
|
|
394
|
+
contentName: loaded.fileName
|
|
395
|
+
}]
|
|
396
|
+
});
|
|
397
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
398
|
+
} catch (err) {
|
|
399
|
+
runtime.error?.(`Google Chat attachment send failed: ${String(err)}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
async function uploadAttachmentForReply(params) {
|
|
405
|
+
const { account, spaceId, buffer, contentType, filename } = params;
|
|
406
|
+
return await uploadGoogleChatAttachment({
|
|
407
|
+
account,
|
|
408
|
+
space: spaceId,
|
|
409
|
+
filename,
|
|
410
|
+
buffer,
|
|
411
|
+
contentType
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region extensions/googlechat/src/monitor-webhook.ts
|
|
416
|
+
function extractBearerToken(header) {
|
|
417
|
+
const authHeader = Array.isArray(header) ? typeof header[0] === "string" ? header[0] : "" : typeof header === "string" ? header : "";
|
|
418
|
+
return normalizeLowercaseStringOrEmpty(authHeader).startsWith("bearer ") ? authHeader.slice(7).trim() : "";
|
|
419
|
+
}
|
|
420
|
+
const ADD_ON_PREAUTH_MAX_BYTES = 16 * 1024;
|
|
421
|
+
const ADD_ON_PREAUTH_TIMEOUT_MS = 3e3;
|
|
422
|
+
function parseGoogleChatInboundPayload(raw, res) {
|
|
423
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
424
|
+
res.statusCode = 400;
|
|
425
|
+
res.end("invalid payload");
|
|
426
|
+
return { ok: false };
|
|
427
|
+
}
|
|
428
|
+
let eventPayload = raw;
|
|
429
|
+
let addOnBearerToken = "";
|
|
430
|
+
const rawObj = raw;
|
|
431
|
+
if (rawObj.commonEventObject?.hostApp === "CHAT" && rawObj.chat?.messagePayload) {
|
|
432
|
+
const chat = rawObj.chat;
|
|
433
|
+
const messagePayload = chat.messagePayload;
|
|
434
|
+
eventPayload = {
|
|
435
|
+
type: "MESSAGE",
|
|
436
|
+
space: messagePayload?.space,
|
|
437
|
+
message: messagePayload?.message,
|
|
438
|
+
user: chat.user,
|
|
439
|
+
eventTime: chat.eventTime
|
|
440
|
+
};
|
|
441
|
+
addOnBearerToken = typeof rawObj.authorizationEventObject?.systemIdToken === "string" ? rawObj.authorizationEventObject.systemIdToken.trim() : "";
|
|
442
|
+
}
|
|
443
|
+
const event = eventPayload;
|
|
444
|
+
const eventType = event.type ?? eventPayload.eventType;
|
|
445
|
+
if (typeof eventType !== "string") {
|
|
446
|
+
res.statusCode = 400;
|
|
447
|
+
res.end("invalid payload");
|
|
448
|
+
return { ok: false };
|
|
449
|
+
}
|
|
450
|
+
if (!event.space || typeof event.space !== "object" || Array.isArray(event.space)) {
|
|
451
|
+
res.statusCode = 400;
|
|
452
|
+
res.end("invalid payload");
|
|
453
|
+
return { ok: false };
|
|
454
|
+
}
|
|
455
|
+
if (eventType === "MESSAGE") {
|
|
456
|
+
if (!event.message || typeof event.message !== "object" || Array.isArray(event.message)) {
|
|
457
|
+
res.statusCode = 400;
|
|
458
|
+
res.end("invalid payload");
|
|
459
|
+
return { ok: false };
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
ok: true,
|
|
464
|
+
event,
|
|
465
|
+
addOnBearerToken
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
async function verifyGoogleChatTargetAuth(target, bearer) {
|
|
469
|
+
const verification = await verifyGoogleChatRequest({
|
|
470
|
+
bearer,
|
|
471
|
+
audienceType: target.audienceType,
|
|
472
|
+
audience: target.audience,
|
|
473
|
+
expectedAddOnPrincipal: target.account.config.appPrincipal
|
|
474
|
+
});
|
|
475
|
+
return verification.ok ? { ok: true } : {
|
|
476
|
+
ok: false,
|
|
477
|
+
reason: verification.reason ?? "unknown"
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function logGoogleChatWebhookAuthRejections(rejections) {
|
|
481
|
+
for (const rejection of rejections) rejection.target.runtime.log?.(`[${rejection.target.account.accountId}] Google Chat webhook auth rejected: ${rejection.reason}`);
|
|
482
|
+
}
|
|
483
|
+
function logGoogleChatWebhookAuthRejectedForTargets(targets, reason) {
|
|
484
|
+
logGoogleChatWebhookAuthRejections(targets.map((target) => ({
|
|
485
|
+
target,
|
|
486
|
+
reason
|
|
487
|
+
})));
|
|
488
|
+
}
|
|
489
|
+
async function resolveGoogleChatWebhookTargetWithAuthOrReject(params) {
|
|
490
|
+
const rejections = [];
|
|
491
|
+
let verifiedTargetCount = 0;
|
|
492
|
+
const selectedTarget = await resolveWebhookTargetWithAuthOrReject({
|
|
493
|
+
targets: params.targets,
|
|
494
|
+
res: params.res,
|
|
495
|
+
isMatch: async (target) => {
|
|
496
|
+
const verification = await verifyGoogleChatTargetAuth(target, params.bearer);
|
|
497
|
+
if (verification.ok) {
|
|
498
|
+
verifiedTargetCount += 1;
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
rejections.push({
|
|
502
|
+
target,
|
|
503
|
+
reason: verification.reason
|
|
504
|
+
});
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
if (!selectedTarget && verifiedTargetCount === 0) logGoogleChatWebhookAuthRejections(rejections);
|
|
509
|
+
return selectedTarget;
|
|
510
|
+
}
|
|
511
|
+
function warnAppPrincipalMisconfiguration(params) {
|
|
512
|
+
if (params.audienceType !== "app-url") return;
|
|
513
|
+
const principal = params.appPrincipal?.trim();
|
|
514
|
+
if (!principal) params.log?.(`[${params.accountId}] appPrincipal is missing for audienceType "app-url"; add-on token verification will fail. Set appPrincipal to the numeric OAuth 2.0 client ID (uniqueId, 21 digits), not an email.`);
|
|
515
|
+
else if (principal.includes("@")) params.log?.(`[${params.accountId}] appPrincipal "${principal}" looks like an email address. Set appPrincipal to the numeric OAuth 2.0 client ID (uniqueId, 21 digits), not an email.`);
|
|
516
|
+
}
|
|
517
|
+
function createGoogleChatWebhookRequestHandler(params) {
|
|
518
|
+
return async (req, res) => {
|
|
519
|
+
const path = normalizeWebhookPath(new URL(req.url ?? "/", "http://localhost").pathname);
|
|
520
|
+
const config = params.webhookTargets.get(path)?.[0]?.config;
|
|
521
|
+
const clientIp = resolveRequestClientIp(req, config?.gateway?.trustedProxies, config?.gateway?.allowRealIpFallback === true) ?? "unknown";
|
|
522
|
+
return await withResolvedWebhookRequestPipeline({
|
|
523
|
+
req,
|
|
524
|
+
res,
|
|
525
|
+
targetsByPath: params.webhookTargets,
|
|
526
|
+
allowMethods: ["POST"],
|
|
527
|
+
requireJsonContentType: true,
|
|
528
|
+
rateLimiter: params.webhookRateLimiter,
|
|
529
|
+
rateLimitKey: `${path}:${clientIp}`,
|
|
530
|
+
inFlightLimiter: params.webhookInFlightLimiter,
|
|
531
|
+
handle: async ({ targets }) => {
|
|
532
|
+
const headerBearer = extractBearerToken(req.headers.authorization);
|
|
533
|
+
let selectedTarget = null;
|
|
534
|
+
let parsedEvent = null;
|
|
535
|
+
const readAndParseEvent = async (profile) => {
|
|
536
|
+
const body = await readJsonWebhookBodyOrReject({
|
|
537
|
+
req,
|
|
538
|
+
res,
|
|
539
|
+
profile,
|
|
540
|
+
...profile === "pre-auth" ? {
|
|
541
|
+
maxBytes: ADD_ON_PREAUTH_MAX_BYTES,
|
|
542
|
+
timeoutMs: ADD_ON_PREAUTH_TIMEOUT_MS
|
|
543
|
+
} : {},
|
|
544
|
+
emptyObjectOnEmpty: false,
|
|
545
|
+
invalidJsonMessage: "invalid payload"
|
|
546
|
+
});
|
|
547
|
+
if (!body.ok) return null;
|
|
548
|
+
const parsed = parseGoogleChatInboundPayload(body.value, res);
|
|
549
|
+
return parsed.ok ? parsed : null;
|
|
550
|
+
};
|
|
551
|
+
if (headerBearer) {
|
|
552
|
+
selectedTarget = await resolveGoogleChatWebhookTargetWithAuthOrReject({
|
|
553
|
+
targets,
|
|
554
|
+
res,
|
|
555
|
+
bearer: headerBearer
|
|
556
|
+
});
|
|
557
|
+
if (!selectedTarget) return true;
|
|
558
|
+
const parsed = await readAndParseEvent("post-auth");
|
|
559
|
+
if (!parsed) return true;
|
|
560
|
+
parsedEvent = parsed.event;
|
|
561
|
+
} else {
|
|
562
|
+
const parsed = await readAndParseEvent("pre-auth");
|
|
563
|
+
if (!parsed) return true;
|
|
564
|
+
parsedEvent = parsed.event;
|
|
565
|
+
if (!parsed.addOnBearerToken) {
|
|
566
|
+
logGoogleChatWebhookAuthRejectedForTargets(targets, "missing token");
|
|
567
|
+
res.statusCode = 401;
|
|
568
|
+
res.end("unauthorized");
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
selectedTarget = await resolveGoogleChatWebhookTargetWithAuthOrReject({
|
|
572
|
+
targets,
|
|
573
|
+
res,
|
|
574
|
+
bearer: parsed.addOnBearerToken
|
|
575
|
+
});
|
|
576
|
+
if (!selectedTarget) return true;
|
|
577
|
+
}
|
|
578
|
+
if (!selectedTarget || !parsedEvent) {
|
|
579
|
+
res.statusCode = 401;
|
|
580
|
+
res.end("unauthorized");
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
const dispatchTarget = selectedTarget;
|
|
584
|
+
dispatchTarget.statusSink?.({ lastInboundAt: Date.now() });
|
|
585
|
+
params.processEvent(parsedEvent, dispatchTarget).catch((err) => {
|
|
586
|
+
dispatchTarget.runtime.error?.(`[${dispatchTarget.account.accountId}] Google Chat webhook failed: ${String(err)}`);
|
|
587
|
+
});
|
|
588
|
+
res.statusCode = 200;
|
|
589
|
+
res.setHeader("Content-Type", "application/json");
|
|
590
|
+
res.end("{}");
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
597
|
+
//#region extensions/googlechat/src/monitor-routing.ts
|
|
598
|
+
const webhookTargets = /* @__PURE__ */ new Map();
|
|
599
|
+
const webhookRateLimiter = createFixedWindowRateLimiter({
|
|
600
|
+
windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs,
|
|
601
|
+
maxRequests: WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests,
|
|
602
|
+
maxTrackedKeys: WEBHOOK_RATE_LIMIT_DEFAULTS.maxTrackedKeys
|
|
603
|
+
});
|
|
604
|
+
const webhookInFlightLimiter = createWebhookInFlightLimiter();
|
|
605
|
+
let processGoogleChatEvent$1 = async () => {};
|
|
606
|
+
function setGoogleChatWebhookEventProcessor(processEvent) {
|
|
607
|
+
processGoogleChatEvent$1 = processEvent;
|
|
608
|
+
}
|
|
609
|
+
const googleChatWebhookRequestHandler = createGoogleChatWebhookRequestHandler({
|
|
610
|
+
webhookTargets,
|
|
611
|
+
webhookRateLimiter,
|
|
612
|
+
webhookInFlightLimiter,
|
|
613
|
+
processEvent: async (event, target) => {
|
|
614
|
+
await processGoogleChatEvent$1(event, target);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
function registerGoogleChatWebhookTarget(target) {
|
|
618
|
+
return registerWebhookTargetWithPluginRoute({
|
|
619
|
+
targetsByPath: webhookTargets,
|
|
620
|
+
target,
|
|
621
|
+
route: {
|
|
622
|
+
auth: "plugin",
|
|
623
|
+
match: "exact",
|
|
624
|
+
pluginId: "googlechat",
|
|
625
|
+
source: "googlechat-webhook",
|
|
626
|
+
accountId: target.account.accountId,
|
|
627
|
+
log: target.runtime.log,
|
|
628
|
+
handler: async (req, res) => {
|
|
629
|
+
if (!await handleGoogleChatWebhookRequest(req, res) && !res.headersSent) {
|
|
630
|
+
res.statusCode = 404;
|
|
631
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
632
|
+
res.end("Not Found");
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}).unregister;
|
|
637
|
+
}
|
|
638
|
+
async function handleGoogleChatWebhookRequest(req, res) {
|
|
639
|
+
return await googleChatWebhookRequestHandler(req, res);
|
|
640
|
+
}
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region extensions/googlechat/src/monitor.ts
|
|
643
|
+
setGoogleChatWebhookEventProcessor(processGoogleChatEvent);
|
|
644
|
+
function logVerbose(core, runtime, message) {
|
|
645
|
+
if (core.logging.shouldLogVerbose()) runtime.log?.(`[googlechat] ${message}`);
|
|
646
|
+
}
|
|
647
|
+
function normalizeAudienceType(value) {
|
|
648
|
+
const normalized = normalizeOptionalLowercaseString(value);
|
|
649
|
+
if (normalized === "app-url" || normalized === "app_url" || normalized === "app") return "app-url";
|
|
650
|
+
if (normalized === "project-number" || normalized === "project_number" || normalized === "project") return "project-number";
|
|
651
|
+
}
|
|
652
|
+
function resolveGoogleChatTimestampMs(eventTime) {
|
|
653
|
+
if (!eventTime) return;
|
|
654
|
+
const parsed = Date.parse(eventTime);
|
|
655
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
656
|
+
}
|
|
657
|
+
function resolveGoogleChatBotLoopProtection(params) {
|
|
658
|
+
if (!params.allowBots || !params.isBotSender || !params.senderId || params.senderId === params.appUserId) return;
|
|
659
|
+
return {
|
|
660
|
+
scopeId: params.accountId,
|
|
661
|
+
conversationId: params.conversationId,
|
|
662
|
+
senderId: params.senderId,
|
|
663
|
+
receiverId: params.appUserId,
|
|
664
|
+
config: params.config,
|
|
665
|
+
defaultsConfig: params.defaultsConfig,
|
|
666
|
+
defaultEnabled: true,
|
|
667
|
+
nowMs: resolveGoogleChatTimestampMs(params.eventTime)
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function resolveGoogleChatBotLoopProtectionConfig(params) {
|
|
671
|
+
return mergePairLoopGuardConfig(params.accountConfig, params.groupConfig);
|
|
672
|
+
}
|
|
673
|
+
function shouldSuppressGoogleChatBotLoop(params) {
|
|
674
|
+
if (!params.botLoopProtection) return false;
|
|
675
|
+
if (!recordChannelBotPairLoopAndCheckSuppression(params.botLoopProtection).suppressed) return false;
|
|
676
|
+
logVerbose(params.core, params.runtime, `skip bot-to-bot loop in ${params.botLoopProtection.conversationId}`);
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
async function processGoogleChatEvent(event, target) {
|
|
680
|
+
if ((event.type ?? event.eventType) !== "MESSAGE") return;
|
|
681
|
+
if (!event.message || !event.space) return;
|
|
682
|
+
await processMessageWithPipeline({
|
|
683
|
+
event,
|
|
684
|
+
account: target.account,
|
|
685
|
+
config: target.config,
|
|
686
|
+
runtime: target.runtime,
|
|
687
|
+
core: target.core,
|
|
688
|
+
statusSink: target.statusSink,
|
|
689
|
+
mediaMaxMb: target.mediaMaxMb
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Resolve bot display name with fallback chain:
|
|
694
|
+
* 1. Account config name
|
|
695
|
+
* 2. Agent name from config
|
|
696
|
+
* 3. "Klaw" as generic fallback
|
|
697
|
+
*/
|
|
698
|
+
function resolveBotDisplayName(params) {
|
|
699
|
+
const { accountName, agentId, config } = params;
|
|
700
|
+
if (accountName?.trim()) return accountName.trim();
|
|
701
|
+
const agent = config.agents?.list?.find((a) => a.id === agentId);
|
|
702
|
+
if (agent?.name?.trim()) return agent.name.trim();
|
|
703
|
+
return "Klaw";
|
|
704
|
+
}
|
|
705
|
+
async function processMessageWithPipeline(params) {
|
|
706
|
+
const { event, account, config, runtime, core, statusSink, mediaMaxMb } = params;
|
|
707
|
+
const space = event.space;
|
|
708
|
+
const message = event.message;
|
|
709
|
+
if (!space || !message) return;
|
|
710
|
+
const spaceId = space.name ?? "";
|
|
711
|
+
if (!spaceId) return;
|
|
712
|
+
const isGroup = (space.type ?? "").toUpperCase() !== "DM";
|
|
713
|
+
const sender = message.sender ?? event.user;
|
|
714
|
+
const senderId = sender?.name ?? "";
|
|
715
|
+
const senderName = sender?.displayName ?? "";
|
|
716
|
+
const senderEmail = sender?.email ?? void 0;
|
|
717
|
+
const isBotSender = sender?.type?.toUpperCase() === "BOT";
|
|
718
|
+
const appUserId = account.config.botUser?.trim() || "users/app";
|
|
719
|
+
const allowBots = account.config.allowBots === true;
|
|
720
|
+
if (!allowBots) {
|
|
721
|
+
if (isBotSender) {
|
|
722
|
+
logVerbose(core, runtime, `skip bot-authored message (${senderId || "unknown"})`);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (senderId === "users/app") {
|
|
726
|
+
logVerbose(core, runtime, "skip app-authored message");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const messageText = (message.argumentText ?? message.text ?? "").trim();
|
|
731
|
+
const attachments = message.attachment ?? [];
|
|
732
|
+
const hasMedia = attachments.length > 0;
|
|
733
|
+
const rawBody = messageText || (hasMedia ? "<media:attachment>" : "");
|
|
734
|
+
if (!rawBody) return;
|
|
735
|
+
const access = await applyGoogleChatInboundAccessPolicy({
|
|
736
|
+
account,
|
|
737
|
+
config,
|
|
738
|
+
core,
|
|
739
|
+
space,
|
|
740
|
+
message,
|
|
741
|
+
isGroup,
|
|
742
|
+
senderId,
|
|
743
|
+
senderName,
|
|
744
|
+
senderEmail,
|
|
745
|
+
rawBody,
|
|
746
|
+
statusSink,
|
|
747
|
+
logVerbose: (message) => logVerbose(core, runtime, message)
|
|
748
|
+
});
|
|
749
|
+
if (!access.ok) return;
|
|
750
|
+
const { commandAuthorized, effectiveWasMentioned, groupBotLoopProtection, groupSystemPrompt } = access;
|
|
751
|
+
if (shouldSuppressGoogleChatBotLoop({
|
|
752
|
+
botLoopProtection: resolveGoogleChatBotLoopProtection({
|
|
753
|
+
allowBots,
|
|
754
|
+
isBotSender,
|
|
755
|
+
senderId,
|
|
756
|
+
appUserId,
|
|
757
|
+
accountId: account.accountId,
|
|
758
|
+
conversationId: spaceId,
|
|
759
|
+
config: resolveGoogleChatBotLoopProtectionConfig({
|
|
760
|
+
accountConfig: account.config.botLoopProtection,
|
|
761
|
+
groupConfig: groupBotLoopProtection
|
|
762
|
+
}),
|
|
763
|
+
defaultsConfig: config.channels?.defaults?.botLoopProtection,
|
|
764
|
+
eventTime: event.eventTime
|
|
765
|
+
}),
|
|
766
|
+
core,
|
|
767
|
+
runtime
|
|
768
|
+
})) return;
|
|
769
|
+
const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
770
|
+
cfg: config,
|
|
771
|
+
channel: "googlechat",
|
|
772
|
+
accountId: account.accountId,
|
|
773
|
+
peer: {
|
|
774
|
+
kind: isGroup ? "group" : "direct",
|
|
775
|
+
id: spaceId
|
|
776
|
+
},
|
|
777
|
+
runtime: core.channel,
|
|
778
|
+
sessionStore: config.session?.store
|
|
779
|
+
});
|
|
780
|
+
let mediaPath;
|
|
781
|
+
let mediaType;
|
|
782
|
+
if (attachments.length > 0) {
|
|
783
|
+
const first = attachments[0];
|
|
784
|
+
const attachmentData = await downloadAttachment(first, account, mediaMaxMb, core);
|
|
785
|
+
if (attachmentData) {
|
|
786
|
+
mediaPath = attachmentData.path;
|
|
787
|
+
mediaType = attachmentData.contentType;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const fromLabel = isGroup ? space.displayName || `space:${spaceId}` : senderName || `user:${senderId}`;
|
|
791
|
+
const { storePath, body } = buildEnvelope({
|
|
792
|
+
channel: "Google Chat",
|
|
793
|
+
from: fromLabel,
|
|
794
|
+
timestamp: event.eventTime ? Date.parse(event.eventTime) : void 0,
|
|
795
|
+
body: rawBody
|
|
796
|
+
});
|
|
797
|
+
const ctxPayload = core.channel.turn.buildContext({
|
|
798
|
+
channel: "googlechat",
|
|
799
|
+
accountId: route.accountId,
|
|
800
|
+
messageId: message.name,
|
|
801
|
+
messageIdFull: message.name,
|
|
802
|
+
timestamp: event.eventTime ? Date.parse(event.eventTime) : void 0,
|
|
803
|
+
from: `googlechat:${senderId}`,
|
|
804
|
+
sender: {
|
|
805
|
+
id: senderId,
|
|
806
|
+
name: senderName || void 0,
|
|
807
|
+
username: senderEmail
|
|
808
|
+
},
|
|
809
|
+
conversation: {
|
|
810
|
+
kind: isGroup ? "channel" : "direct",
|
|
811
|
+
id: spaceId,
|
|
812
|
+
label: fromLabel,
|
|
813
|
+
routePeer: {
|
|
814
|
+
kind: isGroup ? "group" : "direct",
|
|
815
|
+
id: spaceId
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
route: {
|
|
819
|
+
agentId: route.agentId,
|
|
820
|
+
accountId: route.accountId,
|
|
821
|
+
routeSessionKey: route.sessionKey
|
|
822
|
+
},
|
|
823
|
+
reply: {
|
|
824
|
+
to: `googlechat:${spaceId}`,
|
|
825
|
+
originatingTo: `googlechat:${spaceId}`,
|
|
826
|
+
replyToId: message.thread?.name,
|
|
827
|
+
replyToIdFull: message.thread?.name
|
|
828
|
+
},
|
|
829
|
+
message: {
|
|
830
|
+
body,
|
|
831
|
+
bodyForAgent: rawBody,
|
|
832
|
+
rawBody,
|
|
833
|
+
commandBody: rawBody,
|
|
834
|
+
envelopeFrom: fromLabel
|
|
835
|
+
},
|
|
836
|
+
media: mediaPath || mediaType ? [{
|
|
837
|
+
path: mediaPath,
|
|
838
|
+
url: mediaPath,
|
|
839
|
+
contentType: mediaType
|
|
840
|
+
}] : void 0,
|
|
841
|
+
supplemental: { groupSystemPrompt: isGroup ? groupSystemPrompt : void 0 },
|
|
842
|
+
extra: {
|
|
843
|
+
ChatType: isGroup ? "channel" : "direct",
|
|
844
|
+
WasMentioned: isGroup ? effectiveWasMentioned : void 0,
|
|
845
|
+
CommandAuthorized: commandAuthorized,
|
|
846
|
+
GroupSubject: void 0,
|
|
847
|
+
GroupSpace: isGroup ? space.displayName ?? void 0 : void 0
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
let typingIndicator = account.config.typingIndicator ?? "message";
|
|
851
|
+
if (typingIndicator === "reaction") {
|
|
852
|
+
runtime.error?.(`[${account.accountId}] typingIndicator="reaction" requires user OAuth (not supported with service account). Falling back to "message" mode.`);
|
|
853
|
+
typingIndicator = "message";
|
|
854
|
+
}
|
|
855
|
+
let typingMessageName;
|
|
856
|
+
if (typingIndicator === "message") try {
|
|
857
|
+
typingMessageName = (await sendGoogleChatMessage({
|
|
858
|
+
account,
|
|
859
|
+
space: spaceId,
|
|
860
|
+
text: `_${resolveBotDisplayName({
|
|
861
|
+
accountName: account.config.name,
|
|
862
|
+
agentId: route.agentId,
|
|
863
|
+
config
|
|
864
|
+
})} is typing..._`,
|
|
865
|
+
thread: message.thread?.name
|
|
866
|
+
}))?.messageName;
|
|
867
|
+
} catch (err) {
|
|
868
|
+
runtime.error?.(`Failed sending typing message: ${String(err)}`);
|
|
869
|
+
}
|
|
870
|
+
await core.channel.turn.run({
|
|
871
|
+
channel: "googlechat",
|
|
872
|
+
accountId: route.accountId,
|
|
873
|
+
raw: message,
|
|
874
|
+
adapter: {
|
|
875
|
+
ingest: () => ({
|
|
876
|
+
id: message.name ?? spaceId,
|
|
877
|
+
timestamp: event.eventTime ? Date.parse(event.eventTime) : void 0,
|
|
878
|
+
rawText: rawBody,
|
|
879
|
+
textForAgent: rawBody,
|
|
880
|
+
textForCommands: rawBody,
|
|
881
|
+
raw: message
|
|
882
|
+
}),
|
|
883
|
+
resolveTurn: () => ({
|
|
884
|
+
cfg: config,
|
|
885
|
+
channel: "googlechat",
|
|
886
|
+
accountId: route.accountId,
|
|
887
|
+
agentId: route.agentId,
|
|
888
|
+
routeSessionKey: route.sessionKey,
|
|
889
|
+
storePath,
|
|
890
|
+
ctxPayload,
|
|
891
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
892
|
+
dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
893
|
+
delivery: {
|
|
894
|
+
durable: (payload, info) => resolveGoogleChatDurableReplyOptions({
|
|
895
|
+
payload,
|
|
896
|
+
infoKind: info.kind,
|
|
897
|
+
spaceId,
|
|
898
|
+
typingMessageName
|
|
899
|
+
}),
|
|
900
|
+
deliver: async (payload) => {
|
|
901
|
+
await deliverGoogleChatReply({
|
|
902
|
+
payload,
|
|
903
|
+
account,
|
|
904
|
+
spaceId,
|
|
905
|
+
runtime,
|
|
906
|
+
core,
|
|
907
|
+
config,
|
|
908
|
+
statusSink,
|
|
909
|
+
typingMessageName
|
|
910
|
+
});
|
|
911
|
+
typingMessageName = void 0;
|
|
912
|
+
},
|
|
913
|
+
onDelivered: () => {
|
|
914
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
915
|
+
},
|
|
916
|
+
onError: (err, info) => {
|
|
917
|
+
runtime.error?.(`[${account.accountId}] Google Chat ${info.kind} reply failed: ${String(err)}`);
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
replyPipeline: {},
|
|
921
|
+
record: { onRecordError: (err) => {
|
|
922
|
+
runtime.error?.(`googlechat: failed updating session meta: ${String(err)}`);
|
|
923
|
+
} }
|
|
924
|
+
})
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
async function downloadAttachment(attachment, account, mediaMaxMb, core) {
|
|
929
|
+
const resourceName = attachment.attachmentDataRef?.resourceName;
|
|
930
|
+
if (!resourceName) return null;
|
|
931
|
+
const maxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
|
|
932
|
+
const downloaded = await downloadGoogleChatMedia({
|
|
933
|
+
account,
|
|
934
|
+
resourceName,
|
|
935
|
+
maxBytes
|
|
936
|
+
});
|
|
937
|
+
const saved = await core.channel.media.saveMediaBuffer(downloaded.buffer, downloaded.contentType ?? attachment.contentType, "inbound", maxBytes, attachment.contentName);
|
|
938
|
+
return {
|
|
939
|
+
path: saved.path,
|
|
940
|
+
contentType: saved.contentType
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
function monitorGoogleChatProvider(options) {
|
|
944
|
+
const core = getGoogleChatRuntime();
|
|
945
|
+
const webhookPath = resolveWebhookPath({
|
|
946
|
+
webhookPath: options.webhookPath,
|
|
947
|
+
webhookUrl: options.webhookUrl,
|
|
948
|
+
defaultPath: "/googlechat"
|
|
949
|
+
});
|
|
950
|
+
if (!webhookPath) {
|
|
951
|
+
options.runtime.error?.(`[${options.account.accountId}] invalid webhook path`);
|
|
952
|
+
return () => {};
|
|
953
|
+
}
|
|
954
|
+
const audienceType = normalizeAudienceType(options.account.config.audienceType);
|
|
955
|
+
const audience = options.account.config.audience?.trim();
|
|
956
|
+
const mediaMaxMb = options.account.config.mediaMaxMb ?? 20;
|
|
957
|
+
warnAppPrincipalMisconfiguration({
|
|
958
|
+
accountId: options.account.accountId,
|
|
959
|
+
audienceType,
|
|
960
|
+
appPrincipal: options.account.config.appPrincipal,
|
|
961
|
+
log: options.runtime.log
|
|
962
|
+
});
|
|
963
|
+
const unregisterTarget = registerGoogleChatWebhookTarget({
|
|
964
|
+
account: options.account,
|
|
965
|
+
config: options.config,
|
|
966
|
+
runtime: options.runtime,
|
|
967
|
+
core,
|
|
968
|
+
path: webhookPath,
|
|
969
|
+
audienceType,
|
|
970
|
+
audience,
|
|
971
|
+
statusSink: options.statusSink,
|
|
972
|
+
mediaMaxMb
|
|
973
|
+
});
|
|
974
|
+
return () => {
|
|
975
|
+
unregisterTarget();
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
async function startGoogleChatMonitor(params) {
|
|
979
|
+
return monitorGoogleChatProvider(params);
|
|
980
|
+
}
|
|
981
|
+
function resolveGoogleChatWebhookPath(params) {
|
|
982
|
+
return resolveWebhookPath({
|
|
983
|
+
webhookPath: params.account.config.webhookPath,
|
|
984
|
+
webhookUrl: params.account.config.webhookUrl,
|
|
985
|
+
defaultPath: "/googlechat"
|
|
986
|
+
}) ?? "/googlechat";
|
|
987
|
+
}
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region extensions/googlechat/src/channel.runtime.ts
|
|
990
|
+
const googleChatChannelRuntime = {
|
|
991
|
+
probeGoogleChat,
|
|
992
|
+
sendGoogleChatMessage,
|
|
993
|
+
uploadGoogleChatAttachment,
|
|
994
|
+
resolveGoogleChatWebhookPath,
|
|
995
|
+
startGoogleChatMonitor
|
|
996
|
+
};
|
|
997
|
+
//#endregion
|
|
998
|
+
export { googleChatChannelRuntime };
|