@openclaw/msteams 2026.5.2 → 2026.5.3-beta.2

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 (197) hide show
  1. package/dist/api.js +3 -0
  2. package/dist/channel-D7hdreTh.js +984 -0
  3. package/dist/channel-config-api.js +2 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/channel.runtime-BC1ruIfN.js +573 -0
  6. package/dist/config-schema-B8QezH6t.js +15 -0
  7. package/dist/contract-api.js +2 -0
  8. package/dist/graph-users-9uQJepqr.js +1354 -0
  9. package/dist/index.js +22 -0
  10. package/dist/oauth-BWJyilR1.js +114 -0
  11. package/dist/oauth.token-xxpoLWy5.js +115 -0
  12. package/dist/policy-DTnU2GR7.js +142 -0
  13. package/dist/probe-D_H8yFps.js +2194 -0
  14. package/dist/resolve-allowlist-D41JSziq.js +219 -0
  15. package/dist/runtime-api-DV1iVMn1.js +28 -0
  16. package/dist/runtime-api.js +2 -0
  17. package/dist/secret-contract-BuoEXmPS.js +35 -0
  18. package/dist/secret-contract-api.js +2 -0
  19. package/dist/setup-entry.js +15 -0
  20. package/dist/setup-plugin-api.js +64 -0
  21. package/dist/setup-surface-BLkFQYIQ.js +313 -0
  22. package/dist/src-CFp1QpFd.js +4064 -0
  23. package/dist/test-api.js +2 -0
  24. package/package.json +14 -6
  25. package/api.ts +0 -3
  26. package/channel-config-api.ts +0 -1
  27. package/channel-plugin-api.ts +0 -2
  28. package/config-api.ts +0 -4
  29. package/contract-api.ts +0 -4
  30. package/index.ts +0 -20
  31. package/runtime-api.ts +0 -73
  32. package/secret-contract-api.ts +0 -5
  33. package/setup-entry.ts +0 -13
  34. package/setup-plugin-api.ts +0 -3
  35. package/src/ai-entity.ts +0 -7
  36. package/src/approval-auth.ts +0 -44
  37. package/src/attachments/bot-framework.test.ts +0 -461
  38. package/src/attachments/bot-framework.ts +0 -362
  39. package/src/attachments/download.ts +0 -311
  40. package/src/attachments/graph.test.ts +0 -416
  41. package/src/attachments/graph.ts +0 -484
  42. package/src/attachments/html.ts +0 -122
  43. package/src/attachments/payload.ts +0 -14
  44. package/src/attachments/remote-media.test.ts +0 -137
  45. package/src/attachments/remote-media.ts +0 -112
  46. package/src/attachments/shared.test.ts +0 -530
  47. package/src/attachments/shared.ts +0 -626
  48. package/src/attachments/types.ts +0 -47
  49. package/src/attachments.graph.test.ts +0 -342
  50. package/src/attachments.helpers.test.ts +0 -246
  51. package/src/attachments.test-helpers.ts +0 -17
  52. package/src/attachments.test.ts +0 -687
  53. package/src/attachments.ts +0 -18
  54. package/src/block-streaming-config.test.ts +0 -61
  55. package/src/channel-api.ts +0 -1
  56. package/src/channel.actions.test.ts +0 -742
  57. package/src/channel.directory.test.ts +0 -200
  58. package/src/channel.runtime.ts +0 -56
  59. package/src/channel.setup.ts +0 -77
  60. package/src/channel.test.ts +0 -128
  61. package/src/channel.ts +0 -1136
  62. package/src/config-schema.ts +0 -6
  63. package/src/config-ui-hints.ts +0 -12
  64. package/src/conversation-store-fs.test.ts +0 -74
  65. package/src/conversation-store-fs.ts +0 -149
  66. package/src/conversation-store-helpers.test.ts +0 -202
  67. package/src/conversation-store-helpers.ts +0 -105
  68. package/src/conversation-store-memory.ts +0 -51
  69. package/src/conversation-store.shared.test.ts +0 -225
  70. package/src/conversation-store.ts +0 -71
  71. package/src/directory-live.test.ts +0 -156
  72. package/src/directory-live.ts +0 -111
  73. package/src/doctor.ts +0 -27
  74. package/src/errors.test.ts +0 -133
  75. package/src/errors.ts +0 -246
  76. package/src/feedback-reflection-prompt.ts +0 -117
  77. package/src/feedback-reflection-store.ts +0 -114
  78. package/src/feedback-reflection.test.ts +0 -237
  79. package/src/feedback-reflection.ts +0 -283
  80. package/src/file-consent-helpers.test.ts +0 -326
  81. package/src/file-consent-helpers.ts +0 -126
  82. package/src/file-consent-invoke.ts +0 -150
  83. package/src/file-consent.test.ts +0 -363
  84. package/src/file-consent.ts +0 -287
  85. package/src/graph-chat.ts +0 -55
  86. package/src/graph-group-management.test.ts +0 -318
  87. package/src/graph-group-management.ts +0 -168
  88. package/src/graph-members.test.ts +0 -89
  89. package/src/graph-members.ts +0 -48
  90. package/src/graph-messages.actions.test.ts +0 -243
  91. package/src/graph-messages.read.test.ts +0 -391
  92. package/src/graph-messages.search.test.ts +0 -213
  93. package/src/graph-messages.test-helpers.ts +0 -50
  94. package/src/graph-messages.ts +0 -534
  95. package/src/graph-teams.test.ts +0 -215
  96. package/src/graph-teams.ts +0 -114
  97. package/src/graph-thread.test.ts +0 -246
  98. package/src/graph-thread.ts +0 -146
  99. package/src/graph-upload.test.ts +0 -258
  100. package/src/graph-upload.ts +0 -531
  101. package/src/graph-users.ts +0 -29
  102. package/src/graph.test.ts +0 -516
  103. package/src/graph.ts +0 -293
  104. package/src/inbound.test.ts +0 -221
  105. package/src/inbound.ts +0 -148
  106. package/src/index.ts +0 -4
  107. package/src/media-helpers.test.ts +0 -202
  108. package/src/media-helpers.ts +0 -105
  109. package/src/mentions.test.ts +0 -244
  110. package/src/mentions.ts +0 -114
  111. package/src/messenger.test.ts +0 -865
  112. package/src/messenger.ts +0 -605
  113. package/src/monitor-handler/access.ts +0 -125
  114. package/src/monitor-handler/inbound-media.test.ts +0 -289
  115. package/src/monitor-handler/inbound-media.ts +0 -180
  116. package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
  117. package/src/monitor-handler/message-handler.authz.test.ts +0 -669
  118. package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
  119. package/src/monitor-handler/message-handler.test-support.ts +0 -100
  120. package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -223
  121. package/src/monitor-handler/message-handler.thread-session.test.ts +0 -77
  122. package/src/monitor-handler/message-handler.ts +0 -1000
  123. package/src/monitor-handler/reaction-handler.test.ts +0 -267
  124. package/src/monitor-handler/reaction-handler.ts +0 -210
  125. package/src/monitor-handler/thread-session.ts +0 -17
  126. package/src/monitor-handler.adaptive-card.test.ts +0 -162
  127. package/src/monitor-handler.feedback-authz.test.ts +0 -314
  128. package/src/monitor-handler.file-consent.test.ts +0 -423
  129. package/src/monitor-handler.sso.test.ts +0 -563
  130. package/src/monitor-handler.test-helpers.ts +0 -180
  131. package/src/monitor-handler.ts +0 -534
  132. package/src/monitor-handler.types.ts +0 -27
  133. package/src/monitor-types.ts +0 -6
  134. package/src/monitor.lifecycle.test.ts +0 -278
  135. package/src/monitor.test.ts +0 -119
  136. package/src/monitor.ts +0 -442
  137. package/src/oauth.flow.ts +0 -77
  138. package/src/oauth.shared.ts +0 -37
  139. package/src/oauth.test.ts +0 -305
  140. package/src/oauth.token.ts +0 -158
  141. package/src/oauth.ts +0 -130
  142. package/src/outbound.test.ts +0 -130
  143. package/src/outbound.ts +0 -71
  144. package/src/pending-uploads-fs.test.ts +0 -246
  145. package/src/pending-uploads-fs.ts +0 -235
  146. package/src/pending-uploads.test.ts +0 -173
  147. package/src/pending-uploads.ts +0 -121
  148. package/src/policy.test.ts +0 -240
  149. package/src/policy.ts +0 -262
  150. package/src/polls-store-memory.ts +0 -32
  151. package/src/polls.test.ts +0 -160
  152. package/src/polls.ts +0 -323
  153. package/src/presentation.ts +0 -68
  154. package/src/probe.test.ts +0 -77
  155. package/src/probe.ts +0 -132
  156. package/src/reply-dispatcher.test.ts +0 -437
  157. package/src/reply-dispatcher.ts +0 -346
  158. package/src/reply-stream-controller.test.ts +0 -235
  159. package/src/reply-stream-controller.ts +0 -147
  160. package/src/resolve-allowlist.test.ts +0 -250
  161. package/src/resolve-allowlist.ts +0 -309
  162. package/src/revoked-context.ts +0 -17
  163. package/src/runtime.ts +0 -9
  164. package/src/sdk-types.ts +0 -59
  165. package/src/sdk.test.ts +0 -666
  166. package/src/sdk.ts +0 -884
  167. package/src/secret-contract.ts +0 -49
  168. package/src/secret-input.ts +0 -7
  169. package/src/send-context.ts +0 -231
  170. package/src/send.test.ts +0 -493
  171. package/src/send.ts +0 -637
  172. package/src/sent-message-cache.test.ts +0 -15
  173. package/src/sent-message-cache.ts +0 -56
  174. package/src/session-route.ts +0 -40
  175. package/src/setup-core.ts +0 -160
  176. package/src/setup-surface.test.ts +0 -202
  177. package/src/setup-surface.ts +0 -320
  178. package/src/sso-token-store.test.ts +0 -72
  179. package/src/sso-token-store.ts +0 -166
  180. package/src/sso.ts +0 -300
  181. package/src/storage.ts +0 -25
  182. package/src/store-fs.ts +0 -44
  183. package/src/streaming-message.test.ts +0 -262
  184. package/src/streaming-message.ts +0 -297
  185. package/src/test-runtime.ts +0 -16
  186. package/src/thread-parent-context.test.ts +0 -224
  187. package/src/thread-parent-context.ts +0 -159
  188. package/src/token-response.ts +0 -11
  189. package/src/token.test.ts +0 -259
  190. package/src/token.ts +0 -195
  191. package/src/user-agent.test.ts +0 -86
  192. package/src/user-agent.ts +0 -53
  193. package/src/webhook-timeouts.ts +0 -27
  194. package/src/welcome-card.test.ts +0 -81
  195. package/src/welcome-card.ts +0 -57
  196. package/test-api.ts +0 -1
  197. package/tsconfig.json +0 -16
