@openclaw/msteams 2026.1.29 → 2026.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/index.ts +0 -1
- package/openclaw.plugin.json +1 -3
- package/package.json +13 -10
- package/src/attachments/download.ts +98 -21
- package/src/attachments/graph.ts +50 -16
- package/src/attachments/html.ts +23 -9
- package/src/attachments/shared.ts +74 -18
- package/src/attachments.test.ts +37 -2
- package/src/channel.directory.test.ts +7 -5
- package/src/channel.ts +46 -23
- package/src/conversation-store-fs.test.ts +7 -8
- package/src/conversation-store-fs.ts +15 -5
- package/src/conversation-store-memory.ts +3 -1
- package/src/directory-live.ts +41 -15
- package/src/errors.test.ts +0 -1
- package/src/errors.ts +48 -16
- package/src/file-consent-helpers.test.ts +12 -3
- package/src/file-consent.ts +6 -2
- package/src/graph-chat.ts +5 -4
- package/src/graph-upload.ts +23 -15
- package/src/inbound.test.ts +0 -1
- package/src/inbound.ts +15 -5
- package/src/media-helpers.test.ts +9 -6
- package/src/media-helpers.ts +15 -6
- package/src/messenger.test.ts +7 -4
- package/src/messenger.ts +55 -20
- package/src/monitor-handler/inbound-media.ts +7 -2
- package/src/monitor-handler/message-handler.ts +66 -55
- package/src/monitor-handler.ts +3 -7
- package/src/monitor.ts +19 -14
- package/src/onboarding.ts +10 -11
- package/src/outbound.ts +0 -1
- package/src/pending-uploads.ts +7 -5
- package/src/policy.test.ts +1 -2
- package/src/policy.ts +39 -13
- package/src/polls-store-memory.ts +3 -1
- package/src/polls-store.test.ts +1 -3
- package/src/polls.test.ts +5 -6
- package/src/polls.ts +24 -9
- package/src/probe.test.ts +4 -3
- package/src/probe.ts +18 -10
- package/src/reply-dispatcher.ts +5 -3
- package/src/resolve-allowlist.ts +39 -19
- package/src/send-context.ts +12 -4
- package/src/send.ts +49 -19
- package/src/sent-message-cache.test.ts +0 -1
- package/src/sent-message-cache.ts +9 -3
- package/src/storage.ts +6 -3
- package/src/store-fs.ts +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,48 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.2.1
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Version alignment with core OpenClaw release numbers.
|
|
8
|
+
|
|
9
|
+
## 2026.1.31
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment with core OpenClaw release numbers.
|
|
14
|
+
|
|
15
|
+
## 2026.1.30
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
- Version alignment with core OpenClaw release numbers.
|
|
20
|
+
|
|
3
21
|
## 2026.1.29
|
|
4
22
|
|
|
5
23
|
### Changes
|
|
24
|
+
|
|
6
25
|
- Version alignment with core OpenClaw release numbers.
|
|
7
26
|
|
|
8
27
|
## 2026.1.23
|
|
9
28
|
|
|
10
29
|
### Changes
|
|
30
|
+
|
|
11
31
|
- Version alignment with core OpenClaw release numbers.
|
|
12
32
|
|
|
13
33
|
## 2026.1.22
|
|
14
34
|
|
|
15
35
|
### Changes
|
|
36
|
+
|
|
16
37
|
- Version alignment with core OpenClaw release numbers.
|
|
17
38
|
|
|
18
39
|
## 2026.1.21
|
|
19
40
|
|
|
20
41
|
### Changes
|
|
42
|
+
|
|
21
43
|
- Version alignment with core OpenClaw release numbers.
|
|
22
44
|
|
|
23
45
|
## 2026.1.20
|
|
24
46
|
|
|
25
47
|
### Changes
|
|
48
|
+
|
|
26
49
|
- Version alignment with core OpenClaw release numbers.
|
|
27
50
|
|
|
28
51
|
## 2026.1.17-1
|
|
29
52
|
|
|
30
53
|
### Changes
|
|
54
|
+
|
|
31
55
|
- Version alignment with core OpenClaw release numbers.
|
|
32
56
|
|
|
33
57
|
## 2026.1.17
|
|
34
58
|
|
|
35
59
|
### Changes
|
|
60
|
+
|
|
36
61
|
- Version alignment with core OpenClaw release numbers.
|
|
37
62
|
|
|
38
63
|
## 2026.1.16
|
|
39
64
|
|
|
40
65
|
### Changes
|
|
66
|
+
|
|
41
67
|
- Version alignment with core OpenClaw release numbers.
|
|
42
68
|
|
|
43
69
|
## 2026.1.15
|
|
44
70
|
|
|
45
71
|
### Features
|
|
72
|
+
|
|
46
73
|
- Bot Framework gateway monitor (Express + JWT auth) with configurable webhook path/port and `/api/messages` fallback.
|
|
47
74
|
- Onboarding flow for Azure Bot credentials (config + env var detection) and DM policy setup.
|
|
48
75
|
- Channel capabilities: DMs, group chats, channels, threads, media, polls, and `teams` alias.
|
package/index.ts
CHANGED
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/msteams",
|
|
3
|
-
"version": "2026.1
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "2026.2.1",
|
|
5
4
|
"description": "OpenClaw Microsoft Teams channel plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@microsoft/agents-hosting": "^1.2.3",
|
|
8
|
+
"@microsoft/agents-hosting-express": "^1.2.3",
|
|
9
|
+
"@microsoft/agents-hosting-extensions-teams": "^1.2.3",
|
|
10
|
+
"express": "^5.2.1",
|
|
11
|
+
"openclaw": "workspace:*",
|
|
12
|
+
"proper-lockfile": "^4.1.2"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"openclaw": "workspace:*"
|
|
16
|
+
},
|
|
6
17
|
"openclaw": {
|
|
7
18
|
"extensions": [
|
|
8
19
|
"./index.ts"
|
|
@@ -24,13 +35,5 @@
|
|
|
24
35
|
"localPath": "extensions/msteams",
|
|
25
36
|
"defaultChoice": "npm"
|
|
26
37
|
}
|
|
27
|
-
},
|
|
28
|
-
"dependencies": {
|
|
29
|
-
"@microsoft/agents-hosting": "^1.2.2",
|
|
30
|
-
"@microsoft/agents-hosting-express": "^1.2.2",
|
|
31
|
-
"@microsoft/agents-hosting-extensions-teams": "^1.2.2",
|
|
32
|
-
"openclaw": "workspace:*",
|
|
33
|
-
"express": "^5.2.1",
|
|
34
|
-
"proper-lockfile": "^4.1.2"
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MSTeamsAccessTokenProvider,
|
|
3
|
+
MSTeamsAttachmentLike,
|
|
4
|
+
MSTeamsInboundMedia,
|
|
5
|
+
} from "./types.js";
|
|
1
6
|
import { getMSTeamsRuntime } from "../runtime.js";
|
|
2
7
|
import {
|
|
3
8
|
extractInlineImageCandidates,
|
|
@@ -6,13 +11,9 @@ import {
|
|
|
6
11
|
isRecord,
|
|
7
12
|
isUrlAllowed,
|
|
8
13
|
normalizeContentType,
|
|
14
|
+
resolveAuthAllowedHosts,
|
|
9
15
|
resolveAllowedHosts,
|
|
10
16
|
} from "./shared.js";
|
|
11
|
-
import type {
|
|
12
|
-
MSTeamsAccessTokenProvider,
|
|
13
|
-
MSTeamsAttachmentLike,
|
|
14
|
-
MSTeamsInboundMedia,
|
|
15
|
-
} from "./types.js";
|
|
16
17
|
|
|
17
18
|
type DownloadCandidate = {
|
|
18
19
|
url: string;
|
|
@@ -26,10 +27,14 @@ function resolveDownloadCandidate(att: MSTeamsAttachmentLike): DownloadCandidate
|
|
|
26
27
|
const name = typeof att.name === "string" ? att.name.trim() : "";
|
|
27
28
|
|
|
28
29
|
if (contentType === "application/vnd.microsoft.teams.file.download.info") {
|
|
29
|
-
if (!isRecord(att.content))
|
|
30
|
+
if (!isRecord(att.content)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
30
33
|
const downloadUrl =
|
|
31
34
|
typeof att.content.downloadUrl === "string" ? att.content.downloadUrl.trim() : "";
|
|
32
|
-
if (!downloadUrl)
|
|
35
|
+
if (!downloadUrl) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
33
38
|
|
|
34
39
|
const fileType = typeof att.content.fileType === "string" ? att.content.fileType.trim() : "";
|
|
35
40
|
const uniqueId = typeof att.content.uniqueId === "string" ? att.content.uniqueId.trim() : "";
|
|
@@ -49,7 +54,9 @@ function resolveDownloadCandidate(att: MSTeamsAttachmentLike): DownloadCandidate
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
const contentUrl = typeof att.contentUrl === "string" ? att.contentUrl.trim() : "";
|
|
52
|
-
if (!contentUrl)
|
|
57
|
+
if (!contentUrl) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
53
60
|
|
|
54
61
|
return {
|
|
55
62
|
url: contentUrl,
|
|
@@ -79,12 +86,23 @@ async function fetchWithAuthFallback(params: {
|
|
|
79
86
|
url: string;
|
|
80
87
|
tokenProvider?: MSTeamsAccessTokenProvider;
|
|
81
88
|
fetchFn?: typeof fetch;
|
|
89
|
+
allowHosts: string[];
|
|
90
|
+
authAllowHosts: string[];
|
|
82
91
|
}): Promise<Response> {
|
|
83
92
|
const fetchFn = params.fetchFn ?? fetch;
|
|
84
93
|
const firstAttempt = await fetchFn(params.url);
|
|
85
|
-
if (firstAttempt.ok)
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
if (firstAttempt.ok) {
|
|
95
|
+
return firstAttempt;
|
|
96
|
+
}
|
|
97
|
+
if (!params.tokenProvider) {
|
|
98
|
+
return firstAttempt;
|
|
99
|
+
}
|
|
100
|
+
if (firstAttempt.status !== 401 && firstAttempt.status !== 403) {
|
|
101
|
+
return firstAttempt;
|
|
102
|
+
}
|
|
103
|
+
if (!isUrlAllowed(params.url, params.authAllowHosts)) {
|
|
104
|
+
return firstAttempt;
|
|
105
|
+
}
|
|
88
106
|
|
|
89
107
|
const scopes = scopeCandidatesForUrl(params.url);
|
|
90
108
|
for (const scope of scopes) {
|
|
@@ -92,8 +110,30 @@ async function fetchWithAuthFallback(params: {
|
|
|
92
110
|
const token = await params.tokenProvider.getAccessToken(scope);
|
|
93
111
|
const res = await fetchFn(params.url, {
|
|
94
112
|
headers: { Authorization: `Bearer ${token}` },
|
|
113
|
+
redirect: "manual",
|
|
95
114
|
});
|
|
96
|
-
if (res.ok)
|
|
115
|
+
if (res.ok) {
|
|
116
|
+
return res;
|
|
117
|
+
}
|
|
118
|
+
const redirectUrl = readRedirectUrl(params.url, res);
|
|
119
|
+
if (redirectUrl && isUrlAllowed(redirectUrl, params.allowHosts)) {
|
|
120
|
+
const redirectRes = await fetchFn(redirectUrl);
|
|
121
|
+
if (redirectRes.ok) {
|
|
122
|
+
return redirectRes;
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
(redirectRes.status === 401 || redirectRes.status === 403) &&
|
|
126
|
+
isUrlAllowed(redirectUrl, params.authAllowHosts)
|
|
127
|
+
) {
|
|
128
|
+
const redirectAuthRes = await fetchFn(redirectUrl, {
|
|
129
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
130
|
+
redirect: "manual",
|
|
131
|
+
});
|
|
132
|
+
if (redirectAuthRes.ok) {
|
|
133
|
+
return redirectAuthRes;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
97
137
|
} catch {
|
|
98
138
|
// Try the next scope.
|
|
99
139
|
}
|
|
@@ -102,6 +142,21 @@ async function fetchWithAuthFallback(params: {
|
|
|
102
142
|
return firstAttempt;
|
|
103
143
|
}
|
|
104
144
|
|
|
145
|
+
function readRedirectUrl(baseUrl: string, res: Response): string | null {
|
|
146
|
+
if (![301, 302, 303, 307, 308].includes(res.status)) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const location = res.headers.get("location");
|
|
150
|
+
if (!location) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
return new URL(location, baseUrl).toString();
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
105
160
|
/**
|
|
106
161
|
* Download all file attachments from a Teams message (images, documents, etc.).
|
|
107
162
|
* Renamed from downloadMSTeamsImageAttachments to support all file types.
|
|
@@ -111,13 +166,17 @@ export async function downloadMSTeamsAttachments(params: {
|
|
|
111
166
|
maxBytes: number;
|
|
112
167
|
tokenProvider?: MSTeamsAccessTokenProvider;
|
|
113
168
|
allowHosts?: string[];
|
|
169
|
+
authAllowHosts?: string[];
|
|
114
170
|
fetchFn?: typeof fetch;
|
|
115
171
|
/** When true, embeds original filename in stored path for later extraction. */
|
|
116
172
|
preserveFilenames?: boolean;
|
|
117
173
|
}): Promise<MSTeamsInboundMedia[]> {
|
|
118
174
|
const list = Array.isArray(params.attachments) ? params.attachments : [];
|
|
119
|
-
if (list.length === 0)
|
|
175
|
+
if (list.length === 0) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
120
178
|
const allowHosts = resolveAllowedHosts(params.allowHosts);
|
|
179
|
+
const authAllowHosts = resolveAuthAllowedHosts(params.authAllowHosts);
|
|
121
180
|
|
|
122
181
|
// Download ANY downloadable attachment (not just images)
|
|
123
182
|
const downloadable = list.filter(isDownloadableAttachment);
|
|
@@ -130,8 +189,12 @@ export async function downloadMSTeamsAttachments(params: {
|
|
|
130
189
|
const seenUrls = new Set<string>();
|
|
131
190
|
for (const inline of inlineCandidates) {
|
|
132
191
|
if (inline.kind === "url") {
|
|
133
|
-
if (!isUrlAllowed(inline.url, allowHosts))
|
|
134
|
-
|
|
192
|
+
if (!isUrlAllowed(inline.url, allowHosts)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (seenUrls.has(inline.url)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
135
198
|
seenUrls.add(inline.url);
|
|
136
199
|
candidates.push({
|
|
137
200
|
url: inline.url,
|
|
@@ -141,12 +204,18 @@ export async function downloadMSTeamsAttachments(params: {
|
|
|
141
204
|
});
|
|
142
205
|
}
|
|
143
206
|
}
|
|
144
|
-
if (candidates.length === 0 && inlineCandidates.length === 0)
|
|
207
|
+
if (candidates.length === 0 && inlineCandidates.length === 0) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
145
210
|
|
|
146
211
|
const out: MSTeamsInboundMedia[] = [];
|
|
147
212
|
for (const inline of inlineCandidates) {
|
|
148
|
-
if (inline.kind !== "data")
|
|
149
|
-
|
|
213
|
+
if (inline.kind !== "data") {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (inline.data.byteLength > params.maxBytes) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
150
219
|
try {
|
|
151
220
|
// Data inline candidates (base64 data URLs) don't have original filenames
|
|
152
221
|
const saved = await getMSTeamsRuntime().channel.media.saveMediaBuffer(
|
|
@@ -165,16 +234,24 @@ export async function downloadMSTeamsAttachments(params: {
|
|
|
165
234
|
}
|
|
166
235
|
}
|
|
167
236
|
for (const candidate of candidates) {
|
|
168
|
-
if (!isUrlAllowed(candidate.url, allowHosts))
|
|
237
|
+
if (!isUrlAllowed(candidate.url, allowHosts)) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
169
240
|
try {
|
|
170
241
|
const res = await fetchWithAuthFallback({
|
|
171
242
|
url: candidate.url,
|
|
172
243
|
tokenProvider: params.tokenProvider,
|
|
173
244
|
fetchFn: params.fetchFn,
|
|
245
|
+
allowHosts,
|
|
246
|
+
authAllowHosts,
|
|
174
247
|
});
|
|
175
|
-
if (!res.ok)
|
|
248
|
+
if (!res.ok) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
176
251
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
177
|
-
if (buffer.byteLength > params.maxBytes)
|
|
252
|
+
if (buffer.byteLength > params.maxBytes) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
178
255
|
const mime = await getMSTeamsRuntime().media.detectMime({
|
|
179
256
|
buffer,
|
|
180
257
|
headerMime: res.headers.get("content-type"),
|
package/src/attachments/graph.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { getMSTeamsRuntime } from "../runtime.js";
|
|
2
|
-
import { downloadMSTeamsAttachments } from "./download.js";
|
|
3
|
-
import { GRAPH_ROOT, inferPlaceholder, isRecord, normalizeContentType, resolveAllowedHosts } from "./shared.js";
|
|
4
1
|
import type {
|
|
5
2
|
MSTeamsAccessTokenProvider,
|
|
6
3
|
MSTeamsAttachmentLike,
|
|
7
4
|
MSTeamsGraphMediaResult,
|
|
8
5
|
MSTeamsInboundMedia,
|
|
9
6
|
} from "./types.js";
|
|
7
|
+
import { getMSTeamsRuntime } from "../runtime.js";
|
|
8
|
+
import { downloadMSTeamsAttachments } from "./download.js";
|
|
9
|
+
import {
|
|
10
|
+
GRAPH_ROOT,
|
|
11
|
+
inferPlaceholder,
|
|
12
|
+
isRecord,
|
|
13
|
+
normalizeContentType,
|
|
14
|
+
resolveAllowedHosts,
|
|
15
|
+
} from "./shared.js";
|
|
10
16
|
|
|
11
17
|
type GraphHostedContent = {
|
|
12
18
|
id?: string | null;
|
|
@@ -26,7 +32,9 @@ type GraphAttachment = {
|
|
|
26
32
|
function readNestedString(value: unknown, keys: Array<string | number>): string | undefined {
|
|
27
33
|
let current: unknown = value;
|
|
28
34
|
for (const key of keys) {
|
|
29
|
-
if (!isRecord(current))
|
|
35
|
+
if (!isRecord(current)) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
30
38
|
current = current[key as keyof typeof current];
|
|
31
39
|
}
|
|
32
40
|
return typeof current === "string" && current.trim() ? current.trim() : undefined;
|
|
@@ -44,7 +52,9 @@ export function buildMSTeamsGraphMessageUrls(params: {
|
|
|
44
52
|
const messageIdCandidates = new Set<string>();
|
|
45
53
|
const pushCandidate = (value: string | null | undefined) => {
|
|
46
54
|
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
47
|
-
if (trimmed)
|
|
55
|
+
if (trimmed) {
|
|
56
|
+
messageIdCandidates.add(trimmed);
|
|
57
|
+
}
|
|
48
58
|
};
|
|
49
59
|
|
|
50
60
|
pushCandidate(params.messageId);
|
|
@@ -62,17 +72,23 @@ export function buildMSTeamsGraphMessageUrls(params: {
|
|
|
62
72
|
readNestedString(params.channelData, ["channel", "id"]) ??
|
|
63
73
|
readNestedString(params.channelData, ["channelId"]) ??
|
|
64
74
|
readNestedString(params.channelData, ["teamsChannelId"]);
|
|
65
|
-
if (!teamId || !channelId)
|
|
75
|
+
if (!teamId || !channelId) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
66
78
|
const urls: string[] = [];
|
|
67
79
|
if (replyToId) {
|
|
68
80
|
for (const candidate of messageIdCandidates) {
|
|
69
|
-
if (candidate === replyToId)
|
|
81
|
+
if (candidate === replyToId) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
70
84
|
urls.push(
|
|
71
85
|
`${GRAPH_ROOT}/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(replyToId)}/replies/${encodeURIComponent(candidate)}`,
|
|
72
86
|
);
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
|
-
if (messageIdCandidates.size === 0 && replyToId)
|
|
89
|
+
if (messageIdCandidates.size === 0 && replyToId) {
|
|
90
|
+
messageIdCandidates.add(replyToId);
|
|
91
|
+
}
|
|
76
92
|
for (const candidate of messageIdCandidates) {
|
|
77
93
|
urls.push(
|
|
78
94
|
`${GRAPH_ROOT}/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(candidate)}`,
|
|
@@ -82,8 +98,12 @@ export function buildMSTeamsGraphMessageUrls(params: {
|
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
const chatId = params.conversationId?.trim() || readNestedString(params.channelData, ["chatId"]);
|
|
85
|
-
if (!chatId)
|
|
86
|
-
|
|
101
|
+
if (!chatId) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
if (messageIdCandidates.size === 0 && replyToId) {
|
|
105
|
+
messageIdCandidates.add(replyToId);
|
|
106
|
+
}
|
|
87
107
|
const urls = Array.from(messageIdCandidates).map(
|
|
88
108
|
(candidate) =>
|
|
89
109
|
`${GRAPH_ROOT}/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(candidate)}`,
|
|
@@ -101,7 +121,9 @@ async function fetchGraphCollection<T>(params: {
|
|
|
101
121
|
headers: { Authorization: `Bearer ${params.accessToken}` },
|
|
102
122
|
});
|
|
103
123
|
const status = res.status;
|
|
104
|
-
if (!res.ok)
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
return { status, items: [] };
|
|
126
|
+
}
|
|
105
127
|
try {
|
|
106
128
|
const data = (await res.json()) as { value?: T[] };
|
|
107
129
|
return { status, items: Array.isArray(data.value) ? data.value : [] };
|
|
@@ -151,14 +173,18 @@ async function downloadGraphHostedContent(params: {
|
|
|
151
173
|
const out: MSTeamsInboundMedia[] = [];
|
|
152
174
|
for (const item of hosted.items) {
|
|
153
175
|
const contentBytes = typeof item.contentBytes === "string" ? item.contentBytes : "";
|
|
154
|
-
if (!contentBytes)
|
|
176
|
+
if (!contentBytes) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
155
179
|
let buffer: Buffer;
|
|
156
180
|
try {
|
|
157
181
|
buffer = Buffer.from(contentBytes, "base64");
|
|
158
182
|
} catch {
|
|
159
183
|
continue;
|
|
160
184
|
}
|
|
161
|
-
if (buffer.byteLength > params.maxBytes)
|
|
185
|
+
if (buffer.byteLength > params.maxBytes) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
162
188
|
const mime = await getMSTeamsRuntime().media.detectMime({
|
|
163
189
|
buffer,
|
|
164
190
|
headerMime: item.contentType ?? undefined,
|
|
@@ -189,11 +215,14 @@ export async function downloadMSTeamsGraphMedia(params: {
|
|
|
189
215
|
tokenProvider?: MSTeamsAccessTokenProvider;
|
|
190
216
|
maxBytes: number;
|
|
191
217
|
allowHosts?: string[];
|
|
218
|
+
authAllowHosts?: string[];
|
|
192
219
|
fetchFn?: typeof fetch;
|
|
193
220
|
/** When true, embeds original filename in stored path for later extraction. */
|
|
194
221
|
preserveFilenames?: boolean;
|
|
195
222
|
}): Promise<MSTeamsGraphMediaResult> {
|
|
196
|
-
if (!params.messageUrl || !params.tokenProvider)
|
|
223
|
+
if (!params.messageUrl || !params.tokenProvider) {
|
|
224
|
+
return { media: [] };
|
|
225
|
+
}
|
|
197
226
|
const allowHosts = resolveAllowedHosts(params.allowHosts);
|
|
198
227
|
const messageUrl = params.messageUrl;
|
|
199
228
|
let accessToken: string;
|
|
@@ -293,9 +322,13 @@ export async function downloadMSTeamsGraphMedia(params: {
|
|
|
293
322
|
sharePointMedia.length > 0
|
|
294
323
|
? normalizedAttachments.filter((att) => {
|
|
295
324
|
const contentType = att.contentType?.toLowerCase();
|
|
296
|
-
if (contentType !== "reference")
|
|
325
|
+
if (contentType !== "reference") {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
297
328
|
const url = typeof att.contentUrl === "string" ? att.contentUrl : "";
|
|
298
|
-
if (!url)
|
|
329
|
+
if (!url) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
299
332
|
return !downloadedReferenceUrls.has(url);
|
|
300
333
|
})
|
|
301
334
|
: normalizedAttachments;
|
|
@@ -304,6 +337,7 @@ export async function downloadMSTeamsGraphMedia(params: {
|
|
|
304
337
|
maxBytes: params.maxBytes,
|
|
305
338
|
tokenProvider: params.tokenProvider,
|
|
306
339
|
allowHosts,
|
|
340
|
+
authAllowHosts: params.authAllowHosts,
|
|
307
341
|
fetchFn: params.fetchFn,
|
|
308
342
|
preserveFilenames: params.preserveFilenames,
|
|
309
343
|
});
|
package/src/attachments/html.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MSTeamsAttachmentLike, MSTeamsHtmlAttachmentSummary } from "./types.js";
|
|
1
2
|
import {
|
|
2
3
|
ATTACHMENT_TAG_RE,
|
|
3
4
|
extractHtmlFromAttachment,
|
|
@@ -6,13 +7,14 @@ import {
|
|
|
6
7
|
isLikelyImageAttachment,
|
|
7
8
|
safeHostForUrl,
|
|
8
9
|
} from "./shared.js";
|
|
9
|
-
import type { MSTeamsAttachmentLike, MSTeamsHtmlAttachmentSummary } from "./types.js";
|
|
10
10
|
|
|
11
11
|
export function summarizeMSTeamsHtmlAttachments(
|
|
12
12
|
attachments: MSTeamsAttachmentLike[] | undefined,
|
|
13
13
|
): MSTeamsHtmlAttachmentSummary | undefined {
|
|
14
14
|
const list = Array.isArray(attachments) ? attachments : [];
|
|
15
|
-
if (list.length === 0)
|
|
15
|
+
if (list.length === 0) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
16
18
|
let htmlAttachments = 0;
|
|
17
19
|
let imgTags = 0;
|
|
18
20
|
let dataImages = 0;
|
|
@@ -23,7 +25,9 @@ export function summarizeMSTeamsHtmlAttachments(
|
|
|
23
25
|
|
|
24
26
|
for (const att of list) {
|
|
25
27
|
const html = extractHtmlFromAttachment(att);
|
|
26
|
-
if (!html)
|
|
28
|
+
if (!html) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
27
31
|
htmlAttachments += 1;
|
|
28
32
|
IMG_SRC_RE.lastIndex = 0;
|
|
29
33
|
let match: RegExpExecArray | null = IMG_SRC_RE.exec(html);
|
|
@@ -31,9 +35,13 @@ export function summarizeMSTeamsHtmlAttachments(
|
|
|
31
35
|
imgTags += 1;
|
|
32
36
|
const src = match[1]?.trim();
|
|
33
37
|
if (src) {
|
|
34
|
-
if (src.startsWith("data:"))
|
|
35
|
-
|
|
36
|
-
else
|
|
38
|
+
if (src.startsWith("data:")) {
|
|
39
|
+
dataImages += 1;
|
|
40
|
+
} else if (src.startsWith("cid:")) {
|
|
41
|
+
cidImages += 1;
|
|
42
|
+
} else {
|
|
43
|
+
srcHosts.add(safeHostForUrl(src));
|
|
44
|
+
}
|
|
37
45
|
}
|
|
38
46
|
match = IMG_SRC_RE.exec(html);
|
|
39
47
|
}
|
|
@@ -43,12 +51,16 @@ export function summarizeMSTeamsHtmlAttachments(
|
|
|
43
51
|
while (attachmentMatch) {
|
|
44
52
|
attachmentTags += 1;
|
|
45
53
|
const id = attachmentMatch[1]?.trim();
|
|
46
|
-
if (id)
|
|
54
|
+
if (id) {
|
|
55
|
+
attachmentIds.add(id);
|
|
56
|
+
}
|
|
47
57
|
attachmentMatch = ATTACHMENT_TAG_RE.exec(html);
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
|
|
51
|
-
if (htmlAttachments === 0)
|
|
61
|
+
if (htmlAttachments === 0) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
52
64
|
return {
|
|
53
65
|
htmlAttachments,
|
|
54
66
|
imgTags,
|
|
@@ -64,7 +76,9 @@ export function buildMSTeamsAttachmentPlaceholder(
|
|
|
64
76
|
attachments: MSTeamsAttachmentLike[] | undefined,
|
|
65
77
|
): string {
|
|
66
78
|
const list = Array.isArray(attachments) ? attachments : [];
|
|
67
|
-
if (list.length === 0)
|
|
79
|
+
if (list.length === 0) {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
68
82
|
const imageCount = list.filter(isLikelyImageAttachment).length;
|
|
69
83
|
const inlineCount = extractInlineImageCandidates(list).length;
|
|
70
84
|
const totalImages = imageCount + inlineCount;
|