@kodelyth/msteams 2026.5.42 → 2026.6.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.
Files changed (177) hide show
  1. package/klaw.plugin.json +726 -2
  2. package/package.json +16 -4
  3. package/api.ts +0 -3
  4. package/channel-config-api.ts +0 -1
  5. package/channel-plugin-api.ts +0 -2
  6. package/config-api.ts +0 -4
  7. package/contract-api.ts +0 -4
  8. package/index.ts +0 -20
  9. package/runtime-api.ts +0 -66
  10. package/secret-contract-api.ts +0 -5
  11. package/setup-entry.ts +0 -13
  12. package/setup-plugin-api.ts +0 -3
  13. package/src/ai-entity.ts +0 -7
  14. package/src/approval-auth.ts +0 -44
  15. package/src/attachments/bot-framework.test.ts +0 -506
  16. package/src/attachments/bot-framework.ts +0 -348
  17. package/src/attachments/download.ts +0 -328
  18. package/src/attachments/graph.test.ts +0 -441
  19. package/src/attachments/graph.ts +0 -489
  20. package/src/attachments/html.ts +0 -122
  21. package/src/attachments/payload.ts +0 -14
  22. package/src/attachments/remote-media.test.ts +0 -187
  23. package/src/attachments/remote-media.ts +0 -86
  24. package/src/attachments/shared.test.ts +0 -547
  25. package/src/attachments/shared.ts +0 -655
  26. package/src/attachments/types.ts +0 -47
  27. package/src/attachments.graph.test.ts +0 -414
  28. package/src/attachments.helpers.test.ts +0 -245
  29. package/src/attachments.test-helpers.ts +0 -17
  30. package/src/attachments.test.ts +0 -754
  31. package/src/attachments.ts +0 -18
  32. package/src/block-streaming-config.test.ts +0 -61
  33. package/src/channel-api.ts +0 -1
  34. package/src/channel.actions.test.ts +0 -797
  35. package/src/channel.directory.test.ts +0 -176
  36. package/src/channel.message-adapter.test.ts +0 -227
  37. package/src/channel.runtime.ts +0 -56
  38. package/src/channel.setup.ts +0 -77
  39. package/src/channel.test.ts +0 -136
  40. package/src/channel.ts +0 -1176
  41. package/src/config-schema.ts +0 -6
  42. package/src/config-ui-hints.ts +0 -40
  43. package/src/conversation-store-fs.test.ts +0 -81
  44. package/src/conversation-store-fs.ts +0 -149
  45. package/src/conversation-store-helpers.test.ts +0 -202
  46. package/src/conversation-store-helpers.ts +0 -105
  47. package/src/conversation-store-memory.ts +0 -51
  48. package/src/conversation-store.shared.test.ts +0 -260
  49. package/src/conversation-store.ts +0 -71
  50. package/src/directory-live.test.ts +0 -156
  51. package/src/directory-live.ts +0 -111
  52. package/src/doctor.ts +0 -27
  53. package/src/errors.test.ts +0 -154
  54. package/src/errors.ts +0 -270
  55. package/src/feedback-reflection-prompt.ts +0 -117
  56. package/src/feedback-reflection-store.ts +0 -113
  57. package/src/feedback-reflection.test.ts +0 -237
  58. package/src/feedback-reflection.ts +0 -268
  59. package/src/file-consent-helpers.test.ts +0 -328
  60. package/src/file-consent-helpers.ts +0 -115
  61. package/src/file-consent-invoke.ts +0 -150
  62. package/src/file-consent.test.ts +0 -378
  63. package/src/file-consent.ts +0 -223
  64. package/src/graph-chat.ts +0 -36
  65. package/src/graph-group-management.test.ts +0 -332
  66. package/src/graph-group-management.ts +0 -168
  67. package/src/graph-members.test.ts +0 -89
  68. package/src/graph-members.ts +0 -48
  69. package/src/graph-messages.actions.test.ts +0 -253
  70. package/src/graph-messages.read.test.ts +0 -391
  71. package/src/graph-messages.search.test.ts +0 -227
  72. package/src/graph-messages.test-helpers.ts +0 -50
  73. package/src/graph-messages.ts +0 -534
  74. package/src/graph-teams.test.ts +0 -222
  75. package/src/graph-teams.ts +0 -114
  76. package/src/graph-thread.test.ts +0 -252
  77. package/src/graph-thread.ts +0 -146
  78. package/src/graph-upload.test.ts +0 -253
  79. package/src/graph-upload.ts +0 -531
  80. package/src/graph-users.ts +0 -29
  81. package/src/graph.test.ts +0 -540
  82. package/src/graph.ts +0 -308
  83. package/src/inbound.test.ts +0 -221
  84. package/src/inbound.ts +0 -148
  85. package/src/index.ts +0 -4
  86. package/src/media-helpers.test.ts +0 -220
  87. package/src/media-helpers.ts +0 -105
  88. package/src/mentions.test.ts +0 -254
  89. package/src/mentions.ts +0 -114
  90. package/src/messenger.test.ts +0 -961
  91. package/src/messenger.ts +0 -608
  92. package/src/monitor-handler/access.ts +0 -136
  93. package/src/monitor-handler/inbound-media.test.ts +0 -314
  94. package/src/monitor-handler/inbound-media.ts +0 -180
  95. package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
  96. package/src/monitor-handler/message-handler.authz.test.ts +0 -739
  97. package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
  98. package/src/monitor-handler/message-handler.test-support.ts +0 -99
  99. package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -225
  100. package/src/monitor-handler/message-handler.thread-session.test.ts +0 -132
  101. package/src/monitor-handler/message-handler.ts +0 -1003
  102. package/src/monitor-handler/reaction-handler.test.ts +0 -325
  103. package/src/monitor-handler/reaction-handler.ts +0 -122
  104. package/src/monitor-handler/thread-session.ts +0 -30
  105. package/src/monitor-handler.adaptive-card.test.ts +0 -158
  106. package/src/monitor-handler.feedback-authz.test.ts +0 -357
  107. package/src/monitor-handler.file-consent.test.ts +0 -443
  108. package/src/monitor-handler.sso.test.ts +0 -576
  109. package/src/monitor-handler.test-helpers.ts +0 -181
  110. package/src/monitor-handler.ts +0 -538
  111. package/src/monitor-handler.types.ts +0 -27
  112. package/src/monitor-types.ts +0 -6
  113. package/src/monitor.lifecycle.test.ts +0 -457
  114. package/src/monitor.test.ts +0 -119
  115. package/src/monitor.ts +0 -476
  116. package/src/oauth.flow.ts +0 -77
  117. package/src/oauth.shared.ts +0 -37
  118. package/src/oauth.test.ts +0 -350
  119. package/src/oauth.token.ts +0 -162
  120. package/src/oauth.ts +0 -130
  121. package/src/outbound.test.ts +0 -400
  122. package/src/outbound.ts +0 -198
  123. package/src/pending-uploads-fs.test.ts +0 -261
  124. package/src/pending-uploads-fs.ts +0 -235
  125. package/src/pending-uploads.test.ts +0 -186
  126. package/src/pending-uploads.ts +0 -121
  127. package/src/policy.test.ts +0 -156
  128. package/src/policy.ts +0 -245
  129. package/src/polls-store-memory.ts +0 -32
  130. package/src/polls.test.ts +0 -169
  131. package/src/polls.ts +0 -312
  132. package/src/presentation.ts +0 -93
  133. package/src/probe.test.ts +0 -79
  134. package/src/probe.ts +0 -132
  135. package/src/reply-dispatcher.test.ts +0 -543
  136. package/src/reply-dispatcher.ts +0 -523
  137. package/src/reply-stream-controller.test.ts +0 -424
  138. package/src/reply-stream-controller.ts +0 -334
  139. package/src/resolve-allowlist.test.ts +0 -253
  140. package/src/resolve-allowlist.ts +0 -309
  141. package/src/revoked-context.ts +0 -17
  142. package/src/runtime.ts +0 -12
  143. package/src/sdk-types.ts +0 -59
  144. package/src/sdk.test.ts +0 -727
  145. package/src/sdk.ts +0 -916
  146. package/src/secret-contract.ts +0 -49
  147. package/src/secret-input.ts +0 -7
  148. package/src/send-context.test.ts +0 -93
  149. package/src/send-context.ts +0 -269
  150. package/src/send.test.ts +0 -588
  151. package/src/send.ts +0 -697
  152. package/src/sent-message-cache.test.ts +0 -106
  153. package/src/sent-message-cache.ts +0 -174
  154. package/src/session-route.ts +0 -40
  155. package/src/setup-core.ts +0 -162
  156. package/src/setup-surface.test.ts +0 -175
  157. package/src/setup-surface.ts +0 -319
  158. package/src/sso-token-store.test.ts +0 -74
  159. package/src/sso-token-store.ts +0 -166
  160. package/src/sso.ts +0 -300
  161. package/src/storage.ts +0 -25
  162. package/src/store-fs.ts +0 -42
  163. package/src/streaming-message.test.ts +0 -323
  164. package/src/streaming-message.ts +0 -327
  165. package/src/test-runtime.ts +0 -16
  166. package/src/thread-parent-context.test.ts +0 -224
  167. package/src/thread-parent-context.ts +0 -159
  168. package/src/token-response.ts +0 -11
  169. package/src/token.test.ts +0 -268
  170. package/src/token.ts +0 -194
  171. package/src/user-agent.test.ts +0 -121
  172. package/src/user-agent.ts +0 -53
  173. package/src/webhook-timeouts.ts +0 -27
  174. package/src/welcome-card.test.ts +0 -104
  175. package/src/welcome-card.ts +0 -57
  176. package/test-api.ts +0 -1
  177. package/tsconfig.json +0 -16
