@kodelyth/msteams 2026.5.39 → 2026.5.42
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/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/dist/api.js +3 -0
- package/dist/channel-BvTXHuGs.js +1161 -0
- package/dist/channel-config-api.js +2 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-NssGKZm5.js +650 -0
- package/dist/config-schema-Btk-XCOd.js +43 -0
- package/dist/contract-api.js +2 -0
- package/dist/graph-users-D-gKCguI.js +1411 -0
- package/dist/index.js +22 -0
- package/dist/oauth-BUxlphX3.js +114 -0
- package/dist/oauth.token-ebId9946.js +116 -0
- package/dist/probe-Cj2KsAGF.js +2190 -0
- package/dist/runtime-api-BL4DOWXD.js +28 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-Bo7kdUrT.js +35 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +64 -0
- package/dist/setup-surface-COTQDcTQ.js +531 -0
- package/dist/src-tvpsGYPV.js +4226 -0
- package/dist/test-api.js +2 -0
- package/index.ts +20 -0
- package/klaw.plugin.json +2 -726
- package/package.json +4 -4
- 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.test.ts +506 -0
- package/src/attachments/bot-framework.ts +348 -0
- package/src/attachments/download.ts +328 -0
- package/src/attachments/graph.test.ts +441 -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.test.ts +187 -0
- package/src/attachments/remote-media.ts +86 -0
- package/src/attachments/shared.test.ts +547 -0
- package/src/attachments/shared.ts +655 -0
- package/src/attachments/types.ts +47 -0
- package/src/attachments.graph.test.ts +414 -0
- package/src/attachments.helpers.test.ts +245 -0
- package/src/attachments.test-helpers.ts +17 -0
- package/src/attachments.test.ts +754 -0
- package/src/attachments.ts +18 -0
- package/src/block-streaming-config.test.ts +61 -0
- package/src/channel-api.ts +1 -0
- package/src/channel.actions.test.ts +797 -0
- package/src/channel.directory.test.ts +176 -0
- package/src/channel.message-adapter.test.ts +227 -0
- package/src/channel.runtime.ts +56 -0
- package/src/channel.setup.ts +77 -0
- package/src/channel.test.ts +136 -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.test.ts +81 -0
- package/src/conversation-store-fs.ts +149 -0
- package/src/conversation-store-helpers.test.ts +202 -0
- package/src/conversation-store-helpers.ts +105 -0
- package/src/conversation-store-memory.ts +51 -0
- package/src/conversation-store.shared.test.ts +260 -0
- package/src/conversation-store.ts +71 -0
- package/src/directory-live.test.ts +156 -0
- package/src/directory-live.ts +111 -0
- package/src/doctor.ts +27 -0
- package/src/errors.test.ts +154 -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.test.ts +237 -0
- package/src/feedback-reflection.ts +268 -0
- package/src/file-consent-helpers.test.ts +328 -0
- package/src/file-consent-helpers.ts +115 -0
- package/src/file-consent-invoke.ts +150 -0
- package/src/file-consent.test.ts +378 -0
- package/src/file-consent.ts +223 -0
- package/src/graph-chat.ts +36 -0
- package/src/graph-group-management.test.ts +332 -0
- package/src/graph-group-management.ts +168 -0
- package/src/graph-members.test.ts +89 -0
- package/src/graph-members.ts +48 -0
- package/src/graph-messages.actions.test.ts +253 -0
- package/src/graph-messages.read.test.ts +391 -0
- package/src/graph-messages.search.test.ts +227 -0
- package/src/graph-messages.test-helpers.ts +50 -0
- package/src/graph-messages.ts +534 -0
- package/src/graph-teams.test.ts +222 -0
- package/src/graph-teams.ts +114 -0
- package/src/graph-thread.test.ts +252 -0
- package/src/graph-thread.ts +146 -0
- package/src/graph-upload.test.ts +253 -0
- package/src/graph-upload.ts +531 -0
- package/src/graph-users.ts +29 -0
- package/src/graph.test.ts +540 -0
- package/src/graph.ts +308 -0
- package/src/inbound.test.ts +221 -0
- package/src/inbound.ts +148 -0
- package/src/index.ts +4 -0
- package/src/media-helpers.test.ts +220 -0
- package/src/media-helpers.ts +105 -0
- package/src/mentions.test.ts +254 -0
- package/src/mentions.ts +114 -0
- package/src/messenger.test.ts +961 -0
- package/src/messenger.ts +608 -0
- package/src/monitor-handler/access.ts +136 -0
- package/src/monitor-handler/inbound-media.test.ts +314 -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.authz.test.ts +739 -0
- package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
- package/src/monitor-handler/message-handler.test-support.ts +99 -0
- package/src/monitor-handler/message-handler.thread-parent.test.ts +225 -0
- package/src/monitor-handler/message-handler.thread-session.test.ts +132 -0
- package/src/monitor-handler/message-handler.ts +1003 -0
- package/src/monitor-handler/reaction-handler.test.ts +325 -0
- package/src/monitor-handler/reaction-handler.ts +122 -0
- package/src/monitor-handler/thread-session.ts +30 -0
- package/src/monitor-handler.adaptive-card.test.ts +158 -0
- package/src/monitor-handler.feedback-authz.test.ts +357 -0
- package/src/monitor-handler.file-consent.test.ts +443 -0
- package/src/monitor-handler.sso.test.ts +576 -0
- package/src/monitor-handler.test-helpers.ts +181 -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.lifecycle.test.ts +457 -0
- package/src/monitor.test.ts +119 -0
- package/src/monitor.ts +476 -0
- package/src/oauth.flow.ts +77 -0
- package/src/oauth.shared.ts +37 -0
- package/src/oauth.test.ts +350 -0
- package/src/oauth.token.ts +162 -0
- package/src/oauth.ts +130 -0
- package/src/outbound.test.ts +400 -0
- package/src/outbound.ts +198 -0
- package/src/pending-uploads-fs.test.ts +261 -0
- package/src/pending-uploads-fs.ts +235 -0
- package/src/pending-uploads.test.ts +186 -0
- package/src/pending-uploads.ts +121 -0
- package/src/policy.test.ts +156 -0
- package/src/policy.ts +245 -0
- package/src/polls-store-memory.ts +32 -0
- package/src/polls.test.ts +169 -0
- package/src/polls.ts +312 -0
- package/src/presentation.ts +93 -0
- package/src/probe.test.ts +79 -0
- package/src/probe.ts +132 -0
- package/src/reply-dispatcher.test.ts +543 -0
- package/src/reply-dispatcher.ts +523 -0
- package/src/reply-stream-controller.test.ts +424 -0
- package/src/reply-stream-controller.ts +334 -0
- package/src/resolve-allowlist.test.ts +253 -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.test.ts +727 -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.test.ts +93 -0
- package/src/send-context.ts +269 -0
- package/src/send.test.ts +588 -0
- package/src/send.ts +697 -0
- package/src/sent-message-cache.test.ts +106 -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.test.ts +175 -0
- package/src/setup-surface.ts +319 -0
- package/src/sso-token-store.test.ts +74 -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.test.ts +323 -0
- package/src/streaming-message.ts +327 -0
- package/src/test-runtime.ts +16 -0
- package/src/thread-parent-context.test.ts +224 -0
- package/src/thread-parent-context.ts +159 -0
- package/src/token-response.ts +11 -0
- package/src/token.test.ts +268 -0
- package/src/token.ts +194 -0
- package/src/user-agent.test.ts +121 -0
- package/src/user-agent.ts +53 -0
- package/src/webhook-timeouts.ts +27 -0
- package/src/welcome-card.test.ts +104 -0
- package/src/welcome-card.ts +57 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/api.js +0 -7
- package/channel-config-api.js +0 -7
- package/channel-plugin-api.js +0 -7
- package/contract-api.js +0 -7
- package/index.js +0 -7
- package/runtime-api.js +0 -7
- package/secret-contract-api.js +0 -7
- package/setup-entry.js +0 -7
- package/setup-plugin-api.js +0 -7
- package/test-api.js +0 -7
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { KlawConfig } from "../runtime-api.js";
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({
|
|
5
|
+
sendAdaptiveCardMSTeams: vi.fn(),
|
|
6
|
+
sendMessageMSTeams: vi.fn(),
|
|
7
|
+
sendPollMSTeams: vi.fn(),
|
|
8
|
+
createPoll: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("./send.js", () => ({
|
|
12
|
+
sendAdaptiveCardMSTeams: mocks.sendAdaptiveCardMSTeams,
|
|
13
|
+
sendMessageMSTeams: mocks.sendMessageMSTeams,
|
|
14
|
+
sendPollMSTeams: mocks.sendPollMSTeams,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("./polls.js", () => ({
|
|
18
|
+
createMSTeamsPollStoreFs: () => ({
|
|
19
|
+
createPoll: mocks.createPoll,
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import { msteamsOutbound } from "./outbound.js";
|
|
24
|
+
|
|
25
|
+
const cfg = {
|
|
26
|
+
channels: {
|
|
27
|
+
msteams: {
|
|
28
|
+
appId: "resolved-app-id",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
} as KlawConfig;
|
|
32
|
+
|
|
33
|
+
type MSTeamsSendText = NonNullable<typeof msteamsOutbound.sendText>;
|
|
34
|
+
type MSTeamsSendMedia = NonNullable<typeof msteamsOutbound.sendMedia>;
|
|
35
|
+
type MSTeamsSendPayload = NonNullable<typeof msteamsOutbound.sendPayload>;
|
|
36
|
+
type MSTeamsSendPoll = NonNullable<typeof msteamsOutbound.sendPoll>;
|
|
37
|
+
type MSTeamsRenderPresentation = NonNullable<typeof msteamsOutbound.renderPresentation>;
|
|
38
|
+
|
|
39
|
+
function requireSendText(): MSTeamsSendText {
|
|
40
|
+
const sendText = msteamsOutbound.sendText;
|
|
41
|
+
if (!sendText) {
|
|
42
|
+
throw new Error("Expected msteams outbound sendText");
|
|
43
|
+
}
|
|
44
|
+
return sendText;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function requireSendMedia(): MSTeamsSendMedia {
|
|
48
|
+
const sendMedia = msteamsOutbound.sendMedia;
|
|
49
|
+
if (!sendMedia) {
|
|
50
|
+
throw new Error("Expected msteams outbound sendMedia");
|
|
51
|
+
}
|
|
52
|
+
return sendMedia;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function requireSendPayload(): MSTeamsSendPayload {
|
|
56
|
+
const sendPayload = msteamsOutbound.sendPayload;
|
|
57
|
+
if (!sendPayload) {
|
|
58
|
+
throw new Error("Expected msteams outbound sendPayload");
|
|
59
|
+
}
|
|
60
|
+
return sendPayload;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function requireSendPoll(): MSTeamsSendPoll {
|
|
64
|
+
const sendPoll = msteamsOutbound.sendPoll;
|
|
65
|
+
if (!sendPoll) {
|
|
66
|
+
throw new Error("Expected msteams outbound sendPoll");
|
|
67
|
+
}
|
|
68
|
+
return sendPoll;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function requireRenderPresentation(): MSTeamsRenderPresentation {
|
|
72
|
+
const renderPresentation = msteamsOutbound.renderPresentation;
|
|
73
|
+
if (!renderPresentation) {
|
|
74
|
+
throw new Error("Expected msteams outbound renderPresentation");
|
|
75
|
+
}
|
|
76
|
+
return renderPresentation;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type PollRecord = Record<string, unknown> & { createdAt: string };
|
|
80
|
+
|
|
81
|
+
function firstPollRecord(): PollRecord {
|
|
82
|
+
const [call] = mocks.createPoll.mock.calls;
|
|
83
|
+
if (!call) {
|
|
84
|
+
throw new Error("expected createPoll call");
|
|
85
|
+
}
|
|
86
|
+
const [pollRecord] = call;
|
|
87
|
+
if (!pollRecord || typeof pollRecord !== "object" || Array.isArray(pollRecord)) {
|
|
88
|
+
throw new Error("expected createPoll record");
|
|
89
|
+
}
|
|
90
|
+
if (typeof (pollRecord as { createdAt?: unknown }).createdAt !== "string") {
|
|
91
|
+
throw new Error("expected createPoll record timestamp");
|
|
92
|
+
}
|
|
93
|
+
return pollRecord as PollRecord;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("msteamsOutbound cfg threading", () => {
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
mocks.sendMessageMSTeams.mockReset();
|
|
99
|
+
mocks.sendAdaptiveCardMSTeams.mockReset();
|
|
100
|
+
mocks.sendPollMSTeams.mockReset();
|
|
101
|
+
mocks.createPoll.mockReset();
|
|
102
|
+
mocks.sendMessageMSTeams.mockResolvedValue({
|
|
103
|
+
messageId: "msg-1",
|
|
104
|
+
conversationId: "conv-1",
|
|
105
|
+
});
|
|
106
|
+
mocks.sendPollMSTeams.mockResolvedValue({
|
|
107
|
+
pollId: "poll-1",
|
|
108
|
+
messageId: "msg-poll-1",
|
|
109
|
+
conversationId: "conv-1",
|
|
110
|
+
});
|
|
111
|
+
mocks.sendAdaptiveCardMSTeams.mockResolvedValue({
|
|
112
|
+
messageId: "msg-card-1",
|
|
113
|
+
conversationId: "conv-card-1",
|
|
114
|
+
});
|
|
115
|
+
mocks.createPoll.mockResolvedValue(undefined);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("advertises durable payload delivery for presentation cards", () => {
|
|
119
|
+
expect(msteamsOutbound.deliveryCapabilities?.durableFinal).toMatchObject({
|
|
120
|
+
text: true,
|
|
121
|
+
media: true,
|
|
122
|
+
payload: true,
|
|
123
|
+
messageSendingHooks: true,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("passes resolved cfg to sendMessageMSTeams for text sends", async () => {
|
|
128
|
+
const cfg = {
|
|
129
|
+
channels: {
|
|
130
|
+
msteams: {
|
|
131
|
+
appId: "resolved-app-id",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
} as KlawConfig;
|
|
135
|
+
|
|
136
|
+
await requireSendText()({
|
|
137
|
+
cfg,
|
|
138
|
+
to: "conversation:abc",
|
|
139
|
+
text: "hello",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(mocks.sendMessageMSTeams).toHaveBeenCalledWith({
|
|
143
|
+
cfg,
|
|
144
|
+
to: "conversation:abc",
|
|
145
|
+
text: "hello",
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("passes resolved cfg and media roots for media sends", async () => {
|
|
150
|
+
const cfg = {
|
|
151
|
+
channels: {
|
|
152
|
+
msteams: {
|
|
153
|
+
appId: "resolved-app-id",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
} as KlawConfig;
|
|
157
|
+
|
|
158
|
+
await requireSendMedia()({
|
|
159
|
+
cfg,
|
|
160
|
+
to: "conversation:abc",
|
|
161
|
+
text: "photo",
|
|
162
|
+
mediaUrl: "file:///tmp/photo.png",
|
|
163
|
+
mediaLocalRoots: ["/tmp"],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(mocks.sendMessageMSTeams).toHaveBeenCalledWith({
|
|
167
|
+
cfg,
|
|
168
|
+
to: "conversation:abc",
|
|
169
|
+
text: "photo",
|
|
170
|
+
mediaUrl: "file:///tmp/photo.png",
|
|
171
|
+
mediaLocalRoots: ["/tmp"],
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("renders and sends presentation payloads as Adaptive Cards", async () => {
|
|
176
|
+
const presentation = {
|
|
177
|
+
title: "Deploy",
|
|
178
|
+
blocks: [
|
|
179
|
+
{ type: "text" as const, text: "Finished" },
|
|
180
|
+
{
|
|
181
|
+
type: "buttons" as const,
|
|
182
|
+
buttons: [{ label: "Open", value: "open" }],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
const payload = {
|
|
187
|
+
text: "Deploy finished",
|
|
188
|
+
presentation,
|
|
189
|
+
};
|
|
190
|
+
const rendered = await requireRenderPresentation()({
|
|
191
|
+
payload,
|
|
192
|
+
presentation,
|
|
193
|
+
ctx: {
|
|
194
|
+
cfg,
|
|
195
|
+
to: "conversation:abc",
|
|
196
|
+
text: "Deploy finished",
|
|
197
|
+
payload,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(rendered?.presentation).toBe(presentation);
|
|
202
|
+
expect(rendered?.channelData?.msteams).toEqual({
|
|
203
|
+
presentationCard: {
|
|
204
|
+
type: "AdaptiveCard",
|
|
205
|
+
version: "1.4",
|
|
206
|
+
body: [
|
|
207
|
+
{ type: "TextBlock", text: "Deploy finished", wrap: true },
|
|
208
|
+
{ type: "TextBlock", text: "Deploy", weight: "Bolder", size: "Medium", wrap: true },
|
|
209
|
+
{ type: "TextBlock", text: "Finished", wrap: true },
|
|
210
|
+
],
|
|
211
|
+
actions: [{ type: "Action.Submit", title: "Open", data: { value: "open", label: "Open" } }],
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const result = await requireSendPayload()({
|
|
216
|
+
cfg,
|
|
217
|
+
to: "conversation:abc",
|
|
218
|
+
text: "Deploy finished",
|
|
219
|
+
payload: rendered!,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(mocks.sendAdaptiveCardMSTeams).toHaveBeenCalledWith({
|
|
223
|
+
cfg,
|
|
224
|
+
to: "conversation:abc",
|
|
225
|
+
card: (rendered?.channelData?.msteams as { presentationCard: unknown }).presentationCard,
|
|
226
|
+
});
|
|
227
|
+
expect(result).toEqual({
|
|
228
|
+
channel: "msteams",
|
|
229
|
+
messageId: "msg-card-1",
|
|
230
|
+
conversationId: "conv-card-1",
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("falls back to text/media delivery when payload rendering did not produce a card", async () => {
|
|
235
|
+
const result = await requireSendPayload()({
|
|
236
|
+
cfg,
|
|
237
|
+
to: "conversation:abc",
|
|
238
|
+
text: "hello",
|
|
239
|
+
payload: {
|
|
240
|
+
text: "hello",
|
|
241
|
+
channelData: { msteams: { traceId: "trace-1" } },
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(mocks.sendMessageMSTeams).toHaveBeenCalledWith({
|
|
246
|
+
cfg,
|
|
247
|
+
to: "conversation:abc",
|
|
248
|
+
text: "hello",
|
|
249
|
+
});
|
|
250
|
+
expect(result).toEqual({
|
|
251
|
+
channel: "msteams",
|
|
252
|
+
messageId: "msg-1",
|
|
253
|
+
conversationId: "conv-1",
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("chunks text fallback payloads that only carry channel metadata", async () => {
|
|
258
|
+
mocks.sendMessageMSTeams
|
|
259
|
+
.mockResolvedValueOnce({ messageId: "msg-text-1", conversationId: "conv-text" })
|
|
260
|
+
.mockResolvedValueOnce({ messageId: "msg-text-2", conversationId: "conv-text" });
|
|
261
|
+
const text = "x".repeat(4001);
|
|
262
|
+
|
|
263
|
+
const result = await requireSendPayload()({
|
|
264
|
+
cfg,
|
|
265
|
+
to: "conversation:abc",
|
|
266
|
+
text,
|
|
267
|
+
payload: {
|
|
268
|
+
text,
|
|
269
|
+
channelData: { msteams: { traceId: "trace-1" } },
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(mocks.sendMessageMSTeams).toHaveBeenNthCalledWith(1, {
|
|
274
|
+
cfg,
|
|
275
|
+
to: "conversation:abc",
|
|
276
|
+
text: "x".repeat(4000),
|
|
277
|
+
});
|
|
278
|
+
expect(mocks.sendMessageMSTeams).toHaveBeenNthCalledWith(2, {
|
|
279
|
+
cfg,
|
|
280
|
+
to: "conversation:abc",
|
|
281
|
+
text: "x",
|
|
282
|
+
});
|
|
283
|
+
expect(result).toEqual({
|
|
284
|
+
channel: "msteams",
|
|
285
|
+
messageId: "msg-text-2",
|
|
286
|
+
conversationId: "conv-text",
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("keeps multi-media payloads on the media fallback path", async () => {
|
|
291
|
+
mocks.sendMessageMSTeams
|
|
292
|
+
.mockResolvedValueOnce({ messageId: "msg-media-1", conversationId: "conv-media" })
|
|
293
|
+
.mockResolvedValueOnce({ messageId: "msg-media-2", conversationId: "conv-media" });
|
|
294
|
+
|
|
295
|
+
const result = await requireSendPayload()({
|
|
296
|
+
cfg,
|
|
297
|
+
to: "conversation:abc",
|
|
298
|
+
text: "album",
|
|
299
|
+
payload: {
|
|
300
|
+
text: "album",
|
|
301
|
+
mediaUrls: ["file:///tmp/one.png", "file:///tmp/two.png"],
|
|
302
|
+
channelData: { msteams: { traceId: "trace-1" } },
|
|
303
|
+
},
|
|
304
|
+
mediaLocalRoots: ["/tmp"],
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(mocks.sendMessageMSTeams).toHaveBeenNthCalledWith(1, {
|
|
308
|
+
cfg,
|
|
309
|
+
to: "conversation:abc",
|
|
310
|
+
text: "album",
|
|
311
|
+
mediaUrl: "file:///tmp/one.png",
|
|
312
|
+
mediaLocalRoots: ["/tmp"],
|
|
313
|
+
mediaReadFile: undefined,
|
|
314
|
+
});
|
|
315
|
+
expect(mocks.sendMessageMSTeams).toHaveBeenNthCalledWith(2, {
|
|
316
|
+
cfg,
|
|
317
|
+
to: "conversation:abc",
|
|
318
|
+
text: "",
|
|
319
|
+
mediaUrl: "file:///tmp/two.png",
|
|
320
|
+
mediaLocalRoots: ["/tmp"],
|
|
321
|
+
mediaReadFile: undefined,
|
|
322
|
+
});
|
|
323
|
+
expect(result).toEqual({
|
|
324
|
+
channel: "msteams",
|
|
325
|
+
messageId: "msg-media-2",
|
|
326
|
+
conversationId: "conv-media",
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("lets media payloads use text fallback instead of card rendering", async () => {
|
|
331
|
+
const payload = {
|
|
332
|
+
text: "photo",
|
|
333
|
+
mediaUrl: "file:///tmp/photo.png",
|
|
334
|
+
presentation: {
|
|
335
|
+
blocks: [{ type: "buttons" as const, buttons: [{ label: "Open", value: "open" }] }],
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
const rendered = await requireRenderPresentation()({
|
|
339
|
+
payload,
|
|
340
|
+
presentation: payload.presentation,
|
|
341
|
+
ctx: {
|
|
342
|
+
cfg,
|
|
343
|
+
to: "conversation:abc",
|
|
344
|
+
text: "photo",
|
|
345
|
+
mediaUrl: "file:///tmp/photo.png",
|
|
346
|
+
payload,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(rendered).toBeNull();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("passes resolved cfg to sendPollMSTeams and stores poll metadata", async () => {
|
|
354
|
+
const cfg = {
|
|
355
|
+
channels: {
|
|
356
|
+
msteams: {
|
|
357
|
+
appId: "resolved-app-id",
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
} as KlawConfig;
|
|
361
|
+
|
|
362
|
+
await requireSendPoll()({
|
|
363
|
+
cfg,
|
|
364
|
+
to: "conversation:abc",
|
|
365
|
+
poll: {
|
|
366
|
+
question: "Snack?",
|
|
367
|
+
options: ["Pizza", "Sushi"],
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(mocks.sendPollMSTeams).toHaveBeenCalledWith({
|
|
372
|
+
cfg,
|
|
373
|
+
to: "conversation:abc",
|
|
374
|
+
question: "Snack?",
|
|
375
|
+
options: ["Pizza", "Sushi"],
|
|
376
|
+
maxSelections: 1,
|
|
377
|
+
});
|
|
378
|
+
const pollRecord = firstPollRecord();
|
|
379
|
+
expect(pollRecord).toEqual({
|
|
380
|
+
id: "poll-1",
|
|
381
|
+
question: "Snack?",
|
|
382
|
+
options: ["Pizza", "Sushi"],
|
|
383
|
+
maxSelections: 1,
|
|
384
|
+
createdAt: pollRecord?.createdAt,
|
|
385
|
+
conversationId: "conv-1",
|
|
386
|
+
messageId: "msg-poll-1",
|
|
387
|
+
votes: {},
|
|
388
|
+
});
|
|
389
|
+
expect(Number.isNaN(Date.parse(pollRecord?.createdAt))).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("chunks outbound text without requiring MSTeams runtime initialization", () => {
|
|
393
|
+
const chunker = msteamsOutbound.chunker;
|
|
394
|
+
if (!chunker) {
|
|
395
|
+
throw new Error("msteams outbound.chunker unavailable");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
expect(chunker("alpha beta", 5)).toEqual(["alpha", "beta"]);
|
|
399
|
+
});
|
|
400
|
+
});
|
package/src/outbound.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachChannelToResult,
|
|
3
|
+
createAttachedChannelResultAdapter,
|
|
4
|
+
} from "klaw/plugin-sdk/channel-send-result";
|
|
5
|
+
import { resolveOutboundSendDep } from "klaw/plugin-sdk/outbound-send-deps";
|
|
6
|
+
import {
|
|
7
|
+
resolvePayloadMediaUrls,
|
|
8
|
+
resolveTextChunksWithFallback,
|
|
9
|
+
sendPayloadMediaSequence,
|
|
10
|
+
} from "klaw/plugin-sdk/reply-payload";
|
|
11
|
+
import { chunkTextForOutbound, type ChannelOutboundAdapter } from "../runtime-api.js";
|
|
12
|
+
import { createMSTeamsPollStoreFs } from "./polls.js";
|
|
13
|
+
import { buildMSTeamsPresentationCard, MSTEAMS_PRESENTATION_CAPABILITIES } from "./presentation.js";
|
|
14
|
+
import { sendAdaptiveCardMSTeams, sendMessageMSTeams, sendPollMSTeams } from "./send.js";
|
|
15
|
+
|
|
16
|
+
function asObjectRecord(value: unknown): Record<string, unknown> | undefined {
|
|
17
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
18
|
+
? (value as Record<string, unknown>)
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MSTEAMS_TEXT_CHUNK_LIMIT = 4000;
|
|
23
|
+
|
|
24
|
+
export const msteamsOutbound: ChannelOutboundAdapter = {
|
|
25
|
+
deliveryMode: "direct",
|
|
26
|
+
chunker: chunkTextForOutbound,
|
|
27
|
+
chunkerMode: "markdown",
|
|
28
|
+
textChunkLimit: MSTEAMS_TEXT_CHUNK_LIMIT,
|
|
29
|
+
pollMaxOptions: 12,
|
|
30
|
+
deliveryCapabilities: {
|
|
31
|
+
durableFinal: {
|
|
32
|
+
text: true,
|
|
33
|
+
media: true,
|
|
34
|
+
payload: true,
|
|
35
|
+
messageSendingHooks: true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
presentationCapabilities: MSTEAMS_PRESENTATION_CAPABILITIES,
|
|
39
|
+
renderPresentation: ({ payload, presentation }) => {
|
|
40
|
+
if (payload.mediaUrl || payload.mediaUrls?.length) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const card = buildMSTeamsPresentationCard({
|
|
44
|
+
presentation,
|
|
45
|
+
text: payload.text,
|
|
46
|
+
});
|
|
47
|
+
const msteamsData = asObjectRecord(payload.channelData?.msteams) ?? {};
|
|
48
|
+
return {
|
|
49
|
+
...payload,
|
|
50
|
+
channelData: {
|
|
51
|
+
...payload.channelData,
|
|
52
|
+
msteams: {
|
|
53
|
+
...msteamsData,
|
|
54
|
+
presentationCard: card,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
sendPayload: async ({
|
|
60
|
+
cfg,
|
|
61
|
+
to,
|
|
62
|
+
text,
|
|
63
|
+
mediaUrl,
|
|
64
|
+
mediaLocalRoots,
|
|
65
|
+
mediaReadFile,
|
|
66
|
+
payload,
|
|
67
|
+
deps,
|
|
68
|
+
}) => {
|
|
69
|
+
const msteamsData = asObjectRecord(payload.channelData?.msteams);
|
|
70
|
+
const presentationCard = msteamsData?.presentationCard;
|
|
71
|
+
if (
|
|
72
|
+
presentationCard &&
|
|
73
|
+
typeof presentationCard === "object" &&
|
|
74
|
+
!Array.isArray(presentationCard)
|
|
75
|
+
) {
|
|
76
|
+
const result = await sendAdaptiveCardMSTeams({
|
|
77
|
+
cfg,
|
|
78
|
+
to,
|
|
79
|
+
card: presentationCard as Record<string, unknown>,
|
|
80
|
+
});
|
|
81
|
+
return attachChannelToResult("msteams", result);
|
|
82
|
+
}
|
|
83
|
+
const mediaUrls = resolvePayloadMediaUrls({
|
|
84
|
+
...payload,
|
|
85
|
+
mediaUrl: payload.mediaUrl ?? mediaUrl,
|
|
86
|
+
})
|
|
87
|
+
.map((url) => url.trim())
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
if (mediaUrls.length > 0) {
|
|
90
|
+
type SendFn = (
|
|
91
|
+
to: string,
|
|
92
|
+
text: string,
|
|
93
|
+
opts?: {
|
|
94
|
+
mediaUrl?: string;
|
|
95
|
+
mediaLocalRoots?: readonly string[];
|
|
96
|
+
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
|
97
|
+
},
|
|
98
|
+
) => Promise<{ messageId: string; conversationId: string }>;
|
|
99
|
+
const send =
|
|
100
|
+
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
|
|
101
|
+
((to, text, opts) =>
|
|
102
|
+
sendMessageMSTeams({
|
|
103
|
+
cfg,
|
|
104
|
+
to,
|
|
105
|
+
text,
|
|
106
|
+
mediaUrl: opts?.mediaUrl,
|
|
107
|
+
mediaLocalRoots: opts?.mediaLocalRoots,
|
|
108
|
+
mediaReadFile: opts?.mediaReadFile,
|
|
109
|
+
}));
|
|
110
|
+
const result = await sendPayloadMediaSequence({
|
|
111
|
+
text,
|
|
112
|
+
mediaUrls,
|
|
113
|
+
send: async ({ text, mediaUrl }) =>
|
|
114
|
+
await send(to, text, { mediaUrl, mediaLocalRoots, mediaReadFile }),
|
|
115
|
+
});
|
|
116
|
+
if (result) {
|
|
117
|
+
return attachChannelToResult("msteams", result);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (text.trim()) {
|
|
121
|
+
type SendFn = (
|
|
122
|
+
to: string,
|
|
123
|
+
text: string,
|
|
124
|
+
) => Promise<{ messageId: string; conversationId: string }>;
|
|
125
|
+
const send =
|
|
126
|
+
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
|
|
127
|
+
((to, text) => sendMessageMSTeams({ cfg, to, text }));
|
|
128
|
+
const chunks = resolveTextChunksWithFallback(
|
|
129
|
+
text,
|
|
130
|
+
chunkTextForOutbound(text, MSTEAMS_TEXT_CHUNK_LIMIT),
|
|
131
|
+
);
|
|
132
|
+
let result: Awaited<ReturnType<SendFn>>;
|
|
133
|
+
for (const chunk of chunks) {
|
|
134
|
+
result = await send(to, chunk);
|
|
135
|
+
}
|
|
136
|
+
return attachChannelToResult("msteams", result!);
|
|
137
|
+
}
|
|
138
|
+
throw new Error("MS Teams payload send requires text, media, or a presentation card.");
|
|
139
|
+
},
|
|
140
|
+
...createAttachedChannelResultAdapter({
|
|
141
|
+
channel: "msteams",
|
|
142
|
+
sendText: async ({ cfg, to, text, deps }) => {
|
|
143
|
+
type SendFn = (
|
|
144
|
+
to: string,
|
|
145
|
+
text: string,
|
|
146
|
+
) => Promise<{ messageId: string; conversationId: string }>;
|
|
147
|
+
const send =
|
|
148
|
+
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
|
|
149
|
+
((to, text) => sendMessageMSTeams({ cfg, to, text }));
|
|
150
|
+
return await send(to, text);
|
|
151
|
+
},
|
|
152
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, mediaReadFile, deps }) => {
|
|
153
|
+
type SendFn = (
|
|
154
|
+
to: string,
|
|
155
|
+
text: string,
|
|
156
|
+
opts?: {
|
|
157
|
+
mediaUrl?: string;
|
|
158
|
+
mediaLocalRoots?: readonly string[];
|
|
159
|
+
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
|
160
|
+
},
|
|
161
|
+
) => Promise<{ messageId: string; conversationId: string }>;
|
|
162
|
+
const send =
|
|
163
|
+
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
|
|
164
|
+
((to, text, opts) =>
|
|
165
|
+
sendMessageMSTeams({
|
|
166
|
+
cfg,
|
|
167
|
+
to,
|
|
168
|
+
text,
|
|
169
|
+
mediaUrl: opts?.mediaUrl,
|
|
170
|
+
mediaLocalRoots: opts?.mediaLocalRoots,
|
|
171
|
+
mediaReadFile: opts?.mediaReadFile,
|
|
172
|
+
}));
|
|
173
|
+
return await send(to, text, { mediaUrl, mediaLocalRoots, mediaReadFile });
|
|
174
|
+
},
|
|
175
|
+
sendPoll: async ({ cfg, to, poll }) => {
|
|
176
|
+
const maxSelections = poll.maxSelections ?? 1;
|
|
177
|
+
const result = await sendPollMSTeams({
|
|
178
|
+
cfg,
|
|
179
|
+
to,
|
|
180
|
+
question: poll.question,
|
|
181
|
+
options: poll.options,
|
|
182
|
+
maxSelections,
|
|
183
|
+
});
|
|
184
|
+
const pollStore = createMSTeamsPollStoreFs();
|
|
185
|
+
await pollStore.createPoll({
|
|
186
|
+
id: result.pollId,
|
|
187
|
+
question: poll.question,
|
|
188
|
+
options: poll.options,
|
|
189
|
+
maxSelections,
|
|
190
|
+
createdAt: new Date().toISOString(),
|
|
191
|
+
conversationId: result.conversationId,
|
|
192
|
+
messageId: result.messageId,
|
|
193
|
+
votes: {},
|
|
194
|
+
});
|
|
195
|
+
return result;
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
};
|