@patimweb/pi-teams 1.7.1 → 1.7.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patimweb/pi-teams",
3
- "version": "1.7.1",
3
+ "version": "1.7.2",
4
4
  "description": "Microsoft Teams integration for pi coding agent — acts fully as the signed-in user across multiple accounts and tenants: chats, channels, meetings, presence, with configurable guardrails",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -23,7 +23,7 @@ import type {
23
23
  SignedInUser,
24
24
  TeamSummary,
25
25
  } from "../types.ts";
26
- import { imageUrlsInHtml } from "../utils/attachments.ts";
26
+ import { collectStrings, imageUrlsInHtml } from "../utils/attachments.ts";
27
27
  import { htmlToText } from "../utils/formatting.ts";
28
28
 
29
29
  /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -227,9 +227,31 @@ function mapAttachments(raw: Raw): MessageAttachment[] {
227
227
  * `htmlToText` turns every `<img>` into `[image: alt]`, which is right for a
228
228
  * reader that only wants prose and wrong for one that could fetch the image:
229
229
  * the URL it drops points at Graph.
230
+ *
231
+ * A forwarded message keeps its images out of the outer body. The original
232
+ * travels as an attachment of type `forwardedMessageReference`, with the whole
233
+ * message in its `content` — as a JSON string, and nested again if that one had
234
+ * been forwarded too. Searching every string inside it finds the images without
235
+ * modelling that shape, which would only ever be one Graph change out of date.
230
236
  */
231
237
  function mapImageUrls(raw: Raw): string[] {
232
- return imageUrlsInHtml(raw.body?.content ?? "");
238
+ const candidates: string[] = [];
239
+ if (typeof raw.body?.content === "string") candidates.push(raw.body.content);
240
+
241
+ for (const attachment of raw.attachments ?? []) {
242
+ if (typeof attachment?.content !== "string") continue;
243
+ let parsed: unknown;
244
+ try {
245
+ parsed = JSON.parse(attachment.content);
246
+ } catch {
247
+ // Not JSON: search it as it stands rather than dropping it.
248
+ candidates.push(attachment.content);
249
+ continue;
250
+ }
251
+ collectStrings(parsed, candidates);
252
+ }
253
+
254
+ return imageUrlsInHtml(candidates.join("\n"));
233
255
  }
234
256
 
235
257
  function mapMentions(raw: Raw): PersonRef[] {
@@ -142,6 +142,11 @@ export async function saveMessageFiles(
142
142
  }
143
143
 
144
144
  if (!attachment.contentUrl) {
145
+ // A forwarded message is not a file being withheld: its images and files
146
+ // are hosted content of the message that carries it, and they are fetched
147
+ // above. Listing it as unsaved would put a line of noise under every
148
+ // forwarded message.
149
+ if (attachment.contentType === "forwardedMessageReference") continue;
145
150
  skipped.push({ name: label, reason: describeMissing(attachment) });
146
151
  continue;
147
152
  }
@@ -11,6 +11,44 @@
11
11
  * wrong and they can be tested without a Graph call.
12
12
  */
13
13
 
14
+ /**
15
+ * Every string inside a value, however it is nested.
16
+ *
17
+ * Exists for the JSON that Graph hands back for a forwarded message: the
18
+ * original message is in there as data of an undocumented depth, and the only
19
+ * thing needed from it is whatever strings it contains.
20
+ */
21
+ export function collectStrings(value: unknown, out: string[] = [], depth = 0): string[] {
22
+ // A guard against a pathological document, not a real limit.
23
+ if (depth > 8) return out;
24
+
25
+ if (typeof value === "string") {
26
+ out.push(value);
27
+ // A forwarded message inside a forwarded message is JSON stored as a string
28
+ // inside JSON, and its quotes stay escaped until it is parsed. Without this
29
+ // the images two levels down are simply not there to be found.
30
+ const trimmed = value.trim();
31
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
32
+ try {
33
+ collectStrings(JSON.parse(trimmed), out, depth + 1);
34
+ } catch {
35
+ /* a string that merely starts with a brace */
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+ if (Array.isArray(value)) {
41
+ for (const entry of value) collectStrings(entry, out, depth + 1);
42
+ return out;
43
+ }
44
+ if (value && typeof value === "object") {
45
+ for (const entry of Object.values(value as Record<string, unknown>)) {
46
+ collectStrings(entry, out, depth + 1);
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+
14
52
  /** Absolute URLs of the inline images in a message body, in the order shown. */
15
53
  export function imageUrlsInHtml(html: string): string[] {
16
54
  const urls: string[] = [];