@openclaw/msteams 2026.7.1 → 2026.7.2-beta.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/dist/api.cjs +8 -0
- package/dist/{channel-Cf5LJ4aO.js → channel-BSyPdpGN.cjs} +641 -227
- package/dist/channel-config-api.cjs +3 -0
- package/dist/channel-plugin-api.cjs +3 -0
- package/dist/channel.runtime-DQTOzzet.cjs +1372 -0
- package/dist/{config-schema-BL4qQZiA.js → config-schema-BSNLJUta.cjs} +13 -3
- package/dist/{directory-contract-api.js → directory-contract-api.cjs} +8 -7
- package/dist/{doctor-contract-api.js → doctor-contract-api.cjs} +73 -57
- package/dist/index.cjs +21 -0
- package/dist/{probe-BnyZRZsD.js → messenger-DtS9gqkV.cjs} +369 -861
- package/dist/{oauth-BifWxWzq.js → oauth-C5E5powI.cjs} +21 -21
- package/dist/{polls-B_hs4aKm.js → polls-BhoI4VaN.cjs} +188 -38
- package/dist/{resolve-allowlist-Dnw4aNOu.js → resolve-allowlist-CyQ57MfH.cjs} +817 -140
- package/dist/rolldown-runtime-D6vf50IK.cjs +28 -0
- package/dist/runtime-api.cjs +207 -0
- package/dist/runtime-gZTAuFux.cjs +24 -0
- package/dist/secret-contract-api.cjs +31 -0
- package/dist/setup-entry.cjs +14 -0
- package/dist/setup-plugin-api.cjs +65 -0
- package/dist/{setup-surface-qW-ECHL-.js → setup-surface-BDTPN3dX.cjs} +123 -49
- package/dist/{src-DZFs5XtZ.js → src-CUMz2B6f.cjs} +672 -740
- package/dist/{sso-token-store-DrVbnCUF.js → sso-token-store-CktCz88k.cjs} +51 -10
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +10 -31
- package/package.json +8 -7
- package/dist/api.js +0 -3
- package/dist/channel-config-api.js +0 -2
- package/dist/channel-plugin-api.js +0 -2
- package/dist/channel.runtime-Bwu6Tjhf.js +0 -664
- package/dist/index.js +0 -22
- package/dist/runtime-BS5AZrKK.js +0 -8
- package/dist/runtime-api.js +0 -20
- package/dist/secret-contract-api.js +0 -35
- package/dist/setup-entry.js +0 -15
- package/dist/setup-plugin-api.js +0 -64
|
@@ -0,0 +1,1372 @@
|
|
|
1
|
+
const require_runtime = require("./runtime-gZTAuFux.cjs");
|
|
2
|
+
require("./runtime-api.cjs");
|
|
3
|
+
const require_resolve_allowlist = require("./resolve-allowlist-CyQ57MfH.cjs");
|
|
4
|
+
const require_channel = require("./channel-BSyPdpGN.cjs");
|
|
5
|
+
const require_setup_surface = require("./setup-surface-BDTPN3dX.cjs");
|
|
6
|
+
const require_polls = require("./polls-BhoI4VaN.cjs");
|
|
7
|
+
const require_messenger = require("./messenger-DtS9gqkV.cjs");
|
|
8
|
+
let openclaw_plugin_sdk_channel_outbound = require("openclaw/plugin-sdk/channel-outbound");
|
|
9
|
+
let openclaw_plugin_sdk_string_coerce_runtime = require("openclaw/plugin-sdk/string-coerce-runtime");
|
|
10
|
+
let openclaw_plugin_sdk_reply_payload = require("openclaw/plugin-sdk/reply-payload");
|
|
11
|
+
let openclaw_plugin_sdk_text_chunking = require("openclaw/plugin-sdk/text-chunking");
|
|
12
|
+
let openclaw_plugin_sdk_number_runtime = require("openclaw/plugin-sdk/number-runtime");
|
|
13
|
+
let openclaw_plugin_sdk_channel_send_result = require("openclaw/plugin-sdk/channel-send-result");
|
|
14
|
+
let openclaw_plugin_sdk_markdown_table_runtime = require("openclaw/plugin-sdk/markdown-table-runtime");
|
|
15
|
+
let openclaw_plugin_sdk_media_runtime = require("openclaw/plugin-sdk/media-runtime");
|
|
16
|
+
let openclaw_plugin_sdk_outbound_media = require("openclaw/plugin-sdk/outbound-media");
|
|
17
|
+
let openclaw_plugin_sdk_string_normalization_runtime = require("openclaw/plugin-sdk/string-normalization-runtime");
|
|
18
|
+
//#region extensions/msteams/src/directory-live.ts
|
|
19
|
+
async function listMSTeamsDirectoryPeersLive(params) {
|
|
20
|
+
const query = require_resolve_allowlist.normalizeQuery(params.query);
|
|
21
|
+
if (!query) return [];
|
|
22
|
+
return (await require_resolve_allowlist.searchGraphUsers({
|
|
23
|
+
token: await require_resolve_allowlist.resolveGraphToken(params.cfg),
|
|
24
|
+
query,
|
|
25
|
+
top: typeof params.limit === "number" && params.limit > 0 ? params.limit : 20
|
|
26
|
+
})).map((user) => {
|
|
27
|
+
const id = user.id?.trim();
|
|
28
|
+
if (!id) return null;
|
|
29
|
+
const name = user.displayName?.trim();
|
|
30
|
+
const handle = user.userPrincipalName?.trim() || user.mail?.trim();
|
|
31
|
+
return {
|
|
32
|
+
kind: "user",
|
|
33
|
+
id: `user:${id}`,
|
|
34
|
+
name: name || void 0,
|
|
35
|
+
handle: handle ? `@${handle}` : void 0,
|
|
36
|
+
raw: user
|
|
37
|
+
};
|
|
38
|
+
}).filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
async function listMSTeamsDirectoryGroupsLive(params) {
|
|
41
|
+
const rawQuery = require_resolve_allowlist.normalizeQuery(params.query);
|
|
42
|
+
if (!rawQuery) return [];
|
|
43
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg);
|
|
44
|
+
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 20;
|
|
45
|
+
const [teamQuery, channelQuery] = rawQuery.includes("/") ? (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeStringEntries)(rawQuery.split("/", 2)) : [rawQuery, null];
|
|
46
|
+
const teams = await require_resolve_allowlist.listTeamsByName(token, teamQuery);
|
|
47
|
+
const results = [];
|
|
48
|
+
for (const team of teams) {
|
|
49
|
+
const teamId = team.id?.trim();
|
|
50
|
+
if (!teamId) continue;
|
|
51
|
+
const teamName = team.displayName?.trim() || teamQuery;
|
|
52
|
+
if (!channelQuery) {
|
|
53
|
+
results.push({
|
|
54
|
+
kind: "group",
|
|
55
|
+
id: `team:${teamId}`,
|
|
56
|
+
name: teamName,
|
|
57
|
+
handle: teamName ? `#${teamName}` : void 0,
|
|
58
|
+
raw: team
|
|
59
|
+
});
|
|
60
|
+
if (results.length >= limit) return results;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const channels = await require_resolve_allowlist.listChannelsForTeam(token, teamId);
|
|
64
|
+
for (const channel of channels) {
|
|
65
|
+
const name = channel.displayName?.trim();
|
|
66
|
+
if (!name) continue;
|
|
67
|
+
if (!(0, openclaw_plugin_sdk_string_coerce_runtime.normalizeLowercaseStringOrEmpty)(name).includes((0, openclaw_plugin_sdk_string_coerce_runtime.normalizeLowercaseStringOrEmpty)(channelQuery))) continue;
|
|
68
|
+
results.push({
|
|
69
|
+
kind: "group",
|
|
70
|
+
id: `conversation:${channel.id}`,
|
|
71
|
+
name: `${teamName}/${name}`,
|
|
72
|
+
handle: `#${name}`,
|
|
73
|
+
raw: channel
|
|
74
|
+
});
|
|
75
|
+
if (results.length >= limit) return results;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region extensions/msteams/src/graph-messages.ts
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the Graph API path prefix for a conversation.
|
|
84
|
+
* If `to` contains "/" it's a `teamId/channelId` (channel path),
|
|
85
|
+
* otherwise it's a chat ID.
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* Strip common target prefixes (`conversation:`, `user:`) so raw
|
|
89
|
+
* conversation IDs can be used directly in Graph paths.
|
|
90
|
+
*/
|
|
91
|
+
function stripTargetPrefix(raw) {
|
|
92
|
+
const trimmed = raw.trim();
|
|
93
|
+
if (/^conversation:/i.test(trimmed)) return trimmed.slice(13).trim();
|
|
94
|
+
if (/^user:/i.test(trimmed)) return trimmed.slice(5).trim();
|
|
95
|
+
return trimmed;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a target to a Graph-compatible conversation ID.
|
|
99
|
+
* `user:<aadId>` targets are looked up in the conversation store to find the
|
|
100
|
+
* actual `19:xxx@thread.*` chat ID that Graph API requires.
|
|
101
|
+
* Conversation IDs and `teamId/channelId` pairs pass through unchanged.
|
|
102
|
+
*/
|
|
103
|
+
async function resolveGraphConversationId(to) {
|
|
104
|
+
const trimmed = to.trim();
|
|
105
|
+
const isUserTarget = /^user:/i.test(trimmed);
|
|
106
|
+
const cleaned = stripTargetPrefix(trimmed);
|
|
107
|
+
if (!isUserTarget) return cleaned;
|
|
108
|
+
const found = await require_polls.createMSTeamsConversationStoreState().findPreferredDmByUserId(cleaned);
|
|
109
|
+
if (!found) throw new Error(`No conversation found for user:${cleaned}. The bot must receive a message from this user before Graph API operations work.`);
|
|
110
|
+
if (found.conversationId.startsWith("19:")) return found.conversationId;
|
|
111
|
+
throw new Error(`Conversation for user:${cleaned} uses a Bot Framework ID (${found.conversationId}) that Graph API does not accept. Use a Graph-native conversation:19:... target when available.`);
|
|
112
|
+
}
|
|
113
|
+
function resolveConversationPath(to) {
|
|
114
|
+
const cleaned = stripTargetPrefix(to);
|
|
115
|
+
const separatorIndex = cleaned.indexOf("/");
|
|
116
|
+
if (separatorIndex !== -1) {
|
|
117
|
+
const teamId = cleaned.slice(0, separatorIndex);
|
|
118
|
+
const channelId = cleaned.slice(separatorIndex + 1).replace(/\/.*$/, "");
|
|
119
|
+
return {
|
|
120
|
+
kind: "channel",
|
|
121
|
+
basePath: `/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}`,
|
|
122
|
+
teamId,
|
|
123
|
+
channelId
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
kind: "chat",
|
|
128
|
+
basePath: `/chats/${encodeURIComponent(cleaned)}`,
|
|
129
|
+
chatId: cleaned
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Retrieve a single message by ID from a chat or channel via Graph API.
|
|
134
|
+
*/
|
|
135
|
+
async function getMessageMSTeams(params) {
|
|
136
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg);
|
|
137
|
+
const { basePath } = resolveConversationPath(await resolveGraphConversationId(params.to));
|
|
138
|
+
const msg = await require_resolve_allowlist.fetchGraphJson({
|
|
139
|
+
token,
|
|
140
|
+
path: `${basePath}/messages/${encodeURIComponent(params.messageId)}`
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
id: msg.id ?? params.messageId,
|
|
144
|
+
text: msg.body?.content,
|
|
145
|
+
from: msg.from,
|
|
146
|
+
createdAt: msg.createdDateTime
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Pin a message in a chat conversation via Graph API.
|
|
151
|
+
*
|
|
152
|
+
* Chat pinning uses the v1.0 endpoint: `POST /chats/{chatId}/pinnedMessages`.
|
|
153
|
+
*
|
|
154
|
+
* Channel pinning uses `POST /teams/{teamId}/channels/{channelId}/pinnedMessages`.
|
|
155
|
+
* **Note:** The channel pin endpoint may require the Graph beta API or specific
|
|
156
|
+
* tenant-level permissions. As of March 2026, general availability is not
|
|
157
|
+
* confirmed for all tenants. If the call returns 404 or 403, the endpoint may
|
|
158
|
+
* not be enabled for the target tenant.
|
|
159
|
+
*/
|
|
160
|
+
async function pinMessageMSTeams(params) {
|
|
161
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg);
|
|
162
|
+
const conversationId = await resolveGraphConversationId(params.to);
|
|
163
|
+
const conv = resolveConversationPath(conversationId);
|
|
164
|
+
if (conv.kind === "channel") throw new Error("Pin/unpin is not supported for channel messages on Graph v1.0. Only chat conversations support pinned messages.");
|
|
165
|
+
const body = { "message@odata.bind": `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(params.messageId)}` };
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
pinnedMessageId: (await require_resolve_allowlist.postGraphJson({
|
|
169
|
+
token,
|
|
170
|
+
path: `${conv.basePath}/pinnedMessages`,
|
|
171
|
+
body
|
|
172
|
+
})).id
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Unpin a message in a chat conversation via Graph API.
|
|
177
|
+
* `pinnedMessageId` is the pinned-message resource ID (from pin or list-pins),
|
|
178
|
+
* not the underlying chat message ID.
|
|
179
|
+
*
|
|
180
|
+
* Channel unpin uses `DELETE /teams/{teamId}/channels/{channelId}/pinnedMessages/{id}`.
|
|
181
|
+
* See the note on {@link pinMessageMSTeams} regarding beta/GA status.
|
|
182
|
+
*/
|
|
183
|
+
async function unpinMessageMSTeams(params) {
|
|
184
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg);
|
|
185
|
+
const conv = resolveConversationPath(await resolveGraphConversationId(params.to));
|
|
186
|
+
if (conv.kind === "channel") throw new Error("Pin/unpin is not supported for channel messages on Graph v1.0. Only chat conversations support pinned messages.");
|
|
187
|
+
await require_resolve_allowlist.deleteGraphRequest({
|
|
188
|
+
token,
|
|
189
|
+
path: `${conv.basePath}/pinnedMessages/${encodeURIComponent(params.pinnedMessageId)}`
|
|
190
|
+
});
|
|
191
|
+
return { ok: true };
|
|
192
|
+
}
|
|
193
|
+
/** Maximum number of pagination pages to follow to avoid unbounded loops. */
|
|
194
|
+
const LIST_PINS_MAX_PAGES = 10;
|
|
195
|
+
/**
|
|
196
|
+
* List all pinned messages in a chat conversation via Graph API.
|
|
197
|
+
* Follows `@odata.nextLink` pagination to collect the full pin set.
|
|
198
|
+
*
|
|
199
|
+
* Channel list-pins uses the same endpoint pattern as channel pin/unpin.
|
|
200
|
+
* See the note on {@link pinMessageMSTeams} regarding beta/GA status.
|
|
201
|
+
*/
|
|
202
|
+
async function listPinsMSTeams(params) {
|
|
203
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg);
|
|
204
|
+
const conv = resolveConversationPath(await resolveGraphConversationId(params.to));
|
|
205
|
+
if (conv.kind === "channel") throw new Error("Listing pinned messages is not supported for channels on Graph v1.0. Only chat conversations support pinned messages.");
|
|
206
|
+
const path = `${conv.basePath}/pinnedMessages?$expand=message`;
|
|
207
|
+
const allPins = [];
|
|
208
|
+
let res = await require_resolve_allowlist.fetchGraphJson({
|
|
209
|
+
token,
|
|
210
|
+
path
|
|
211
|
+
});
|
|
212
|
+
let pages = 1;
|
|
213
|
+
while (true) {
|
|
214
|
+
for (const pin of res.value ?? []) allPins.push({
|
|
215
|
+
id: pin.id ?? "",
|
|
216
|
+
pinnedMessageId: pin.id ?? "",
|
|
217
|
+
messageId: pin.message?.id,
|
|
218
|
+
text: pin.message?.body?.content
|
|
219
|
+
});
|
|
220
|
+
const nextLink = res["@odata.nextLink"];
|
|
221
|
+
if (!nextLink || pages >= LIST_PINS_MAX_PAGES) break;
|
|
222
|
+
res = await require_resolve_allowlist.fetchGraphAbsoluteUrl({
|
|
223
|
+
token,
|
|
224
|
+
url: nextLink
|
|
225
|
+
});
|
|
226
|
+
pages++;
|
|
227
|
+
}
|
|
228
|
+
return { pins: allPins };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Add an emoji reaction to a message via Graph API (beta).
|
|
232
|
+
*
|
|
233
|
+
* Writes (setReaction) require a Delegated token, so we pass
|
|
234
|
+
* `preferDelegated: true`. The resolver falls back to the app-only token when
|
|
235
|
+
* delegated auth is not configured, preserving today's behavior while letting
|
|
236
|
+
* delegated-auth-enabled deployments hit the user-scoped endpoint.
|
|
237
|
+
*/
|
|
238
|
+
async function reactMessageMSTeams(params) {
|
|
239
|
+
const reactionType = require_messenger.resolveMSTeamsReactionEmoji(params.reactionType);
|
|
240
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg, { preferDelegated: true });
|
|
241
|
+
const { basePath } = resolveConversationPath(await resolveGraphConversationId(params.to));
|
|
242
|
+
await require_resolve_allowlist.postGraphBetaJson({
|
|
243
|
+
token,
|
|
244
|
+
path: `${basePath}/messages/${encodeURIComponent(params.messageId)}/setReaction`,
|
|
245
|
+
body: { reactionType }
|
|
246
|
+
});
|
|
247
|
+
return { ok: true };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Remove an emoji reaction from a message via Graph API (beta).
|
|
251
|
+
*
|
|
252
|
+
* Writes (unsetReaction) require a Delegated token, so we pass
|
|
253
|
+
* `preferDelegated: true`. See `reactMessageMSTeams` for fallback rules.
|
|
254
|
+
*/
|
|
255
|
+
async function unreactMessageMSTeams(params) {
|
|
256
|
+
const reactionType = require_messenger.resolveMSTeamsReactionEmoji(params.reactionType);
|
|
257
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg, { preferDelegated: true });
|
|
258
|
+
const { basePath } = resolveConversationPath(await resolveGraphConversationId(params.to));
|
|
259
|
+
await require_resolve_allowlist.postGraphBetaJson({
|
|
260
|
+
token,
|
|
261
|
+
path: `${basePath}/messages/${encodeURIComponent(params.messageId)}/unsetReaction`,
|
|
262
|
+
body: { reactionType }
|
|
263
|
+
});
|
|
264
|
+
return { ok: true };
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* List reactions on a message, grouped by type.
|
|
268
|
+
* Uses Graph v1.0 (reactions are included in the message resource).
|
|
269
|
+
*/
|
|
270
|
+
async function listReactionsMSTeams(params) {
|
|
271
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg);
|
|
272
|
+
const { basePath } = resolveConversationPath(await resolveGraphConversationId(params.to));
|
|
273
|
+
const msg = await require_resolve_allowlist.fetchGraphJson({
|
|
274
|
+
token,
|
|
275
|
+
path: `${basePath}/messages/${encodeURIComponent(params.messageId)}`
|
|
276
|
+
});
|
|
277
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
278
|
+
for (const reaction of msg.reactions ?? []) {
|
|
279
|
+
const type = reaction.reactionType ?? "unknown";
|
|
280
|
+
if (!grouped.has(type)) grouped.set(type, {
|
|
281
|
+
count: 0,
|
|
282
|
+
users: []
|
|
283
|
+
});
|
|
284
|
+
const group = grouped.get(type);
|
|
285
|
+
group.count++;
|
|
286
|
+
if (reaction.user?.id) group.users.push({
|
|
287
|
+
id: reaction.user.id,
|
|
288
|
+
displayName: reaction.user.displayName
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return { reactions: Array.from(grouped.entries()).map(([type, group]) => ({
|
|
292
|
+
reactionType: type,
|
|
293
|
+
name: type,
|
|
294
|
+
emoji: require_messenger.getMSTeamsReactionEmoji(type),
|
|
295
|
+
count: group.count,
|
|
296
|
+
users: group.users
|
|
297
|
+
})) };
|
|
298
|
+
}
|
|
299
|
+
const SEARCH_DEFAULT_LIMIT = 25;
|
|
300
|
+
const SEARCH_MAX_LIMIT = 50;
|
|
301
|
+
const SEARCH_PAGE_SIZE = 50;
|
|
302
|
+
const SEARCH_MAX_PAGES = 10;
|
|
303
|
+
function normalizeSearchText(message) {
|
|
304
|
+
const content = message.body?.content ?? "";
|
|
305
|
+
return message.body?.contentType?.toLowerCase() === "html" ? require_messenger.stripHtmlFromTeamsMessage(content) : content.trim();
|
|
306
|
+
}
|
|
307
|
+
function matchesSearchSender(message, from) {
|
|
308
|
+
const normalized = from?.trim().toLowerCase();
|
|
309
|
+
if (!normalized) return true;
|
|
310
|
+
const sender = message.from?.user ?? message.from?.application;
|
|
311
|
+
return [sender?.id, sender?.displayName].some((value) => value?.trim().toLowerCase() === normalized);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Search messages within one already-authorized chat or channel.
|
|
315
|
+
* Graph does not support collection `$search` here, so filter bounded pages
|
|
316
|
+
* locally without widening the read to the account's global message index.
|
|
317
|
+
*/
|
|
318
|
+
async function searchMessagesMSTeams(params) {
|
|
319
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg);
|
|
320
|
+
const { basePath } = resolveConversationPath(await resolveGraphConversationId(params.to));
|
|
321
|
+
const rawLimit = params.limit ?? SEARCH_DEFAULT_LIMIT;
|
|
322
|
+
const top = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 1), SEARCH_MAX_LIMIT) : SEARCH_DEFAULT_LIMIT;
|
|
323
|
+
const query = params.query.trim().toLowerCase();
|
|
324
|
+
const messages = [];
|
|
325
|
+
let nextUrl;
|
|
326
|
+
let truncated = false;
|
|
327
|
+
for (let page = 0; page < SEARCH_MAX_PAGES; page++) {
|
|
328
|
+
const response = nextUrl ? await require_resolve_allowlist.fetchGraphAbsoluteUrl({
|
|
329
|
+
token,
|
|
330
|
+
url: nextUrl
|
|
331
|
+
}) : await require_resolve_allowlist.fetchGraphJson({
|
|
332
|
+
token,
|
|
333
|
+
path: `${basePath}/messages?$top=${SEARCH_PAGE_SIZE}`
|
|
334
|
+
});
|
|
335
|
+
for (const message of response.value ?? []) if (normalizeSearchText(message).toLowerCase().includes(query) && matchesSearchSender(message, params.from)) {
|
|
336
|
+
if (messages.length >= top) return {
|
|
337
|
+
messages,
|
|
338
|
+
truncated: true
|
|
339
|
+
};
|
|
340
|
+
messages.push({
|
|
341
|
+
id: message.id ?? "",
|
|
342
|
+
text: message.body?.content,
|
|
343
|
+
from: message.from,
|
|
344
|
+
createdAt: message.createdDateTime
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
nextUrl = response["@odata.nextLink"];
|
|
348
|
+
if (messages.length >= top) return {
|
|
349
|
+
messages,
|
|
350
|
+
truncated: Boolean(nextUrl)
|
|
351
|
+
};
|
|
352
|
+
if (!nextUrl) return {
|
|
353
|
+
messages,
|
|
354
|
+
truncated: false
|
|
355
|
+
};
|
|
356
|
+
truncated = page === SEARCH_MAX_PAGES - 1;
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
messages,
|
|
360
|
+
truncated
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region extensions/msteams/src/graph-conversation-members.ts
|
|
365
|
+
const MAX_CONVERSATION_MEMBER_PAGES = 100;
|
|
366
|
+
async function findMSTeamsConversationMember(params) {
|
|
367
|
+
const conversationId = await resolveGraphConversationId(params.to);
|
|
368
|
+
const conversation = resolveConversationPath(conversationId);
|
|
369
|
+
const collection = conversation.kind === "channel" && params.includeIndirectChannelMembers ? "allMembers" : "members";
|
|
370
|
+
let nextPath = `${conversation.basePath}/${collection}`;
|
|
371
|
+
let pages = 0;
|
|
372
|
+
let member;
|
|
373
|
+
while (nextPath && pages < MAX_CONVERSATION_MEMBER_PAGES && !member) {
|
|
374
|
+
const response = await require_resolve_allowlist.fetchGraphJson({
|
|
375
|
+
token: params.token,
|
|
376
|
+
path: nextPath
|
|
377
|
+
});
|
|
378
|
+
const userId = params.userId.trim().toLowerCase();
|
|
379
|
+
member = (response.value ?? []).find((candidate) => candidate.userId?.trim().toLowerCase() === userId || candidate.email?.trim().toLowerCase() === userId);
|
|
380
|
+
nextPath = response["@odata.nextLink"]?.replace("https://graph.microsoft.com/v1.0", "");
|
|
381
|
+
pages += 1;
|
|
382
|
+
}
|
|
383
|
+
if (nextPath && !member) throw new Error("MS Teams conversation member pagination limit exceeded");
|
|
384
|
+
return {
|
|
385
|
+
conversationId,
|
|
386
|
+
member
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region extensions/msteams/src/graph-group-management.ts
|
|
391
|
+
function normalizeConversationMemberRole(role) {
|
|
392
|
+
const normalized = role?.trim().toLowerCase() ?? "";
|
|
393
|
+
if (!normalized) return "member";
|
|
394
|
+
if (normalized === "member" || normalized === "owner") return normalized;
|
|
395
|
+
throw new Error("MS Teams participant role must be \"member\" or \"owner\".");
|
|
396
|
+
}
|
|
397
|
+
function resolveConversationMemberRoles(role, kind) {
|
|
398
|
+
const normalized = normalizeConversationMemberRole(role);
|
|
399
|
+
if (kind === "chat") return ["owner"];
|
|
400
|
+
return normalized === "owner" ? ["owner"] : [];
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Add a user to a chat or channel via Graph API.
|
|
404
|
+
*/
|
|
405
|
+
async function addParticipantMSTeams(params) {
|
|
406
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg);
|
|
407
|
+
const conversationId = await resolveGraphConversationId(params.to);
|
|
408
|
+
const conv = resolveConversationPath(conversationId);
|
|
409
|
+
const body = {
|
|
410
|
+
"@odata.type": "#microsoft.graph.aadUserConversationMember",
|
|
411
|
+
roles: resolveConversationMemberRoles(params.role, conv.kind),
|
|
412
|
+
"user@odata.bind": `https://graph.microsoft.com/v1.0/users('${require_resolve_allowlist.escapeOData(params.userId)}')`
|
|
413
|
+
};
|
|
414
|
+
await require_resolve_allowlist.postGraphJson({
|
|
415
|
+
token,
|
|
416
|
+
path: `${conv.basePath}/members`,
|
|
417
|
+
body
|
|
418
|
+
});
|
|
419
|
+
return { added: {
|
|
420
|
+
userId: params.userId,
|
|
421
|
+
chatId: conversationId
|
|
422
|
+
} };
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Remove a user from a chat or channel via Graph API.
|
|
426
|
+
* Lists members first to resolve the membership ID, then deletes.
|
|
427
|
+
*/
|
|
428
|
+
async function removeParticipantMSTeams(params) {
|
|
429
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg);
|
|
430
|
+
const { conversationId, member } = await findMSTeamsConversationMember({
|
|
431
|
+
token,
|
|
432
|
+
to: params.to,
|
|
433
|
+
userId: params.userId
|
|
434
|
+
});
|
|
435
|
+
if (!member?.id) throw new Error(`User ${params.userId} is not a member of this conversation`);
|
|
436
|
+
await require_resolve_allowlist.deleteGraphRequest({
|
|
437
|
+
token,
|
|
438
|
+
path: `${resolveConversationPath(conversationId).basePath}/members/${encodeURIComponent(member.id)}`
|
|
439
|
+
});
|
|
440
|
+
return { removed: {
|
|
441
|
+
userId: params.userId,
|
|
442
|
+
chatId: conversationId
|
|
443
|
+
} };
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Rename a chat (topic) or channel (displayName) via Graph API.
|
|
447
|
+
*/
|
|
448
|
+
async function renameGroupMSTeams(params) {
|
|
449
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg);
|
|
450
|
+
const conversationId = await resolveGraphConversationId(params.to);
|
|
451
|
+
const conv = resolveConversationPath(conversationId);
|
|
452
|
+
const body = conv.kind === "chat" ? { topic: params.name } : { displayName: params.name };
|
|
453
|
+
await require_resolve_allowlist.patchGraphJson({
|
|
454
|
+
token,
|
|
455
|
+
path: conv.basePath,
|
|
456
|
+
body
|
|
457
|
+
});
|
|
458
|
+
return { renamed: {
|
|
459
|
+
chatId: conversationId,
|
|
460
|
+
newName: params.name
|
|
461
|
+
} };
|
|
462
|
+
}
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region extensions/msteams/src/graph-members.ts
|
|
465
|
+
const MAX_TEAM_MEMBER_PAGES = 100;
|
|
466
|
+
function normalizeUserId(value) {
|
|
467
|
+
return value?.replace(/^(msteams|teams|user):/i, "").trim().toLowerCase() ?? "";
|
|
468
|
+
}
|
|
469
|
+
async function findStandardChannelMember(params) {
|
|
470
|
+
const conversation = resolveConversationPath(await resolveGraphConversationId(params.to));
|
|
471
|
+
if (conversation.kind !== "channel" || !conversation.teamId) return;
|
|
472
|
+
if ((await require_resolve_allowlist.fetchGraphJson({
|
|
473
|
+
token: params.token,
|
|
474
|
+
path: `${conversation.basePath}?$select=membershipType`
|
|
475
|
+
})).membershipType !== "standard") throw new Error("Microsoft Teams member-info requires a standard channel when using the configured permission baseline.");
|
|
476
|
+
const requestedUserId = normalizeUserId(params.userId);
|
|
477
|
+
let nextPath = `/teams/${encodeURIComponent(conversation.teamId)}/members`;
|
|
478
|
+
let pages = 0;
|
|
479
|
+
while (nextPath && pages < MAX_TEAM_MEMBER_PAGES) {
|
|
480
|
+
const response = await require_resolve_allowlist.fetchGraphJson({
|
|
481
|
+
token: params.token,
|
|
482
|
+
path: nextPath
|
|
483
|
+
});
|
|
484
|
+
const member = (response.value ?? []).find((candidate) => normalizeUserId(candidate.userId) === requestedUserId || normalizeUserId(candidate.email) === requestedUserId);
|
|
485
|
+
if (member) return member;
|
|
486
|
+
nextPath = response["@odata.nextLink"]?.replace("https://graph.microsoft.com/v1.0", "");
|
|
487
|
+
pages += 1;
|
|
488
|
+
}
|
|
489
|
+
if (nextPath) throw new Error("Microsoft Teams team member pagination limit exceeded");
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Fetch a user profile from Microsoft Graph by user ID.
|
|
493
|
+
*/
|
|
494
|
+
async function getMemberInfoMSTeams(params) {
|
|
495
|
+
if (normalizeUserId(params.userId) === normalizeUserId(params.currentRequesterId) && resolveConversationPath(params.to).kind === "chat") return { user: {
|
|
496
|
+
id: params.currentRequesterId ?? void 0,
|
|
497
|
+
displayName: void 0,
|
|
498
|
+
mail: void 0,
|
|
499
|
+
jobTitle: void 0,
|
|
500
|
+
userPrincipalName: void 0,
|
|
501
|
+
officeLocation: void 0,
|
|
502
|
+
roles: []
|
|
503
|
+
} };
|
|
504
|
+
const member = resolveConversationPath(await resolveGraphConversationId(params.to)).kind === "channel" ? await findStandardChannelMember({
|
|
505
|
+
token: await require_resolve_allowlist.resolveGraphToken(params.cfg),
|
|
506
|
+
to: params.to,
|
|
507
|
+
userId: params.userId
|
|
508
|
+
}) : void 0;
|
|
509
|
+
if (!member?.userId) throw new Error(`User ${params.userId} is not a member of this conversation`);
|
|
510
|
+
return { user: {
|
|
511
|
+
id: member.userId,
|
|
512
|
+
displayName: member.displayName,
|
|
513
|
+
mail: member.email,
|
|
514
|
+
jobTitle: void 0,
|
|
515
|
+
userPrincipalName: member.email,
|
|
516
|
+
officeLocation: void 0,
|
|
517
|
+
roles: member.roles ?? []
|
|
518
|
+
} };
|
|
519
|
+
}
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region extensions/msteams/src/graph-teams.ts
|
|
522
|
+
/**
|
|
523
|
+
* List channels in a team via Graph API.
|
|
524
|
+
* Returns id, displayName, description, and membershipType for each channel.
|
|
525
|
+
* Follows @odata.nextLink for paginated results (up to 10 pages).
|
|
526
|
+
*/
|
|
527
|
+
async function listChannelsMSTeams(params) {
|
|
528
|
+
const token = await require_resolve_allowlist.resolveGraphToken(params.cfg);
|
|
529
|
+
const firstPath = `/teams/${encodeURIComponent(params.teamId)}/channels?$select=id,displayName,description,membershipType`;
|
|
530
|
+
const collected = [];
|
|
531
|
+
let nextPath = firstPath;
|
|
532
|
+
const MAX_PAGES = 10;
|
|
533
|
+
let page = 0;
|
|
534
|
+
while (nextPath && page < MAX_PAGES) {
|
|
535
|
+
const res = await require_resolve_allowlist.fetchGraphJson({
|
|
536
|
+
token,
|
|
537
|
+
path: nextPath
|
|
538
|
+
});
|
|
539
|
+
collected.push(...res.value ?? []);
|
|
540
|
+
const nextLink = res["@odata.nextLink"];
|
|
541
|
+
nextPath = nextLink ? nextLink.replace("https://graph.microsoft.com/v1.0", "") : void 0;
|
|
542
|
+
page++;
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
channels: collected.map((ch) => ({
|
|
546
|
+
id: ch.id,
|
|
547
|
+
displayName: ch.displayName,
|
|
548
|
+
description: ch.description,
|
|
549
|
+
membershipType: ch.membershipType
|
|
550
|
+
})),
|
|
551
|
+
truncated: Boolean(nextPath)
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Get detailed information about a single channel in a team via Graph API.
|
|
556
|
+
* Returns id, displayName, description, membershipType, webUrl, and createdDateTime.
|
|
557
|
+
*/
|
|
558
|
+
async function getChannelInfoMSTeams(params) {
|
|
559
|
+
const ch = await require_resolve_allowlist.fetchGraphJson({
|
|
560
|
+
token: await require_resolve_allowlist.resolveGraphToken(params.cfg),
|
|
561
|
+
path: `/teams/${encodeURIComponent(params.teamId)}/channels/${encodeURIComponent(params.channelId)}?$select=id,displayName,description,membershipType,webUrl,createdDateTime`
|
|
562
|
+
});
|
|
563
|
+
return { channel: {
|
|
564
|
+
id: ch.id,
|
|
565
|
+
displayName: ch.displayName,
|
|
566
|
+
description: ch.description,
|
|
567
|
+
membershipType: ch.membershipType,
|
|
568
|
+
webUrl: ch.webUrl,
|
|
569
|
+
createdDateTime: ch.createdDateTime
|
|
570
|
+
} };
|
|
571
|
+
}
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region extensions/msteams/src/send-context.ts
|
|
574
|
+
function resolveMSTeamsProactiveReplyStyle(params) {
|
|
575
|
+
const threadRootId = params.ref.threadId ?? params.ref.activityId;
|
|
576
|
+
if (params.conversationType !== "channel" || !threadRootId) return "top-level";
|
|
577
|
+
const routeConfig = require_channel.resolveMSTeamsRouteConfig({
|
|
578
|
+
cfg: params.cfg,
|
|
579
|
+
teamId: params.ref.teamId,
|
|
580
|
+
conversationId: params.conversationId,
|
|
581
|
+
allowNameMatching: false
|
|
582
|
+
});
|
|
583
|
+
const { replyStyle } = require_channel.resolveMSTeamsReplyPolicy({
|
|
584
|
+
isDirectMessage: false,
|
|
585
|
+
globalConfig: params.cfg,
|
|
586
|
+
teamConfig: routeConfig.teamConfig,
|
|
587
|
+
channelConfig: routeConfig.channelConfig
|
|
588
|
+
});
|
|
589
|
+
return replyStyle;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Parse the target value into a conversation reference lookup key.
|
|
593
|
+
* Supported formats:
|
|
594
|
+
* - conversation:19:abc@thread.tacv2 → lookup by conversation ID
|
|
595
|
+
* - user:aad-object-id → lookup by user AAD object ID
|
|
596
|
+
* - 19:abc@thread.tacv2 → direct conversation ID
|
|
597
|
+
*/
|
|
598
|
+
function parseRecipient(to) {
|
|
599
|
+
const trimmed = to.trim();
|
|
600
|
+
const finalize = (type, id) => {
|
|
601
|
+
const normalized = id.trim();
|
|
602
|
+
if (!normalized) throw new Error(`Invalid target value: missing ${type} id`);
|
|
603
|
+
return {
|
|
604
|
+
type,
|
|
605
|
+
id: normalized
|
|
606
|
+
};
|
|
607
|
+
};
|
|
608
|
+
if (trimmed.startsWith("conversation:")) return finalize("conversation", trimmed.slice(13));
|
|
609
|
+
if (trimmed.startsWith("user:")) return finalize("user", trimmed.slice(5));
|
|
610
|
+
if (trimmed.startsWith("19:") || trimmed.includes("@thread")) return finalize("conversation", trimmed);
|
|
611
|
+
return finalize("user", trimmed);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Find a stored conversation reference for the given recipient.
|
|
615
|
+
*/
|
|
616
|
+
async function findConversationReference(recipient) {
|
|
617
|
+
if (recipient.type === "conversation") {
|
|
618
|
+
const ref = await recipient.store.get(recipient.id);
|
|
619
|
+
if (ref) return {
|
|
620
|
+
conversationId: recipient.id,
|
|
621
|
+
ref
|
|
622
|
+
};
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
const found = await recipient.store.findPreferredDmByUserId(recipient.id);
|
|
626
|
+
if (!found) return null;
|
|
627
|
+
return {
|
|
628
|
+
conversationId: found.conversationId,
|
|
629
|
+
ref: found.reference
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async function resolveMSTeamsSendContext(params) {
|
|
633
|
+
const msteamsCfg = params.cfg.channels?.msteams;
|
|
634
|
+
if (!msteamsCfg?.enabled) throw new Error("msteams provider is not enabled");
|
|
635
|
+
const creds = require_resolve_allowlist.resolveMSTeamsCredentials(msteamsCfg);
|
|
636
|
+
if (!creds) throw new Error("msteams credentials not configured");
|
|
637
|
+
const store = require_polls.createMSTeamsConversationStoreState();
|
|
638
|
+
const recipient = parseRecipient(params.to);
|
|
639
|
+
const found = await findConversationReference({
|
|
640
|
+
...recipient,
|
|
641
|
+
store
|
|
642
|
+
});
|
|
643
|
+
if (!found) throw new Error(`No conversation reference found for ${recipient.type}:${recipient.id}. The bot must receive a message from this conversation before it can send proactively.`);
|
|
644
|
+
const { conversationId, ref } = found;
|
|
645
|
+
const log = require_runtime.getMSTeamsRuntime().logging.getChildLogger({ name: "msteams:send" });
|
|
646
|
+
if (ref.serviceUrl && !require_resolve_allowlist.isAllowedBotFrameworkServiceUrl(ref.serviceUrl)) {
|
|
647
|
+
try {
|
|
648
|
+
await store.remove(conversationId);
|
|
649
|
+
} catch (err) {
|
|
650
|
+
log.warn?.("failed to remove blocked msteams conversation reference", {
|
|
651
|
+
conversationId,
|
|
652
|
+
error: require_setup_surface.formatUnknownError(err)
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
throw new Error(`Stored Microsoft Teams conversation reference has blocked serviceUrl host: ${require_resolve_allowlist.describeBotFrameworkServiceUrlHost(ref.serviceUrl)}. The bot must receive a new message from this conversation before it can send proactively.`);
|
|
656
|
+
}
|
|
657
|
+
const safeRef = ref.serviceUrl ? {
|
|
658
|
+
...ref,
|
|
659
|
+
serviceUrl: require_resolve_allowlist.normalizeBotFrameworkServiceUrl(ref.serviceUrl)
|
|
660
|
+
} : ref;
|
|
661
|
+
if (recipient.type === "user") {
|
|
662
|
+
const resolvedType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeLowercaseStringOrEmpty)(safeRef.conversation?.conversationType ?? "");
|
|
663
|
+
if (resolvedType && resolvedType !== "personal") throw new Error(`Conversation reference for user:${recipient.id} resolved to a ${resolvedType} conversation (${conversationId}) instead of a personal DM. The bot must receive a DM from this user before it can send proactively.`);
|
|
664
|
+
}
|
|
665
|
+
const sdkCloudOptions = require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg);
|
|
666
|
+
const { app } = await require_resolve_allowlist.loadMSTeamsSdkWithAuth(creds, sdkCloudOptions);
|
|
667
|
+
require_resolve_allowlist.validateMSTeamsProactiveServiceUrlBoundary({
|
|
668
|
+
cloud: sdkCloudOptions.cloud,
|
|
669
|
+
conversationId,
|
|
670
|
+
storedServiceUrl: safeRef.serviceUrl,
|
|
671
|
+
configuredServiceUrl: sdkCloudOptions.serviceUrl
|
|
672
|
+
});
|
|
673
|
+
const tokenProvider = require_resolve_allowlist.createMSTeamsTokenProvider(app);
|
|
674
|
+
const storedConversationType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeLowercaseStringOrEmpty)(safeRef.conversation?.conversationType ?? "");
|
|
675
|
+
let conversationType;
|
|
676
|
+
if (storedConversationType === "personal") conversationType = "personal";
|
|
677
|
+
else if (storedConversationType === "channel") conversationType = "channel";
|
|
678
|
+
else conversationType = "groupChat";
|
|
679
|
+
const replyStyle = resolveMSTeamsProactiveReplyStyle({
|
|
680
|
+
cfg: msteamsCfg,
|
|
681
|
+
conversationId,
|
|
682
|
+
ref: safeRef,
|
|
683
|
+
conversationType
|
|
684
|
+
});
|
|
685
|
+
const sharePointSiteId = msteamsCfg.sharePointSiteId;
|
|
686
|
+
const mediaMaxBytes = (0, openclaw_plugin_sdk_media_runtime.resolveChannelMediaMaxBytes)({
|
|
687
|
+
cfg: params.cfg,
|
|
688
|
+
resolveChannelLimitMb: ({ cfg }) => cfg.channels?.msteams?.mediaMaxMb
|
|
689
|
+
});
|
|
690
|
+
return {
|
|
691
|
+
appId: creds.appId,
|
|
692
|
+
conversationId,
|
|
693
|
+
ref: safeRef,
|
|
694
|
+
app,
|
|
695
|
+
log,
|
|
696
|
+
conversationType,
|
|
697
|
+
replyStyle,
|
|
698
|
+
sdkCloudOptions,
|
|
699
|
+
tokenProvider,
|
|
700
|
+
sharePointSiteId,
|
|
701
|
+
mediaMaxBytes
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
//#endregion
|
|
705
|
+
//#region extensions/msteams/src/send.ts
|
|
706
|
+
/** Threshold for large files that require FileConsentCard flow in personal chats */
|
|
707
|
+
const FILE_CONSENT_THRESHOLD_BYTES = 4 * 1024 * 1024;
|
|
708
|
+
/**
|
|
709
|
+
* MSTeams-specific media size limit (100MB).
|
|
710
|
+
* Higher than the default to support Teams file-consent and SharePoint uploads.
|
|
711
|
+
*/
|
|
712
|
+
const MSTEAMS_MAX_MEDIA_BYTES = 100 * 1024 * 1024;
|
|
713
|
+
function createMSTeamsSendReceipt(params) {
|
|
714
|
+
const receipt = (0, openclaw_plugin_sdk_channel_outbound.createMessageReceiptFromOutboundResults)({
|
|
715
|
+
kind: params.kind,
|
|
716
|
+
results: params.platformMessageIds.map((messageId) => ({
|
|
717
|
+
channel: "msteams",
|
|
718
|
+
messageId,
|
|
719
|
+
conversationId: params.conversationId
|
|
720
|
+
}))
|
|
721
|
+
});
|
|
722
|
+
if (!params.kinds) return receipt;
|
|
723
|
+
const kinds = params.kinds;
|
|
724
|
+
return {
|
|
725
|
+
...receipt,
|
|
726
|
+
parts: receipt.parts.map((part, index) => {
|
|
727
|
+
const nextPart = {
|
|
728
|
+
platformMessageId: part.platformMessageId,
|
|
729
|
+
kind: kinds[index] ?? params.kind,
|
|
730
|
+
index: part.index
|
|
731
|
+
};
|
|
732
|
+
if (part.threadId) nextPart.threadId = part.threadId;
|
|
733
|
+
if (part.replyToId) nextPart.replyToId = part.replyToId;
|
|
734
|
+
if (part.raw) nextPart.raw = part.raw;
|
|
735
|
+
return nextPart;
|
|
736
|
+
})
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function createMSTeamsSendResult(params) {
|
|
740
|
+
const platformMessageIds = (params.platformMessageIds?.length ? [...params.platformMessageIds] : [params.messageId]).map((messageId) => messageId.trim()).filter((messageId) => messageId && messageId !== "unknown");
|
|
741
|
+
return {
|
|
742
|
+
messageId: params.messageId,
|
|
743
|
+
conversationId: params.conversationId,
|
|
744
|
+
receipt: createMSTeamsSendReceipt({
|
|
745
|
+
conversationId: params.conversationId,
|
|
746
|
+
platformMessageIds,
|
|
747
|
+
kind: params.kind
|
|
748
|
+
}),
|
|
749
|
+
...params.pendingUploadId ? { pendingUploadId: params.pendingUploadId } : {}
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Send a message to a Teams conversation or user.
|
|
754
|
+
*
|
|
755
|
+
* Uses the stored ConversationReference from previous interactions.
|
|
756
|
+
* The bot must have received at least one message from the conversation
|
|
757
|
+
* before proactive messaging works.
|
|
758
|
+
*
|
|
759
|
+
* File handling by conversation type:
|
|
760
|
+
* - Personal (1:1) chats: small images (<4MB) use base64, large files and non-images use FileConsentCard
|
|
761
|
+
* - Group chats / channels: files require configured SharePoint storage
|
|
762
|
+
*/
|
|
763
|
+
async function sendMessageMSTeams(params) {
|
|
764
|
+
const { cfg, to, text, mediaUrl, filename, mediaLocalRoots, mediaReadFile } = params;
|
|
765
|
+
const tableMode = (0, openclaw_plugin_sdk_markdown_table_runtime.resolveMarkdownTableMode)({
|
|
766
|
+
cfg,
|
|
767
|
+
channel: "msteams"
|
|
768
|
+
});
|
|
769
|
+
const messageText = (0, openclaw_plugin_sdk_text_chunking.convertMarkdownTables)(text ?? "", tableMode);
|
|
770
|
+
const ctx = await resolveMSTeamsSendContext({
|
|
771
|
+
cfg,
|
|
772
|
+
to
|
|
773
|
+
});
|
|
774
|
+
const { app, conversationId, ref, log, conversationType, tokenProvider, sharePointSiteId, sdkCloudOptions } = ctx;
|
|
775
|
+
log.debug?.("sending proactive message", {
|
|
776
|
+
conversationId,
|
|
777
|
+
conversationType,
|
|
778
|
+
textLength: messageText.length,
|
|
779
|
+
hasMedia: Boolean(mediaUrl)
|
|
780
|
+
});
|
|
781
|
+
if (mediaUrl) {
|
|
782
|
+
const media = await (0, openclaw_plugin_sdk_outbound_media.loadOutboundMediaFromUrl)(mediaUrl, {
|
|
783
|
+
maxBytes: ctx.mediaMaxBytes ?? MSTEAMS_MAX_MEDIA_BYTES,
|
|
784
|
+
mediaLocalRoots,
|
|
785
|
+
mediaReadFile
|
|
786
|
+
});
|
|
787
|
+
const isLargeFile = media.buffer.length >= FILE_CONSENT_THRESHOLD_BYTES;
|
|
788
|
+
const isImage = media.contentType?.startsWith("image/") ?? false;
|
|
789
|
+
const fallbackFileName = await require_messenger.extractFilename(mediaUrl);
|
|
790
|
+
const fileName = filename?.trim() || media.fileName || fallbackFileName;
|
|
791
|
+
log.debug?.("processing media", {
|
|
792
|
+
fileName,
|
|
793
|
+
contentType: media.contentType,
|
|
794
|
+
size: media.buffer.length,
|
|
795
|
+
isLargeFile,
|
|
796
|
+
isImage,
|
|
797
|
+
conversationType
|
|
798
|
+
});
|
|
799
|
+
if (require_messenger.requiresFileConsent({
|
|
800
|
+
conversationType,
|
|
801
|
+
contentType: media.contentType,
|
|
802
|
+
bufferSize: media.buffer.length,
|
|
803
|
+
thresholdBytes: FILE_CONSENT_THRESHOLD_BYTES
|
|
804
|
+
})) {
|
|
805
|
+
const { activity, uploadId } = await require_messenger.prepareFileConsentActivityFs({
|
|
806
|
+
media: {
|
|
807
|
+
buffer: media.buffer,
|
|
808
|
+
filename: fileName,
|
|
809
|
+
contentType: media.contentType
|
|
810
|
+
},
|
|
811
|
+
conversationId,
|
|
812
|
+
description: messageText || void 0
|
|
813
|
+
});
|
|
814
|
+
log.debug?.("sending file consent card", {
|
|
815
|
+
uploadId,
|
|
816
|
+
fileName,
|
|
817
|
+
size: media.buffer.length
|
|
818
|
+
});
|
|
819
|
+
const messageId = await sendProactiveActivity({
|
|
820
|
+
app,
|
|
821
|
+
ref,
|
|
822
|
+
activity,
|
|
823
|
+
errorPrefix: "msteams consent card send",
|
|
824
|
+
serviceUrlBoundary: sdkCloudOptions
|
|
825
|
+
});
|
|
826
|
+
require_messenger.setPendingUploadActivityId(uploadId, messageId);
|
|
827
|
+
await require_messenger.setPendingUploadActivityIdFs(uploadId, messageId);
|
|
828
|
+
log.info("sent file consent card", {
|
|
829
|
+
conversationId,
|
|
830
|
+
messageId,
|
|
831
|
+
uploadId
|
|
832
|
+
});
|
|
833
|
+
return createMSTeamsSendResult({
|
|
834
|
+
messageId,
|
|
835
|
+
conversationId,
|
|
836
|
+
kind: "card",
|
|
837
|
+
pendingUploadId: uploadId
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
if (conversationType === "personal") {
|
|
841
|
+
const base64 = media.buffer.toString("base64");
|
|
842
|
+
return sendTextWithMedia(ctx, messageText, `data:${media.contentType};base64,${base64}`);
|
|
843
|
+
}
|
|
844
|
+
if (isImage && !sharePointSiteId) {
|
|
845
|
+
const base64 = media.buffer.toString("base64");
|
|
846
|
+
return sendTextWithMedia(ctx, messageText, `data:${media.contentType};base64,${base64}`);
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
const siteId = require_messenger.requireMSTeamsSharePointSiteId(sharePointSiteId);
|
|
850
|
+
log.debug?.("uploading to SharePoint for native file card", {
|
|
851
|
+
fileName,
|
|
852
|
+
conversationType,
|
|
853
|
+
siteId
|
|
854
|
+
});
|
|
855
|
+
const uploaded = await require_messenger.uploadAndShareSharePoint({
|
|
856
|
+
buffer: media.buffer,
|
|
857
|
+
filename: fileName,
|
|
858
|
+
contentType: media.contentType,
|
|
859
|
+
tokenProvider,
|
|
860
|
+
siteId,
|
|
861
|
+
chatId: conversationId,
|
|
862
|
+
usePerUserSharing: conversationType === "groupChat"
|
|
863
|
+
});
|
|
864
|
+
log.debug?.("SharePoint upload complete", {
|
|
865
|
+
itemId: uploaded.itemId,
|
|
866
|
+
shareUrl: uploaded.shareUrl
|
|
867
|
+
});
|
|
868
|
+
const driveItem = await require_messenger.getDriveItemProperties({
|
|
869
|
+
siteId,
|
|
870
|
+
itemId: uploaded.itemId,
|
|
871
|
+
tokenProvider
|
|
872
|
+
});
|
|
873
|
+
log.debug?.("driveItem properties retrieved", {
|
|
874
|
+
eTag: driveItem.eTag,
|
|
875
|
+
webDavUrl: driveItem.webDavUrl
|
|
876
|
+
});
|
|
877
|
+
const fileCardAttachment = require_messenger.buildTeamsFileInfoCard(driveItem);
|
|
878
|
+
const messageId = await sendProactiveActivityRaw({
|
|
879
|
+
app,
|
|
880
|
+
ref,
|
|
881
|
+
activity: {
|
|
882
|
+
type: "message",
|
|
883
|
+
text: messageText || void 0,
|
|
884
|
+
attachments: [fileCardAttachment]
|
|
885
|
+
},
|
|
886
|
+
serviceUrlBoundary: sdkCloudOptions
|
|
887
|
+
});
|
|
888
|
+
log.info("sent native file card", {
|
|
889
|
+
conversationId,
|
|
890
|
+
messageId,
|
|
891
|
+
fileName: driveItem.name
|
|
892
|
+
});
|
|
893
|
+
return createMSTeamsSendResult({
|
|
894
|
+
messageId,
|
|
895
|
+
conversationId,
|
|
896
|
+
kind: "media"
|
|
897
|
+
});
|
|
898
|
+
} catch (err) {
|
|
899
|
+
const classification = require_setup_surface.classifyMSTeamsSendError(err);
|
|
900
|
+
const hint = require_setup_surface.formatMSTeamsSendErrorHint(classification);
|
|
901
|
+
const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
|
|
902
|
+
throw new Error(`msteams file send failed${status}: ${require_setup_surface.formatUnknownError(err)}${hint ? ` (${hint})` : ""}`, { cause: err });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return sendTextWithMedia(ctx, messageText, void 0);
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Send a text message with optional base64 media URL.
|
|
909
|
+
*/
|
|
910
|
+
async function sendTextWithMedia(ctx, text, mediaUrl) {
|
|
911
|
+
const { app, appId, conversationId, ref, log, tokenProvider, sharePointSiteId, mediaMaxBytes, replyStyle } = ctx;
|
|
912
|
+
const messages = text && mediaUrl ? [{ text }, { mediaUrl }] : [{
|
|
913
|
+
text: text || void 0,
|
|
914
|
+
mediaUrl
|
|
915
|
+
}];
|
|
916
|
+
let platformMessageIds;
|
|
917
|
+
try {
|
|
918
|
+
platformMessageIds = await require_messenger.sendMSTeamsMessages({
|
|
919
|
+
replyStyle,
|
|
920
|
+
app,
|
|
921
|
+
appId,
|
|
922
|
+
conversationRef: ref,
|
|
923
|
+
messages,
|
|
924
|
+
retry: {},
|
|
925
|
+
onRetry: (event) => {
|
|
926
|
+
log.debug?.("retrying send", {
|
|
927
|
+
conversationId,
|
|
928
|
+
...event
|
|
929
|
+
});
|
|
930
|
+
},
|
|
931
|
+
tokenProvider,
|
|
932
|
+
sharePointSiteId,
|
|
933
|
+
mediaMaxBytes,
|
|
934
|
+
serviceUrlBoundary: ctx.sdkCloudOptions
|
|
935
|
+
});
|
|
936
|
+
} catch (err) {
|
|
937
|
+
const classification = require_setup_surface.classifyMSTeamsSendError(err);
|
|
938
|
+
const hint = require_setup_surface.formatMSTeamsSendErrorHint(classification);
|
|
939
|
+
const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
|
|
940
|
+
throw new Error(`msteams send failed${status}: ${require_setup_surface.formatUnknownError(err)}${hint ? ` (${hint})` : ""}`, { cause: err });
|
|
941
|
+
}
|
|
942
|
+
const messageId = platformMessageIds[0] ?? "unknown";
|
|
943
|
+
log.info("sent proactive message", {
|
|
944
|
+
conversationId,
|
|
945
|
+
messageId
|
|
946
|
+
});
|
|
947
|
+
return {
|
|
948
|
+
messageId,
|
|
949
|
+
conversationId,
|
|
950
|
+
receipt: createMSTeamsSendReceipt({
|
|
951
|
+
conversationId,
|
|
952
|
+
platformMessageIds,
|
|
953
|
+
kind: mediaUrl ? "media" : "text",
|
|
954
|
+
...text && mediaUrl ? { kinds: ["text", "media"] } : {}
|
|
955
|
+
})
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
async function sendProactiveActivityRaw({ app, ref, activity, serviceUrlBoundary }) {
|
|
959
|
+
return require_messenger.extractMessageId(await require_messenger.sendMSTeamsActivityWithReference(app, require_messenger.buildConversationReference(ref), activity, { serviceUrlBoundary })) ?? "unknown";
|
|
960
|
+
}
|
|
961
|
+
async function sendProactiveActivity({ app, ref, activity, errorPrefix, serviceUrlBoundary }) {
|
|
962
|
+
try {
|
|
963
|
+
return await sendProactiveActivityRaw({
|
|
964
|
+
app,
|
|
965
|
+
ref,
|
|
966
|
+
activity,
|
|
967
|
+
serviceUrlBoundary
|
|
968
|
+
});
|
|
969
|
+
} catch (err) {
|
|
970
|
+
const classification = require_setup_surface.classifyMSTeamsSendError(err);
|
|
971
|
+
const hint = require_setup_surface.formatMSTeamsSendErrorHint(classification);
|
|
972
|
+
const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
|
|
973
|
+
throw new Error(`${errorPrefix} failed${status}: ${require_setup_surface.formatUnknownError(err)}${hint ? ` (${hint})` : ""}`, { cause: err });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Send a poll (Adaptive Card) to a Teams conversation or user.
|
|
978
|
+
*/
|
|
979
|
+
async function sendPollMSTeams(params) {
|
|
980
|
+
const { cfg, to, question, options, maxSelections } = params;
|
|
981
|
+
const { app, conversationId, ref, log, sdkCloudOptions } = await resolveMSTeamsSendContext({
|
|
982
|
+
cfg,
|
|
983
|
+
to
|
|
984
|
+
});
|
|
985
|
+
const pollCard = require_polls.buildMSTeamsPollCard({
|
|
986
|
+
question,
|
|
987
|
+
options,
|
|
988
|
+
maxSelections
|
|
989
|
+
});
|
|
990
|
+
log.debug?.("sending poll", {
|
|
991
|
+
conversationId,
|
|
992
|
+
pollId: pollCard.pollId,
|
|
993
|
+
optionCount: pollCard.options.length
|
|
994
|
+
});
|
|
995
|
+
const messageId = await sendProactiveActivity({
|
|
996
|
+
app,
|
|
997
|
+
ref,
|
|
998
|
+
activity: {
|
|
999
|
+
type: "message",
|
|
1000
|
+
attachments: [{
|
|
1001
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
1002
|
+
content: pollCard.card
|
|
1003
|
+
}]
|
|
1004
|
+
},
|
|
1005
|
+
errorPrefix: "msteams poll send",
|
|
1006
|
+
serviceUrlBoundary: sdkCloudOptions
|
|
1007
|
+
});
|
|
1008
|
+
log.info("sent poll", {
|
|
1009
|
+
conversationId,
|
|
1010
|
+
pollId: pollCard.pollId,
|
|
1011
|
+
messageId
|
|
1012
|
+
});
|
|
1013
|
+
return {
|
|
1014
|
+
pollId: pollCard.pollId,
|
|
1015
|
+
messageId,
|
|
1016
|
+
conversationId
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Send an arbitrary Adaptive Card to a Teams conversation or user.
|
|
1021
|
+
*/
|
|
1022
|
+
async function sendAdaptiveCardMSTeams(params) {
|
|
1023
|
+
const { cfg, to, card } = params;
|
|
1024
|
+
const { app, conversationId, ref, log, sdkCloudOptions } = await resolveMSTeamsSendContext({
|
|
1025
|
+
cfg,
|
|
1026
|
+
to
|
|
1027
|
+
});
|
|
1028
|
+
log.debug?.("sending adaptive card", {
|
|
1029
|
+
conversationId,
|
|
1030
|
+
cardType: card.type,
|
|
1031
|
+
cardVersion: card.version
|
|
1032
|
+
});
|
|
1033
|
+
const messageId = await sendProactiveActivity({
|
|
1034
|
+
app,
|
|
1035
|
+
ref,
|
|
1036
|
+
activity: {
|
|
1037
|
+
type: "message",
|
|
1038
|
+
attachments: [{
|
|
1039
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
1040
|
+
content: card
|
|
1041
|
+
}]
|
|
1042
|
+
},
|
|
1043
|
+
errorPrefix: "msteams card send",
|
|
1044
|
+
serviceUrlBoundary: sdkCloudOptions
|
|
1045
|
+
});
|
|
1046
|
+
log.info("sent adaptive card", {
|
|
1047
|
+
conversationId,
|
|
1048
|
+
messageId
|
|
1049
|
+
});
|
|
1050
|
+
return {
|
|
1051
|
+
messageId,
|
|
1052
|
+
conversationId
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Edit (update) a previously sent message in a Teams conversation.
|
|
1057
|
+
*
|
|
1058
|
+
* Uses the Bot Framework REST API for proactive edits outside of the
|
|
1059
|
+
* original turn context.
|
|
1060
|
+
*/
|
|
1061
|
+
async function editMessageMSTeams(params) {
|
|
1062
|
+
const { cfg, to, activityId, text } = params;
|
|
1063
|
+
const { app, conversationId, ref, log, sdkCloudOptions } = await resolveMSTeamsSendContext({
|
|
1064
|
+
cfg,
|
|
1065
|
+
to
|
|
1066
|
+
});
|
|
1067
|
+
log.debug?.("editing proactive message", {
|
|
1068
|
+
conversationId,
|
|
1069
|
+
activityId,
|
|
1070
|
+
textLength: text.length
|
|
1071
|
+
});
|
|
1072
|
+
try {
|
|
1073
|
+
await require_messenger.updateMSTeamsActivityWithReference(app, require_messenger.buildConversationReference(ref), activityId, {
|
|
1074
|
+
type: "message",
|
|
1075
|
+
id: activityId,
|
|
1076
|
+
text
|
|
1077
|
+
}, { serviceUrlBoundary: sdkCloudOptions });
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
const classification = require_setup_surface.classifyMSTeamsSendError(err);
|
|
1080
|
+
const hint = require_setup_surface.formatMSTeamsSendErrorHint(classification);
|
|
1081
|
+
const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
|
|
1082
|
+
throw new Error(`msteams edit failed${status}: ${require_setup_surface.formatUnknownError(err)}${hint ? ` (${hint})` : ""}`, { cause: err });
|
|
1083
|
+
}
|
|
1084
|
+
log.info("edited proactive message", {
|
|
1085
|
+
conversationId,
|
|
1086
|
+
activityId
|
|
1087
|
+
});
|
|
1088
|
+
return { conversationId };
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Delete a previously sent message in a Teams conversation.
|
|
1092
|
+
*
|
|
1093
|
+
* Uses the Bot Framework REST API for proactive deletes outside of the
|
|
1094
|
+
* original turn context.
|
|
1095
|
+
*/
|
|
1096
|
+
async function deleteMessageMSTeams(params) {
|
|
1097
|
+
const { cfg, to, activityId } = params;
|
|
1098
|
+
const { app, conversationId, ref, log, sdkCloudOptions } = await resolveMSTeamsSendContext({
|
|
1099
|
+
cfg,
|
|
1100
|
+
to
|
|
1101
|
+
});
|
|
1102
|
+
log.debug?.("deleting proactive message", {
|
|
1103
|
+
conversationId,
|
|
1104
|
+
activityId
|
|
1105
|
+
});
|
|
1106
|
+
try {
|
|
1107
|
+
await require_messenger.deleteMSTeamsActivityWithReference(app, require_messenger.buildConversationReference(ref), activityId, { serviceUrlBoundary: sdkCloudOptions });
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
const classification = require_setup_surface.classifyMSTeamsSendError(err);
|
|
1110
|
+
const hint = require_setup_surface.formatMSTeamsSendErrorHint(classification);
|
|
1111
|
+
const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
|
|
1112
|
+
throw new Error(`msteams delete failed${status}: ${require_setup_surface.formatUnknownError(err)}${hint ? ` (${hint})` : ""}`, { cause: err });
|
|
1113
|
+
}
|
|
1114
|
+
log.info("deleted proactive message", {
|
|
1115
|
+
conversationId,
|
|
1116
|
+
activityId
|
|
1117
|
+
});
|
|
1118
|
+
return { conversationId };
|
|
1119
|
+
}
|
|
1120
|
+
//#endregion
|
|
1121
|
+
//#region extensions/msteams/src/outbound.ts
|
|
1122
|
+
function asObjectRecord(value) {
|
|
1123
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1124
|
+
}
|
|
1125
|
+
const MSTEAMS_TEXT_CHUNK_LIMIT = 4e3;
|
|
1126
|
+
function resolveMSTeamsTextSend(params) {
|
|
1127
|
+
return (0, openclaw_plugin_sdk_channel_outbound.resolveOutboundSendDep)(params.deps, "msteams") ?? ((to, text) => sendMessageMSTeams({
|
|
1128
|
+
cfg: params.cfg,
|
|
1129
|
+
to,
|
|
1130
|
+
text
|
|
1131
|
+
}));
|
|
1132
|
+
}
|
|
1133
|
+
function resolveMSTeamsMediaSend(params) {
|
|
1134
|
+
return (0, openclaw_plugin_sdk_channel_outbound.resolveOutboundSendDep)(params.deps, "msteams") ?? ((to, text, opts) => sendMessageMSTeams({
|
|
1135
|
+
cfg: params.cfg,
|
|
1136
|
+
to,
|
|
1137
|
+
text,
|
|
1138
|
+
mediaUrl: opts?.mediaUrl,
|
|
1139
|
+
mediaLocalRoots: opts?.mediaLocalRoots,
|
|
1140
|
+
mediaReadFile: opts?.mediaReadFile
|
|
1141
|
+
}));
|
|
1142
|
+
}
|
|
1143
|
+
const msteamsOutbound = {
|
|
1144
|
+
deliveryMode: "direct",
|
|
1145
|
+
chunker: openclaw_plugin_sdk_text_chunking.chunkTextForOutbound,
|
|
1146
|
+
chunkerMode: "markdown",
|
|
1147
|
+
textChunkLimit: MSTEAMS_TEXT_CHUNK_LIMIT,
|
|
1148
|
+
pollMaxOptions: 12,
|
|
1149
|
+
deliveryCapabilities: { durableFinal: {
|
|
1150
|
+
text: true,
|
|
1151
|
+
media: true,
|
|
1152
|
+
payload: true,
|
|
1153
|
+
messageSendingHooks: true
|
|
1154
|
+
} },
|
|
1155
|
+
presentationCapabilities: require_channel.MSTEAMS_PRESENTATION_CAPABILITIES,
|
|
1156
|
+
renderPresentation: ({ payload, presentation }) => {
|
|
1157
|
+
if (payload.mediaUrl || payload.mediaUrls?.length) return null;
|
|
1158
|
+
const card = require_channel.buildMSTeamsPresentationCard({
|
|
1159
|
+
presentation,
|
|
1160
|
+
text: payload.text
|
|
1161
|
+
});
|
|
1162
|
+
const msteamsData = asObjectRecord(payload.channelData?.msteams) ?? {};
|
|
1163
|
+
return {
|
|
1164
|
+
...payload,
|
|
1165
|
+
channelData: {
|
|
1166
|
+
...payload.channelData,
|
|
1167
|
+
msteams: {
|
|
1168
|
+
...msteamsData,
|
|
1169
|
+
presentationCard: card
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
},
|
|
1174
|
+
sendPayload: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, mediaReadFile, payload, deps, onDeliveryResult }) => {
|
|
1175
|
+
const presentationCard = asObjectRecord(payload.channelData?.msteams)?.presentationCard;
|
|
1176
|
+
if (presentationCard && typeof presentationCard === "object" && !Array.isArray(presentationCard)) return (0, openclaw_plugin_sdk_channel_send_result.attachChannelToResult)("msteams", await sendAdaptiveCardMSTeams({
|
|
1177
|
+
cfg,
|
|
1178
|
+
to,
|
|
1179
|
+
card: presentationCard
|
|
1180
|
+
}));
|
|
1181
|
+
const mediaUrls = (0, openclaw_plugin_sdk_string_normalization_runtime.normalizeStringEntries)((0, openclaw_plugin_sdk_reply_payload.resolvePayloadMediaUrls)({
|
|
1182
|
+
...payload,
|
|
1183
|
+
mediaUrl: payload.mediaUrl ?? mediaUrl
|
|
1184
|
+
}));
|
|
1185
|
+
if (mediaUrls.length > 0) {
|
|
1186
|
+
const send = resolveMSTeamsMediaSend({
|
|
1187
|
+
cfg,
|
|
1188
|
+
deps
|
|
1189
|
+
});
|
|
1190
|
+
const result = await (0, openclaw_plugin_sdk_reply_payload.sendPayloadMediaSequence)({
|
|
1191
|
+
text,
|
|
1192
|
+
mediaUrls,
|
|
1193
|
+
onResult: async (deliveryResult) => {
|
|
1194
|
+
await onDeliveryResult?.((0, openclaw_plugin_sdk_channel_send_result.attachChannelToResult)("msteams", deliveryResult));
|
|
1195
|
+
},
|
|
1196
|
+
send: async ({ text: textLocal, mediaUrl: mediaUrlLocal }) => await send(to, textLocal, {
|
|
1197
|
+
mediaUrl: mediaUrlLocal,
|
|
1198
|
+
mediaLocalRoots,
|
|
1199
|
+
mediaReadFile
|
|
1200
|
+
})
|
|
1201
|
+
});
|
|
1202
|
+
if (result) return (0, openclaw_plugin_sdk_channel_send_result.attachChannelToResult)("msteams", result);
|
|
1203
|
+
}
|
|
1204
|
+
if (text.trim()) {
|
|
1205
|
+
const send = resolveMSTeamsTextSend({
|
|
1206
|
+
cfg,
|
|
1207
|
+
deps
|
|
1208
|
+
});
|
|
1209
|
+
const chunks = (0, openclaw_plugin_sdk_reply_payload.resolveTextChunksWithFallback)(text, (0, openclaw_plugin_sdk_text_chunking.chunkTextForOutbound)(text, MSTEAMS_TEXT_CHUNK_LIMIT));
|
|
1210
|
+
let result;
|
|
1211
|
+
for (const chunk of chunks) {
|
|
1212
|
+
result = await send(to, chunk);
|
|
1213
|
+
await onDeliveryResult?.((0, openclaw_plugin_sdk_channel_send_result.attachChannelToResult)("msteams", result));
|
|
1214
|
+
}
|
|
1215
|
+
return (0, openclaw_plugin_sdk_channel_send_result.attachChannelToResult)("msteams", result);
|
|
1216
|
+
}
|
|
1217
|
+
throw new Error("MS Teams payload send requires text, media, or a presentation card.");
|
|
1218
|
+
},
|
|
1219
|
+
...(0, openclaw_plugin_sdk_channel_send_result.createAttachedChannelResultAdapter)({
|
|
1220
|
+
channel: "msteams",
|
|
1221
|
+
sendText: async ({ cfg, to, text, deps }) => {
|
|
1222
|
+
return await resolveMSTeamsTextSend({
|
|
1223
|
+
cfg,
|
|
1224
|
+
deps
|
|
1225
|
+
})(to, text);
|
|
1226
|
+
},
|
|
1227
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, mediaReadFile, deps }) => {
|
|
1228
|
+
return await resolveMSTeamsMediaSend({
|
|
1229
|
+
cfg,
|
|
1230
|
+
deps
|
|
1231
|
+
})(to, text, {
|
|
1232
|
+
mediaUrl,
|
|
1233
|
+
mediaLocalRoots,
|
|
1234
|
+
mediaReadFile
|
|
1235
|
+
});
|
|
1236
|
+
},
|
|
1237
|
+
sendPoll: async ({ cfg, to, poll }) => {
|
|
1238
|
+
const maxSelections = poll.maxSelections ?? 1;
|
|
1239
|
+
const result = await sendPollMSTeams({
|
|
1240
|
+
cfg,
|
|
1241
|
+
to,
|
|
1242
|
+
question: poll.question,
|
|
1243
|
+
options: poll.options,
|
|
1244
|
+
maxSelections
|
|
1245
|
+
});
|
|
1246
|
+
await require_polls.createMSTeamsPollStoreState().createPoll({
|
|
1247
|
+
id: result.pollId,
|
|
1248
|
+
question: poll.question,
|
|
1249
|
+
options: poll.options,
|
|
1250
|
+
maxSelections,
|
|
1251
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1252
|
+
conversationId: result.conversationId,
|
|
1253
|
+
messageId: result.messageId,
|
|
1254
|
+
votes: {}
|
|
1255
|
+
});
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
1258
|
+
})
|
|
1259
|
+
};
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region extensions/msteams/src/probe.ts
|
|
1262
|
+
function decodeJwtPayload(token) {
|
|
1263
|
+
const parts = token.split(".");
|
|
1264
|
+
if (parts.length < 2) return null;
|
|
1265
|
+
const payload = parts[1] ?? "";
|
|
1266
|
+
const normalized = payload.padEnd(payload.length + (4 - payload.length % 4) % 4, "=").replace(/-/g, "+").replace(/_/g, "/");
|
|
1267
|
+
try {
|
|
1268
|
+
const decoded = Buffer.from(normalized, "base64").toString("utf8");
|
|
1269
|
+
const parsed = JSON.parse(decoded);
|
|
1270
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
1271
|
+
} catch {
|
|
1272
|
+
return null;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
function readStringArray(value) {
|
|
1276
|
+
if (!Array.isArray(value)) return;
|
|
1277
|
+
const out = (0, openclaw_plugin_sdk_string_normalization_runtime.normalizeStringEntries)(value);
|
|
1278
|
+
return out.length > 0 ? out : void 0;
|
|
1279
|
+
}
|
|
1280
|
+
function readScopes(value) {
|
|
1281
|
+
if (typeof value !== "string") return;
|
|
1282
|
+
const out = (0, openclaw_plugin_sdk_string_normalization_runtime.normalizeStringEntries)(value.split(/\s+/));
|
|
1283
|
+
return out.length > 0 ? out : void 0;
|
|
1284
|
+
}
|
|
1285
|
+
async function probeMSTeams(cfg) {
|
|
1286
|
+
const creds = require_resolve_allowlist.resolveMSTeamsCredentials(cfg);
|
|
1287
|
+
if (!creds) return {
|
|
1288
|
+
ok: false,
|
|
1289
|
+
error: "missing credentials (appId, appPassword, tenantId)"
|
|
1290
|
+
};
|
|
1291
|
+
try {
|
|
1292
|
+
const { app } = await require_resolve_allowlist.loadMSTeamsSdkWithAuth(creds, require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(cfg));
|
|
1293
|
+
const tokenProvider = require_resolve_allowlist.createMSTeamsTokenProvider(app);
|
|
1294
|
+
if (!await tokenProvider.getAccessToken("https://api.botframework.com")) throw new Error("Failed to acquire bot token");
|
|
1295
|
+
let graph;
|
|
1296
|
+
try {
|
|
1297
|
+
const accessToken = require_resolve_allowlist.readAccessToken(await tokenProvider.getAccessToken("https://graph.microsoft.com"));
|
|
1298
|
+
const payload = accessToken ? decodeJwtPayload(accessToken) : null;
|
|
1299
|
+
graph = {
|
|
1300
|
+
ok: true,
|
|
1301
|
+
roles: readStringArray(payload?.roles),
|
|
1302
|
+
scopes: readScopes(payload?.scp)
|
|
1303
|
+
};
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
graph = {
|
|
1306
|
+
ok: false,
|
|
1307
|
+
error: require_setup_surface.formatUnknownError(err)
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
let delegatedAuth;
|
|
1311
|
+
if (cfg?.delegatedAuth?.enabled) try {
|
|
1312
|
+
const tokens = require_resolve_allowlist.loadDelegatedTokens();
|
|
1313
|
+
if (tokens) {
|
|
1314
|
+
const isExpired = !(0, openclaw_plugin_sdk_number_runtime.isFutureDateTimestampMs)(tokens.expiresAt);
|
|
1315
|
+
delegatedAuth = {
|
|
1316
|
+
ok: !isExpired,
|
|
1317
|
+
scopes: tokens.scopes,
|
|
1318
|
+
userPrincipalName: tokens.userPrincipalName,
|
|
1319
|
+
...isExpired ? { error: "token expired (will auto-refresh on next use)" } : {}
|
|
1320
|
+
};
|
|
1321
|
+
} else delegatedAuth = {
|
|
1322
|
+
ok: false,
|
|
1323
|
+
error: "no delegated tokens found (run setup wizard)"
|
|
1324
|
+
};
|
|
1325
|
+
} catch {
|
|
1326
|
+
delegatedAuth = {
|
|
1327
|
+
ok: false,
|
|
1328
|
+
error: "failed to load delegated tokens"
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
return {
|
|
1332
|
+
ok: true,
|
|
1333
|
+
appId: creds.appId,
|
|
1334
|
+
...graph ? { graph } : {},
|
|
1335
|
+
...delegatedAuth ? { delegatedAuth } : {}
|
|
1336
|
+
};
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
return {
|
|
1339
|
+
ok: false,
|
|
1340
|
+
appId: creds.appId,
|
|
1341
|
+
error: require_setup_surface.formatUnknownError(err)
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
//#endregion
|
|
1346
|
+
//#region extensions/msteams/src/channel.runtime.ts
|
|
1347
|
+
const msTeamsChannelRuntime = {
|
|
1348
|
+
addParticipantMSTeams,
|
|
1349
|
+
deleteMessageMSTeams,
|
|
1350
|
+
editMessageMSTeams,
|
|
1351
|
+
getChannelInfoMSTeams,
|
|
1352
|
+
getMemberInfoMSTeams,
|
|
1353
|
+
getMessageMSTeams,
|
|
1354
|
+
listChannelsMSTeams,
|
|
1355
|
+
listPinsMSTeams,
|
|
1356
|
+
listReactionsMSTeams,
|
|
1357
|
+
pinMessageMSTeams,
|
|
1358
|
+
reactMessageMSTeams,
|
|
1359
|
+
removeParticipantMSTeams,
|
|
1360
|
+
renameGroupMSTeams,
|
|
1361
|
+
searchMessagesMSTeams,
|
|
1362
|
+
unpinMessageMSTeams,
|
|
1363
|
+
unreactMessageMSTeams,
|
|
1364
|
+
listMSTeamsDirectoryGroupsLive,
|
|
1365
|
+
listMSTeamsDirectoryPeersLive,
|
|
1366
|
+
msteamsOutbound: { ...msteamsOutbound },
|
|
1367
|
+
probeMSTeams,
|
|
1368
|
+
sendAdaptiveCardMSTeams,
|
|
1369
|
+
sendMessageMSTeams
|
|
1370
|
+
};
|
|
1371
|
+
//#endregion
|
|
1372
|
+
exports.msTeamsChannelRuntime = msTeamsChannelRuntime;
|