msteams-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +229 -0
  2. package/dist/__fixtures__/api-responses.d.ts +228 -0
  3. package/dist/__fixtures__/api-responses.js +217 -0
  4. package/dist/api/chatsvc-api.d.ts +171 -0
  5. package/dist/api/chatsvc-api.js +459 -0
  6. package/dist/api/csa-api.d.ts +44 -0
  7. package/dist/api/csa-api.js +148 -0
  8. package/dist/api/index.d.ts +6 -0
  9. package/dist/api/index.js +6 -0
  10. package/dist/api/substrate-api.d.ts +50 -0
  11. package/dist/api/substrate-api.js +305 -0
  12. package/dist/auth/crypto.d.ts +32 -0
  13. package/dist/auth/crypto.js +66 -0
  14. package/dist/auth/index.d.ts +6 -0
  15. package/dist/auth/index.js +6 -0
  16. package/dist/auth/session-store.d.ts +82 -0
  17. package/dist/auth/session-store.js +136 -0
  18. package/dist/auth/token-extractor.d.ts +69 -0
  19. package/dist/auth/token-extractor.js +330 -0
  20. package/dist/browser/auth.d.ts +43 -0
  21. package/dist/browser/auth.js +232 -0
  22. package/dist/browser/context.d.ts +40 -0
  23. package/dist/browser/context.js +121 -0
  24. package/dist/browser/session.d.ts +34 -0
  25. package/dist/browser/session.js +92 -0
  26. package/dist/constants.d.ts +54 -0
  27. package/dist/constants.js +72 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.js +12 -0
  30. package/dist/research/explore.d.ts +11 -0
  31. package/dist/research/explore.js +267 -0
  32. package/dist/research/search-research.d.ts +17 -0
  33. package/dist/research/search-research.js +317 -0
  34. package/dist/server.d.ts +64 -0
  35. package/dist/server.js +291 -0
  36. package/dist/teams/api-interceptor.d.ts +54 -0
  37. package/dist/teams/api-interceptor.js +391 -0
  38. package/dist/teams/direct-api.d.ts +321 -0
  39. package/dist/teams/direct-api.js +1305 -0
  40. package/dist/teams/messages.d.ts +14 -0
  41. package/dist/teams/messages.js +142 -0
  42. package/dist/teams/search.d.ts +40 -0
  43. package/dist/teams/search.js +458 -0
  44. package/dist/test/cli.d.ts +12 -0
  45. package/dist/test/cli.js +328 -0
  46. package/dist/test/debug-search.d.ts +10 -0
  47. package/dist/test/debug-search.js +147 -0
  48. package/dist/test/manual-test.d.ts +11 -0
  49. package/dist/test/manual-test.js +160 -0
  50. package/dist/test/mcp-harness.d.ts +17 -0
  51. package/dist/test/mcp-harness.js +427 -0
  52. package/dist/tools/auth-tools.d.ts +26 -0
  53. package/dist/tools/auth-tools.js +127 -0
  54. package/dist/tools/index.d.ts +45 -0
  55. package/dist/tools/index.js +12 -0
  56. package/dist/tools/message-tools.d.ts +139 -0
  57. package/dist/tools/message-tools.js +433 -0
  58. package/dist/tools/people-tools.d.ts +46 -0
  59. package/dist/tools/people-tools.js +123 -0
  60. package/dist/tools/registry.d.ts +23 -0
  61. package/dist/tools/registry.js +61 -0
  62. package/dist/tools/search-tools.d.ts +79 -0
  63. package/dist/tools/search-tools.js +168 -0
  64. package/dist/types/errors.d.ts +58 -0
  65. package/dist/types/errors.js +132 -0
  66. package/dist/types/result.d.ts +43 -0
  67. package/dist/types/result.js +51 -0
  68. package/dist/types/teams.d.ts +79 -0
  69. package/dist/types/teams.js +5 -0
  70. package/dist/utils/api-config.d.ts +66 -0
  71. package/dist/utils/api-config.js +113 -0
  72. package/dist/utils/auth-guards.d.ts +29 -0
  73. package/dist/utils/auth-guards.js +54 -0
  74. package/dist/utils/http.d.ts +29 -0
  75. package/dist/utils/http.js +111 -0
  76. package/dist/utils/parsers.d.ts +187 -0
  77. package/dist/utils/parsers.js +574 -0
  78. package/dist/utils/parsers.test.d.ts +7 -0
  79. package/dist/utils/parsers.test.js +360 -0
  80. package/package.json +58 -0
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Chat Service API client for messaging operations.
3
+ *
4
+ * Handles all calls to teams.microsoft.com/api/chatsvc endpoints.
5
+ */
6
+ import { type Result } from '../types/result.js';
7
+ /** Result of sending a message. */
8
+ export interface SendMessageResult {
9
+ messageId: string;
10
+ timestamp?: number;
11
+ }
12
+ /** A message from a thread/conversation. */
13
+ export interface ThreadMessage {
14
+ id: string;
15
+ content: string;
16
+ contentType: string;
17
+ sender: {
18
+ mri: string;
19
+ displayName?: string;
20
+ };
21
+ timestamp: string;
22
+ conversationId: string;
23
+ clientMessageId?: string;
24
+ isFromMe?: boolean;
25
+ messageLink?: string;
26
+ }
27
+ /** Result of getting thread messages. */
28
+ export interface GetThreadResult {
29
+ conversationId: string;
30
+ messages: ThreadMessage[];
31
+ }
32
+ /** Result of saving/unsaving a message. */
33
+ export interface SaveMessageResult {
34
+ conversationId: string;
35
+ messageId: string;
36
+ saved: boolean;
37
+ }
38
+ /** Options for sending a message. */
39
+ export interface SendMessageOptions {
40
+ /** Region for the API call (default: 'amer'). */
41
+ region?: string;
42
+ /**
43
+ * Message ID of the thread root to reply to.
44
+ *
45
+ * When provided, the message is posted as a reply to an existing thread
46
+ * in a channel. The conversationId should be the channel ID, and this
47
+ * should be the ID of the first message in the thread.
48
+ *
49
+ * For chats (1:1, group, meeting), this is not needed - all messages
50
+ * are part of the same flat conversation.
51
+ */
52
+ replyToMessageId?: string;
53
+ }
54
+ /**
55
+ * Sends a message to a Teams conversation.
56
+ *
57
+ * For channels, you can either:
58
+ * - Post a new top-level message: just provide the channel's conversationId
59
+ * - Reply to a thread: provide the channel's conversationId AND replyToMessageId
60
+ *
61
+ * For chats (1:1, group, meeting), all messages go to the same conversation
62
+ * without threading - just provide the conversationId.
63
+ */
64
+ export declare function sendMessage(conversationId: string, content: string, options?: SendMessageOptions): Promise<Result<SendMessageResult>>;
65
+ /**
66
+ * Sends a message to your own notes/self-chat.
67
+ */
68
+ export declare function sendNoteToSelf(content: string): Promise<Result<SendMessageResult>>;
69
+ /** Result of replying to a thread. */
70
+ export interface ReplyToThreadResult extends SendMessageResult {
71
+ /** The thread root message ID used for the reply. */
72
+ threadRootMessageId: string;
73
+ /** The conversation ID (channel) the reply was posted to. */
74
+ conversationId: string;
75
+ }
76
+ /**
77
+ * Replies to a thread in a Teams channel.
78
+ *
79
+ * Uses the provided messageId directly as the thread root. In Teams channels:
80
+ * - If messageId is a top-level post, the reply goes under that post
81
+ * - If messageId is already a reply within a thread, the reply goes to the same thread
82
+ *
83
+ * For channel messages from search results, the messageId is typically the thread root
84
+ * (the original message that started the thread).
85
+ *
86
+ * @param conversationId - The channel conversation ID (from search results)
87
+ * @param messageId - The message ID to reply to (typically the thread root from search)
88
+ * @param content - The reply content
89
+ * @param region - API region (default: 'amer')
90
+ * @returns The result including the new message ID
91
+ */
92
+ export declare function replyToThread(conversationId: string, messageId: string, content: string, region?: string): Promise<Result<ReplyToThreadResult>>;
93
+ /**
94
+ * Gets messages from a Teams conversation/thread.
95
+ */
96
+ export declare function getThreadMessages(conversationId: string, options?: {
97
+ limit?: number;
98
+ startTime?: number;
99
+ }, region?: string): Promise<Result<GetThreadResult>>;
100
+ /**
101
+ * Saves (bookmarks) a message.
102
+ */
103
+ export declare function saveMessage(conversationId: string, messageId: string, region?: string): Promise<Result<SaveMessageResult>>;
104
+ /**
105
+ * Unsaves (removes bookmark from) a message.
106
+ */
107
+ export declare function unsaveMessage(conversationId: string, messageId: string, region?: string): Promise<Result<SaveMessageResult>>;
108
+ /** Result of editing a message. */
109
+ export interface EditMessageResult {
110
+ messageId: string;
111
+ conversationId: string;
112
+ }
113
+ /** Result of deleting a message. */
114
+ export interface DeleteMessageResult {
115
+ messageId: string;
116
+ conversationId: string;
117
+ }
118
+ /**
119
+ * Edits an existing message.
120
+ *
121
+ * Note: You can only edit your own messages. The API will reject
122
+ * attempts to edit messages from other users.
123
+ *
124
+ * @param conversationId - The conversation containing the message
125
+ * @param messageId - The ID of the message to edit
126
+ * @param newContent - The new content for the message
127
+ * @param region - API region (default: 'amer')
128
+ */
129
+ export declare function editMessage(conversationId: string, messageId: string, newContent: string, region?: string): Promise<Result<EditMessageResult>>;
130
+ /**
131
+ * Deletes a message (soft delete).
132
+ *
133
+ * Note: You can only delete your own messages, unless you are a
134
+ * channel owner/moderator. The API will reject unauthorised attempts.
135
+ *
136
+ * @param conversationId - The conversation containing the message
137
+ * @param messageId - The ID of the message to delete
138
+ * @param region - API region (default: 'amer')
139
+ */
140
+ export declare function deleteMessage(conversationId: string, messageId: string, region?: string): Promise<Result<DeleteMessageResult>>;
141
+ /**
142
+ * Gets properties for a single conversation.
143
+ */
144
+ export declare function getConversationProperties(conversationId: string, region?: string): Promise<Result<{
145
+ displayName?: string;
146
+ conversationType?: string;
147
+ }>>;
148
+ /**
149
+ * Extracts unique participant names from recent messages.
150
+ */
151
+ export declare function extractParticipantNames(conversationId: string, region?: string): Promise<Result<string | undefined>>;
152
+ /** Result of getting a 1:1 conversation. */
153
+ export interface GetOneOnOneChatResult {
154
+ conversationId: string;
155
+ otherUserId: string;
156
+ currentUserId: string;
157
+ }
158
+ /**
159
+ * Gets the conversation ID for a 1:1 chat with another user.
160
+ *
161
+ * This constructs the predictable conversation ID format used by Teams
162
+ * for 1:1 chats. The conversation ID is: `19:{id1}_{id2}@unq.gbl.spaces`
163
+ * where id1 and id2 are the two users' object IDs sorted lexicographically.
164
+ *
165
+ * Note: This doesn't create the conversation - it just returns the ID.
166
+ * The conversation is implicitly created when the first message is sent.
167
+ *
168
+ * @param otherUserIdentifier - The other user's MRI, object ID, or ID with tenant
169
+ * @returns The conversation ID, or an error if auth is missing or ID is invalid
170
+ */
171
+ export declare function getOneOnOneChatId(otherUserIdentifier: string): Result<GetOneOnOneChatResult>;
@@ -0,0 +1,459 @@
1
+ /**
2
+ * Chat Service API client for messaging operations.
3
+ *
4
+ * Handles all calls to teams.microsoft.com/api/chatsvc endpoints.
5
+ */
6
+ import { httpRequest } from '../utils/http.js';
7
+ import { CHATSVC_API, getMessagingHeaders, getSkypeAuthHeaders, validateRegion } from '../utils/api-config.js';
8
+ import { ErrorCode, createError } from '../types/errors.js';
9
+ import { ok, err } from '../types/result.js';
10
+ import { getUserDisplayName } from '../auth/token-extractor.js';
11
+ import { requireMessageAuth } from '../utils/auth-guards.js';
12
+ import { stripHtml, buildMessageLink, buildOneOnOneConversationId, extractObjectId } from '../utils/parsers.js';
13
+ /**
14
+ * Sends a message to a Teams conversation.
15
+ *
16
+ * For channels, you can either:
17
+ * - Post a new top-level message: just provide the channel's conversationId
18
+ * - Reply to a thread: provide the channel's conversationId AND replyToMessageId
19
+ *
20
+ * For chats (1:1, group, meeting), all messages go to the same conversation
21
+ * without threading - just provide the conversationId.
22
+ */
23
+ export async function sendMessage(conversationId, content, options = {}) {
24
+ const authResult = requireMessageAuth();
25
+ if (!authResult.ok) {
26
+ return authResult;
27
+ }
28
+ const auth = authResult.value;
29
+ const { region = 'amer', replyToMessageId } = options;
30
+ const validRegion = validateRegion(region);
31
+ const displayName = getUserDisplayName() || 'User';
32
+ // Generate unique message ID
33
+ const clientMessageId = Date.now().toString();
34
+ // Wrap content in paragraph if not already HTML
35
+ const htmlContent = content.startsWith('<') ? content : `<p>${escapeHtml(content)}</p>`;
36
+ const body = {
37
+ content: htmlContent,
38
+ messagetype: 'RichText/Html',
39
+ contenttype: 'text',
40
+ imdisplayname: displayName,
41
+ clientmessageid: clientMessageId,
42
+ };
43
+ const url = CHATSVC_API.messages(validRegion, conversationId, replyToMessageId);
44
+ const response = await httpRequest(url, {
45
+ method: 'POST',
46
+ headers: getMessagingHeaders(auth.skypeToken, auth.authToken),
47
+ body: JSON.stringify(body),
48
+ });
49
+ if (!response.ok) {
50
+ return response;
51
+ }
52
+ return ok({
53
+ messageId: clientMessageId,
54
+ timestamp: response.value.data.OriginalArrivalTime,
55
+ });
56
+ }
57
+ /**
58
+ * Sends a message to your own notes/self-chat.
59
+ */
60
+ export async function sendNoteToSelf(content) {
61
+ return sendMessage('48:notes', content);
62
+ }
63
+ /**
64
+ * Replies to a thread in a Teams channel.
65
+ *
66
+ * Uses the provided messageId directly as the thread root. In Teams channels:
67
+ * - If messageId is a top-level post, the reply goes under that post
68
+ * - If messageId is already a reply within a thread, the reply goes to the same thread
69
+ *
70
+ * For channel messages from search results, the messageId is typically the thread root
71
+ * (the original message that started the thread).
72
+ *
73
+ * @param conversationId - The channel conversation ID (from search results)
74
+ * @param messageId - The message ID to reply to (typically the thread root from search)
75
+ * @param content - The reply content
76
+ * @param region - API region (default: 'amer')
77
+ * @returns The result including the new message ID
78
+ */
79
+ export async function replyToThread(conversationId, messageId, content, region = 'amer') {
80
+ // Use the provided messageId directly as the thread root
81
+ // Search results return the thread root ID for channel messages
82
+ const threadRootMessageId = messageId;
83
+ // Send the reply using the provided message ID as the thread root
84
+ const sendResult = await sendMessage(conversationId, content, {
85
+ region,
86
+ replyToMessageId: threadRootMessageId,
87
+ });
88
+ if (!sendResult.ok) {
89
+ return sendResult;
90
+ }
91
+ return ok({
92
+ messageId: sendResult.value.messageId,
93
+ timestamp: sendResult.value.timestamp,
94
+ threadRootMessageId,
95
+ conversationId,
96
+ });
97
+ }
98
+ /**
99
+ * Gets messages from a Teams conversation/thread.
100
+ */
101
+ export async function getThreadMessages(conversationId, options = {}, region = 'amer') {
102
+ const authResult = requireMessageAuth();
103
+ if (!authResult.ok) {
104
+ return authResult;
105
+ }
106
+ const auth = authResult.value;
107
+ const validRegion = validateRegion(region);
108
+ const limit = options.limit ?? 50;
109
+ let url = CHATSVC_API.messages(validRegion, conversationId);
110
+ url += `?view=msnp24Equivalent&pageSize=${limit}`;
111
+ if (options.startTime) {
112
+ url += `&startTime=${options.startTime}`;
113
+ }
114
+ const response = await httpRequest(url, {
115
+ method: 'GET',
116
+ headers: getSkypeAuthHeaders(auth.skypeToken, auth.authToken),
117
+ });
118
+ if (!response.ok) {
119
+ return response;
120
+ }
121
+ const rawMessages = response.value.data.messages;
122
+ if (!Array.isArray(rawMessages)) {
123
+ return ok({
124
+ conversationId,
125
+ messages: [],
126
+ });
127
+ }
128
+ const messages = [];
129
+ for (const raw of rawMessages) {
130
+ const msg = raw;
131
+ // Skip non-message types
132
+ const messageType = msg.messagetype;
133
+ if (!messageType || messageType.startsWith('Control/') || messageType === 'ThreadActivity/AddMember') {
134
+ continue;
135
+ }
136
+ const id = msg.id || msg.originalarrivaltime;
137
+ if (!id)
138
+ continue;
139
+ const content = msg.content || '';
140
+ const contentType = msg.messagetype || 'Text';
141
+ const fromMri = msg.from || '';
142
+ const displayName = msg.imdisplayname || msg.displayName;
143
+ const timestamp = msg.originalarrivaltime ||
144
+ msg.composetime ||
145
+ new Date(parseInt(id, 10)).toISOString();
146
+ // Build message link
147
+ const messageLink = /^\d+$/.test(id)
148
+ ? buildMessageLink(conversationId, id)
149
+ : undefined;
150
+ messages.push({
151
+ id,
152
+ content: stripHtml(content),
153
+ contentType,
154
+ sender: {
155
+ mri: fromMri,
156
+ displayName,
157
+ },
158
+ timestamp,
159
+ conversationId,
160
+ clientMessageId: msg.clientmessageid,
161
+ isFromMe: fromMri === auth.userMri,
162
+ messageLink,
163
+ });
164
+ }
165
+ // Sort by timestamp (oldest first)
166
+ messages.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
167
+ return ok({
168
+ conversationId,
169
+ messages,
170
+ });
171
+ }
172
+ /**
173
+ * Saves (bookmarks) a message.
174
+ */
175
+ export async function saveMessage(conversationId, messageId, region = 'amer') {
176
+ return setMessageSavedState(conversationId, messageId, true, region);
177
+ }
178
+ /**
179
+ * Unsaves (removes bookmark from) a message.
180
+ */
181
+ export async function unsaveMessage(conversationId, messageId, region = 'amer') {
182
+ return setMessageSavedState(conversationId, messageId, false, region);
183
+ }
184
+ /**
185
+ * Internal function to set the saved state of a message.
186
+ */
187
+ async function setMessageSavedState(conversationId, messageId, saved, region) {
188
+ const authResult = requireMessageAuth();
189
+ if (!authResult.ok) {
190
+ return authResult;
191
+ }
192
+ const auth = authResult.value;
193
+ const validRegion = validateRegion(region);
194
+ const url = CHATSVC_API.messageMetadata(validRegion, conversationId, messageId);
195
+ const response = await httpRequest(url, {
196
+ method: 'PUT',
197
+ headers: getSkypeAuthHeaders(auth.skypeToken, auth.authToken),
198
+ body: JSON.stringify({
199
+ s: saved ? 1 : 0,
200
+ mid: parseInt(messageId, 10),
201
+ }),
202
+ });
203
+ if (!response.ok) {
204
+ return response;
205
+ }
206
+ return ok({
207
+ conversationId,
208
+ messageId,
209
+ saved,
210
+ });
211
+ }
212
+ /**
213
+ * Edits an existing message.
214
+ *
215
+ * Note: You can only edit your own messages. The API will reject
216
+ * attempts to edit messages from other users.
217
+ *
218
+ * @param conversationId - The conversation containing the message
219
+ * @param messageId - The ID of the message to edit
220
+ * @param newContent - The new content for the message
221
+ * @param region - API region (default: 'amer')
222
+ */
223
+ export async function editMessage(conversationId, messageId, newContent, region = 'amer') {
224
+ const authResult = requireMessageAuth();
225
+ if (!authResult.ok) {
226
+ return authResult;
227
+ }
228
+ const auth = authResult.value;
229
+ const validRegion = validateRegion(region);
230
+ const displayName = getUserDisplayName() || 'User';
231
+ // Wrap content in paragraph if not already HTML
232
+ const htmlContent = newContent.startsWith('<') ? newContent : `<p>${escapeHtml(newContent)}</p>`;
233
+ // Build the edit request body
234
+ // The API requires the message structure with updated content
235
+ const body = {
236
+ id: messageId,
237
+ type: 'Message',
238
+ conversationid: conversationId,
239
+ content: htmlContent,
240
+ messagetype: 'RichText/Html',
241
+ contenttype: 'text',
242
+ imdisplayname: displayName,
243
+ };
244
+ const url = CHATSVC_API.editMessage(validRegion, conversationId, messageId);
245
+ const response = await httpRequest(url, {
246
+ method: 'PUT',
247
+ headers: getMessagingHeaders(auth.skypeToken, auth.authToken),
248
+ body: JSON.stringify(body),
249
+ });
250
+ if (!response.ok) {
251
+ return response;
252
+ }
253
+ return ok({
254
+ messageId,
255
+ conversationId,
256
+ });
257
+ }
258
+ /**
259
+ * Deletes a message (soft delete).
260
+ *
261
+ * Note: You can only delete your own messages, unless you are a
262
+ * channel owner/moderator. The API will reject unauthorised attempts.
263
+ *
264
+ * @param conversationId - The conversation containing the message
265
+ * @param messageId - The ID of the message to delete
266
+ * @param region - API region (default: 'amer')
267
+ */
268
+ export async function deleteMessage(conversationId, messageId, region = 'amer') {
269
+ const authResult = requireMessageAuth();
270
+ if (!authResult.ok) {
271
+ return authResult;
272
+ }
273
+ const auth = authResult.value;
274
+ const validRegion = validateRegion(region);
275
+ const url = CHATSVC_API.deleteMessage(validRegion, conversationId, messageId);
276
+ const response = await httpRequest(url, {
277
+ method: 'DELETE',
278
+ headers: getMessagingHeaders(auth.skypeToken, auth.authToken),
279
+ });
280
+ if (!response.ok) {
281
+ return response;
282
+ }
283
+ return ok({
284
+ messageId,
285
+ conversationId,
286
+ });
287
+ }
288
+ /**
289
+ * Gets properties for a single conversation.
290
+ */
291
+ export async function getConversationProperties(conversationId, region = 'amer') {
292
+ const authResult = requireMessageAuth();
293
+ if (!authResult.ok) {
294
+ return authResult;
295
+ }
296
+ const auth = authResult.value;
297
+ const validRegion = validateRegion(region);
298
+ const url = CHATSVC_API.conversation(validRegion, conversationId) + '?view=msnp24Equivalent';
299
+ const response = await httpRequest(url, {
300
+ method: 'GET',
301
+ headers: getSkypeAuthHeaders(auth.skypeToken, auth.authToken),
302
+ });
303
+ if (!response.ok) {
304
+ return response;
305
+ }
306
+ const data = response.value.data;
307
+ const threadProps = data.threadProperties;
308
+ const productType = threadProps?.productThreadType;
309
+ // Try to get display name from various sources
310
+ let displayName;
311
+ if (threadProps?.topicThreadTopic) {
312
+ displayName = threadProps.topicThreadTopic;
313
+ }
314
+ if (!displayName && threadProps?.topic) {
315
+ displayName = threadProps.topic;
316
+ }
317
+ if (!displayName && threadProps?.spaceThreadTopic) {
318
+ displayName = threadProps.spaceThreadTopic;
319
+ }
320
+ if (!displayName && threadProps?.threadtopic) {
321
+ displayName = threadProps.threadtopic;
322
+ }
323
+ // For chats without a topic: build from members
324
+ if (!displayName) {
325
+ const members = data.members;
326
+ if (members && members.length > 0) {
327
+ const otherMembers = members
328
+ .filter(m => m.mri !== auth.userMri && m.id !== auth.userMri)
329
+ .map(m => (m.friendlyName || m.displayName || m.name))
330
+ .filter((name) => !!name);
331
+ if (otherMembers.length > 0) {
332
+ displayName = otherMembers.length <= 3
333
+ ? otherMembers.join(', ')
334
+ : `${otherMembers.slice(0, 3).join(', ')} + ${otherMembers.length - 3} more`;
335
+ }
336
+ }
337
+ }
338
+ // Determine conversation type
339
+ let conversationType;
340
+ if (productType) {
341
+ if (productType === 'Meeting') {
342
+ conversationType = 'Meeting';
343
+ }
344
+ else if (productType.includes('Channel') || productType === 'TeamsTeam') {
345
+ conversationType = 'Channel';
346
+ }
347
+ else if (productType === 'Chat' || productType === 'OneOnOne') {
348
+ conversationType = 'Chat';
349
+ }
350
+ }
351
+ // Fallback to ID pattern detection
352
+ if (!conversationType) {
353
+ if (conversationId.includes('meeting_')) {
354
+ conversationType = 'Meeting';
355
+ }
356
+ else if (threadProps?.groupId) {
357
+ conversationType = 'Channel';
358
+ }
359
+ else if (conversationId.includes('@thread.tacv2') || conversationId.includes('@thread.v2')) {
360
+ conversationType = 'Chat';
361
+ }
362
+ else if (conversationId.startsWith('8:')) {
363
+ conversationType = 'Chat';
364
+ }
365
+ }
366
+ return ok({ displayName, conversationType });
367
+ }
368
+ /**
369
+ * Extracts unique participant names from recent messages.
370
+ */
371
+ export async function extractParticipantNames(conversationId, region = 'amer') {
372
+ const authResult = requireMessageAuth();
373
+ if (!authResult.ok) {
374
+ return ok(undefined); // Non-critical: just return undefined if not authenticated
375
+ }
376
+ const auth = authResult.value;
377
+ const validRegion = validateRegion(region);
378
+ let url = CHATSVC_API.messages(validRegion, conversationId);
379
+ url += '?view=msnp24Equivalent&pageSize=10';
380
+ const response = await httpRequest(url, {
381
+ method: 'GET',
382
+ headers: getSkypeAuthHeaders(auth.skypeToken, auth.authToken),
383
+ });
384
+ if (!response.ok) {
385
+ return ok(undefined);
386
+ }
387
+ const messages = response.value.data.messages;
388
+ if (!messages || messages.length === 0) {
389
+ return ok(undefined);
390
+ }
391
+ const senderNames = new Set();
392
+ for (const msg of messages) {
393
+ const m = msg;
394
+ const fromMri = m.from || '';
395
+ const displayName = m.imdisplayname;
396
+ if (fromMri === auth.userMri || !displayName) {
397
+ continue;
398
+ }
399
+ senderNames.add(displayName);
400
+ }
401
+ if (senderNames.size === 0) {
402
+ return ok(undefined);
403
+ }
404
+ const names = Array.from(senderNames);
405
+ const result = names.length <= 3
406
+ ? names.join(', ')
407
+ : `${names.slice(0, 3).join(', ')} + ${names.length - 3} more`;
408
+ return ok(result);
409
+ }
410
+ /**
411
+ * Escapes HTML special characters.
412
+ */
413
+ function escapeHtml(text) {
414
+ return text
415
+ .replace(/&/g, '&amp;')
416
+ .replace(/</g, '&lt;')
417
+ .replace(/>/g, '&gt;')
418
+ .replace(/"/g, '&quot;');
419
+ }
420
+ /**
421
+ * Gets the conversation ID for a 1:1 chat with another user.
422
+ *
423
+ * This constructs the predictable conversation ID format used by Teams
424
+ * for 1:1 chats. The conversation ID is: `19:{id1}_{id2}@unq.gbl.spaces`
425
+ * where id1 and id2 are the two users' object IDs sorted lexicographically.
426
+ *
427
+ * Note: This doesn't create the conversation - it just returns the ID.
428
+ * The conversation is implicitly created when the first message is sent.
429
+ *
430
+ * @param otherUserIdentifier - The other user's MRI, object ID, or ID with tenant
431
+ * @returns The conversation ID, or an error if auth is missing or ID is invalid
432
+ */
433
+ export function getOneOnOneChatId(otherUserIdentifier) {
434
+ const authResult = requireMessageAuth();
435
+ if (!authResult.ok) {
436
+ return authResult;
437
+ }
438
+ const auth = authResult.value;
439
+ // Extract the current user's object ID from their MRI
440
+ const currentUserId = extractObjectId(auth.userMri);
441
+ if (!currentUserId) {
442
+ return err(createError(ErrorCode.AUTH_REQUIRED, 'Could not extract user ID from session. Please try logging in again.'));
443
+ }
444
+ // Extract the other user's object ID
445
+ const otherUserId = extractObjectId(otherUserIdentifier);
446
+ if (!otherUserId) {
447
+ return err(createError(ErrorCode.INVALID_INPUT, `Invalid user identifier: ${otherUserIdentifier}. Expected MRI (8:orgid:guid), ID with tenant (guid@tenant), or raw GUID.`));
448
+ }
449
+ const conversationId = buildOneOnOneConversationId(currentUserId, otherUserId);
450
+ if (!conversationId) {
451
+ // This shouldn't happen if both IDs were validated above, but handle it anyway
452
+ return err(createError(ErrorCode.UNKNOWN, 'Failed to construct conversation ID.'));
453
+ }
454
+ return ok({
455
+ conversationId,
456
+ otherUserId,
457
+ currentUserId,
458
+ });
459
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * CSA (Chat Service Aggregator) API client for favorites and teams operations.
3
+ *
4
+ * Handles all calls to teams.microsoft.com/api/csa endpoints.
5
+ */
6
+ import { type Result } from '../types/result.js';
7
+ import { type TeamWithChannels } from '../utils/parsers.js';
8
+ /** A favourite/pinned conversation item. */
9
+ export interface FavoriteItem {
10
+ conversationId: string;
11
+ displayName?: string;
12
+ conversationType?: string;
13
+ createdTime?: number;
14
+ lastUpdatedTime?: number;
15
+ }
16
+ /** Response from getting favorites. */
17
+ export interface FavoritesResult {
18
+ favorites: FavoriteItem[];
19
+ folderHierarchyVersion?: number;
20
+ folderId?: string;
21
+ }
22
+ /**
23
+ * Gets the user's favourite/pinned conversations.
24
+ */
25
+ export declare function getFavorites(region?: string): Promise<Result<FavoritesResult>>;
26
+ /**
27
+ * Adds a conversation to the user's favourites.
28
+ */
29
+ export declare function addFavorite(conversationId: string, region?: string): Promise<Result<void>>;
30
+ /**
31
+ * Removes a conversation from the user's favourites.
32
+ */
33
+ export declare function removeFavorite(conversationId: string, region?: string): Promise<Result<void>>;
34
+ /** Response from getting the user's teams and channels. */
35
+ export interface TeamsListResult {
36
+ teams: TeamWithChannels[];
37
+ }
38
+ /**
39
+ * Gets all teams and channels the user is a member of.
40
+ *
41
+ * This returns the complete list of teams with their channels - not a search,
42
+ * but a full enumeration of the user's memberships.
43
+ */
44
+ export declare function getMyTeamsAndChannels(region?: string): Promise<Result<TeamsListResult>>;