@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/dist/index.js CHANGED
@@ -1,19 +1,21 @@
1
1
  // src/index.ts
2
2
  import {
3
- convertEmojiPlaceholders as convertEmojiPlaceholders2,
4
- defaultEmojiResolver,
5
- isCardElement,
6
- RateLimitError
7
- } from "chat";
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
- convertEmojiPlaceholders
13
- } from "chat";
14
- function convertEmoji(text) {
15
- return convertEmojiPlaceholders(text, "gchat");
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
- const parts = [];
158
- if (card.title) {
159
- parts.push(`*${convertEmoji(card.title)}*`);
160
- }
161
- if (card.subtitle) {
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
- const parts = [];
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
- switch (node.type) {
214
- case "paragraph":
215
- return node.children.map((child) => this.nodeToGChat(child)).join("");
216
- case "text": {
217
- return node.value;
218
- }
219
- case "strong":
220
- return `*${node.children.map((child) => this.nodeToGChat(child)).join("")}*`;
221
- case "emphasis":
222
- return `_${node.children.map((child) => this.nodeToGChat(child)).join("")}_`;
223
- case "delete":
224
- return `~${node.children.map((child) => this.nodeToGChat(child)).join("")}~`;
225
- case "inlineCode":
226
- return `\`${node.value}\``;
227
- case "code": {
228
- const codeNode = node;
229
- return `\`\`\`
230
- ${codeNode.value}
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
- case "link": {
234
- const linkNode = node;
235
- const linkText = linkNode.children.map((child) => this.nodeToGChat(child)).join("");
236
- if (linkText === linkNode.url) {
237
- return linkNode.url;
238
- }
239
- return `${linkText} (${linkNode.url})`;
240
- }
241
- case "blockquote":
242
- return node.children.map((child) => `> ${this.nodeToGChat(child)}`).join("\n");
243
- case "list":
244
- return node.children.map((item, i) => {
245
- const prefix = node.ordered ? `${i + 1}.` : "\u2022";
246
- const content = item.children.map((child) => this.nodeToGChat(child)).join("");
247
- return `${prefix} ${content}`;
248
- }).join("\n");
249
- case "listItem":
250
- return node.children.map((child) => this.nodeToGChat(child)).join("");
251
- case "break":
252
- return "\n";
253
- case "thematicBreak":
254
- return "---";
255
- default:
256
- if ("children" in node && Array.isArray(node.children)) {
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 = null;
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 Error(
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.logger = chat.getLogger(this.name);
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?.debug("Restored bot user ID from state", {
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?.info("onThreadSubscribe called", {
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?.warn(
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?.info("ensureSpaceSubscription called", {
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?.warn("ensureSpaceSubscription skipped - missing config", {
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?.debug("Space subscription still valid", {
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?.debug("Space subscription expiring soon, will refresh", {
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?.debug("Subscription creation already in progress", {
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?.info("createSpaceSubscriptionWithCache", {
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?.error(
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?.debug("Found existing subscription", {
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?.info("Creating Workspace Events subscription", {
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?.info("Workspace Events subscription created", {
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?.error("Failed to create Workspace Events subscription", {
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?.debug("Error checking existing subscriptions", { error });
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?.debug("Auto-detected endpoint URL", {
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?.debug("GChat webhook raw body", { body });
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?.debug("Bot added to space", {
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?.debug("Bot removed from space", {
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?.debug("message event", {
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?.debug("Non-message event received", {
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?.debug("Skipping unsupported Pub/Sub event", { eventType });
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?.debug("Pub/Sub notification decoded", {
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?.error("Error processing Pub/Sub message", { error });
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?.debug("Subscription refresh failed", { error: err });
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?.debug("Fetched thread context for reaction", {
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?.warn("Failed to fetch message for thread context", {
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 Error("PubSub notification missing message");
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.resolveUserDisplayName(
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?.debug("Pub/Sub parsed message", {
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?.warn("Chat instance not initialized, ignoring card click");
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?.debug("Card click missing actionId", {
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?.warn("Card click missing space info");
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?.debug("Processing GChat card click", {
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?.warn("Chat instance not initialized, ignoring event");
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?.debug("Ignoring event without messagePayload");
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 Error("Event has no message payload");
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.cacheUserInfo(userId, displayName, message.sender?.email).catch(
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 = this.extractFiles(message);
1197
+ const files = extractFiles(message);
1105
1198
  if (files.length > 0) {
1106
- this.logger?.warn(
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 = this.extractCard(message);
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?.debug("GChat API: spaces.messages.create (card)", {
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?.debug("GChat API: spaces.messages.create response", {
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 = convertEmojiPlaceholders2(
1234
+ const text = convertEmojiPlaceholders(
1142
1235
  this.formatConverter.renderPostable(message),
1143
1236
  "gchat"
1144
1237
  );
1145
- this.logger?.debug("GChat API: spaces.messages.create", {
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?.debug("GChat API: spaces.messages.create response", {
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 Error("Cannot fetch file: no auth client configured");
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 Error("Failed to get access token");
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 Error(
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 = this.extractCard(message);
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?.debug("GChat API: spaces.messages.update (card)", {
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?.debug("GChat API: spaces.messages.update response", {
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 = convertEmojiPlaceholders2(
1345
+ const text = convertEmojiPlaceholders(
1267
1346
  this.formatConverter.renderPostable(message),
1268
1347
  "gchat"
1269
1348
  );
1270
- this.logger?.debug("GChat API: spaces.messages.update", {
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?.debug("GChat API: spaces.messages.update response", {
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?.debug("GChat API: spaces.messages.delete", { messageId });
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?.debug("GChat API: spaces.messages.delete response", {
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?.debug("GChat API: spaces.messages.reactions.create", {
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?.debug(
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?.debug("GChat API: spaces.messages.reactions.list", {
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?.debug("GChat API: spaces.messages.reactions.list response", {
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?.debug("Reaction not found to remove", {
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?.debug("GChat API: spaces.messages.reactions.delete", {
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?.debug(
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?.debug("GChat API: spaces.findDirectMessage", { userId });
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?.debug("GChat API: Found existing DM space", {
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?.debug("GChat API: findDirectMessage failed", { error });
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?.warn(
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?.debug("GChat API: spaces.setup (DM)", {
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 Error("Failed to create DM - no space name returned");
1507
+ throw new NetworkError(
1508
+ "gchat",
1509
+ "Failed to create DM - no space name returned"
1510
+ );
1429
1511
  }
1430
- this.logger?.debug("GChat API: spaces.setup response", { spaceName });
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
- this.logger?.debug("GChat API: spaces.messages.list", {
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
- pageSize: options.limit || 100,
1443
- impersonated: !!this.impersonatedChatApi
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: options.limit || 100,
1448
- pageToken: options.before
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 messages = response.data.messages || [];
1451
- this.logger?.debug("GChat API: spaces.messages.list response", {
1452
- messageCount: messages.length
1453
- });
1454
- return messages.map((msg) => {
1455
- const msgThreadId = this.encodeThreadId({
1456
- spaceName,
1457
- threadName: msg.thread?.name ?? void 0
1458
- });
1459
- const msgIsBot = msg.sender?.type === "BOT";
1460
- return {
1461
- id: msg.name || "",
1462
- threadId: msgThreadId,
1463
- text: this.formatConverter.extractPlainText(msg.text || ""),
1464
- formatted: this.formatConverter.toAst(msg.text || ""),
1465
- raw: msg,
1466
- author: {
1467
- userId: msg.sender?.name || "unknown",
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?.debug("GChat API: spaces.get", { spaceName });
1693
+ this.logger.debug("GChat API: spaces.get", { spaceName });
1488
1694
  const response = await this.chatApi.spaces.get({ name: spaceName });
1489
- this.logger?.debug("GChat API: spaces.get response", {
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
- const threadPart = platformData.threadName ? `:${Buffer.from(platformData.threadName).toString("base64url")}` : "";
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.endsWith(":dm");
1717
+ return isDMThread(threadId);
1516
1718
  }
1517
1719
  decodeThreadId(threadId) {
1518
- const isDM = threadId.endsWith(":dm");
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 Error("Cannot parse non-message event");
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?.info("Learned bot user ID from mention", {
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?.debug("Failed to persist botUserId", { error: err })
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?.debug("Normalized bot mention", {
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?.debug(
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?.error(`GChat API error${context ? ` (${context})` : ""}`, {
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 RateLimitError(
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
  }