@@ -1,534 +0,0 @@
1
- import type { KlawConfig } from "../runtime-api.js";
2
- import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
3
- import {
4
- type GraphResponse,
5
- deleteGraphRequest,
6
- escapeOData,
7
- fetchGraphAbsoluteUrl,
8
- fetchGraphJson,
9
- postGraphBetaJson,
10
- postGraphJson,
11
- resolveGraphToken,
12
- } from "./graph.js";
13
-
14
- type GraphMessageBody = {
15
- content?: string;
16
- contentType?: string;
17
- };
18
-
19
- type GraphMessageFrom = {
20
- user?: { id?: string; displayName?: string };
21
- application?: { id?: string; displayName?: string };
22
- };
23
-
24
- type GraphMessage = {
25
- id?: string;
26
- body?: GraphMessageBody;
27
- from?: GraphMessageFrom;
28
- createdDateTime?: string;
29
- };
30
-
31
- type GraphPinnedMessage = {
32
- id?: string;
33
- message?: GraphMessage;
34
- };
35
-
36
- type GraphPinnedMessagesResponse = {
37
- value?: GraphPinnedMessage[];
38
- "@odata.nextLink"?: string;
39
- };
40
-
41
- /**
42
- * Resolve the Graph API path prefix for a conversation.
43
- * If `to` contains "/" it's a `teamId/channelId` (channel path),
44
- * otherwise it's a chat ID.
45
- */
46
- /**
47
- * Strip common target prefixes (`conversation:`, `user:`) so raw
48
- * conversation IDs can be used directly in Graph paths.
49
- */
50
- function stripTargetPrefix(raw: string): string {
51
- const trimmed = raw.trim();
52
- if (/^conversation:/i.test(trimmed)) {
53
- return trimmed.slice("conversation:".length).trim();
54
- }
55
- if (/^user:/i.test(trimmed)) {
56
- return trimmed.slice("user:".length).trim();
57
- }
58
- return trimmed;
59
- }
60
-
61
- /**
62
- * Resolve a target to a Graph-compatible conversation ID.
63
- * `user:<aadId>` targets are looked up in the conversation store to find the
64
- * actual `19:xxx@thread.*` chat ID that Graph API requires.
65
- * Conversation IDs and `teamId/channelId` pairs pass through unchanged.
66
- */
67
- export async function resolveGraphConversationId(to: string): Promise<string> {
68
- const trimmed = to.trim();
69
- const isUserTarget = /^user:/i.test(trimmed);
70
- const cleaned = stripTargetPrefix(trimmed);
71
-
72
- // teamId/channelId or already a conversation ID (19:xxx) — use directly
73
- if (!isUserTarget) {
74
- return cleaned;
75
- }
76
-
77
- // user:<aadId> — look up the conversation store for the real chat ID
78
- const store = createMSTeamsConversationStoreFs();
79
- const found = await store.findPreferredDmByUserId(cleaned);
80
- if (!found) {
81
- throw new Error(
82
- `No conversation found for user:${cleaned}. ` +
83
- "The bot must receive a message from this user before Graph API operations work.",
84
- );
85
- }
86
-
87
- // Prefer the cached Graph-native chat ID (19:xxx format) over the Bot Framework
88
- // conversation ID, which may be in a non-Graph format (a:xxx / 8:orgid:xxx) for
89
- // personal DMs. send-context.ts resolves and caches this on first send.
90
- if (found.reference.graphChatId) {
91
- return found.reference.graphChatId;
92
- }
93
- if (found.conversationId.startsWith("19:")) {
94
- return found.conversationId;
95
- }
96
- throw new Error(
97
- `Conversation for user:${cleaned} uses a Bot Framework ID (${found.conversationId}) ` +
98
- "that Graph API does not accept. Send a message to this user first so the Graph chat ID is cached.",
99
- );
100
- }
101
-
102
- export function resolveConversationPath(to: string): {
103
- kind: "chat" | "channel";
104
- basePath: string;
105
- chatId?: string;
106
- teamId?: string;
107
- channelId?: string;
108
- } {
109
- const cleaned = stripTargetPrefix(to);
110
- if (cleaned.includes("/")) {
111
- const [teamId, channelId] = cleaned.split("/", 2);
112
- return {
113
- kind: "channel",
114
- basePath: `/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}`,
115
- teamId,
116
- channelId,
117
- };
118
- }
119
- // Conversation IDs like 19:xxx@thread.tacv2 may represent either group chats
120
- // or channel threads. Without a teamId/channelId pair (format "teamId/channelId")
121
- // we route through /chats/{id} which works for group chats and 1:1 DMs.
122
- // Channel operations that require /teams/{teamId}/channels/{channelId} paths
123
- // must be called with the explicit teamId/channelId target format.
124
- return {
125
- kind: "chat",
126
- basePath: `/chats/${encodeURIComponent(cleaned)}`,
127
- chatId: cleaned,
128
- };
129
- }
130
-
131
- export type GetMessageMSTeamsParams = {
132
- cfg: KlawConfig;
133
- to: string;
134
- messageId: string;
135
- };
136
-
137
- export type GetMessageMSTeamsResult = {
138
- id: string;
139
- text: string | undefined;
140
- from: GraphMessageFrom | undefined;
141
- createdAt: string | undefined;
142
- };
143
-
144
- /**
145
- * Retrieve a single message by ID from a chat or channel via Graph API.
146
- */
147
- export async function getMessageMSTeams(
148
- params: GetMessageMSTeamsParams,
149
- ): Promise<GetMessageMSTeamsResult> {
150
- const token = await resolveGraphToken(params.cfg);
151
- const conversationId = await resolveGraphConversationId(params.to);
152
- const { basePath } = resolveConversationPath(conversationId);
153
- const path = `${basePath}/messages/${encodeURIComponent(params.messageId)}`;
154
- const msg = await fetchGraphJson<GraphMessage>({ token, path });
155
- return {
156
- id: msg.id ?? params.messageId,
157
- text: msg.body?.content,
158
- from: msg.from,
159
- createdAt: msg.createdDateTime,
160
- };
161
- }
162
-
163
- export type PinMessageMSTeamsParams = {
164
- cfg: KlawConfig;
165
- to: string;
166
- messageId: string;
167
- };
168
-
169
- /**
170
- * Pin a message in a chat conversation via Graph API.
171
- *
172
- * Chat pinning uses the v1.0 endpoint: `POST /chats/{chatId}/pinnedMessages`.
173
- *
174
- * Channel pinning uses `POST /teams/{teamId}/channels/{channelId}/pinnedMessages`.
175
- * **Note:** The channel pin endpoint may require the Graph beta API or specific
176
- * tenant-level permissions. As of March 2026, general availability is not
177
- * confirmed for all tenants. If the call returns 404 or 403, the endpoint may
178
- * not be enabled for the target tenant.
179
- */
180
- export async function pinMessageMSTeams(
181
- params: PinMessageMSTeamsParams,
182
- ): Promise<{ ok: true; pinnedMessageId?: string }> {
183
- const token = await resolveGraphToken(params.cfg);
184
- const conversationId = await resolveGraphConversationId(params.to);
185
- const conv = resolveConversationPath(conversationId);
186
-
187
- if (conv.kind === "channel") {
188
- // Graph v1.0 does not expose pinnedMessages on channels — only on chats.
189
- // Attempting this would 404.
190
- throw new Error(
191
- "Pin/unpin is not supported for channel messages on Graph v1.0. " +
192
- "Only chat conversations support pinned messages.",
193
- );
194
- }
195
-
196
- // Graph API expects message@odata.bind with the full message resource URI
197
- const body = {
198
- "message@odata.bind": `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(params.messageId)}`,
199
- };
200
- const result = await postGraphJson<{ id?: string }>({
201
- token,
202
- path: `${conv.basePath}/pinnedMessages`,
203
- body,
204
- });
205
- return { ok: true, pinnedMessageId: result.id };
206
- }
207
-
208
- export type UnpinMessageMSTeamsParams = {
209
- cfg: KlawConfig;
210
- to: string;
211
- /** The pinned-message resource ID returned by pin or list-pins (not the message ID). */
212
- pinnedMessageId: string;
213
- };
214
-
215
- /**
216
- * Unpin a message in a chat conversation via Graph API.
217
- * `pinnedMessageId` is the pinned-message resource ID (from pin or list-pins),
218
- * not the underlying chat message ID.
219
- *
220
- * Channel unpin uses `DELETE /teams/{teamId}/channels/{channelId}/pinnedMessages/{id}`.
221
- * See the note on {@link pinMessageMSTeams} regarding beta/GA status.
222
- */
223
- export async function unpinMessageMSTeams(
224
- params: UnpinMessageMSTeamsParams,
225
- ): Promise<{ ok: true }> {
226
- const token = await resolveGraphToken(params.cfg);
227
- const conversationId = await resolveGraphConversationId(params.to);
228
- const conv = resolveConversationPath(conversationId);
229
- if (conv.kind === "channel") {
230
- throw new Error(
231
- "Pin/unpin is not supported for channel messages on Graph v1.0. " +
232
- "Only chat conversations support pinned messages.",
233
- );
234
- }
235
- const path = `${conv.basePath}/pinnedMessages/${encodeURIComponent(params.pinnedMessageId)}`;
236
- await deleteGraphRequest({ token, path });
237
- return { ok: true };
238
- }
239
-
240
- export type ListPinsMSTeamsParams = {
241
- cfg: KlawConfig;
242
- to: string;
243
- };
244
-
245
- export type ListPinsMSTeamsResult = {
246
- pins: Array<{ id: string; pinnedMessageId: string; messageId?: string; text?: string }>;
247
- };
248
-
249
- /** Maximum number of pagination pages to follow to avoid unbounded loops. */
250
- const LIST_PINS_MAX_PAGES = 10;
251
-
252
- /**
253
- * List all pinned messages in a chat conversation via Graph API.
254
- * Follows `@odata.nextLink` pagination to collect the full pin set.
255
- *
256
- * Channel list-pins uses the same endpoint pattern as channel pin/unpin.
257
- * See the note on {@link pinMessageMSTeams} regarding beta/GA status.
258
- */
259
- export async function listPinsMSTeams(
260
- params: ListPinsMSTeamsParams,
261
- ): Promise<ListPinsMSTeamsResult> {
262
- const token = await resolveGraphToken(params.cfg);
263
- const conversationId = await resolveGraphConversationId(params.to);
264
- const conv = resolveConversationPath(conversationId);
265
-
266
- if (conv.kind === "channel") {
267
- throw new Error(
268
- "Listing pinned messages is not supported for channels on Graph v1.0. " +
269
- "Only chat conversations support pinned messages.",
270
- );
271
- }
272
-
273
- const path = `${conv.basePath}/pinnedMessages?$expand=message`;
274
- const allPins: Array<{ id: string; pinnedMessageId: string; messageId?: string; text?: string }> =
275
- [];
276
-
277
- let res = await fetchGraphJson<GraphPinnedMessagesResponse>({ token, path });
278
- let pages = 1;
279
-
280
- while (true) {
281
- for (const pin of res.value ?? []) {
282
- allPins.push({
283
- id: pin.id ?? "",
284
- pinnedMessageId: pin.id ?? "",
285
- messageId: pin.message?.id,
286
- text: pin.message?.body?.content,
287
- });
288
- }
289
-
290
- const nextLink = res["@odata.nextLink"];
291
- if (!nextLink || pages >= LIST_PINS_MAX_PAGES) {
292
- break;
293
- }
294
-
295
- res = await fetchGraphAbsoluteUrl<GraphPinnedMessagesResponse>({ token, url: nextLink });
296
- pages++;
297
- }
298
-
299
- return { pins: allPins };
300
- }
301
-
302
- // ---------------------------------------------------------------------------
303
- // Reactions
304
- // ---------------------------------------------------------------------------
305
-
306
- export const TEAMS_REACTION_TYPES = [
307
- "like",
308
- "heart",
309
- "laugh",
310
- "surprised",
311
- "sad",
312
- "angry",
313
- ] as const;
314
- export type TeamsReactionType = (typeof TEAMS_REACTION_TYPES)[number];
315
-
316
- type GraphReaction = {
317
- reactionType?: string;
318
- user?: { id?: string; displayName?: string };
319
- createdDateTime?: string;
320
- };
321
-
322
- type GraphMessageWithReactions = GraphMessage & {
323
- reactions?: GraphReaction[];
324
- };
325
-
326
- export type ReactMessageMSTeamsParams = {
327
- cfg: KlawConfig;
328
- to: string;
329
- messageId: string;
330
- reactionType: string;
331
- };
332
-
333
- export type ListReactionsMSTeamsParams = {
334
- cfg: KlawConfig;
335
- to: string;
336
- messageId: string;
337
- };
338
-
339
- /** Map well-known reaction type names to representative emoji for CLI display. */
340
- const REACTION_TYPE_EMOJI: Record<string, string> = {
341
- like: "\u{1F44D}",
342
- heart: "\u2764\uFE0F",
343
- laugh: "\u{1F606}",
344
- surprised: "\u{1F62E}",
345
- sad: "\u{1F622}",
346
- angry: "\u{1F621}",
347
- };
348
-
349
- export type ReactionSummary = {
350
- reactionType: string;
351
- /** Display name for the reaction (matches reactionType for known types). */
352
- name: string;
353
- /** Emoji representation when available. */
354
- emoji?: string;
355
- count: number;
356
- users: Array<{ id: string; displayName?: string }>;
357
- };
358
-
359
- export type ListReactionsMSTeamsResult = {
360
- reactions: ReactionSummary[];
361
- };
362
-
363
- /**
364
- * Normalize a reaction type string. Graph setReaction/unsetReaction accepts
365
- * the well-known legacy names (like, heart, laugh, surprised, sad, angry)
366
- * as well as Unicode emoji values — so we pass unknown types through rather
367
- * than rejecting them.
368
- */
369
- function normalizeReactionType(raw: string): string {
370
- const normalized = raw.trim();
371
- if (!normalized) {
372
- throw new Error(`Reaction type is required. Common types: ${TEAMS_REACTION_TYPES.join(", ")}`);
373
- }
374
- // Lowercase only the well-known names; Unicode emoji should pass through as-is
375
- const lowered = normalized.toLowerCase();
376
- if (TEAMS_REACTION_TYPES.includes(lowered as TeamsReactionType)) {
377
- return lowered;
378
- }
379
- return normalized;
380
- }
381
-
382
- /**
383
- * Add an emoji reaction to a message via Graph API (beta).
384
- *
385
- * Writes (setReaction) require a Delegated token, so we pass
386
- * `preferDelegated: true`. The resolver falls back to the app-only token when
387
- * delegated auth is not configured, preserving today's behavior while letting
388
- * delegated-auth-enabled deployments hit the user-scoped endpoint.
389
- */
390
- export async function reactMessageMSTeams(
391
- params: ReactMessageMSTeamsParams,
392
- ): Promise<{ ok: true }> {
393
- const reactionType = normalizeReactionType(params.reactionType);
394
- const token = await resolveGraphToken(params.cfg, { preferDelegated: true });
395
- const conversationId = await resolveGraphConversationId(params.to);
396
- const { basePath } = resolveConversationPath(conversationId);
397
- const path = `${basePath}/messages/${encodeURIComponent(params.messageId)}/setReaction`;
398
- await postGraphBetaJson<unknown>({ token, path, body: { reactionType } });
399
- return { ok: true };
400
- }
401
-
402
- /**
403
- * Remove an emoji reaction from a message via Graph API (beta).
404
- *
405
- * Writes (unsetReaction) require a Delegated token, so we pass
406
- * `preferDelegated: true`. See `reactMessageMSTeams` for fallback rules.
407
- */
408
- export async function unreactMessageMSTeams(
409
- params: ReactMessageMSTeamsParams,
410
- ): Promise<{ ok: true }> {
411
- const reactionType = normalizeReactionType(params.reactionType);
412
- const token = await resolveGraphToken(params.cfg, { preferDelegated: true });
413
- const conversationId = await resolveGraphConversationId(params.to);
414
- const { basePath } = resolveConversationPath(conversationId);
415
- const path = `${basePath}/messages/${encodeURIComponent(params.messageId)}/unsetReaction`;
416
- await postGraphBetaJson<unknown>({ token, path, body: { reactionType } });
417
- return { ok: true };
418
- }
419
-
420
- /**
421
- * List reactions on a message, grouped by type.
422
- * Uses Graph v1.0 (reactions are included in the message resource).
423
- */
424
- export async function listReactionsMSTeams(
425
- params: ListReactionsMSTeamsParams,
426
- ): Promise<ListReactionsMSTeamsResult> {
427
- const token = await resolveGraphToken(params.cfg);
428
- const conversationId = await resolveGraphConversationId(params.to);
429
- const { basePath } = resolveConversationPath(conversationId);
430
- const path = `${basePath}/messages/${encodeURIComponent(params.messageId)}`;
431
- const msg = await fetchGraphJson<GraphMessageWithReactions>({ token, path });
432
-
433
- const grouped = new Map<
434
- string,
435
- { count: number; users: Array<{ id: string; displayName?: string }> }
436
- >();
437
- for (const reaction of msg.reactions ?? []) {
438
- const type = reaction.reactionType ?? "unknown";
439
- if (!grouped.has(type)) {
440
- grouped.set(type, { count: 0, users: [] });
441
- }
442
- const group = grouped.get(type)!;
443
- // Count every reaction regardless of whether the user ID is present
444
- // (deleted accounts, guests, or anonymous users may lack a user ID)
445
- group.count++;
446
- if (reaction.user?.id) {
447
- group.users.push({
448
- id: reaction.user.id,
449
- displayName: reaction.user.displayName,
450
- });
451
- }
452
- }
453
-
454
- const reactions: ReactionSummary[] = Array.from(grouped.entries()).map(([type, group]) => ({
455
- reactionType: type,
456
- name: type,
457
- emoji: REACTION_TYPE_EMOJI[type],
458
- count: group.count,
459
- users: group.users,
460
- }));
461
-
462
- return { reactions };
463
- }
464
-
465
- // ---------------------------------------------------------------------------
466
- // Search
467
- // ---------------------------------------------------------------------------
468
-
469
- export type SearchMessagesMSTeamsParams = {
470
- cfg: KlawConfig;
471
- to: string;
472
- query: string;
473
- from?: string;
474
- limit?: number;
475
- };
476
-
477
- export type SearchMessagesMSTeamsResult = {
478
- messages: Array<{
479
- id: string;
480
- text: string | undefined;
481
- from: GraphMessageFrom | undefined;
482
- createdAt: string | undefined;
483
- }>;
484
- };
485
-
486
- const SEARCH_DEFAULT_LIMIT = 25;
487
- const SEARCH_MAX_LIMIT = 50;
488
-
489
- /**
490
- * Search messages in a chat or channel by content via Graph API.
491
- * Uses `$search` for full-text body search and optional `$filter` for sender.
492
- */
493
- export async function searchMessagesMSTeams(
494
- params: SearchMessagesMSTeamsParams,
495
- ): Promise<SearchMessagesMSTeamsResult> {
496
- const token = await resolveGraphToken(params.cfg);
497
- const conversationId = await resolveGraphConversationId(params.to);
498
- const { basePath } = resolveConversationPath(conversationId);
499
-
500
- const rawLimit = params.limit ?? SEARCH_DEFAULT_LIMIT;
501
- const top = Number.isFinite(rawLimit)
502
- ? Math.min(Math.max(Math.floor(rawLimit), 1), SEARCH_MAX_LIMIT)
503
- : SEARCH_DEFAULT_LIMIT;
504
-
505
- // Strip double quotes from the query to prevent OData $search injection
506
- const sanitizedQuery = params.query.replace(/"/g, "");
507
-
508
- // Build query string manually (not URLSearchParams) to preserve literal $
509
- // in OData parameter names, consistent with other Graph calls in this module.
510
- const parts = [`$search=${encodeURIComponent(`"${sanitizedQuery}"`)}`];
511
- parts.push(`$top=${top}`);
512
- if (params.from) {
513
- parts.push(
514
- `$filter=${encodeURIComponent(`from/user/displayName eq '${escapeOData(params.from)}'`)}`,
515
- );
516
- }
517
-
518
- const path = `${basePath}/messages?${parts.join("&")}`;
519
- // ConsistencyLevel: eventual is required by Graph API for $search queries
520
- const res = await fetchGraphJson<GraphResponse<GraphMessage>>({
521
- token,
522
- path,
523
- headers: { ConsistencyLevel: "eventual" },
524
- });
525
-
526
- const messages = (res.value ?? []).map((msg) => ({
527
- id: msg.id ?? "",
528
- text: msg.body?.content,
529
- from: msg.from,
530
- createdAt: msg.createdDateTime,
531
- }));
532
-
533
- return { messages };
534
- }