@chat-adapter/gchat 4.1.0 → 4.3.0
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/README.md +174 -22
- package/dist/index.d.ts +41 -31
- package/dist/index.js +449 -294
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
AdapterRateLimitError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
extractCard,
|
|
6
|
+
extractFiles,
|
|
7
|
+
NetworkError,
|
|
8
|
+
ValidationError as ValidationError2
|
|
9
|
+
} from "@chat-adapter/shared";
|
|
10
|
+
import { convertEmojiPlaceholders, defaultEmojiResolver } from "chat";
|
|
8
11
|
import { google as google2 } from "googleapis";
|
|
9
12
|
|
|
10
13
|
// src/cards.ts
|
|
11
14
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
15
|
+
createEmojiConverter,
|
|
16
|
+
cardToFallbackText as sharedCardToFallbackText
|
|
17
|
+
} from "@chat-adapter/shared";
|
|
18
|
+
var convertEmoji = createEmojiConverter("gchat");
|
|
17
19
|
function cardToGoogleCard(card, options) {
|
|
18
20
|
const opts = typeof options === "string" ? { cardId: options } : options || {};
|
|
19
21
|
const sections = [];
|
|
@@ -154,39 +156,29 @@ function convertFieldsToWidgets(element) {
|
|
|
154
156
|
}));
|
|
155
157
|
}
|
|
156
158
|
function cardToFallbackText(card) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
parts.push(convertEmoji(card.subtitle));
|
|
163
|
-
}
|
|
164
|
-
for (const child of card.children) {
|
|
165
|
-
const text = childToFallbackText(child);
|
|
166
|
-
if (text) {
|
|
167
|
-
parts.push(text);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return parts.join("\n");
|
|
171
|
-
}
|
|
172
|
-
function childToFallbackText(child) {
|
|
173
|
-
switch (child.type) {
|
|
174
|
-
case "text":
|
|
175
|
-
return convertEmoji(child.content);
|
|
176
|
-
case "fields":
|
|
177
|
-
return child.children.map((f) => `*${convertEmoji(f.label)}*: ${convertEmoji(f.value)}`).join("\n");
|
|
178
|
-
case "actions":
|
|
179
|
-
return `[${child.children.map((b) => convertEmoji(b.label)).join("] [")}]`;
|
|
180
|
-
case "section":
|
|
181
|
-
return child.children.map((c) => childToFallbackText(c)).filter(Boolean).join("\n");
|
|
182
|
-
default:
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
159
|
+
return sharedCardToFallbackText(card, {
|
|
160
|
+
boldFormat: "*",
|
|
161
|
+
lineBreak: "\n",
|
|
162
|
+
platform: "gchat"
|
|
163
|
+
});
|
|
185
164
|
}
|
|
186
165
|
|
|
187
166
|
// src/markdown.ts
|
|
188
167
|
import {
|
|
189
168
|
BaseFormatConverter,
|
|
169
|
+
getNodeChildren,
|
|
170
|
+
getNodeValue,
|
|
171
|
+
isBlockquoteNode,
|
|
172
|
+
isCodeNode,
|
|
173
|
+
isDeleteNode,
|
|
174
|
+
isEmphasisNode,
|
|
175
|
+
isInlineCodeNode,
|
|
176
|
+
isLinkNode,
|
|
177
|
+
isListItemNode,
|
|
178
|
+
isListNode,
|
|
179
|
+
isParagraphNode,
|
|
180
|
+
isStrongNode,
|
|
181
|
+
isTextNode,
|
|
190
182
|
parseMarkdown
|
|
191
183
|
} from "chat";
|
|
192
184
|
var GoogleChatFormatConverter = class extends BaseFormatConverter {
|
|
@@ -194,11 +186,7 @@ var GoogleChatFormatConverter = class extends BaseFormatConverter {
|
|
|
194
186
|
* Render an AST to Google Chat format.
|
|
195
187
|
*/
|
|
196
188
|
fromAst(ast) {
|
|
197
|
-
|
|
198
|
-
for (const node of ast.children) {
|
|
199
|
-
parts.push(this.nodeToGChat(node));
|
|
200
|
-
}
|
|
201
|
-
return parts.join("\n\n");
|
|
189
|
+
return this.fromAstWithNodeConverter(ast, (node) => this.nodeToGChat(node));
|
|
202
190
|
}
|
|
203
191
|
/**
|
|
204
192
|
* Parse Google Chat message into an AST.
|
|
@@ -210,57 +198,155 @@ var GoogleChatFormatConverter = class extends BaseFormatConverter {
|
|
|
210
198
|
return parseMarkdown(markdown);
|
|
211
199
|
}
|
|
212
200
|
nodeToGChat(node) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
201
|
+
if (isParagraphNode(node)) {
|
|
202
|
+
return getNodeChildren(node).map((child) => this.nodeToGChat(child)).join("");
|
|
203
|
+
}
|
|
204
|
+
if (isTextNode(node)) {
|
|
205
|
+
return node.value;
|
|
206
|
+
}
|
|
207
|
+
if (isStrongNode(node)) {
|
|
208
|
+
const content = getNodeChildren(node).map((child) => this.nodeToGChat(child)).join("");
|
|
209
|
+
return `*${content}*`;
|
|
210
|
+
}
|
|
211
|
+
if (isEmphasisNode(node)) {
|
|
212
|
+
const content = getNodeChildren(node).map((child) => this.nodeToGChat(child)).join("");
|
|
213
|
+
return `_${content}_`;
|
|
214
|
+
}
|
|
215
|
+
if (isDeleteNode(node)) {
|
|
216
|
+
const content = getNodeChildren(node).map((child) => this.nodeToGChat(child)).join("");
|
|
217
|
+
return `~${content}~`;
|
|
218
|
+
}
|
|
219
|
+
if (isInlineCodeNode(node)) {
|
|
220
|
+
return `\`${node.value}\``;
|
|
221
|
+
}
|
|
222
|
+
if (isCodeNode(node)) {
|
|
223
|
+
return `\`\`\`
|
|
224
|
+
${node.value}
|
|
231
225
|
\`\`\``;
|
|
226
|
+
}
|
|
227
|
+
if (isLinkNode(node)) {
|
|
228
|
+
const linkText = getNodeChildren(node).map((child) => this.nodeToGChat(child)).join("");
|
|
229
|
+
if (linkText === node.url) {
|
|
230
|
+
return node.url;
|
|
232
231
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return node.children.map((child) => this.nodeToGChat(child)).join("");
|
|
258
|
-
}
|
|
259
|
-
if ("value" in node) {
|
|
260
|
-
return String(node.value);
|
|
261
|
-
}
|
|
262
|
-
return "";
|
|
232
|
+
return `${linkText} (${node.url})`;
|
|
233
|
+
}
|
|
234
|
+
if (isBlockquoteNode(node)) {
|
|
235
|
+
return getNodeChildren(node).map((child) => `> ${this.nodeToGChat(child)}`).join("\n");
|
|
236
|
+
}
|
|
237
|
+
if (isListNode(node)) {
|
|
238
|
+
return getNodeChildren(node).map((item, i) => {
|
|
239
|
+
const prefix = node.ordered ? `${i + 1}.` : "\u2022";
|
|
240
|
+
const content = getNodeChildren(item).map((child) => this.nodeToGChat(child)).join("");
|
|
241
|
+
return `${prefix} ${content}`;
|
|
242
|
+
}).join("\n");
|
|
243
|
+
}
|
|
244
|
+
if (isListItemNode(node)) {
|
|
245
|
+
return getNodeChildren(node).map((child) => this.nodeToGChat(child)).join("");
|
|
246
|
+
}
|
|
247
|
+
if (node.type === "break") {
|
|
248
|
+
return "\n";
|
|
249
|
+
}
|
|
250
|
+
if (node.type === "thematicBreak") {
|
|
251
|
+
return "---";
|
|
252
|
+
}
|
|
253
|
+
const children = getNodeChildren(node);
|
|
254
|
+
if (children.length > 0) {
|
|
255
|
+
return children.map((child) => this.nodeToGChat(child)).join("");
|
|
263
256
|
}
|
|
257
|
+
return getNodeValue(node);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/thread-utils.ts
|
|
262
|
+
import { ValidationError } from "@chat-adapter/shared";
|
|
263
|
+
function encodeThreadId(platformData) {
|
|
264
|
+
const threadPart = platformData.threadName ? `:${Buffer.from(platformData.threadName).toString("base64url")}` : "";
|
|
265
|
+
const dmPart = platformData.isDM ? ":dm" : "";
|
|
266
|
+
return `gchat:${platformData.spaceName}${threadPart}${dmPart}`;
|
|
267
|
+
}
|
|
268
|
+
function decodeThreadId(threadId) {
|
|
269
|
+
const isDM = threadId.endsWith(":dm");
|
|
270
|
+
const cleanId = isDM ? threadId.slice(0, -3) : threadId;
|
|
271
|
+
const parts = cleanId.split(":");
|
|
272
|
+
if (parts.length < 2 || parts[0] !== "gchat") {
|
|
273
|
+
throw new ValidationError(
|
|
274
|
+
"gchat",
|
|
275
|
+
`Invalid Google Chat thread ID: ${threadId}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const spaceName = parts[1];
|
|
279
|
+
const threadName = parts[2] ? Buffer.from(parts[2], "base64url").toString("utf-8") : void 0;
|
|
280
|
+
return { spaceName, threadName, isDM };
|
|
281
|
+
}
|
|
282
|
+
function isDMThread(threadId) {
|
|
283
|
+
return threadId.endsWith(":dm");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/user-info.ts
|
|
287
|
+
var USER_INFO_KEY_PREFIX = "gchat:user:";
|
|
288
|
+
var USER_INFO_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
289
|
+
var UserInfoCache = class {
|
|
290
|
+
constructor(state, logger) {
|
|
291
|
+
this.state = state;
|
|
292
|
+
this.logger = logger;
|
|
293
|
+
}
|
|
294
|
+
inMemoryCache = /* @__PURE__ */ new Map();
|
|
295
|
+
/**
|
|
296
|
+
* Cache user info for later lookup.
|
|
297
|
+
*/
|
|
298
|
+
async set(userId, displayName, email) {
|
|
299
|
+
if (!displayName || displayName === "unknown") return;
|
|
300
|
+
const userInfo = { displayName, email };
|
|
301
|
+
this.inMemoryCache.set(userId, userInfo);
|
|
302
|
+
if (this.state) {
|
|
303
|
+
const cacheKey = `${USER_INFO_KEY_PREFIX}${userId}`;
|
|
304
|
+
await this.state.set(
|
|
305
|
+
cacheKey,
|
|
306
|
+
userInfo,
|
|
307
|
+
USER_INFO_CACHE_TTL_MS
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get cached user info. Checks in-memory cache first, then falls back to state adapter.
|
|
313
|
+
*/
|
|
314
|
+
async get(userId) {
|
|
315
|
+
const inMemory = this.inMemoryCache.get(userId);
|
|
316
|
+
if (inMemory) {
|
|
317
|
+
return inMemory;
|
|
318
|
+
}
|
|
319
|
+
if (!this.state) return null;
|
|
320
|
+
const cacheKey = `${USER_INFO_KEY_PREFIX}${userId}`;
|
|
321
|
+
const fromState = await this.state.get(cacheKey);
|
|
322
|
+
if (fromState) {
|
|
323
|
+
this.inMemoryCache.set(userId, fromState);
|
|
324
|
+
}
|
|
325
|
+
return fromState;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Resolve user display name, using cache if available.
|
|
329
|
+
*
|
|
330
|
+
* @param userId - The user's resource name (e.g., "users/123456")
|
|
331
|
+
* @param providedDisplayName - Display name from the message if available
|
|
332
|
+
* @param botUserId - The bot's user ID (for self-identification)
|
|
333
|
+
* @param botUserName - The bot's configured username
|
|
334
|
+
*/
|
|
335
|
+
async resolveDisplayName(userId, providedDisplayName, botUserId, botUserName) {
|
|
336
|
+
if (providedDisplayName && providedDisplayName !== "unknown") {
|
|
337
|
+
this.set(userId, providedDisplayName).catch((err) => {
|
|
338
|
+
this.logger.error("Failed to cache user info", { userId, error: err });
|
|
339
|
+
});
|
|
340
|
+
return providedDisplayName;
|
|
341
|
+
}
|
|
342
|
+
if (botUserId && userId === botUserId) {
|
|
343
|
+
return botUserName;
|
|
344
|
+
}
|
|
345
|
+
const cached = await this.get(userId);
|
|
346
|
+
if (cached?.displayName) {
|
|
347
|
+
return cached.displayName;
|
|
348
|
+
}
|
|
349
|
+
return userId.replace("users/", "User ");
|
|
264
350
|
}
|
|
265
351
|
};
|
|
266
352
|
|
|
@@ -409,8 +495,6 @@ function verifyPubSubRequest(request, _expectedAudience) {
|
|
|
409
495
|
var SUBSCRIPTION_REFRESH_BUFFER_MS = 60 * 60 * 1e3;
|
|
410
496
|
var SUBSCRIPTION_CACHE_TTL_MS = 25 * 60 * 60 * 1e3;
|
|
411
497
|
var SPACE_SUB_KEY_PREFIX = "gchat:space-sub:";
|
|
412
|
-
var USER_INFO_KEY_PREFIX = "gchat:user:";
|
|
413
|
-
var USER_INFO_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
414
498
|
var GoogleChatAdapter = class {
|
|
415
499
|
name = "gchat";
|
|
416
500
|
userName;
|
|
@@ -419,7 +503,7 @@ var GoogleChatAdapter = class {
|
|
|
419
503
|
chatApi;
|
|
420
504
|
chat = null;
|
|
421
505
|
state = null;
|
|
422
|
-
logger
|
|
506
|
+
logger;
|
|
423
507
|
formatConverter = new GoogleChatFormatConverter();
|
|
424
508
|
pubsubTopic;
|
|
425
509
|
credentials;
|
|
@@ -436,8 +520,12 @@ var GoogleChatAdapter = class {
|
|
|
436
520
|
impersonatedChatApi;
|
|
437
521
|
/** HTTP endpoint URL for button click actions */
|
|
438
522
|
endpointUrl;
|
|
523
|
+
/** User info cache for display name lookups - initialized later in initialize() */
|
|
524
|
+
userInfoCache;
|
|
439
525
|
constructor(config) {
|
|
526
|
+
this.logger = config.logger;
|
|
440
527
|
this.userName = config.userName || "bot";
|
|
528
|
+
this.userInfoCache = new UserInfoCache(null, this.logger);
|
|
441
529
|
this.pubsubTopic = config.pubsubTopic;
|
|
442
530
|
this.impersonateUser = config.impersonateUser;
|
|
443
531
|
this.endpointUrl = config.endpointUrl;
|
|
@@ -465,7 +553,8 @@ var GoogleChatAdapter = class {
|
|
|
465
553
|
this.customAuth = config.auth;
|
|
466
554
|
auth = config.auth;
|
|
467
555
|
} else {
|
|
468
|
-
throw new
|
|
556
|
+
throw new ValidationError2(
|
|
557
|
+
"gchat",
|
|
469
558
|
"GoogleChatAdapter requires one of: credentials, useApplicationDefaultCredentials, or auth"
|
|
470
559
|
);
|
|
471
560
|
}
|
|
@@ -508,12 +597,12 @@ var GoogleChatAdapter = class {
|
|
|
508
597
|
async initialize(chat) {
|
|
509
598
|
this.chat = chat;
|
|
510
599
|
this.state = chat.getState();
|
|
511
|
-
this.
|
|
600
|
+
this.userInfoCache = new UserInfoCache(this.state, this.logger);
|
|
512
601
|
if (!this.botUserId) {
|
|
513
602
|
const savedBotUserId = await this.state.get("gchat:botUserId");
|
|
514
603
|
if (savedBotUserId) {
|
|
515
604
|
this.botUserId = savedBotUserId;
|
|
516
|
-
this.logger
|
|
605
|
+
this.logger.debug("Restored bot user ID from state", {
|
|
517
606
|
botUserId: this.botUserId
|
|
518
607
|
});
|
|
519
608
|
}
|
|
@@ -524,13 +613,13 @@ var GoogleChatAdapter = class {
|
|
|
524
613
|
* Ensures the space has a Workspace Events subscription so we receive all messages.
|
|
525
614
|
*/
|
|
526
615
|
async onThreadSubscribe(threadId) {
|
|
527
|
-
this.logger
|
|
616
|
+
this.logger.info("onThreadSubscribe called", {
|
|
528
617
|
threadId,
|
|
529
618
|
hasPubsubTopic: !!this.pubsubTopic,
|
|
530
619
|
pubsubTopic: this.pubsubTopic
|
|
531
620
|
});
|
|
532
621
|
if (!this.pubsubTopic) {
|
|
533
|
-
this.logger
|
|
622
|
+
this.logger.warn(
|
|
534
623
|
"No pubsubTopic configured, skipping space subscription. Set GOOGLE_CHAT_PUBSUB_TOPIC env var."
|
|
535
624
|
);
|
|
536
625
|
return;
|
|
@@ -543,7 +632,7 @@ var GoogleChatAdapter = class {
|
|
|
543
632
|
* Creates one if it doesn't exist or is about to expire.
|
|
544
633
|
*/
|
|
545
634
|
async ensureSpaceSubscription(spaceName) {
|
|
546
|
-
this.logger
|
|
635
|
+
this.logger.info("ensureSpaceSubscription called", {
|
|
547
636
|
spaceName,
|
|
548
637
|
hasPubsubTopic: !!this.pubsubTopic,
|
|
549
638
|
hasState: !!this.state,
|
|
@@ -551,7 +640,7 @@ var GoogleChatAdapter = class {
|
|
|
551
640
|
hasADC: this.useADC
|
|
552
641
|
});
|
|
553
642
|
if (!this.pubsubTopic || !this.state) {
|
|
554
|
-
this.logger
|
|
643
|
+
this.logger.warn("ensureSpaceSubscription skipped - missing config", {
|
|
555
644
|
hasPubsubTopic: !!this.pubsubTopic,
|
|
556
645
|
hasState: !!this.state
|
|
557
646
|
});
|
|
@@ -562,20 +651,20 @@ var GoogleChatAdapter = class {
|
|
|
562
651
|
if (cached) {
|
|
563
652
|
const timeUntilExpiry = cached.expireTime - Date.now();
|
|
564
653
|
if (timeUntilExpiry > SUBSCRIPTION_REFRESH_BUFFER_MS) {
|
|
565
|
-
this.logger
|
|
654
|
+
this.logger.debug("Space subscription still valid", {
|
|
566
655
|
spaceName,
|
|
567
656
|
expiresIn: Math.round(timeUntilExpiry / 1e3 / 60)
|
|
568
657
|
});
|
|
569
658
|
return;
|
|
570
659
|
}
|
|
571
|
-
this.logger
|
|
660
|
+
this.logger.debug("Space subscription expiring soon, will refresh", {
|
|
572
661
|
spaceName,
|
|
573
662
|
expiresIn: Math.round(timeUntilExpiry / 1e3 / 60)
|
|
574
663
|
});
|
|
575
664
|
}
|
|
576
665
|
const pending = this.pendingSubscriptions.get(spaceName);
|
|
577
666
|
if (pending) {
|
|
578
|
-
this.logger
|
|
667
|
+
this.logger.debug("Subscription creation already in progress", {
|
|
579
668
|
spaceName
|
|
580
669
|
});
|
|
581
670
|
return pending;
|
|
@@ -596,14 +685,14 @@ var GoogleChatAdapter = class {
|
|
|
596
685
|
*/
|
|
597
686
|
async createSpaceSubscriptionWithCache(spaceName, cacheKey) {
|
|
598
687
|
const authOptions = this.getAuthOptions();
|
|
599
|
-
this.logger
|
|
688
|
+
this.logger.info("createSpaceSubscriptionWithCache", {
|
|
600
689
|
spaceName,
|
|
601
690
|
hasAuthOptions: !!authOptions,
|
|
602
691
|
hasCredentials: !!this.credentials,
|
|
603
692
|
hasADC: this.useADC
|
|
604
693
|
});
|
|
605
694
|
if (!authOptions) {
|
|
606
|
-
this.logger
|
|
695
|
+
this.logger.error(
|
|
607
696
|
"Cannot create subscription: no auth configured. Use GOOGLE_CHAT_CREDENTIALS, GOOGLE_CHAT_USE_ADC=true, or custom auth."
|
|
608
697
|
);
|
|
609
698
|
return;
|
|
@@ -616,7 +705,7 @@ var GoogleChatAdapter = class {
|
|
|
616
705
|
authOptions
|
|
617
706
|
);
|
|
618
707
|
if (existing) {
|
|
619
|
-
this.logger
|
|
708
|
+
this.logger.debug("Found existing subscription", {
|
|
620
709
|
spaceName,
|
|
621
710
|
subscriptionName: existing.subscriptionName
|
|
622
711
|
});
|
|
@@ -629,7 +718,7 @@ var GoogleChatAdapter = class {
|
|
|
629
718
|
}
|
|
630
719
|
return;
|
|
631
720
|
}
|
|
632
|
-
this.logger
|
|
721
|
+
this.logger.info("Creating Workspace Events subscription", {
|
|
633
722
|
spaceName,
|
|
634
723
|
pubsubTopic
|
|
635
724
|
});
|
|
@@ -648,13 +737,13 @@ var GoogleChatAdapter = class {
|
|
|
648
737
|
SUBSCRIPTION_CACHE_TTL_MS
|
|
649
738
|
);
|
|
650
739
|
}
|
|
651
|
-
this.logger
|
|
740
|
+
this.logger.info("Workspace Events subscription created", {
|
|
652
741
|
spaceName,
|
|
653
742
|
subscriptionName: result.name,
|
|
654
743
|
expireTime: result.expireTime
|
|
655
744
|
});
|
|
656
745
|
} catch (error) {
|
|
657
|
-
this.logger
|
|
746
|
+
this.logger.error("Failed to create Workspace Events subscription", {
|
|
658
747
|
spaceName,
|
|
659
748
|
error
|
|
660
749
|
});
|
|
@@ -679,7 +768,7 @@ var GoogleChatAdapter = class {
|
|
|
679
768
|
}
|
|
680
769
|
}
|
|
681
770
|
} catch (error) {
|
|
682
|
-
this.logger
|
|
771
|
+
this.logger.error("Error checking existing subscriptions", { error });
|
|
683
772
|
}
|
|
684
773
|
return null;
|
|
685
774
|
}
|
|
@@ -709,14 +798,14 @@ var GoogleChatAdapter = class {
|
|
|
709
798
|
try {
|
|
710
799
|
const url = new URL(request.url);
|
|
711
800
|
this.endpointUrl = url.toString();
|
|
712
|
-
this.logger
|
|
801
|
+
this.logger.debug("Auto-detected endpoint URL", {
|
|
713
802
|
endpointUrl: this.endpointUrl
|
|
714
803
|
});
|
|
715
804
|
} catch {
|
|
716
805
|
}
|
|
717
806
|
}
|
|
718
807
|
const body = await request.text();
|
|
719
|
-
this.logger
|
|
808
|
+
this.logger.debug("GChat webhook raw body", { body });
|
|
720
809
|
let parsed;
|
|
721
810
|
try {
|
|
722
811
|
parsed = JSON.parse(body);
|
|
@@ -730,7 +819,7 @@ var GoogleChatAdapter = class {
|
|
|
730
819
|
const event = parsed;
|
|
731
820
|
const addedPayload = event.chat?.addedToSpacePayload;
|
|
732
821
|
if (addedPayload) {
|
|
733
|
-
this.logger
|
|
822
|
+
this.logger.debug("Bot added to space", {
|
|
734
823
|
space: addedPayload.space.name,
|
|
735
824
|
spaceType: addedPayload.space.type
|
|
736
825
|
});
|
|
@@ -738,7 +827,7 @@ var GoogleChatAdapter = class {
|
|
|
738
827
|
}
|
|
739
828
|
const removedPayload = event.chat?.removedFromSpacePayload;
|
|
740
829
|
if (removedPayload) {
|
|
741
|
-
this.logger
|
|
830
|
+
this.logger.debug("Bot removed from space", {
|
|
742
831
|
space: removedPayload.space.name
|
|
743
832
|
});
|
|
744
833
|
}
|
|
@@ -752,14 +841,14 @@ var GoogleChatAdapter = class {
|
|
|
752
841
|
}
|
|
753
842
|
const messagePayload = event.chat?.messagePayload;
|
|
754
843
|
if (messagePayload) {
|
|
755
|
-
this.logger
|
|
844
|
+
this.logger.debug("message event", {
|
|
756
845
|
space: messagePayload.space.name,
|
|
757
846
|
sender: messagePayload.message.sender?.displayName,
|
|
758
847
|
text: messagePayload.message.text?.slice(0, 50)
|
|
759
848
|
});
|
|
760
849
|
this.handleMessageEvent(event, options);
|
|
761
850
|
} else if (!addedPayload && !removedPayload) {
|
|
762
|
-
this.logger
|
|
851
|
+
this.logger.debug("Non-message event received", {
|
|
763
852
|
hasChat: !!event.chat,
|
|
764
853
|
hasCommonEventObject: !!event.commonEventObject
|
|
765
854
|
});
|
|
@@ -780,14 +869,14 @@ var GoogleChatAdapter = class {
|
|
|
780
869
|
"google.workspace.chat.reaction.v1.deleted"
|
|
781
870
|
];
|
|
782
871
|
if (eventType && !allowedEventTypes.includes(eventType)) {
|
|
783
|
-
this.logger
|
|
872
|
+
this.logger.debug("Skipping unsupported Pub/Sub event", { eventType });
|
|
784
873
|
return new Response(JSON.stringify({ success: true }), {
|
|
785
874
|
headers: { "Content-Type": "application/json" }
|
|
786
875
|
});
|
|
787
876
|
}
|
|
788
877
|
try {
|
|
789
878
|
const notification = decodePubSubMessage(pushMessage);
|
|
790
|
-
this.logger
|
|
879
|
+
this.logger.debug("Pub/Sub notification decoded", {
|
|
791
880
|
eventType: notification.eventType,
|
|
792
881
|
messageId: notification.message?.name,
|
|
793
882
|
reactionName: notification.reaction?.name
|
|
@@ -802,7 +891,7 @@ var GoogleChatAdapter = class {
|
|
|
802
891
|
headers: { "Content-Type": "application/json" }
|
|
803
892
|
});
|
|
804
893
|
} catch (error) {
|
|
805
|
-
this.logger
|
|
894
|
+
this.logger.error("Error processing Pub/Sub message", { error });
|
|
806
895
|
return new Response(JSON.stringify({ error: "Processing failed" }), {
|
|
807
896
|
status: 200,
|
|
808
897
|
headers: { "Content-Type": "application/json" }
|
|
@@ -830,7 +919,10 @@ var GoogleChatAdapter = class {
|
|
|
830
919
|
if (resolvedSpaceName && options?.waitUntil) {
|
|
831
920
|
options.waitUntil(
|
|
832
921
|
this.ensureSpaceSubscription(resolvedSpaceName).catch((err) => {
|
|
833
|
-
this.logger
|
|
922
|
+
this.logger.error("Subscription refresh failed", {
|
|
923
|
+
spaceName: resolvedSpaceName,
|
|
924
|
+
error: err
|
|
925
|
+
});
|
|
834
926
|
})
|
|
835
927
|
);
|
|
836
928
|
}
|
|
@@ -876,13 +968,13 @@ var GoogleChatAdapter = class {
|
|
|
876
968
|
spaceName: spaceName || "",
|
|
877
969
|
threadName: threadName ?? void 0
|
|
878
970
|
});
|
|
879
|
-
this.logger
|
|
971
|
+
this.logger.debug("Fetched thread context for reaction", {
|
|
880
972
|
messageName,
|
|
881
973
|
threadName,
|
|
882
974
|
threadId
|
|
883
975
|
});
|
|
884
976
|
} catch (error) {
|
|
885
|
-
this.logger
|
|
977
|
+
this.logger.warn("Failed to fetch message for thread context", {
|
|
886
978
|
messageName,
|
|
887
979
|
error
|
|
888
980
|
});
|
|
@@ -926,15 +1018,17 @@ var GoogleChatAdapter = class {
|
|
|
926
1018
|
async parsePubSubMessage(notification, threadId) {
|
|
927
1019
|
const message = notification.message;
|
|
928
1020
|
if (!message) {
|
|
929
|
-
throw new
|
|
1021
|
+
throw new ValidationError2("gchat", "PubSub notification missing message");
|
|
930
1022
|
}
|
|
931
1023
|
const text = this.normalizeBotMentions(message);
|
|
932
1024
|
const isBot = message.sender?.type === "BOT";
|
|
933
1025
|
const isMe = this.isMessageFromSelf(message);
|
|
934
1026
|
const userId = message.sender?.name || "unknown";
|
|
935
|
-
const displayName = await this.
|
|
1027
|
+
const displayName = await this.userInfoCache.resolveDisplayName(
|
|
936
1028
|
userId,
|
|
937
|
-
message.sender?.displayName
|
|
1029
|
+
message.sender?.displayName,
|
|
1030
|
+
this.botUserId,
|
|
1031
|
+
this.userName
|
|
938
1032
|
);
|
|
939
1033
|
const parsedMessage = {
|
|
940
1034
|
id: message.name,
|
|
@@ -957,7 +1051,7 @@ var GoogleChatAdapter = class {
|
|
|
957
1051
|
(att) => this.createAttachment(att)
|
|
958
1052
|
)
|
|
959
1053
|
};
|
|
960
|
-
this.logger
|
|
1054
|
+
this.logger.debug("Pub/Sub parsed message", {
|
|
961
1055
|
threadId,
|
|
962
1056
|
messageId: parsedMessage.id,
|
|
963
1057
|
text: parsedMessage.text,
|
|
@@ -983,14 +1077,14 @@ var GoogleChatAdapter = class {
|
|
|
983
1077
|
*/
|
|
984
1078
|
handleCardClick(event, options) {
|
|
985
1079
|
if (!this.chat) {
|
|
986
|
-
this.logger
|
|
1080
|
+
this.logger.warn("Chat instance not initialized, ignoring card click");
|
|
987
1081
|
return;
|
|
988
1082
|
}
|
|
989
1083
|
const buttonPayload = event.chat?.buttonClickedPayload;
|
|
990
1084
|
const commonEvent = event.commonEventObject;
|
|
991
1085
|
const actionId = commonEvent?.parameters?.actionId || commonEvent?.invokedFunction;
|
|
992
1086
|
if (!actionId) {
|
|
993
|
-
this.logger
|
|
1087
|
+
this.logger.debug("Card click missing actionId", {
|
|
994
1088
|
parameters: commonEvent?.parameters,
|
|
995
1089
|
invokedFunction: commonEvent?.invokedFunction
|
|
996
1090
|
});
|
|
@@ -1001,7 +1095,7 @@ var GoogleChatAdapter = class {
|
|
|
1001
1095
|
const message = buttonPayload?.message;
|
|
1002
1096
|
const user = buttonPayload?.user || event.chat?.user;
|
|
1003
1097
|
if (!space) {
|
|
1004
|
-
this.logger
|
|
1098
|
+
this.logger.warn("Card click missing space info");
|
|
1005
1099
|
return;
|
|
1006
1100
|
}
|
|
1007
1101
|
const threadName = message?.thread?.name || message?.name;
|
|
@@ -1024,7 +1118,7 @@ var GoogleChatAdapter = class {
|
|
|
1024
1118
|
adapter: this,
|
|
1025
1119
|
raw: event
|
|
1026
1120
|
};
|
|
1027
|
-
this.logger
|
|
1121
|
+
this.logger.debug("Processing GChat card click", {
|
|
1028
1122
|
actionId,
|
|
1029
1123
|
value,
|
|
1030
1124
|
messageId: actionEvent.messageId,
|
|
@@ -1037,12 +1131,12 @@ var GoogleChatAdapter = class {
|
|
|
1037
1131
|
*/
|
|
1038
1132
|
handleMessageEvent(event, options) {
|
|
1039
1133
|
if (!this.chat) {
|
|
1040
|
-
this.logger
|
|
1134
|
+
this.logger.warn("Chat instance not initialized, ignoring event");
|
|
1041
1135
|
return;
|
|
1042
1136
|
}
|
|
1043
1137
|
const messagePayload = event.chat?.messagePayload;
|
|
1044
1138
|
if (!messagePayload) {
|
|
1045
|
-
this.logger
|
|
1139
|
+
this.logger.debug("Ignoring event without messagePayload");
|
|
1046
1140
|
return;
|
|
1047
1141
|
}
|
|
1048
1142
|
const message = messagePayload.message;
|
|
@@ -1063,7 +1157,7 @@ var GoogleChatAdapter = class {
|
|
|
1063
1157
|
parseGoogleChatMessage(event, threadId) {
|
|
1064
1158
|
const message = event.chat?.messagePayload?.message;
|
|
1065
1159
|
if (!message) {
|
|
1066
|
-
throw new
|
|
1160
|
+
throw new ValidationError2("gchat", "Event has no message payload");
|
|
1067
1161
|
}
|
|
1068
1162
|
const text = this.normalizeBotMentions(message);
|
|
1069
1163
|
const isBot = message.sender?.type === "BOT";
|
|
@@ -1071,10 +1165,9 @@ var GoogleChatAdapter = class {
|
|
|
1071
1165
|
const userId = message.sender?.name || "unknown";
|
|
1072
1166
|
const displayName = message.sender?.displayName || "unknown";
|
|
1073
1167
|
if (userId !== "unknown" && displayName !== "unknown") {
|
|
1074
|
-
this.
|
|
1075
|
-
(
|
|
1076
|
-
|
|
1077
|
-
);
|
|
1168
|
+
this.userInfoCache.set(userId, displayName, message.sender?.email).catch((error) => {
|
|
1169
|
+
this.logger.error("Failed to cache user info", { userId, error });
|
|
1170
|
+
});
|
|
1078
1171
|
}
|
|
1079
1172
|
return {
|
|
1080
1173
|
id: message.name,
|
|
@@ -1101,21 +1194,21 @@ var GoogleChatAdapter = class {
|
|
|
1101
1194
|
async postMessage(threadId, message) {
|
|
1102
1195
|
const { spaceName, threadName } = this.decodeThreadId(threadId);
|
|
1103
1196
|
try {
|
|
1104
|
-
const files =
|
|
1197
|
+
const files = extractFiles(message);
|
|
1105
1198
|
if (files.length > 0) {
|
|
1106
|
-
this.logger
|
|
1199
|
+
this.logger.warn(
|
|
1107
1200
|
"File uploads are not yet supported for Google Chat. Files will be ignored.",
|
|
1108
1201
|
{ fileCount: files.length }
|
|
1109
1202
|
);
|
|
1110
1203
|
}
|
|
1111
|
-
const card =
|
|
1204
|
+
const card = extractCard(message);
|
|
1112
1205
|
if (card) {
|
|
1113
1206
|
const cardId = `card-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1114
1207
|
const googleCard = cardToGoogleCard(card, {
|
|
1115
1208
|
cardId,
|
|
1116
1209
|
endpointUrl: this.endpointUrl
|
|
1117
1210
|
});
|
|
1118
|
-
this.logger
|
|
1211
|
+
this.logger.debug("GChat API: spaces.messages.create (card)", {
|
|
1119
1212
|
spaceName,
|
|
1120
1213
|
threadName,
|
|
1121
1214
|
googleCard: JSON.stringify(googleCard)
|
|
@@ -1129,7 +1222,7 @@ var GoogleChatAdapter = class {
|
|
|
1129
1222
|
thread: threadName ? { name: threadName } : void 0
|
|
1130
1223
|
}
|
|
1131
1224
|
});
|
|
1132
|
-
this.logger
|
|
1225
|
+
this.logger.debug("GChat API: spaces.messages.create response", {
|
|
1133
1226
|
messageName: response2.data.name
|
|
1134
1227
|
});
|
|
1135
1228
|
return {
|
|
@@ -1138,11 +1231,11 @@ var GoogleChatAdapter = class {
|
|
|
1138
1231
|
raw: response2.data
|
|
1139
1232
|
};
|
|
1140
1233
|
}
|
|
1141
|
-
const text =
|
|
1234
|
+
const text = convertEmojiPlaceholders(
|
|
1142
1235
|
this.formatConverter.renderPostable(message),
|
|
1143
1236
|
"gchat"
|
|
1144
1237
|
);
|
|
1145
|
-
this.logger
|
|
1238
|
+
this.logger.debug("GChat API: spaces.messages.create", {
|
|
1146
1239
|
spaceName,
|
|
1147
1240
|
threadName,
|
|
1148
1241
|
textLength: text.length
|
|
@@ -1156,7 +1249,7 @@ var GoogleChatAdapter = class {
|
|
|
1156
1249
|
thread: threadName ? { name: threadName } : void 0
|
|
1157
1250
|
}
|
|
1158
1251
|
});
|
|
1159
|
-
this.logger
|
|
1252
|
+
this.logger.debug("GChat API: spaces.messages.create response", {
|
|
1160
1253
|
messageName: response.data.name
|
|
1161
1254
|
});
|
|
1162
1255
|
return {
|
|
@@ -1168,27 +1261,6 @@ var GoogleChatAdapter = class {
|
|
|
1168
1261
|
this.handleGoogleChatError(error, "postMessage");
|
|
1169
1262
|
}
|
|
1170
1263
|
}
|
|
1171
|
-
/**
|
|
1172
|
-
* Extract card element from a message if present.
|
|
1173
|
-
*/
|
|
1174
|
-
extractCard(message) {
|
|
1175
|
-
if (isCardElement(message)) {
|
|
1176
|
-
return message;
|
|
1177
|
-
}
|
|
1178
|
-
if (typeof message === "object" && message !== null && "card" in message) {
|
|
1179
|
-
return message.card;
|
|
1180
|
-
}
|
|
1181
|
-
return null;
|
|
1182
|
-
}
|
|
1183
|
-
/**
|
|
1184
|
-
* Extract files from a message if present.
|
|
1185
|
-
*/
|
|
1186
|
-
extractFiles(message) {
|
|
1187
|
-
if (typeof message === "object" && message !== null && "files" in message) {
|
|
1188
|
-
return message.files ?? [];
|
|
1189
|
-
}
|
|
1190
|
-
return [];
|
|
1191
|
-
}
|
|
1192
1264
|
/**
|
|
1193
1265
|
* Create an Attachment object from a Google Chat attachment.
|
|
1194
1266
|
*/
|
|
@@ -1211,12 +1283,18 @@ var GoogleChatAdapter = class {
|
|
|
1211
1283
|
mimeType: att.contentType || void 0,
|
|
1212
1284
|
fetchData: url ? async () => {
|
|
1213
1285
|
if (typeof auth === "string" || !auth) {
|
|
1214
|
-
throw new
|
|
1286
|
+
throw new AuthenticationError(
|
|
1287
|
+
"gchat",
|
|
1288
|
+
"Cannot fetch file: no auth client configured"
|
|
1289
|
+
);
|
|
1215
1290
|
}
|
|
1216
1291
|
const tokenResult = await auth.getAccessToken();
|
|
1217
1292
|
const token = typeof tokenResult === "string" ? tokenResult : tokenResult?.token;
|
|
1218
1293
|
if (!token) {
|
|
1219
|
-
throw new
|
|
1294
|
+
throw new AuthenticationError(
|
|
1295
|
+
"gchat",
|
|
1296
|
+
"Failed to get access token"
|
|
1297
|
+
);
|
|
1220
1298
|
}
|
|
1221
1299
|
const response = await fetch(url, {
|
|
1222
1300
|
headers: {
|
|
@@ -1224,7 +1302,8 @@ var GoogleChatAdapter = class {
|
|
|
1224
1302
|
}
|
|
1225
1303
|
});
|
|
1226
1304
|
if (!response.ok) {
|
|
1227
|
-
throw new
|
|
1305
|
+
throw new NetworkError(
|
|
1306
|
+
"gchat",
|
|
1228
1307
|
`Failed to fetch file: ${response.status} ${response.statusText}`
|
|
1229
1308
|
);
|
|
1230
1309
|
}
|
|
@@ -1235,14 +1314,14 @@ var GoogleChatAdapter = class {
|
|
|
1235
1314
|
}
|
|
1236
1315
|
async editMessage(threadId, messageId, message) {
|
|
1237
1316
|
try {
|
|
1238
|
-
const card =
|
|
1317
|
+
const card = extractCard(message);
|
|
1239
1318
|
if (card) {
|
|
1240
1319
|
const cardId = `card-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1241
1320
|
const googleCard = cardToGoogleCard(card, {
|
|
1242
1321
|
cardId,
|
|
1243
1322
|
endpointUrl: this.endpointUrl
|
|
1244
1323
|
});
|
|
1245
|
-
this.logger
|
|
1324
|
+
this.logger.debug("GChat API: spaces.messages.update (card)", {
|
|
1246
1325
|
messageId,
|
|
1247
1326
|
cardId
|
|
1248
1327
|
});
|
|
@@ -1254,7 +1333,7 @@ var GoogleChatAdapter = class {
|
|
|
1254
1333
|
cardsV2: [googleCard]
|
|
1255
1334
|
}
|
|
1256
1335
|
});
|
|
1257
|
-
this.logger
|
|
1336
|
+
this.logger.debug("GChat API: spaces.messages.update response", {
|
|
1258
1337
|
messageName: response2.data.name
|
|
1259
1338
|
});
|
|
1260
1339
|
return {
|
|
@@ -1263,11 +1342,11 @@ var GoogleChatAdapter = class {
|
|
|
1263
1342
|
raw: response2.data
|
|
1264
1343
|
};
|
|
1265
1344
|
}
|
|
1266
|
-
const text =
|
|
1345
|
+
const text = convertEmojiPlaceholders(
|
|
1267
1346
|
this.formatConverter.renderPostable(message),
|
|
1268
1347
|
"gchat"
|
|
1269
1348
|
);
|
|
1270
|
-
this.logger
|
|
1349
|
+
this.logger.debug("GChat API: spaces.messages.update", {
|
|
1271
1350
|
messageId,
|
|
1272
1351
|
textLength: text.length
|
|
1273
1352
|
});
|
|
@@ -1278,7 +1357,7 @@ var GoogleChatAdapter = class {
|
|
|
1278
1357
|
text
|
|
1279
1358
|
}
|
|
1280
1359
|
});
|
|
1281
|
-
this.logger
|
|
1360
|
+
this.logger.debug("GChat API: spaces.messages.update response", {
|
|
1282
1361
|
messageName: response.data.name
|
|
1283
1362
|
});
|
|
1284
1363
|
return {
|
|
@@ -1292,11 +1371,11 @@ var GoogleChatAdapter = class {
|
|
|
1292
1371
|
}
|
|
1293
1372
|
async deleteMessage(_threadId, messageId) {
|
|
1294
1373
|
try {
|
|
1295
|
-
this.logger
|
|
1374
|
+
this.logger.debug("GChat API: spaces.messages.delete", { messageId });
|
|
1296
1375
|
await this.chatApi.spaces.messages.delete({
|
|
1297
1376
|
name: messageId
|
|
1298
1377
|
});
|
|
1299
|
-
this.logger
|
|
1378
|
+
this.logger.debug("GChat API: spaces.messages.delete response", {
|
|
1300
1379
|
ok: true
|
|
1301
1380
|
});
|
|
1302
1381
|
} catch (error) {
|
|
@@ -1306,7 +1385,7 @@ var GoogleChatAdapter = class {
|
|
|
1306
1385
|
async addReaction(_threadId, messageId, emoji) {
|
|
1307
1386
|
const gchatEmoji = defaultEmojiResolver.toGChat(emoji);
|
|
1308
1387
|
try {
|
|
1309
|
-
this.logger
|
|
1388
|
+
this.logger.debug("GChat API: spaces.messages.reactions.create", {
|
|
1310
1389
|
messageId,
|
|
1311
1390
|
emoji: gchatEmoji
|
|
1312
1391
|
});
|
|
@@ -1316,7 +1395,7 @@ var GoogleChatAdapter = class {
|
|
|
1316
1395
|
emoji: { unicode: gchatEmoji }
|
|
1317
1396
|
}
|
|
1318
1397
|
});
|
|
1319
|
-
this.logger
|
|
1398
|
+
this.logger.debug(
|
|
1320
1399
|
"GChat API: spaces.messages.reactions.create response",
|
|
1321
1400
|
{
|
|
1322
1401
|
ok: true
|
|
@@ -1329,32 +1408,32 @@ var GoogleChatAdapter = class {
|
|
|
1329
1408
|
async removeReaction(_threadId, messageId, emoji) {
|
|
1330
1409
|
const gchatEmoji = defaultEmojiResolver.toGChat(emoji);
|
|
1331
1410
|
try {
|
|
1332
|
-
this.logger
|
|
1411
|
+
this.logger.debug("GChat API: spaces.messages.reactions.list", {
|
|
1333
1412
|
messageId
|
|
1334
1413
|
});
|
|
1335
1414
|
const response = await this.chatApi.spaces.messages.reactions.list({
|
|
1336
1415
|
parent: messageId
|
|
1337
1416
|
});
|
|
1338
|
-
this.logger
|
|
1417
|
+
this.logger.debug("GChat API: spaces.messages.reactions.list response", {
|
|
1339
1418
|
reactionCount: response.data.reactions?.length || 0
|
|
1340
1419
|
});
|
|
1341
1420
|
const reaction = response.data.reactions?.find(
|
|
1342
1421
|
(r) => r.emoji?.unicode === gchatEmoji
|
|
1343
1422
|
);
|
|
1344
1423
|
if (!reaction?.name) {
|
|
1345
|
-
this.logger
|
|
1424
|
+
this.logger.debug("Reaction not found to remove", {
|
|
1346
1425
|
messageId,
|
|
1347
1426
|
emoji: gchatEmoji
|
|
1348
1427
|
});
|
|
1349
1428
|
return;
|
|
1350
1429
|
}
|
|
1351
|
-
this.logger
|
|
1430
|
+
this.logger.debug("GChat API: spaces.messages.reactions.delete", {
|
|
1352
1431
|
reactionName: reaction.name
|
|
1353
1432
|
});
|
|
1354
1433
|
await this.chatApi.spaces.messages.reactions.delete({
|
|
1355
1434
|
name: reaction.name
|
|
1356
1435
|
});
|
|
1357
|
-
this.logger
|
|
1436
|
+
this.logger.debug(
|
|
1358
1437
|
"GChat API: spaces.messages.reactions.delete response",
|
|
1359
1438
|
{
|
|
1360
1439
|
ok: true
|
|
@@ -1377,12 +1456,12 @@ var GoogleChatAdapter = class {
|
|
|
1377
1456
|
*/
|
|
1378
1457
|
async openDM(userId) {
|
|
1379
1458
|
try {
|
|
1380
|
-
this.logger
|
|
1459
|
+
this.logger.debug("GChat API: spaces.findDirectMessage", { userId });
|
|
1381
1460
|
const findResponse = await this.chatApi.spaces.findDirectMessage({
|
|
1382
1461
|
name: userId
|
|
1383
1462
|
});
|
|
1384
1463
|
if (findResponse.data.name) {
|
|
1385
|
-
this.logger
|
|
1464
|
+
this.logger.debug("GChat API: Found existing DM space", {
|
|
1386
1465
|
spaceName: findResponse.data.name
|
|
1387
1466
|
});
|
|
1388
1467
|
return this.encodeThreadId({
|
|
@@ -1393,17 +1472,17 @@ var GoogleChatAdapter = class {
|
|
|
1393
1472
|
} catch (error) {
|
|
1394
1473
|
const gError = error;
|
|
1395
1474
|
if (gError.code !== 404) {
|
|
1396
|
-
this.logger
|
|
1475
|
+
this.logger.debug("GChat API: findDirectMessage failed", { error });
|
|
1397
1476
|
}
|
|
1398
1477
|
}
|
|
1399
1478
|
const chatApi = this.impersonatedChatApi || this.chatApi;
|
|
1400
1479
|
if (!this.impersonatedChatApi) {
|
|
1401
|
-
this.logger
|
|
1480
|
+
this.logger.warn(
|
|
1402
1481
|
"openDM: No existing DM found and no impersonation configured. Creating new DMs requires domain-wide delegation. Set 'impersonateUser' in adapter config."
|
|
1403
1482
|
);
|
|
1404
1483
|
}
|
|
1405
1484
|
try {
|
|
1406
|
-
this.logger
|
|
1485
|
+
this.logger.debug("GChat API: spaces.setup (DM)", {
|
|
1407
1486
|
userId,
|
|
1408
1487
|
hasImpersonation: !!this.impersonatedChatApi,
|
|
1409
1488
|
impersonateUser: this.impersonateUser
|
|
@@ -1425,68 +1504,195 @@ var GoogleChatAdapter = class {
|
|
|
1425
1504
|
});
|
|
1426
1505
|
const spaceName = response.data.name;
|
|
1427
1506
|
if (!spaceName) {
|
|
1428
|
-
throw new
|
|
1507
|
+
throw new NetworkError(
|
|
1508
|
+
"gchat",
|
|
1509
|
+
"Failed to create DM - no space name returned"
|
|
1510
|
+
);
|
|
1429
1511
|
}
|
|
1430
|
-
this.logger
|
|
1512
|
+
this.logger.debug("GChat API: spaces.setup response", { spaceName });
|
|
1431
1513
|
return this.encodeThreadId({ spaceName, isDM: true });
|
|
1432
1514
|
} catch (error) {
|
|
1433
1515
|
this.handleGoogleChatError(error, "openDM");
|
|
1434
1516
|
}
|
|
1435
1517
|
}
|
|
1436
1518
|
async fetchMessages(threadId, options = {}) {
|
|
1437
|
-
const { spaceName } = this.decodeThreadId(threadId);
|
|
1519
|
+
const { spaceName, threadName } = this.decodeThreadId(threadId);
|
|
1520
|
+
const direction = options.direction ?? "backward";
|
|
1521
|
+
const limit = options.limit || 100;
|
|
1438
1522
|
const api = this.impersonatedChatApi || this.chatApi;
|
|
1439
1523
|
try {
|
|
1440
|
-
|
|
1524
|
+
const filter = threadName ? `thread.name = "${threadName}"` : void 0;
|
|
1525
|
+
if (direction === "forward") {
|
|
1526
|
+
return this.fetchMessagesForward(
|
|
1527
|
+
api,
|
|
1528
|
+
spaceName,
|
|
1529
|
+
threadId,
|
|
1530
|
+
filter,
|
|
1531
|
+
limit,
|
|
1532
|
+
options.cursor
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
return this.fetchMessagesBackward(
|
|
1536
|
+
api,
|
|
1441
1537
|
spaceName,
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1538
|
+
threadId,
|
|
1539
|
+
filter,
|
|
1540
|
+
limit,
|
|
1541
|
+
options.cursor
|
|
1542
|
+
);
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
this.handleGoogleChatError(error, "fetchMessages");
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Fetch messages in backward direction (most recent first).
|
|
1549
|
+
* GChat API defaults to createTime ASC (oldest first), so we request DESC
|
|
1550
|
+
* to get the most recent messages, then reverse for chronological order within page.
|
|
1551
|
+
*/
|
|
1552
|
+
async fetchMessagesBackward(api, spaceName, threadId, filter, limit, cursor) {
|
|
1553
|
+
this.logger.debug("GChat API: spaces.messages.list (backward)", {
|
|
1554
|
+
spaceName,
|
|
1555
|
+
filter,
|
|
1556
|
+
pageSize: limit,
|
|
1557
|
+
cursor
|
|
1558
|
+
});
|
|
1559
|
+
const response = await api.spaces.messages.list({
|
|
1560
|
+
parent: spaceName,
|
|
1561
|
+
pageSize: limit,
|
|
1562
|
+
pageToken: cursor,
|
|
1563
|
+
filter,
|
|
1564
|
+
orderBy: "createTime desc"
|
|
1565
|
+
// Get newest messages first
|
|
1566
|
+
});
|
|
1567
|
+
const rawMessages = (response.data.messages || []).reverse();
|
|
1568
|
+
this.logger.debug("GChat API: spaces.messages.list response (backward)", {
|
|
1569
|
+
messageCount: rawMessages.length,
|
|
1570
|
+
hasNextPageToken: !!response.data.nextPageToken
|
|
1571
|
+
});
|
|
1572
|
+
const messages = await Promise.all(
|
|
1573
|
+
rawMessages.map(
|
|
1574
|
+
(msg) => this.parseGChatListMessage(msg, spaceName, threadId)
|
|
1575
|
+
)
|
|
1576
|
+
);
|
|
1577
|
+
return {
|
|
1578
|
+
messages,
|
|
1579
|
+
// nextPageToken points to older messages (backward pagination)
|
|
1580
|
+
nextCursor: response.data.nextPageToken ?? void 0
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Fetch messages in forward direction (oldest first).
|
|
1585
|
+
*
|
|
1586
|
+
* GChat API defaults to createTime ASC (oldest first), which is what we want.
|
|
1587
|
+
* For forward pagination, we:
|
|
1588
|
+
* 1. If no cursor: Fetch ALL messages (already in chronological order)
|
|
1589
|
+
* 2. If cursor: Cursor is a message name, skip to after that message
|
|
1590
|
+
*
|
|
1591
|
+
* Note: This is less efficient than backward for large message histories,
|
|
1592
|
+
* as it requires fetching all messages to find the cursor position.
|
|
1593
|
+
*/
|
|
1594
|
+
async fetchMessagesForward(api, spaceName, threadId, filter, limit, cursor) {
|
|
1595
|
+
this.logger.debug("GChat API: spaces.messages.list (forward)", {
|
|
1596
|
+
spaceName,
|
|
1597
|
+
filter,
|
|
1598
|
+
limit,
|
|
1599
|
+
cursor
|
|
1600
|
+
});
|
|
1601
|
+
const allRawMessages = [];
|
|
1602
|
+
let pageToken;
|
|
1603
|
+
do {
|
|
1445
1604
|
const response = await api.spaces.messages.list({
|
|
1446
1605
|
parent: spaceName,
|
|
1447
|
-
pageSize:
|
|
1448
|
-
|
|
1606
|
+
pageSize: 1e3,
|
|
1607
|
+
// Max page size for efficiency
|
|
1608
|
+
pageToken,
|
|
1609
|
+
filter
|
|
1610
|
+
// Default orderBy is createTime ASC (oldest first) - what we want
|
|
1449
1611
|
});
|
|
1450
|
-
const
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
userName: msg.sender?.displayName || "unknown",
|
|
1469
|
-
fullName: msg.sender?.displayName || "unknown",
|
|
1470
|
-
isBot: msgIsBot,
|
|
1471
|
-
isMe: msgIsBot
|
|
1472
|
-
},
|
|
1473
|
-
metadata: {
|
|
1474
|
-
dateSent: msg.createTime ? new Date(msg.createTime) : /* @__PURE__ */ new Date(),
|
|
1475
|
-
edited: false
|
|
1476
|
-
},
|
|
1477
|
-
attachments: []
|
|
1478
|
-
};
|
|
1479
|
-
});
|
|
1480
|
-
} catch (error) {
|
|
1481
|
-
this.handleGoogleChatError(error, "fetchMessages");
|
|
1612
|
+
const pageMessages = response.data.messages || [];
|
|
1613
|
+
allRawMessages.push(...pageMessages);
|
|
1614
|
+
pageToken = response.data.nextPageToken ?? void 0;
|
|
1615
|
+
} while (pageToken);
|
|
1616
|
+
this.logger.debug(
|
|
1617
|
+
"GChat API: fetched all messages for forward pagination",
|
|
1618
|
+
{
|
|
1619
|
+
totalCount: allRawMessages.length
|
|
1620
|
+
}
|
|
1621
|
+
);
|
|
1622
|
+
let startIndex = 0;
|
|
1623
|
+
if (cursor) {
|
|
1624
|
+
const cursorIndex = allRawMessages.findIndex(
|
|
1625
|
+
(msg) => msg.name === cursor
|
|
1626
|
+
);
|
|
1627
|
+
if (cursorIndex >= 0) {
|
|
1628
|
+
startIndex = cursorIndex + 1;
|
|
1629
|
+
}
|
|
1482
1630
|
}
|
|
1631
|
+
const selectedMessages = allRawMessages.slice(
|
|
1632
|
+
startIndex,
|
|
1633
|
+
startIndex + limit
|
|
1634
|
+
);
|
|
1635
|
+
const messages = await Promise.all(
|
|
1636
|
+
selectedMessages.map(
|
|
1637
|
+
(msg) => this.parseGChatListMessage(msg, spaceName, threadId)
|
|
1638
|
+
)
|
|
1639
|
+
);
|
|
1640
|
+
let nextCursor;
|
|
1641
|
+
if (startIndex + limit < allRawMessages.length && selectedMessages.length > 0) {
|
|
1642
|
+
const lastMsg = selectedMessages[selectedMessages.length - 1];
|
|
1643
|
+
if (lastMsg?.name) {
|
|
1644
|
+
nextCursor = lastMsg.name;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return {
|
|
1648
|
+
messages,
|
|
1649
|
+
nextCursor
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Parse a message from the list API into the standard Message format.
|
|
1654
|
+
* Resolves user display names and properly determines isMe.
|
|
1655
|
+
*/
|
|
1656
|
+
async parseGChatListMessage(msg, spaceName, _threadId) {
|
|
1657
|
+
const msgThreadId = this.encodeThreadId({
|
|
1658
|
+
spaceName,
|
|
1659
|
+
threadName: msg.thread?.name ?? void 0
|
|
1660
|
+
});
|
|
1661
|
+
const msgIsBot = msg.sender?.type === "BOT";
|
|
1662
|
+
const userId = msg.sender?.name || "unknown";
|
|
1663
|
+
const displayName = await this.userInfoCache.resolveDisplayName(
|
|
1664
|
+
userId,
|
|
1665
|
+
msg.sender?.displayName ?? void 0,
|
|
1666
|
+
this.botUserId,
|
|
1667
|
+
this.userName
|
|
1668
|
+
);
|
|
1669
|
+
const isMe = this.isMessageFromSelf(msg);
|
|
1670
|
+
return {
|
|
1671
|
+
id: msg.name || "",
|
|
1672
|
+
threadId: msgThreadId,
|
|
1673
|
+
text: this.formatConverter.extractPlainText(msg.text || ""),
|
|
1674
|
+
formatted: this.formatConverter.toAst(msg.text || ""),
|
|
1675
|
+
raw: msg,
|
|
1676
|
+
author: {
|
|
1677
|
+
userId,
|
|
1678
|
+
userName: displayName,
|
|
1679
|
+
fullName: displayName,
|
|
1680
|
+
isBot: msgIsBot,
|
|
1681
|
+
isMe
|
|
1682
|
+
},
|
|
1683
|
+
metadata: {
|
|
1684
|
+
dateSent: msg.createTime ? new Date(msg.createTime) : /* @__PURE__ */ new Date(),
|
|
1685
|
+
edited: false
|
|
1686
|
+
},
|
|
1687
|
+
attachments: []
|
|
1688
|
+
};
|
|
1483
1689
|
}
|
|
1484
1690
|
async fetchThread(threadId) {
|
|
1485
1691
|
const { spaceName } = this.decodeThreadId(threadId);
|
|
1486
1692
|
try {
|
|
1487
|
-
this.logger
|
|
1693
|
+
this.logger.debug("GChat API: spaces.get", { spaceName });
|
|
1488
1694
|
const response = await this.chatApi.spaces.get({ name: spaceName });
|
|
1489
|
-
this.logger
|
|
1695
|
+
this.logger.debug("GChat API: spaces.get response", {
|
|
1490
1696
|
displayName: response.data.displayName
|
|
1491
1697
|
});
|
|
1492
1698
|
return {
|
|
@@ -1502,34 +1708,22 @@ var GoogleChatAdapter = class {
|
|
|
1502
1708
|
}
|
|
1503
1709
|
}
|
|
1504
1710
|
encodeThreadId(platformData) {
|
|
1505
|
-
|
|
1506
|
-
const dmPart = platformData.isDM ? ":dm" : "";
|
|
1507
|
-
return `gchat:${platformData.spaceName}${threadPart}${dmPart}`;
|
|
1711
|
+
return encodeThreadId(platformData);
|
|
1508
1712
|
}
|
|
1509
1713
|
/**
|
|
1510
1714
|
* Check if a thread is a direct message conversation.
|
|
1511
|
-
* Checks for the :dm marker in the thread ID which is set when
|
|
1512
|
-
* processing DM messages or opening DMs.
|
|
1513
1715
|
*/
|
|
1514
1716
|
isDM(threadId) {
|
|
1515
|
-
return threadId
|
|
1717
|
+
return isDMThread(threadId);
|
|
1516
1718
|
}
|
|
1517
1719
|
decodeThreadId(threadId) {
|
|
1518
|
-
|
|
1519
|
-
const cleanId = isDM ? threadId.slice(0, -3) : threadId;
|
|
1520
|
-
const parts = cleanId.split(":");
|
|
1521
|
-
if (parts.length < 2 || parts[0] !== "gchat") {
|
|
1522
|
-
throw new Error(`Invalid Google Chat thread ID: ${threadId}`);
|
|
1523
|
-
}
|
|
1524
|
-
const spaceName = parts[1];
|
|
1525
|
-
const threadName = parts[2] ? Buffer.from(parts[2], "base64url").toString("utf-8") : void 0;
|
|
1526
|
-
return { spaceName, threadName, isDM };
|
|
1720
|
+
return decodeThreadId(threadId);
|
|
1527
1721
|
}
|
|
1528
1722
|
parseMessage(raw) {
|
|
1529
1723
|
const event = raw;
|
|
1530
1724
|
const messagePayload = event.chat?.messagePayload;
|
|
1531
1725
|
if (!messagePayload) {
|
|
1532
|
-
throw new
|
|
1726
|
+
throw new ValidationError2("gchat", "Cannot parse non-message event");
|
|
1533
1727
|
}
|
|
1534
1728
|
const threadName = messagePayload.message.thread?.name || messagePayload.message.name;
|
|
1535
1729
|
const threadId = this.encodeThreadId({
|
|
@@ -1557,11 +1751,11 @@ var GoogleChatAdapter = class {
|
|
|
1557
1751
|
const botDisplayName = botUser.displayName;
|
|
1558
1752
|
if (botUser.name && !this.botUserId) {
|
|
1559
1753
|
this.botUserId = botUser.name;
|
|
1560
|
-
this.logger
|
|
1754
|
+
this.logger.info("Learned bot user ID from mention", {
|
|
1561
1755
|
botUserId: this.botUserId
|
|
1562
1756
|
});
|
|
1563
1757
|
this.state?.set("gchat:botUserId", this.botUserId).catch(
|
|
1564
|
-
(err) => this.logger
|
|
1758
|
+
(err) => this.logger.error("Failed to persist botUserId", { error: err })
|
|
1565
1759
|
);
|
|
1566
1760
|
}
|
|
1567
1761
|
if (annotation.startIndex !== void 0 && annotation.length !== void 0) {
|
|
@@ -1569,7 +1763,7 @@ var GoogleChatAdapter = class {
|
|
|
1569
1763
|
const length = annotation.length;
|
|
1570
1764
|
const mentionText = text.slice(startIndex, startIndex + length);
|
|
1571
1765
|
text = text.slice(0, startIndex) + `@${this.userName}` + text.slice(startIndex + length);
|
|
1572
|
-
this.logger
|
|
1766
|
+
this.logger.debug("Normalized bot mention", {
|
|
1573
1767
|
original: mentionText,
|
|
1574
1768
|
replacement: `@${this.userName}`
|
|
1575
1769
|
});
|
|
@@ -1597,62 +1791,23 @@ var GoogleChatAdapter = class {
|
|
|
1597
1791
|
return senderId === this.botUserId;
|
|
1598
1792
|
}
|
|
1599
1793
|
if (!this.botUserId && message.sender?.type === "BOT") {
|
|
1600
|
-
this.logger
|
|
1794
|
+
this.logger.debug(
|
|
1601
1795
|
"Cannot determine isMe - bot user ID not yet learned. Bot ID is learned from @mentions. Assuming message is not from self.",
|
|
1602
1796
|
{ senderId }
|
|
1603
1797
|
);
|
|
1604
1798
|
}
|
|
1605
1799
|
return false;
|
|
1606
1800
|
}
|
|
1607
|
-
/**
|
|
1608
|
-
* Cache user info for later lookup (e.g., when processing Pub/Sub messages).
|
|
1609
|
-
*/
|
|
1610
|
-
async cacheUserInfo(userId, displayName, email) {
|
|
1611
|
-
if (!this.state || !displayName || displayName === "unknown") return;
|
|
1612
|
-
const cacheKey = `${USER_INFO_KEY_PREFIX}${userId}`;
|
|
1613
|
-
await this.state.set(
|
|
1614
|
-
cacheKey,
|
|
1615
|
-
{ displayName, email },
|
|
1616
|
-
USER_INFO_CACHE_TTL_MS
|
|
1617
|
-
);
|
|
1618
|
-
}
|
|
1619
|
-
/**
|
|
1620
|
-
* Get cached user info.
|
|
1621
|
-
*/
|
|
1622
|
-
async getCachedUserInfo(userId) {
|
|
1623
|
-
if (!this.state) return null;
|
|
1624
|
-
const cacheKey = `${USER_INFO_KEY_PREFIX}${userId}`;
|
|
1625
|
-
return this.state.get(cacheKey);
|
|
1626
|
-
}
|
|
1627
|
-
/**
|
|
1628
|
-
* Resolve user display name, using cache if available.
|
|
1629
|
-
*/
|
|
1630
|
-
async resolveUserDisplayName(userId, providedDisplayName) {
|
|
1631
|
-
if (providedDisplayName && providedDisplayName !== "unknown") {
|
|
1632
|
-
this.cacheUserInfo(userId, providedDisplayName).catch(() => {
|
|
1633
|
-
});
|
|
1634
|
-
return providedDisplayName;
|
|
1635
|
-
}
|
|
1636
|
-
const cached = await this.getCachedUserInfo(userId);
|
|
1637
|
-
if (cached?.displayName) {
|
|
1638
|
-
return cached.displayName;
|
|
1639
|
-
}
|
|
1640
|
-
return userId.replace("users/", "User ");
|
|
1641
|
-
}
|
|
1642
1801
|
handleGoogleChatError(error, context) {
|
|
1643
1802
|
const gError = error;
|
|
1644
|
-
this.logger
|
|
1803
|
+
this.logger.error(`GChat API error${context ? ` (${context})` : ""}`, {
|
|
1645
1804
|
code: gError.code,
|
|
1646
1805
|
message: gError.message,
|
|
1647
1806
|
errors: gError.errors,
|
|
1648
1807
|
error
|
|
1649
1808
|
});
|
|
1650
1809
|
if (gError.code === 429) {
|
|
1651
|
-
throw new
|
|
1652
|
-
"Google Chat rate limit exceeded",
|
|
1653
|
-
void 0,
|
|
1654
|
-
error
|
|
1655
|
-
);
|
|
1810
|
+
throw new AdapterRateLimitError("gchat");
|
|
1656
1811
|
}
|
|
1657
1812
|
throw error;
|
|
1658
1813
|
}
|