@gakr-gakr/msteams 0.1.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/api.ts +3 -0
- package/autobot.plugin.json +15 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +2 -0
- package/config-api.ts +4 -0
- package/contract-api.ts +4 -0
- package/index.ts +20 -0
- package/package.json +72 -0
- package/runtime-api.ts +66 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/ai-entity.ts +7 -0
- package/src/approval-auth.ts +44 -0
- package/src/attachments/bot-framework.ts +348 -0
- package/src/attachments/download.ts +328 -0
- package/src/attachments/graph.ts +489 -0
- package/src/attachments/html.ts +122 -0
- package/src/attachments/payload.ts +14 -0
- package/src/attachments/remote-media.ts +86 -0
- package/src/attachments/shared.ts +655 -0
- package/src/attachments/types.ts +47 -0
- package/src/attachments.ts +18 -0
- package/src/channel-api.ts +1 -0
- package/src/channel.runtime.ts +56 -0
- package/src/channel.setup.ts +77 -0
- package/src/channel.ts +1176 -0
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +40 -0
- package/src/conversation-store-fs.ts +149 -0
- package/src/conversation-store-helpers.ts +105 -0
- package/src/conversation-store-memory.ts +51 -0
- package/src/conversation-store.ts +71 -0
- package/src/directory-live.ts +111 -0
- package/src/doctor.ts +27 -0
- package/src/errors.ts +270 -0
- package/src/feedback-reflection-prompt.ts +117 -0
- package/src/feedback-reflection-store.ts +113 -0
- package/src/feedback-reflection.ts +271 -0
- package/src/file-consent-helpers.ts +115 -0
- package/src/file-consent-invoke.ts +150 -0
- package/src/file-consent.ts +223 -0
- package/src/graph-chat.ts +36 -0
- package/src/graph-group-management.ts +168 -0
- package/src/graph-members.ts +48 -0
- package/src/graph-messages.ts +534 -0
- package/src/graph-teams.ts +114 -0
- package/src/graph-thread.ts +146 -0
- package/src/graph-upload.ts +531 -0
- package/src/graph-users.ts +29 -0
- package/src/graph.ts +308 -0
- package/src/inbound.ts +148 -0
- package/src/index.ts +4 -0
- package/src/media-helpers.ts +105 -0
- package/src/mentions.ts +114 -0
- package/src/messenger.ts +608 -0
- package/src/monitor-handler/access.ts +136 -0
- package/src/monitor-handler/inbound-media.ts +180 -0
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
- package/src/monitor-handler/message-handler.test-support.ts +102 -0
- package/src/monitor-handler/message-handler.ts +1015 -0
- package/src/monitor-handler/reaction-handler.ts +124 -0
- package/src/monitor-handler/thread-session.ts +30 -0
- package/src/monitor-handler.ts +538 -0
- package/src/monitor-handler.types.ts +27 -0
- package/src/monitor-types.ts +6 -0
- package/src/monitor.ts +476 -0
- package/src/oauth.flow.ts +77 -0
- package/src/oauth.shared.ts +37 -0
- package/src/oauth.token.ts +162 -0
- package/src/oauth.ts +130 -0
- package/src/outbound.ts +198 -0
- package/src/pending-uploads-fs.ts +235 -0
- package/src/pending-uploads.ts +121 -0
- package/src/policy.ts +245 -0
- package/src/polls-store-memory.ts +32 -0
- package/src/polls.ts +312 -0
- package/src/presentation.ts +93 -0
- package/src/probe.ts +132 -0
- package/src/reply-dispatcher.ts +523 -0
- package/src/reply-stream-controller.ts +334 -0
- package/src/resolve-allowlist.ts +309 -0
- package/src/revoked-context.ts +17 -0
- package/src/runtime.ts +12 -0
- package/src/sdk-types.ts +59 -0
- package/src/sdk.ts +916 -0
- package/src/secret-contract.ts +49 -0
- package/src/secret-input.ts +7 -0
- package/src/send-context.ts +269 -0
- package/src/send.ts +697 -0
- package/src/sent-message-cache.ts +174 -0
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +162 -0
- package/src/setup-surface.ts +319 -0
- package/src/sso-token-store.ts +166 -0
- package/src/sso.ts +300 -0
- package/src/storage.ts +25 -0
- package/src/store-fs.ts +42 -0
- package/src/streaming-message.ts +327 -0
- package/src/thread-parent-context.ts +159 -0
- package/src/token-response.ts +11 -0
- package/src/token.ts +194 -0
- package/src/user-agent.ts +53 -0
- package/src/webhook-timeouts.ts +27 -0
- package/src/welcome-card.ts +57 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
package/api.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "msteams",
|
|
3
|
+
"activation": {
|
|
4
|
+
"onStartup": false
|
|
5
|
+
},
|
|
6
|
+
"channels": ["msteams"],
|
|
7
|
+
"channelEnvVars": {
|
|
8
|
+
"msteams": ["MSTEAMS_APP_ID", "MSTEAMS_APP_PASSWORD", "MSTEAMS_TENANT_ID"]
|
|
9
|
+
},
|
|
10
|
+
"configSchema": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"additionalProperties": false,
|
|
13
|
+
"properties": {}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MSTeamsChannelConfigSchema } from "./src/config-schema.js";
|
package/config-api.ts
ADDED
package/contract-api.ts
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineBundledChannelEntry } from "autobot/plugin-sdk/channel-entry-contract";
|
|
2
|
+
|
|
3
|
+
export default defineBundledChannelEntry({
|
|
4
|
+
id: "msteams",
|
|
5
|
+
name: "Microsoft Teams",
|
|
6
|
+
description: "Microsoft Teams channel plugin (Bot Framework)",
|
|
7
|
+
importMetaUrl: import.meta.url,
|
|
8
|
+
plugin: {
|
|
9
|
+
specifier: "./channel-plugin-api.js",
|
|
10
|
+
exportName: "msteamsPlugin",
|
|
11
|
+
},
|
|
12
|
+
secrets: {
|
|
13
|
+
specifier: "./secret-contract-api.js",
|
|
14
|
+
exportName: "channelSecrets",
|
|
15
|
+
},
|
|
16
|
+
runtime: {
|
|
17
|
+
specifier: "./runtime-api.js",
|
|
18
|
+
exportName: "setMSTeamsRuntime",
|
|
19
|
+
},
|
|
20
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gakr-gakr/msteams",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AutoBot Microsoft Teams channel plugin",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/autobot/autobot"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@azure/identity": "4.13.1",
|
|
12
|
+
"@microsoft/teams.api": "2.0.11",
|
|
13
|
+
"@microsoft/teams.apps": "2.0.11",
|
|
14
|
+
"express": "5.2.1",
|
|
15
|
+
"jsonwebtoken": "9.0.3",
|
|
16
|
+
"jwks-rsa": "4.0.1",
|
|
17
|
+
"typebox": "1.1.38"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@gakr-gakr/plugin-sdk": "workspace:*",
|
|
21
|
+
"@gakr-gakr/autobot": "workspace:*",
|
|
22
|
+
"@types/jsonwebtoken": "9.0.10",
|
|
23
|
+
"autobot": "workspace:@gakr-gakr/autobot@*"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@gakr-gakr/autobot": ">=0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"@gakr-gakr/autobot": {
|
|
30
|
+
"optional": true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"autobot": {
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./index.ts"
|
|
36
|
+
],
|
|
37
|
+
"setupEntry": "./setup-entry.ts",
|
|
38
|
+
"channel": {
|
|
39
|
+
"id": "msteams",
|
|
40
|
+
"label": "Microsoft Teams",
|
|
41
|
+
"selectionLabel": "Microsoft Teams (Teams SDK)",
|
|
42
|
+
"docsPath": "/channels/msteams",
|
|
43
|
+
"docsLabel": "msteams",
|
|
44
|
+
"blurb": "Teams SDK; enterprise support.",
|
|
45
|
+
"aliases": [
|
|
46
|
+
"teams"
|
|
47
|
+
],
|
|
48
|
+
"order": 60,
|
|
49
|
+
"doctorCapabilities": {
|
|
50
|
+
"dmAllowFromMode": "topOnly",
|
|
51
|
+
"groupModel": "hybrid",
|
|
52
|
+
"groupAllowFromFallbackToAllowFrom": true,
|
|
53
|
+
"warnOnEmptyGroupSenderAllowlist": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"install": {
|
|
57
|
+
"npmSpec": "@gakr-gakr/msteams",
|
|
58
|
+
"defaultChoice": "npm",
|
|
59
|
+
"minHostVersion": ">=2026.4.10"
|
|
60
|
+
},
|
|
61
|
+
"compat": {
|
|
62
|
+
"pluginApi": ">=2026.5.19"
|
|
63
|
+
},
|
|
64
|
+
"build": {
|
|
65
|
+
"autobotVersion": "2026.5.19"
|
|
66
|
+
},
|
|
67
|
+
"release": {
|
|
68
|
+
"publishToClawHub": true,
|
|
69
|
+
"publishToNpm": true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/runtime-api.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Private runtime barrel for the bundled Microsoft Teams extension.
|
|
2
|
+
// Keep this barrel thin and aligned with the local extension surface.
|
|
3
|
+
|
|
4
|
+
export { DEFAULT_ACCOUNT_ID } from "autobot/plugin-sdk/account-id";
|
|
5
|
+
export type { AllowlistMatch } from "autobot/plugin-sdk/allow-from";
|
|
6
|
+
export {
|
|
7
|
+
mergeAllowlist,
|
|
8
|
+
resolveAllowlistMatchSimple,
|
|
9
|
+
summarizeMapping,
|
|
10
|
+
} from "autobot/plugin-sdk/allow-from";
|
|
11
|
+
export type {
|
|
12
|
+
BaseProbeResult,
|
|
13
|
+
ChannelDirectoryEntry,
|
|
14
|
+
ChannelGroupContext,
|
|
15
|
+
ChannelMessageActionName,
|
|
16
|
+
ChannelOutboundAdapter,
|
|
17
|
+
} from "autobot/plugin-sdk/channel-contract";
|
|
18
|
+
export type { ChannelPlugin } from "autobot/plugin-sdk/channel-core";
|
|
19
|
+
export { logTypingFailure } from "autobot/plugin-sdk/channel-logging";
|
|
20
|
+
export { createChannelPairingController } from "autobot/plugin-sdk/channel-pairing";
|
|
21
|
+
export { resolveToolsBySender } from "autobot/plugin-sdk/channel-policy";
|
|
22
|
+
export { createChannelMessageReplyPipeline } from "autobot/plugin-sdk/channel-message";
|
|
23
|
+
export {
|
|
24
|
+
PAIRING_APPROVED_MESSAGE,
|
|
25
|
+
buildProbeChannelStatusSummary,
|
|
26
|
+
createDefaultChannelRuntimeState,
|
|
27
|
+
} from "autobot/plugin-sdk/channel-status";
|
|
28
|
+
export {
|
|
29
|
+
buildChannelKeyCandidates,
|
|
30
|
+
normalizeChannelSlug,
|
|
31
|
+
resolveChannelEntryMatchWithFallback,
|
|
32
|
+
resolveNestedAllowlistDecision,
|
|
33
|
+
} from "autobot/plugin-sdk/channel-targets";
|
|
34
|
+
export type {
|
|
35
|
+
GroupPolicy,
|
|
36
|
+
GroupToolPolicyConfig,
|
|
37
|
+
MSTeamsChannelConfig,
|
|
38
|
+
MSTeamsConfig,
|
|
39
|
+
MSTeamsReplyStyle,
|
|
40
|
+
MSTeamsTeamConfig,
|
|
41
|
+
MarkdownTableMode,
|
|
42
|
+
AutoBotConfig,
|
|
43
|
+
} from "autobot/plugin-sdk/config-contracts";
|
|
44
|
+
export { isDangerousNameMatchingEnabled } from "autobot/plugin-sdk/dangerous-name-runtime";
|
|
45
|
+
export { resolveDefaultGroupPolicy } from "autobot/plugin-sdk/runtime-group-policy";
|
|
46
|
+
export { withFileLock } from "autobot/plugin-sdk/file-lock";
|
|
47
|
+
export { keepHttpServerTaskAlive } from "autobot/plugin-sdk/channel-lifecycle";
|
|
48
|
+
export {
|
|
49
|
+
detectMime,
|
|
50
|
+
extensionForMime,
|
|
51
|
+
extractOriginalFilename,
|
|
52
|
+
getFileExtension,
|
|
53
|
+
resolveChannelMediaMaxBytes,
|
|
54
|
+
} from "autobot/plugin-sdk/media-runtime";
|
|
55
|
+
export { dispatchReplyFromConfigWithSettledDispatcher } from "autobot/plugin-sdk/inbound-reply-dispatch";
|
|
56
|
+
export { loadOutboundMediaFromUrl } from "autobot/plugin-sdk/outbound-media";
|
|
57
|
+
export { buildMediaPayload } from "autobot/plugin-sdk/reply-payload";
|
|
58
|
+
export type { ReplyPayload } from "autobot/plugin-sdk/reply-payload";
|
|
59
|
+
export type { PluginRuntime } from "autobot/plugin-sdk/runtime-store";
|
|
60
|
+
export type { RuntimeEnv } from "autobot/plugin-sdk/runtime";
|
|
61
|
+
export type { SsrFPolicy } from "autobot/plugin-sdk/ssrf-runtime";
|
|
62
|
+
export { fetchWithSsrFGuard } from "autobot/plugin-sdk/ssrf-runtime";
|
|
63
|
+
export { normalizeStringEntries } from "autobot/plugin-sdk/string-normalization-runtime";
|
|
64
|
+
export { chunkTextForOutbound } from "autobot/plugin-sdk/text-chunking";
|
|
65
|
+
export { DEFAULT_WEBHOOK_MAX_BODY_BYTES } from "autobot/plugin-sdk/webhook-ingress";
|
|
66
|
+
export { setMSTeamsRuntime } from "./src/runtime.js";
|
package/setup-entry.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineBundledChannelSetupEntry } from "autobot/plugin-sdk/channel-entry-contract";
|
|
2
|
+
|
|
3
|
+
export default defineBundledChannelSetupEntry({
|
|
4
|
+
importMetaUrl: import.meta.url,
|
|
5
|
+
plugin: {
|
|
6
|
+
specifier: "./setup-plugin-api.js",
|
|
7
|
+
exportName: "msteamsSetupPlugin",
|
|
8
|
+
},
|
|
9
|
+
secrets: {
|
|
10
|
+
specifier: "./secret-contract-api.js",
|
|
11
|
+
exportName: "channelSecrets",
|
|
12
|
+
},
|
|
13
|
+
});
|
package/src/ai-entity.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createResolvedApproverActionAuthAdapter,
|
|
3
|
+
resolveApprovalApprovers,
|
|
4
|
+
} from "autobot/plugin-sdk/approval-auth-runtime";
|
|
5
|
+
import { normalizeOptionalLowercaseString } from "autobot/plugin-sdk/string-coerce-runtime";
|
|
6
|
+
import type { AutoBotConfig } from "../runtime-api.js";
|
|
7
|
+
import { normalizeMSTeamsMessagingTarget } from "./resolve-allowlist.js";
|
|
8
|
+
|
|
9
|
+
const MSTEAMS_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
|
+
|
|
11
|
+
function normalizeMSTeamsApproverId(value: string | number): string | undefined {
|
|
12
|
+
const normalized = normalizeMSTeamsMessagingTarget(String(value));
|
|
13
|
+
if (!normalized?.startsWith("user:")) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const id = normalizeOptionalLowercaseString(normalized.slice("user:".length));
|
|
17
|
+
if (!id) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return MSTEAMS_ID_RE.test(id) ? id : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveMSTeamsChannelConfig(cfg: AutoBotConfig) {
|
|
24
|
+
return cfg.channels?.msteams;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const msTeamsApprovalAuth = createResolvedApproverActionAuthAdapter({
|
|
28
|
+
channelLabel: "Microsoft Teams",
|
|
29
|
+
resolveApprovers: ({ cfg }) => {
|
|
30
|
+
const channel = resolveMSTeamsChannelConfig(cfg);
|
|
31
|
+
return resolveApprovalApprovers({
|
|
32
|
+
allowFrom: channel?.allowFrom,
|
|
33
|
+
defaultTo: channel?.defaultTo,
|
|
34
|
+
normalizeApprover: normalizeMSTeamsApproverId,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
normalizeSenderId: (value) => {
|
|
38
|
+
const trimmed = normalizeOptionalLowercaseString(value);
|
|
39
|
+
if (!trimmed) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return MSTEAMS_ID_RE.test(trimmed) ? trimmed : undefined;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { getMSTeamsRuntime } from "../runtime.js";
|
|
2
|
+
import { ensureUserAgentHeader } from "../user-agent.js";
|
|
3
|
+
import {
|
|
4
|
+
inferPlaceholder,
|
|
5
|
+
isUrlAllowed,
|
|
6
|
+
type MSTeamsAttachmentDownloadLogger,
|
|
7
|
+
type MSTeamsAttachmentFetchPolicy,
|
|
8
|
+
type MSTeamsAttachmentResolveFn,
|
|
9
|
+
resolveAttachmentFetchPolicy,
|
|
10
|
+
safeFetchWithPolicy,
|
|
11
|
+
} from "./shared.js";
|
|
12
|
+
import type {
|
|
13
|
+
MSTeamsAccessTokenProvider,
|
|
14
|
+
MSTeamsGraphMediaResult,
|
|
15
|
+
MSTeamsInboundMedia,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Bot Framework Service token scope for requesting a token used against
|
|
20
|
+
* the Bot Connector (v3) REST endpoints such as `/v3/attachments/{id}`.
|
|
21
|
+
*/
|
|
22
|
+
const BOT_FRAMEWORK_SCOPE = "https://api.botframework.com";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Detect Bot Framework personal chat ("a:") and MSA orgid ("8:orgid:") conversation
|
|
26
|
+
* IDs. These identifiers are not recognized by Graph's `/chats/{id}` endpoint, so we
|
|
27
|
+
* must fetch media via the Bot Framework v3 attachments endpoint instead.
|
|
28
|
+
*
|
|
29
|
+
* Graph-compatible IDs start with `19:` and are left untouched by this detector.
|
|
30
|
+
*/
|
|
31
|
+
export function isBotFrameworkPersonalChatId(conversationId: string | null | undefined): boolean {
|
|
32
|
+
if (typeof conversationId !== "string") {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const trimmed = conversationId.trim();
|
|
36
|
+
return trimmed.startsWith("a:") || trimmed.startsWith("8:orgid:");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type BotFrameworkView = {
|
|
40
|
+
viewId?: string | null;
|
|
41
|
+
size?: number | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type BotFrameworkAttachmentInfo = {
|
|
45
|
+
name?: string | null;
|
|
46
|
+
type?: string | null;
|
|
47
|
+
views?: BotFrameworkView[] | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function normalizeServiceUrl(serviceUrl: string): string {
|
|
51
|
+
// Bot Framework service URLs sometimes carry a trailing slash; normalize so
|
|
52
|
+
// we can safely append `/v3/attachments/...` below.
|
|
53
|
+
return serviceUrl.replace(/\/+$/, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function fetchBotFrameworkAttachmentInfo(params: {
|
|
57
|
+
serviceUrl: string;
|
|
58
|
+
attachmentId: string;
|
|
59
|
+
accessToken: string;
|
|
60
|
+
policy: MSTeamsAttachmentFetchPolicy;
|
|
61
|
+
fetchFn?: typeof fetch;
|
|
62
|
+
resolveFn?: MSTeamsAttachmentResolveFn;
|
|
63
|
+
logger?: MSTeamsAttachmentDownloadLogger;
|
|
64
|
+
}): Promise<BotFrameworkAttachmentInfo | undefined> {
|
|
65
|
+
const url = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}`;
|
|
66
|
+
// Use `safeFetchWithPolicy` instead of `fetchWithSsrFGuard`. The strict
|
|
67
|
+
// pinned undici dispatcher used by `fetchWithSsrFGuard` is incompatible
|
|
68
|
+
// with Node 24+'s built-in undici v7 and silently breaks Bot Framework
|
|
69
|
+
// attachment downloads (same root cause as the SharePoint fix in #63396).
|
|
70
|
+
// `safeFetchWithPolicy` already enforces hostname allowlist validation
|
|
71
|
+
// across every redirect hop, which is sufficient for these attachment
|
|
72
|
+
// service URLs.
|
|
73
|
+
let response: Response;
|
|
74
|
+
try {
|
|
75
|
+
response = await safeFetchWithPolicy({
|
|
76
|
+
url,
|
|
77
|
+
policy: params.policy,
|
|
78
|
+
fetchFn: params.fetchFn,
|
|
79
|
+
resolveFn: params.resolveFn,
|
|
80
|
+
requestInit: {
|
|
81
|
+
headers: ensureUserAgentHeader({ Authorization: `Bearer ${params.accessToken}` }),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
params.logger?.warn?.("msteams botFramework attachmentInfo fetch failed", {
|
|
86
|
+
error: err instanceof Error ? err.message : String(err),
|
|
87
|
+
});
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
params.logger?.warn?.("msteams botFramework attachmentInfo non-ok", {
|
|
92
|
+
status: response.status,
|
|
93
|
+
});
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
return (await response.json()) as BotFrameworkAttachmentInfo;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
params.logger?.warn?.("msteams botFramework attachmentInfo parse failed", {
|
|
100
|
+
error: err instanceof Error ? err.message : String(err),
|
|
101
|
+
});
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function saveBotFrameworkAttachmentView(params: {
|
|
107
|
+
serviceUrl: string;
|
|
108
|
+
attachmentId: string;
|
|
109
|
+
viewId: string;
|
|
110
|
+
accessToken: string;
|
|
111
|
+
maxBytes: number;
|
|
112
|
+
fileNameHint?: string;
|
|
113
|
+
contentTypeHint?: string;
|
|
114
|
+
preserveFilenames?: boolean;
|
|
115
|
+
policy: MSTeamsAttachmentFetchPolicy;
|
|
116
|
+
fetchFn?: typeof fetch;
|
|
117
|
+
resolveFn?: MSTeamsAttachmentResolveFn;
|
|
118
|
+
logger?: MSTeamsAttachmentDownloadLogger;
|
|
119
|
+
}): Promise<{ path: string; contentType?: string } | undefined> {
|
|
120
|
+
const url = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}/views/${encodeURIComponent(params.viewId)}`;
|
|
121
|
+
// See `fetchBotFrameworkAttachmentInfo` for why this uses
|
|
122
|
+
// `safeFetchWithPolicy` instead of `fetchWithSsrFGuard` on Node 24+ (#63396).
|
|
123
|
+
let response: Response;
|
|
124
|
+
try {
|
|
125
|
+
response = await safeFetchWithPolicy({
|
|
126
|
+
url,
|
|
127
|
+
policy: params.policy,
|
|
128
|
+
fetchFn: params.fetchFn,
|
|
129
|
+
resolveFn: params.resolveFn,
|
|
130
|
+
requestInit: {
|
|
131
|
+
headers: ensureUserAgentHeader({ Authorization: `Bearer ${params.accessToken}` }),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
} catch (err) {
|
|
135
|
+
params.logger?.warn?.("msteams botFramework attachmentView fetch failed", {
|
|
136
|
+
error: err instanceof Error ? err.message : String(err),
|
|
137
|
+
});
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
params.logger?.warn?.("msteams botFramework attachmentView non-ok", {
|
|
142
|
+
status: response.status,
|
|
143
|
+
});
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const contentLength = response.headers.get("content-length");
|
|
147
|
+
if (contentLength && Number(contentLength) > params.maxBytes) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
return await getMSTeamsRuntime().channel.media.saveResponseMedia(response, {
|
|
152
|
+
sourceUrl: url,
|
|
153
|
+
filePathHint: params.fileNameHint,
|
|
154
|
+
maxBytes: params.maxBytes,
|
|
155
|
+
fallbackContentType: params.contentTypeHint,
|
|
156
|
+
subdir: "inbound",
|
|
157
|
+
originalFilename: params.preserveFilenames ? params.fileNameHint : undefined,
|
|
158
|
+
});
|
|
159
|
+
} catch (err) {
|
|
160
|
+
params.logger?.warn?.("msteams botFramework attachmentView save failed", {
|
|
161
|
+
error: err instanceof Error ? err.message : String(err),
|
|
162
|
+
});
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Download media for a single attachment via the Bot Framework v3 attachments
|
|
169
|
+
* endpoint. Used for personal DM conversations where the Graph `/chats/{id}`
|
|
170
|
+
* path is not usable because the Bot Framework conversation ID (`a:...`) is
|
|
171
|
+
* not a valid Graph chat identifier.
|
|
172
|
+
*/
|
|
173
|
+
export async function downloadMSTeamsBotFrameworkAttachment(params: {
|
|
174
|
+
serviceUrl: string;
|
|
175
|
+
attachmentId: string;
|
|
176
|
+
tokenProvider?: MSTeamsAccessTokenProvider;
|
|
177
|
+
maxBytes: number;
|
|
178
|
+
allowHosts?: string[];
|
|
179
|
+
authAllowHosts?: string[];
|
|
180
|
+
fetchFn?: typeof fetch;
|
|
181
|
+
resolveFn?: MSTeamsAttachmentResolveFn;
|
|
182
|
+
fileNameHint?: string | null;
|
|
183
|
+
contentTypeHint?: string | null;
|
|
184
|
+
preserveFilenames?: boolean;
|
|
185
|
+
logger?: MSTeamsAttachmentDownloadLogger;
|
|
186
|
+
}): Promise<MSTeamsInboundMedia | undefined> {
|
|
187
|
+
if (!params.serviceUrl || !params.attachmentId || !params.tokenProvider) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
const policy: MSTeamsAttachmentFetchPolicy = resolveAttachmentFetchPolicy({
|
|
191
|
+
allowHosts: params.allowHosts,
|
|
192
|
+
authAllowHosts: params.authAllowHosts,
|
|
193
|
+
});
|
|
194
|
+
const baseUrl = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}`;
|
|
195
|
+
if (!isUrlAllowed(baseUrl, policy.allowHosts)) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let accessToken: string;
|
|
200
|
+
try {
|
|
201
|
+
accessToken = await params.tokenProvider.getAccessToken(BOT_FRAMEWORK_SCOPE);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
params.logger?.warn?.("msteams botFramework token acquisition failed", {
|
|
204
|
+
error: err instanceof Error ? err.message : String(err),
|
|
205
|
+
});
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
if (!accessToken) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const info = await fetchBotFrameworkAttachmentInfo({
|
|
213
|
+
serviceUrl: params.serviceUrl,
|
|
214
|
+
attachmentId: params.attachmentId,
|
|
215
|
+
accessToken,
|
|
216
|
+
policy,
|
|
217
|
+
fetchFn: params.fetchFn,
|
|
218
|
+
resolveFn: params.resolveFn,
|
|
219
|
+
logger: params.logger,
|
|
220
|
+
});
|
|
221
|
+
if (!info) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const views = Array.isArray(info.views) ? info.views : [];
|
|
226
|
+
// Prefer the "original" view when present, otherwise fall back to the first
|
|
227
|
+
// view the Bot Framework service returned.
|
|
228
|
+
const original = views.find((view) => view?.viewId === "original");
|
|
229
|
+
const candidateView = original ?? views.find((view) => typeof view?.viewId === "string");
|
|
230
|
+
const viewId =
|
|
231
|
+
typeof candidateView?.viewId === "string" && candidateView.viewId
|
|
232
|
+
? candidateView.viewId
|
|
233
|
+
: undefined;
|
|
234
|
+
if (!viewId) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
if (
|
|
238
|
+
typeof candidateView?.size === "number" &&
|
|
239
|
+
candidateView.size > 0 &&
|
|
240
|
+
candidateView.size > params.maxBytes
|
|
241
|
+
) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const fileNameHint =
|
|
246
|
+
(typeof params.fileNameHint === "string" && params.fileNameHint) ||
|
|
247
|
+
(typeof info.name === "string" && info.name) ||
|
|
248
|
+
undefined;
|
|
249
|
+
const contentTypeHint =
|
|
250
|
+
(typeof params.contentTypeHint === "string" && params.contentTypeHint) ||
|
|
251
|
+
(typeof info.type === "string" && info.type) ||
|
|
252
|
+
undefined;
|
|
253
|
+
|
|
254
|
+
const saved = await saveBotFrameworkAttachmentView({
|
|
255
|
+
serviceUrl: params.serviceUrl,
|
|
256
|
+
attachmentId: params.attachmentId,
|
|
257
|
+
viewId,
|
|
258
|
+
accessToken,
|
|
259
|
+
maxBytes: params.maxBytes,
|
|
260
|
+
fileNameHint,
|
|
261
|
+
contentTypeHint,
|
|
262
|
+
preserveFilenames: params.preserveFilenames,
|
|
263
|
+
policy,
|
|
264
|
+
fetchFn: params.fetchFn,
|
|
265
|
+
resolveFn: params.resolveFn,
|
|
266
|
+
logger: params.logger,
|
|
267
|
+
});
|
|
268
|
+
if (!saved) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
path: saved.path,
|
|
274
|
+
contentType: saved.contentType,
|
|
275
|
+
placeholder: inferPlaceholder({ contentType: saved.contentType, fileName: fileNameHint }),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Download media for every attachment referenced by a Bot Framework personal
|
|
281
|
+
* chat activity. Returns all successfully fetched media along with diagnostics
|
|
282
|
+
* compatible with `downloadMSTeamsGraphMedia`'s result shape so callers can
|
|
283
|
+
* reuse the existing logging path.
|
|
284
|
+
*/
|
|
285
|
+
export async function downloadMSTeamsBotFrameworkAttachments(params: {
|
|
286
|
+
serviceUrl: string;
|
|
287
|
+
attachmentIds: string[];
|
|
288
|
+
tokenProvider?: MSTeamsAccessTokenProvider;
|
|
289
|
+
maxBytes: number;
|
|
290
|
+
allowHosts?: string[];
|
|
291
|
+
authAllowHosts?: string[];
|
|
292
|
+
fetchFn?: typeof fetch;
|
|
293
|
+
resolveFn?: MSTeamsAttachmentResolveFn;
|
|
294
|
+
fileNameHint?: string | null;
|
|
295
|
+
contentTypeHint?: string | null;
|
|
296
|
+
preserveFilenames?: boolean;
|
|
297
|
+
logger?: MSTeamsAttachmentDownloadLogger;
|
|
298
|
+
}): Promise<MSTeamsGraphMediaResult> {
|
|
299
|
+
const seen = new Set<string>();
|
|
300
|
+
const unique: string[] = [];
|
|
301
|
+
for (const id of params.attachmentIds ?? []) {
|
|
302
|
+
if (typeof id !== "string") {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const trimmed = id.trim();
|
|
306
|
+
if (!trimmed || seen.has(trimmed)) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
seen.add(trimmed);
|
|
310
|
+
unique.push(trimmed);
|
|
311
|
+
}
|
|
312
|
+
if (unique.length === 0 || !params.serviceUrl || !params.tokenProvider) {
|
|
313
|
+
return { media: [], attachmentCount: unique.length };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const media: MSTeamsInboundMedia[] = [];
|
|
317
|
+
for (const attachmentId of unique) {
|
|
318
|
+
try {
|
|
319
|
+
const item = await downloadMSTeamsBotFrameworkAttachment({
|
|
320
|
+
serviceUrl: params.serviceUrl,
|
|
321
|
+
attachmentId,
|
|
322
|
+
tokenProvider: params.tokenProvider,
|
|
323
|
+
maxBytes: params.maxBytes,
|
|
324
|
+
allowHosts: params.allowHosts,
|
|
325
|
+
authAllowHosts: params.authAllowHosts,
|
|
326
|
+
fetchFn: params.fetchFn,
|
|
327
|
+
resolveFn: params.resolveFn,
|
|
328
|
+
fileNameHint: params.fileNameHint,
|
|
329
|
+
contentTypeHint: params.contentTypeHint,
|
|
330
|
+
preserveFilenames: params.preserveFilenames,
|
|
331
|
+
logger: params.logger,
|
|
332
|
+
});
|
|
333
|
+
if (item) {
|
|
334
|
+
media.push(item);
|
|
335
|
+
}
|
|
336
|
+
} catch (err) {
|
|
337
|
+
params.logger?.warn?.("msteams botFramework attachment download failed", {
|
|
338
|
+
error: err instanceof Error ? err.message : String(err),
|
|
339
|
+
attachmentId,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
media,
|
|
346
|
+
attachmentCount: unique.length,
|
|
347
|
+
};
|
|
348
|
+
}
|