@@ -1,202 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { extractFilename, extractMessageId, getMimeType, isLocalPath } from "./media-helpers.js";
3
-
4
- describe("msteams media-helpers", () => {
5
- describe("getMimeType", () => {
6
- it("detects png from URL", async () => {
7
- expect(await getMimeType("https://example.com/image.png")).toBe("image/png");
8
- });
9
-
10
- it("detects jpeg from URL (both extensions)", async () => {
11
- expect(await getMimeType("https://example.com/photo.jpg")).toBe("image/jpeg");
12
- expect(await getMimeType("https://example.com/photo.jpeg")).toBe("image/jpeg");
13
- });
14
-
15
- it("detects gif from URL", async () => {
16
- expect(await getMimeType("https://example.com/anim.gif")).toBe("image/gif");
17
- });
18
-
19
- it("detects webp from URL", async () => {
20
- expect(await getMimeType("https://example.com/modern.webp")).toBe("image/webp");
21
- });
22
-
23
- it("handles URLs with query strings", async () => {
24
- expect(await getMimeType("https://example.com/image.png?v=123")).toBe("image/png");
25
- });
26
-
27
- it("handles data URLs", async () => {
28
- expect(await getMimeType("data:image/png;base64,iVBORw0KGgo=")).toBe("image/png");
29
- expect(await getMimeType("data:image/jpeg;base64,/9j/4AAQ")).toBe("image/jpeg");
30
- expect(await getMimeType("data:image/gif;base64,R0lGOD")).toBe("image/gif");
31
- });
32
-
33
- it("handles data URLs without base64", async () => {
34
- expect(await getMimeType("data:image/svg+xml,%3Csvg")).toBe("image/svg+xml");
35
- });
36
-
37
- it("handles local paths", async () => {
38
- expect(await getMimeType("/tmp/image.png")).toBe("image/png");
39
- expect(await getMimeType("/Users/test/photo.jpg")).toBe("image/jpeg");
40
- });
41
-
42
- it("handles tilde paths", async () => {
43
- expect(await getMimeType("~/Downloads/image.gif")).toBe("image/gif");
44
- });
45
-
46
- it("defaults to application/octet-stream for unknown extensions", async () => {
47
- expect(await getMimeType("https://example.com/image")).toBe("application/octet-stream");
48
- expect(await getMimeType("https://example.com/image.unknown")).toBe(
49
- "application/octet-stream",
50
- );
51
- });
52
-
53
- it("is case-insensitive", async () => {
54
- expect(await getMimeType("https://example.com/IMAGE.PNG")).toBe("image/png");
55
- expect(await getMimeType("https://example.com/Photo.JPEG")).toBe("image/jpeg");
56
- });
57
-
58
- it("detects document types", async () => {
59
- expect(await getMimeType("https://example.com/doc.pdf")).toBe("application/pdf");
60
- expect(await getMimeType("https://example.com/doc.docx")).toBe(
61
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
62
- );
63
- expect(await getMimeType("https://example.com/spreadsheet.xlsx")).toBe(
64
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
65
- );
66
- });
67
- });
68
-
69
- describe("extractFilename", () => {
70
- it("extracts filename from URL with extension", async () => {
71
- expect(await extractFilename("https://example.com/photo.jpg")).toBe("photo.jpg");
72
- });
73
-
74
- it("extracts filename from URL with path", async () => {
75
- expect(await extractFilename("https://example.com/images/2024/photo.png")).toBe("photo.png");
76
- });
77
-
78
- it("handles URLs without extension by deriving from MIME", async () => {
79
- // Now defaults to application/octet-stream → .bin fallback
80
- expect(await extractFilename("https://example.com/images/photo")).toBe("photo.bin");
81
- });
82
-
83
- it("handles data URLs", async () => {
84
- expect(await extractFilename("data:image/png;base64,iVBORw0KGgo=")).toBe("image.png");
85
- expect(await extractFilename("data:image/jpeg;base64,/9j/4AAQ")).toBe("image.jpg");
86
- });
87
-
88
- it("handles document data URLs", async () => {
89
- expect(await extractFilename("data:application/pdf;base64,JVBERi0")).toBe("file.pdf");
90
- });
91
-
92
- it("handles local paths", async () => {
93
- expect(await extractFilename("/tmp/screenshot.png")).toBe("screenshot.png");
94
- expect(await extractFilename("/Users/test/photo.jpg")).toBe("photo.jpg");
95
- });
96
-
97
- it("handles tilde paths", async () => {
98
- expect(await extractFilename("~/Downloads/image.gif")).toBe("image.gif");
99
- });
100
-
101
- it("returns fallback for empty URL", async () => {
102
- expect(await extractFilename("")).toBe("file.bin");
103
- });
104
-
105
- it("extracts original filename from embedded pattern", async () => {
106
- // Pattern: {original}---{uuid}.{ext}
107
- expect(
108
- await extractFilename("/media/inbound/report---a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf"),
109
- ).toBe("report.pdf");
110
- });
111
-
112
- it("extracts original filename with uppercase UUID", async () => {
113
- expect(
114
- await extractFilename(
115
- "/media/inbound/Document---A1B2C3D4-E5F6-7890-ABCD-EF1234567890.docx",
116
- ),
117
- ).toBe("Document.docx");
118
- });
119
-
120
- it("falls back to UUID filename for legacy paths", async () => {
121
- // UUID-only filename (legacy format, no embedded name)
122
- expect(await extractFilename("/media/inbound/a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf")).toBe(
123
- "a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf",
124
- );
125
- });
126
-
127
- it("handles --- in filename without valid UUID pattern", async () => {
128
- // foo---bar.txt (bar is not a valid UUID)
129
- expect(await extractFilename("/media/inbound/foo---bar.txt")).toBe("foo---bar.txt");
130
- });
131
- });
132
-
133
- describe("isLocalPath", () => {
134
- it("returns true for file:// URLs", () => {
135
- expect(isLocalPath("file:///tmp/image.png")).toBe(true);
136
- expect(isLocalPath("file://localhost/tmp/image.png")).toBe(true);
137
- });
138
-
139
- it("returns true for absolute paths", () => {
140
- expect(isLocalPath("/tmp/image.png")).toBe(true);
141
- expect(isLocalPath("/Users/test/photo.jpg")).toBe(true);
142
- });
143
-
144
- it("returns true for tilde paths", () => {
145
- expect(isLocalPath("~/Downloads/image.png")).toBe(true);
146
- });
147
-
148
- it("returns true for Windows absolute drive paths", () => {
149
- expect(isLocalPath("C:\\Users\\test\\image.png")).toBe(true);
150
- expect(isLocalPath("D:/data/photo.jpg")).toBe(true);
151
- });
152
-
153
- it("returns true for Windows UNC paths", () => {
154
- expect(isLocalPath("\\\\server\\share\\image.png")).toBe(true);
155
- });
156
-
157
- it("returns true for Windows rooted paths", () => {
158
- expect(isLocalPath("\\tmp\\openclaw\\file.txt")).toBe(true);
159
- });
160
-
161
- it("returns false for http URLs", () => {
162
- expect(isLocalPath("http://example.com/image.png")).toBe(false);
163
- expect(isLocalPath("https://example.com/image.png")).toBe(false);
164
- });
165
-
166
- it("returns false for data URLs", () => {
167
- expect(isLocalPath("data:image/png;base64,iVBORw0KGgo=")).toBe(false);
168
- });
169
- });
170
-
171
- describe("extractMessageId", () => {
172
- it("extracts id from valid response", () => {
173
- expect(extractMessageId({ id: "msg123" })).toBe("msg123");
174
- });
175
-
176
- it("returns null for missing id", () => {
177
- expect(extractMessageId({ foo: "bar" })).toBeNull();
178
- });
179
-
180
- it("returns null for empty id", () => {
181
- expect(extractMessageId({ id: "" })).toBeNull();
182
- });
183
-
184
- it("returns null for non-string id", () => {
185
- expect(extractMessageId({ id: 123 })).toBeNull();
186
- expect(extractMessageId({ id: null })).toBeNull();
187
- });
188
-
189
- it("returns null for null response", () => {
190
- expect(extractMessageId(null)).toBeNull();
191
- });
192
-
193
- it("returns null for undefined response", () => {
194
- expect(extractMessageId(undefined)).toBeNull();
195
- });
196
-
197
- it("returns null for non-object response", () => {
198
- expect(extractMessageId("string")).toBeNull();
199
- expect(extractMessageId(123)).toBeNull();
200
- });
201
- });
202
- });
@@ -1,105 +0,0 @@
1
- /**
2
- * MIME type detection and filename extraction for MSTeams media attachments.
3
- */
4
-
5
- import path from "node:path";
6
- import {
7
- detectMime,
8
- extensionForMime,
9
- extractOriginalFilename,
10
- getFileExtension,
11
- } from "../runtime-api.js";
12
-
13
- /**
14
- * Detect MIME type from URL extension or data URL.
15
- * Uses shared MIME detection for consistency with core handling.
16
- */
17
- export async function getMimeType(url: string): Promise<string> {
18
- // Handle data URLs: data:image/png;base64,...
19
- if (url.startsWith("data:")) {
20
- const match = url.match(/^data:([^;,]+)/);
21
- if (match?.[1]) {
22
- return match[1];
23
- }
24
- }
25
-
26
- // Use shared MIME detection (extension-based for URLs)
27
- const detected = await detectMime({ filePath: url });
28
- return detected ?? "application/octet-stream";
29
- }
30
-
31
- /**
32
- * Extract filename from URL or local path.
33
- * For local paths, extracts original filename if stored with embedded name pattern.
34
- * Falls back to deriving the extension from MIME type when no extension present.
35
- */
36
- export async function extractFilename(url: string): Promise<string> {
37
- // Handle data URLs: derive extension from MIME
38
- if (url.startsWith("data:")) {
39
- const mime = await getMimeType(url);
40
- const ext = extensionForMime(mime) ?? ".bin";
41
- const prefix = mime.startsWith("image/") ? "image" : "file";
42
- return `${prefix}${ext}`;
43
- }
44
-
45
- // Try to extract from URL pathname
46
- try {
47
- const pathname = new URL(url).pathname;
48
- const basename = path.basename(pathname);
49
- const existingExt = getFileExtension(pathname);
50
- if (basename && existingExt) {
51
- return basename;
52
- }
53
- // No extension in URL, derive from MIME
54
- const mime = await getMimeType(url);
55
- const ext = extensionForMime(mime) ?? ".bin";
56
- const prefix = mime.startsWith("image/") ? "image" : "file";
57
- return basename ? `${basename}${ext}` : `${prefix}${ext}`;
58
- } catch {
59
- // Local paths - use extractOriginalFilename to extract embedded original name
60
- return extractOriginalFilename(url);
61
- }
62
- }
63
-
64
- /**
65
- * Check if a URL refers to a local file path.
66
- */
67
- export function isLocalPath(url: string): boolean {
68
- if (url.startsWith("file://") || url.startsWith("/") || url.startsWith("~")) {
69
- return true;
70
- }
71
-
72
- // Windows rooted path on current drive (e.g. \tmp\file.txt)
73
- if (url.startsWith("\\") && !url.startsWith("\\\\")) {
74
- return true;
75
- }
76
-
77
- // Windows drive-letter absolute path (e.g. C:\foo\bar.txt or C:/foo/bar.txt)
78
- if (/^[a-zA-Z]:[\\/]/.test(url)) {
79
- return true;
80
- }
81
-
82
- // Windows UNC path (e.g. \\server\share\file.txt)
83
- if (url.startsWith("\\\\")) {
84
- return true;
85
- }
86
-
87
- return false;
88
- }
89
-
90
- /**
91
- * Extract the message ID from a Bot Framework response.
92
- */
93
- export function extractMessageId(response: unknown): string | null {
94
- if (!response || typeof response !== "object") {
95
- return null;
96
- }
97
- if (!("id" in response)) {
98
- return null;
99
- }
100
- const { id } = response as { id?: unknown };
101
- if (typeof id !== "string" || !id) {
102
- return null;
103
- }
104
- return id;
105
- }
@@ -1,244 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { buildMentionEntities, formatMentionText, parseMentions } from "./mentions.js";
3
-
4
- function requireFirstEntity(result: ReturnType<typeof parseMentions>) {
5
- const entity = result.entities[0];
6
- if (!entity) {
7
- throw new Error("expected parseMentions to return at least one entity");
8
- }
9
- return entity;
10
- }
11
-
12
- function requireOnlyEntity(result: ReturnType<typeof parseMentions>) {
13
- expect(result.entities).toHaveLength(1);
14
- return requireFirstEntity(result);
15
- }
16
-
17
- describe("parseMentions", () => {
18
- it("parses single mention", () => {
19
- const result = parseMentions("Hello @[John Doe](28:a1b2c3-d4e5f6)!");
20
-
21
- expect(result.text).toBe("Hello <at>John Doe</at>!");
22
- expect(requireOnlyEntity(result)).toEqual({
23
- type: "mention",
24
- text: "<at>John Doe</at>",
25
- mentioned: {
26
- id: "28:a1b2c3-d4e5f6",
27
- name: "John Doe",
28
- },
29
- });
30
- });
31
-
32
- it("parses multiple mentions", () => {
33
- const result = parseMentions("Hey @[Alice](28:aaa) and @[Bob](28:bbb), can you review this?");
34
-
35
- expect(result.text).toBe("Hey <at>Alice</at> and <at>Bob</at>, can you review this?");
36
- expect(result.entities).toHaveLength(2);
37
- expect(result.entities[0]).toEqual({
38
- type: "mention",
39
- text: "<at>Alice</at>",
40
- mentioned: {
41
- id: "28:aaa",
42
- name: "Alice",
43
- },
44
- });
45
- expect(result.entities[1]).toEqual({
46
- type: "mention",
47
- text: "<at>Bob</at>",
48
- mentioned: {
49
- id: "28:bbb",
50
- name: "Bob",
51
- },
52
- });
53
- });
54
-
55
- it("handles text without mentions", () => {
56
- const result = parseMentions("Hello world!");
57
-
58
- expect(result.text).toBe("Hello world!");
59
- expect(result.entities).toHaveLength(0);
60
- });
61
-
62
- it("handles empty text", () => {
63
- const result = parseMentions("");
64
-
65
- expect(result.text).toBe("");
66
- expect(result.entities).toHaveLength(0);
67
- });
68
-
69
- it("handles mention with spaces in name", () => {
70
- const result = parseMentions("@[John Peter Smith](28:a1b2c3)");
71
-
72
- expect(result.text).toBe("<at>John Peter Smith</at>");
73
- expect(requireFirstEntity(result).mentioned.name).toBe("John Peter Smith");
74
- });
75
-
76
- it("trims whitespace from id and name", () => {
77
- const result = parseMentions("@[ John Doe ]( 28:a1b2c3 )");
78
-
79
- expect(requireOnlyEntity(result)).toEqual({
80
- type: "mention",
81
- text: "<at>John Doe</at>",
82
- mentioned: {
83
- id: "28:a1b2c3",
84
- name: "John Doe",
85
- },
86
- });
87
- });
88
-
89
- it("handles Japanese characters in mention at start of message", () => {
90
- const input = "@[タナカ タロウ](a1b2c3d4-e5f6-7890-abcd-ef1234567890) スキル化完了しました!";
91
- const result = parseMentions(input);
92
-
93
- expect(result.text).toBe("<at>タナカ タロウ</at> スキル化完了しました!");
94
- expect(requireOnlyEntity(result)).toEqual({
95
- type: "mention",
96
- text: "<at>タナカ タロウ</at>",
97
- mentioned: {
98
- id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
99
- name: "タナカ タロウ",
100
- },
101
- });
102
-
103
- // Verify entity text exactly matches what's in the formatted text
104
- const entityText = requireFirstEntity(result).text;
105
- expect(result.text).toContain(entityText);
106
- expect(result.text.indexOf(entityText)).toBe(0);
107
- });
108
-
109
- it("skips mention-like patterns with non-Teams IDs (e.g. in code blocks)", () => {
110
- // This reproduces the actual failing payload: the message contains a real mention
111
- // plus `@[表示名](ユーザーID)` as documentation text inside backticks.
112
- const input =
113
- "@[タナカ タロウ](a1b2c3d4-e5f6-7890-abcd-ef1234567890) スキル化完了しました!📋\n\n" +
114
- "**作成したスキル:** `teams-mention`\n" +
115
- "- 機能: Teamsでのメンション形式 `@[表示名](ユーザーID)`\n\n" +
116
- "**追加対応:**\n" +
117
- "- ユーザーのID `a1b2c3d4-e5f6-7890-abcd-ef1234567890` を登録済み";
118
- const result = parseMentions(input);
119
-
120
- // Only the real mention should be parsed; the documentation example should be left as-is
121
- const firstEntity = requireOnlyEntity(result);
122
- expect(firstEntity.mentioned.id).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890");
123
- expect(firstEntity.mentioned.name).toBe("タナカ タロウ");
124
-
125
- // The documentation pattern must remain untouched in the text
126
- expect(result.text).toContain("`@[表示名](ユーザーID)`");
127
- });
128
-
129
- it("accepts Bot Framework IDs (28:xxx)", () => {
130
- const result = parseMentions("@[Bot](28:abc-123)");
131
- expect(requireOnlyEntity(result).mentioned.id).toBe("28:abc-123");
132
- });
133
-
134
- it("accepts Bot Framework IDs with non-hex payloads (29:xxx)", () => {
135
- const result = parseMentions("@[Bot](29:08q2j2o3jc09au90eucae)");
136
- expect(requireOnlyEntity(result).mentioned.id).toBe("29:08q2j2o3jc09au90eucae");
137
- });
138
-
139
- it("accepts org-scoped IDs with extra segments (8:orgid:...)", () => {
140
- const result = parseMentions("@[User](8:orgid:2d8c2d2c-1111-2222-3333-444444444444)");
141
- expect(requireOnlyEntity(result).mentioned.id).toBe(
142
- "8:orgid:2d8c2d2c-1111-2222-3333-444444444444",
143
- );
144
- });
145
-
146
- it("accepts AAD object IDs (UUIDs)", () => {
147
- const result = parseMentions("@[User](a1b2c3d4-e5f6-7890-abcd-ef1234567890)");
148
- expect(requireOnlyEntity(result).mentioned.id).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890");
149
- });
150
-
151
- it("rejects non-ID strings as mention targets", () => {
152
- const result = parseMentions("See @[docs](https://example.com) for details");
153
- expect(result.entities).toHaveLength(0);
154
- // Original text preserved
155
- expect(result.text).toBe("See @[docs](https://example.com) for details");
156
- });
157
- });
158
-
159
- describe("buildMentionEntities", () => {
160
- it("builds entities from mention info", () => {
161
- const mentions = [
162
- { id: "28:aaa", name: "Alice" },
163
- { id: "28:bbb", name: "Bob" },
164
- ];
165
-
166
- const entities = buildMentionEntities(mentions);
167
-
168
- expect(entities).toHaveLength(2);
169
- expect(entities[0]).toEqual({
170
- type: "mention",
171
- text: "<at>Alice</at>",
172
- mentioned: {
173
- id: "28:aaa",
174
- name: "Alice",
175
- },
176
- });
177
- expect(entities[1]).toEqual({
178
- type: "mention",
179
- text: "<at>Bob</at>",
180
- mentioned: {
181
- id: "28:bbb",
182
- name: "Bob",
183
- },
184
- });
185
- });
186
-
187
- it("handles empty list", () => {
188
- const entities = buildMentionEntities([]);
189
- expect(entities).toHaveLength(0);
190
- });
191
- });
192
-
193
- describe("formatMentionText", () => {
194
- it("formats text with single mention", () => {
195
- const text = "Hello @John!";
196
- const mentions = [{ id: "28:xxx", name: "John" }];
197
-
198
- const result = formatMentionText(text, mentions);
199
-
200
- expect(result).toBe("Hello <at>John</at>!");
201
- });
202
-
203
- it("formats text with multiple mentions", () => {
204
- const text = "Hey @Alice and @Bob";
205
- const mentions = [
206
- { id: "28:aaa", name: "Alice" },
207
- { id: "28:bbb", name: "Bob" },
208
- ];
209
-
210
- const result = formatMentionText(text, mentions);
211
-
212
- expect(result).toBe("Hey <at>Alice</at> and <at>Bob</at>");
213
- });
214
-
215
- it("handles case-insensitive matching", () => {
216
- const text = "Hey @alice and @ALICE";
217
- const mentions = [{ id: "28:aaa", name: "Alice" }];
218
-
219
- const result = formatMentionText(text, mentions);
220
-
221
- expect(result).toBe("Hey <at>Alice</at> and <at>Alice</at>");
222
- });
223
-
224
- it("handles text without mentions", () => {
225
- const text = "Hello world";
226
- const mentions = [{ id: "28:xxx", name: "John" }];
227
-
228
- const result = formatMentionText(text, mentions);
229
-
230
- expect(result).toBe("Hello world");
231
- });
232
-
233
- it("escapes regex metacharacters in names", () => {
234
- const text = "Hey @John(Test) and @Alice.Smith";
235
- const mentions = [
236
- { id: "28:xxx", name: "John(Test)" },
237
- { id: "28:yyy", name: "Alice.Smith" },
238
- ];
239
-
240
- const result = formatMentionText(text, mentions);
241
-
242
- expect(result).toBe("Hey <at>John(Test)</at> and <at>Alice.Smith</at>");
243
- });
244
- });
package/src/mentions.ts DELETED
@@ -1,114 +0,0 @@
1
- /**
2
- * MS Teams mention handling utilities.
3
- *
4
- * Mentions in Teams require:
5
- * 1. Text containing <at>Name</at> tags
6
- * 2. entities array with mention metadata
7
- */
8
-
9
- type MentionEntity = {
10
- type: "mention";
11
- text: string;
12
- mentioned: {
13
- id: string;
14
- name: string;
15
- };
16
- };
17
-
18
- type MentionInfo = {
19
- /** User/bot ID (e.g., "28:xxx" or AAD object ID) */
20
- id: string;
21
- /** Display name */
22
- name: string;
23
- };
24
-
25
- /**
26
- * Check whether an ID looks like a valid Teams user/bot identifier.
27
- * Accepts:
28
- * - Bot Framework IDs: "28:xxx..." / "29:xxx..." / "8:orgid:..."
29
- * - AAD object IDs (UUIDs): "d5318c29-33ac-4e6b-bd42-57b8b793908f"
30
- *
31
- * Keep this permissive enough for real Teams IDs while still rejecting
32
- * documentation placeholders like `@[表示名](ユーザーID)`.
33
- */
34
- const TEAMS_BOT_ID_PATTERN = /^\d+:[a-z0-9._=-]+(?::[a-z0-9._=-]+)*$/i;
35
- const AAD_OBJECT_ID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
36
-
37
- function isValidTeamsId(id: string): boolean {
38
- return TEAMS_BOT_ID_PATTERN.test(id) || AAD_OBJECT_ID_PATTERN.test(id);
39
- }
40
-
41
- /**
42
- * Parse mentions from text in the format @[Name](id).
43
- * Example: "Hello @[John Doe](28:xxx-yyy-zzz)!"
44
- *
45
- * Only matches where the id looks like a real Teams user/bot ID are treated
46
- * as mentions. This avoids false positives from documentation or code samples
47
- * embedded in the message (e.g. `@[表示名](ユーザーID)` in backticks).
48
- *
49
- * Returns both the formatted text with <at> tags and the entities array.
50
- */
51
- export function parseMentions(text: string): {
52
- text: string;
53
- entities: MentionEntity[];
54
- } {
55
- const mentionPattern = /@\[([^\]]+)\]\(([^)]+)\)/g;
56
- const entities: MentionEntity[] = [];
57
-
58
- // Replace @[Name](id) with <at>Name</at> only for valid Teams IDs
59
- const formattedText = text.replace(mentionPattern, (match, name, id) => {
60
- const trimmedId = id.trim();
61
-
62
- // Skip matches where the id doesn't look like a real Teams identifier
63
- if (!isValidTeamsId(trimmedId)) {
64
- return match;
65
- }
66
-
67
- const trimmedName = name.trim();
68
- const mentionTag = `<at>${trimmedName}</at>`;
69
- entities.push({
70
- type: "mention",
71
- text: mentionTag,
72
- mentioned: {
73
- id: trimmedId,
74
- name: trimmedName,
75
- },
76
- });
77
- return mentionTag;
78
- });
79
-
80
- return {
81
- text: formattedText,
82
- entities,
83
- };
84
- }
85
-
86
- /**
87
- * Build mention entities array from a list of mentions.
88
- * Use this when you already have the mention info and formatted text.
89
- */
90
- export function buildMentionEntities(mentions: MentionInfo[]): MentionEntity[] {
91
- return mentions.map((mention) => ({
92
- type: "mention",
93
- text: `<at>${mention.name}</at>`,
94
- mentioned: {
95
- id: mention.id,
96
- name: mention.name,
97
- },
98
- }));
99
- }
100
-
101
- /**
102
- * Format text with mentions using <at> tags.
103
- * This is a convenience function when you want to manually format mentions.
104
- */
105
- export function formatMentionText(text: string, mentions: MentionInfo[]): string {
106
- let formatted = text;
107
- for (const mention of mentions) {
108
- // Replace @Name or @name with <at>Name</at>
109
- const escapedName = mention.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
110
- const namePattern = new RegExp(`@${escapedName}`, "gi");
111
- formatted = formatted.replace(namePattern, `<at>${mention.name}</at>`);
112
- }
113
- return formatted;
114
- }