msteams-mcp 0.2.0 → 0.3.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.
- package/README.md +5 -0
- package/dist/__fixtures__/api-responses.d.ts +6 -6
- package/dist/__fixtures__/api-responses.js +4 -4
- package/dist/api/chatsvc-api.d.ts +85 -21
- package/dist/api/chatsvc-api.js +231 -25
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/token-extractor.js +46 -60
- package/dist/auth/token-refresh.d.ts +47 -0
- package/dist/auth/token-refresh.js +121 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +19 -0
- package/dist/index.js +0 -0
- package/dist/test/cli.js +124 -12
- package/dist/test/mcp-harness.js +31 -0
- package/dist/tools/message-tools.d.ts +39 -0
- package/dist/tools/message-tools.js +177 -2
- package/dist/tools/people-tools.d.ts +2 -2
- package/dist/tools/search-tools.d.ts +8 -2
- package/dist/tools/search-tools.js +49 -2
- package/dist/utils/api-config.d.ts +6 -0
- package/dist/utils/api-config.js +7 -0
- package/dist/utils/auth-guards.d.ts +7 -0
- package/dist/utils/auth-guards.js +42 -1
- package/dist/utils/parsers.d.ts +15 -0
- package/dist/utils/parsers.js +31 -0
- package/dist/utils/parsers.test.js +55 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,7 @@ Then configure:
|
|
|
66
66
|
| `teams_search` | Search messages with operators (`from:`, `sent:`, `in:`, `hasattachment:`, etc.) |
|
|
67
67
|
| `teams_get_thread` | Get messages from a conversation/thread |
|
|
68
68
|
| `teams_find_channel` | Find channels by name (your teams + org-wide discovery) |
|
|
69
|
+
| `teams_get_activity` | Get activity feed (mentions, reactions, replies, notifications) |
|
|
69
70
|
|
|
70
71
|
### Messaging
|
|
71
72
|
|
|
@@ -94,6 +95,8 @@ Then configure:
|
|
|
94
95
|
| `teams_remove_favorite` | Unpin a conversation |
|
|
95
96
|
| `teams_save_message` | Bookmark a message |
|
|
96
97
|
| `teams_unsave_message` | Remove bookmark from a message |
|
|
98
|
+
| `teams_get_unread` | Get unread counts (aggregate or per-conversation) |
|
|
99
|
+
| `teams_mark_read` | Mark a conversation as read up to a message |
|
|
97
100
|
|
|
98
101
|
### Session
|
|
99
102
|
|
|
@@ -166,6 +169,8 @@ npm run test:mcp -- search "your query"
|
|
|
166
169
|
npm run test:mcp -- status
|
|
167
170
|
npm run test:mcp -- people "john smith"
|
|
168
171
|
npm run test:mcp -- favorites
|
|
172
|
+
npm run test:mcp -- activity # Get activity feed
|
|
173
|
+
npm run test:mcp -- unread # Check unread counts
|
|
169
174
|
```
|
|
170
175
|
|
|
171
176
|
## Limitations
|
|
@@ -14,7 +14,7 @@ export declare const searchResultItem: {
|
|
|
14
14
|
HitHighlightedSummary: string;
|
|
15
15
|
Summary: string;
|
|
16
16
|
Source: {
|
|
17
|
-
|
|
17
|
+
DateTimeReceived: string;
|
|
18
18
|
From: {
|
|
19
19
|
EmailAddress: {
|
|
20
20
|
Name: string;
|
|
@@ -37,7 +37,7 @@ export declare const searchResultWithHtml: {
|
|
|
37
37
|
ReferenceId: string;
|
|
38
38
|
HitHighlightedSummary: string;
|
|
39
39
|
Source: {
|
|
40
|
-
|
|
40
|
+
DateTimeReceived: string;
|
|
41
41
|
From: string;
|
|
42
42
|
ClientThreadId: string;
|
|
43
43
|
};
|
|
@@ -69,7 +69,7 @@ export declare const searchEntitySetsResponse: {
|
|
|
69
69
|
HitHighlightedSummary: string;
|
|
70
70
|
Summary: string;
|
|
71
71
|
Source: {
|
|
72
|
-
|
|
72
|
+
DateTimeReceived: string;
|
|
73
73
|
From: {
|
|
74
74
|
EmailAddress: {
|
|
75
75
|
Name: string;
|
|
@@ -88,7 +88,7 @@ export declare const searchEntitySetsResponse: {
|
|
|
88
88
|
ReferenceId: string;
|
|
89
89
|
HitHighlightedSummary: string;
|
|
90
90
|
Source: {
|
|
91
|
-
|
|
91
|
+
DateTimeReceived: string;
|
|
92
92
|
From: string;
|
|
93
93
|
ClientThreadId: string;
|
|
94
94
|
};
|
|
@@ -183,14 +183,14 @@ export declare const jwtPayloadSpaceName: {
|
|
|
183
183
|
*/
|
|
184
184
|
export declare const sourceWithMessageId: {
|
|
185
185
|
MessageId: string;
|
|
186
|
-
|
|
186
|
+
DateTimeReceived: string;
|
|
187
187
|
ClientConversationId: string;
|
|
188
188
|
};
|
|
189
189
|
/**
|
|
190
190
|
* Message source with ID in ClientConversationId.
|
|
191
191
|
*/
|
|
192
192
|
export declare const sourceWithConvIdMessageId: {
|
|
193
|
-
|
|
193
|
+
DateTimeReceived: string;
|
|
194
194
|
ClientConversationId: string;
|
|
195
195
|
};
|
|
196
196
|
/**
|
|
@@ -14,7 +14,7 @@ export const searchResultItem = {
|
|
|
14
14
|
HitHighlightedSummary: 'Let me check the <c0>budget</c0> report for Q3',
|
|
15
15
|
Summary: 'Let me check the budget report for Q3',
|
|
16
16
|
Source: {
|
|
17
|
-
|
|
17
|
+
DateTimeReceived: '2026-01-20T14:30:00.000Z',
|
|
18
18
|
From: {
|
|
19
19
|
EmailAddress: {
|
|
20
20
|
Name: 'Smith, John',
|
|
@@ -37,7 +37,7 @@ export const searchResultWithHtml = {
|
|
|
37
37
|
ReferenceId: 'xyz789.1000.1',
|
|
38
38
|
HitHighlightedSummary: '<p>Meeting <strong>notes</strong> from & yesterday's call</p><br/><div>Action items:</div>',
|
|
39
39
|
Source: {
|
|
40
|
-
|
|
40
|
+
DateTimeReceived: '2026-01-21T09:00:00.000Z',
|
|
41
41
|
From: 'Jane Doe',
|
|
42
42
|
ClientThreadId: '19:meeting123@thread.v2',
|
|
43
43
|
},
|
|
@@ -163,14 +163,14 @@ export const jwtPayloadSpaceName = {
|
|
|
163
163
|
*/
|
|
164
164
|
export const sourceWithMessageId = {
|
|
165
165
|
MessageId: '1705760000000',
|
|
166
|
-
|
|
166
|
+
DateTimeReceived: '2026-01-20T12:00:00.000Z',
|
|
167
167
|
ClientConversationId: '19:thread@tacv2',
|
|
168
168
|
};
|
|
169
169
|
/**
|
|
170
170
|
* Message source with ID in ClientConversationId.
|
|
171
171
|
*/
|
|
172
172
|
export const sourceWithConvIdMessageId = {
|
|
173
|
-
|
|
173
|
+
DateTimeReceived: '2026-01-20T12:00:00.000Z',
|
|
174
174
|
ClientConversationId: '19:thread@tacv2;messageid=1705770000000',
|
|
175
175
|
};
|
|
176
176
|
/**
|
|
@@ -76,18 +76,8 @@ export interface ReplyToThreadResult extends SendMessageResult {
|
|
|
76
76
|
/**
|
|
77
77
|
* Replies to a thread in a Teams channel.
|
|
78
78
|
*
|
|
79
|
-
* Uses the provided messageId
|
|
80
|
-
*
|
|
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
|
|
79
|
+
* Uses the provided messageId as the thread root. For search results,
|
|
80
|
+
* this is typically the original message that started the thread.
|
|
91
81
|
*/
|
|
92
82
|
export declare function replyToThread(conversationId: string, messageId: string, content: string, region?: string): Promise<Result<ReplyToThreadResult>>;
|
|
93
83
|
/**
|
|
@@ -158,14 +148,88 @@ export interface GetOneOnOneChatResult {
|
|
|
158
148
|
/**
|
|
159
149
|
* Gets the conversation ID for a 1:1 chat with another user.
|
|
160
150
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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
|
|
151
|
+
* Constructs the predictable format: `19:{id1}_{id2}@unq.gbl.spaces`
|
|
152
|
+
* where IDs are sorted lexicographically. The conversation is created
|
|
153
|
+
* implicitly when the first message is sent.
|
|
170
154
|
*/
|
|
171
155
|
export declare function getOneOnOneChatId(otherUserIdentifier: string): Result<GetOneOnOneChatResult>;
|
|
156
|
+
/** Consumption horizon information for a conversation. */
|
|
157
|
+
export interface ConsumptionHorizonInfo {
|
|
158
|
+
/** The conversation/thread ID. */
|
|
159
|
+
conversationId: string;
|
|
160
|
+
/** The version timestamp of the consumption horizon. */
|
|
161
|
+
version?: string;
|
|
162
|
+
/** The last read message ID (timestamp). */
|
|
163
|
+
lastReadMessageId?: string;
|
|
164
|
+
/** The last read timestamp. */
|
|
165
|
+
lastReadTimestamp?: number;
|
|
166
|
+
/** Raw consumption horizons array from API. */
|
|
167
|
+
consumptionHorizons: Array<{
|
|
168
|
+
id: string;
|
|
169
|
+
consumptionHorizon: string;
|
|
170
|
+
}>;
|
|
171
|
+
}
|
|
172
|
+
/** Result of marking a conversation as read. */
|
|
173
|
+
export interface MarkAsReadResult {
|
|
174
|
+
conversationId: string;
|
|
175
|
+
markedUpTo: string;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Gets the consumption horizon (read receipts) for a conversation.
|
|
179
|
+
* The consumption horizon indicates where each user has read up to.
|
|
180
|
+
*/
|
|
181
|
+
export declare function getConsumptionHorizon(conversationId: string, region?: string): Promise<Result<ConsumptionHorizonInfo>>;
|
|
182
|
+
/**
|
|
183
|
+
* Marks a conversation as read up to a specific message.
|
|
184
|
+
*/
|
|
185
|
+
export declare function markAsRead(conversationId: string, messageId: string, region?: string): Promise<Result<MarkAsReadResult>>;
|
|
186
|
+
/**
|
|
187
|
+
* Gets unread count for a conversation by comparing consumption horizon
|
|
188
|
+
* with recent messages.
|
|
189
|
+
*/
|
|
190
|
+
export declare function getUnreadStatus(conversationId: string, region?: string): Promise<Result<{
|
|
191
|
+
conversationId: string;
|
|
192
|
+
unreadCount: number;
|
|
193
|
+
lastReadMessageId?: string;
|
|
194
|
+
latestMessageId?: string;
|
|
195
|
+
}>>;
|
|
196
|
+
/** Type of activity item. */
|
|
197
|
+
export type ActivityType = 'mention' | 'reaction' | 'reply' | 'message' | 'unknown';
|
|
198
|
+
/** An activity item from the notifications feed. */
|
|
199
|
+
export interface ActivityItem {
|
|
200
|
+
/** Activity/message ID. */
|
|
201
|
+
id: string;
|
|
202
|
+
/** Type of activity (mention, reaction, reply, etc.). */
|
|
203
|
+
type: ActivityType;
|
|
204
|
+
/** Activity content (HTML stripped). */
|
|
205
|
+
content: string;
|
|
206
|
+
/** Raw content type from API. */
|
|
207
|
+
contentType: string;
|
|
208
|
+
/** Sender information. */
|
|
209
|
+
sender: {
|
|
210
|
+
mri: string;
|
|
211
|
+
displayName?: string;
|
|
212
|
+
};
|
|
213
|
+
/** When the activity occurred. */
|
|
214
|
+
timestamp: string;
|
|
215
|
+
/** The source conversation ID (where the activity happened). */
|
|
216
|
+
conversationId?: string;
|
|
217
|
+
/** The conversation/thread topic name. */
|
|
218
|
+
topic?: string;
|
|
219
|
+
/** Direct link to the activity in Teams. */
|
|
220
|
+
activityLink?: string;
|
|
221
|
+
}
|
|
222
|
+
/** Result of fetching the activity feed. */
|
|
223
|
+
export interface GetActivityResult {
|
|
224
|
+
/** Activity items (newest first). */
|
|
225
|
+
activities: ActivityItem[];
|
|
226
|
+
/** Sync state for incremental polling. */
|
|
227
|
+
syncState?: string;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Gets the activity feed (notifications) for the current user.
|
|
231
|
+
* Includes mentions, reactions, replies, and other notifications.
|
|
232
|
+
*/
|
|
233
|
+
export declare function getActivityFeed(options?: {
|
|
234
|
+
limit?: number;
|
|
235
|
+
}, region?: string): Promise<Result<GetActivityResult>>;
|
package/dist/api/chatsvc-api.js
CHANGED
|
@@ -9,7 +9,8 @@ import { ErrorCode, createError } from '../types/errors.js';
|
|
|
9
9
|
import { ok, err } from '../types/result.js';
|
|
10
10
|
import { getUserDisplayName } from '../auth/token-extractor.js';
|
|
11
11
|
import { requireMessageAuth } from '../utils/auth-guards.js';
|
|
12
|
-
import { stripHtml, buildMessageLink, buildOneOnOneConversationId, extractObjectId } from '../utils/parsers.js';
|
|
12
|
+
import { stripHtml, buildMessageLink, buildOneOnOneConversationId, extractObjectId, extractActivityTimestamp } from '../utils/parsers.js';
|
|
13
|
+
import { DEFAULT_ACTIVITY_LIMIT } from '../constants.js';
|
|
13
14
|
/**
|
|
14
15
|
* Sends a message to a Teams conversation.
|
|
15
16
|
*
|
|
@@ -63,24 +64,11 @@ export async function sendNoteToSelf(content) {
|
|
|
63
64
|
/**
|
|
64
65
|
* Replies to a thread in a Teams channel.
|
|
65
66
|
*
|
|
66
|
-
* Uses the provided messageId
|
|
67
|
-
*
|
|
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
|
|
67
|
+
* Uses the provided messageId as the thread root. For search results,
|
|
68
|
+
* this is typically the original message that started the thread.
|
|
78
69
|
*/
|
|
79
70
|
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
71
|
const threadRootMessageId = messageId;
|
|
83
|
-
// Send the reply using the provided message ID as the thread root
|
|
84
72
|
const sendResult = await sendMessage(conversationId, content, {
|
|
85
73
|
region,
|
|
86
74
|
replyToMessageId: threadRootMessageId,
|
|
@@ -420,15 +408,9 @@ function escapeHtml(text) {
|
|
|
420
408
|
/**
|
|
421
409
|
* Gets the conversation ID for a 1:1 chat with another user.
|
|
422
410
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
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
|
|
411
|
+
* Constructs the predictable format: `19:{id1}_{id2}@unq.gbl.spaces`
|
|
412
|
+
* where IDs are sorted lexicographically. The conversation is created
|
|
413
|
+
* implicitly when the first message is sent.
|
|
432
414
|
*/
|
|
433
415
|
export function getOneOnOneChatId(otherUserIdentifier) {
|
|
434
416
|
const authResult = requireMessageAuth();
|
|
@@ -457,3 +439,227 @@ export function getOneOnOneChatId(otherUserIdentifier) {
|
|
|
457
439
|
currentUserId,
|
|
458
440
|
});
|
|
459
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Gets the consumption horizon (read receipts) for a conversation.
|
|
444
|
+
* The consumption horizon indicates where each user has read up to.
|
|
445
|
+
*/
|
|
446
|
+
export async function getConsumptionHorizon(conversationId, region = 'amer') {
|
|
447
|
+
const authResult = requireMessageAuth();
|
|
448
|
+
if (!authResult.ok) {
|
|
449
|
+
return authResult;
|
|
450
|
+
}
|
|
451
|
+
const auth = authResult.value;
|
|
452
|
+
const validRegion = validateRegion(region);
|
|
453
|
+
const url = CHATSVC_API.consumptionHorizons(validRegion, conversationId);
|
|
454
|
+
const response = await httpRequest(url, {
|
|
455
|
+
method: 'GET',
|
|
456
|
+
headers: getSkypeAuthHeaders(auth.skypeToken, auth.authToken),
|
|
457
|
+
});
|
|
458
|
+
if (!response.ok) {
|
|
459
|
+
return response;
|
|
460
|
+
}
|
|
461
|
+
const data = response.value.data;
|
|
462
|
+
const horizons = data.consumptionhorizons || [];
|
|
463
|
+
// Find the current user's consumption horizon
|
|
464
|
+
let lastReadMessageId;
|
|
465
|
+
let lastReadTimestamp;
|
|
466
|
+
for (const h of horizons) {
|
|
467
|
+
if (h.id === auth.userMri || h.id.includes(auth.userMri)) {
|
|
468
|
+
// Consumption horizon format: "{timestamp};{timestamp};{messageId}"
|
|
469
|
+
const parts = h.consumptionhorizon.split(';');
|
|
470
|
+
if (parts.length >= 3) {
|
|
471
|
+
lastReadMessageId = parts[2];
|
|
472
|
+
lastReadTimestamp = parseInt(parts[0], 10);
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return ok({
|
|
478
|
+
conversationId,
|
|
479
|
+
version: data.version,
|
|
480
|
+
lastReadMessageId,
|
|
481
|
+
lastReadTimestamp,
|
|
482
|
+
consumptionHorizons: horizons.map(h => ({
|
|
483
|
+
id: h.id,
|
|
484
|
+
consumptionHorizon: h.consumptionhorizon,
|
|
485
|
+
})),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Marks a conversation as read up to a specific message.
|
|
490
|
+
*/
|
|
491
|
+
export async function markAsRead(conversationId, messageId, region = 'amer') {
|
|
492
|
+
const authResult = requireMessageAuth();
|
|
493
|
+
if (!authResult.ok) {
|
|
494
|
+
return authResult;
|
|
495
|
+
}
|
|
496
|
+
const auth = authResult.value;
|
|
497
|
+
const validRegion = validateRegion(region);
|
|
498
|
+
const url = CHATSVC_API.updateConsumptionHorizon(validRegion, conversationId);
|
|
499
|
+
// Format: "{messageId};{messageId};{messageId}" - all three values are the same
|
|
500
|
+
const consumptionHorizon = `${messageId};${messageId};${messageId}`;
|
|
501
|
+
const response = await httpRequest(url, {
|
|
502
|
+
method: 'PUT',
|
|
503
|
+
headers: getSkypeAuthHeaders(auth.skypeToken, auth.authToken),
|
|
504
|
+
body: JSON.stringify({
|
|
505
|
+
consumptionhorizon: consumptionHorizon,
|
|
506
|
+
}),
|
|
507
|
+
});
|
|
508
|
+
if (!response.ok) {
|
|
509
|
+
return response;
|
|
510
|
+
}
|
|
511
|
+
return ok({
|
|
512
|
+
conversationId,
|
|
513
|
+
markedUpTo: messageId,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Gets unread count for a conversation by comparing consumption horizon
|
|
518
|
+
* with recent messages.
|
|
519
|
+
*/
|
|
520
|
+
export async function getUnreadStatus(conversationId, region = 'amer') {
|
|
521
|
+
// Get consumption horizon
|
|
522
|
+
const horizonResult = await getConsumptionHorizon(conversationId, region);
|
|
523
|
+
if (!horizonResult.ok) {
|
|
524
|
+
return horizonResult;
|
|
525
|
+
}
|
|
526
|
+
// Get recent messages
|
|
527
|
+
const messagesResult = await getThreadMessages(conversationId, { limit: 50 }, region);
|
|
528
|
+
if (!messagesResult.ok) {
|
|
529
|
+
return messagesResult;
|
|
530
|
+
}
|
|
531
|
+
const lastReadId = horizonResult.value.lastReadMessageId;
|
|
532
|
+
const messages = messagesResult.value.messages;
|
|
533
|
+
// Count messages after the last read position
|
|
534
|
+
let unreadCount = 0;
|
|
535
|
+
let foundLastRead = false;
|
|
536
|
+
let latestMessageId;
|
|
537
|
+
// Messages are sorted oldest-first, so reverse to process newest-first
|
|
538
|
+
const reversedMessages = [...messages].reverse();
|
|
539
|
+
for (const msg of reversedMessages) {
|
|
540
|
+
if (!latestMessageId && !msg.isFromMe) {
|
|
541
|
+
latestMessageId = msg.id;
|
|
542
|
+
}
|
|
543
|
+
if (lastReadId && msg.id === lastReadId) {
|
|
544
|
+
foundLastRead = true;
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
// Count messages not from the current user
|
|
548
|
+
if (!msg.isFromMe) {
|
|
549
|
+
unreadCount++;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// If last read message wasn't in our window, count is a lower bound
|
|
553
|
+
return ok({
|
|
554
|
+
conversationId,
|
|
555
|
+
unreadCount,
|
|
556
|
+
lastReadMessageId: lastReadId,
|
|
557
|
+
latestMessageId,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Determines the activity type from message content and properties.
|
|
562
|
+
*/
|
|
563
|
+
function detectActivityType(msg) {
|
|
564
|
+
const content = msg.content || '';
|
|
565
|
+
const messageType = msg.messagetype || '';
|
|
566
|
+
// Check for @mention
|
|
567
|
+
if (content.includes('itemtype="http://schema.skype.com/Mention"') ||
|
|
568
|
+
content.includes('itemscope itemtype="http://schema.skype.com/Mention"')) {
|
|
569
|
+
return 'mention';
|
|
570
|
+
}
|
|
571
|
+
// Check for reaction-related message types
|
|
572
|
+
if (messageType.toLowerCase().includes('reaction')) {
|
|
573
|
+
return 'reaction';
|
|
574
|
+
}
|
|
575
|
+
// Check for thread/reply indicators
|
|
576
|
+
if (msg.threadtopic || msg.parentMessageId) {
|
|
577
|
+
return 'reply';
|
|
578
|
+
}
|
|
579
|
+
// Standard message
|
|
580
|
+
if (messageType.includes('RichText') || messageType.includes('Text')) {
|
|
581
|
+
return 'message';
|
|
582
|
+
}
|
|
583
|
+
return 'unknown';
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Gets the activity feed (notifications) for the current user.
|
|
587
|
+
* Includes mentions, reactions, replies, and other notifications.
|
|
588
|
+
*/
|
|
589
|
+
export async function getActivityFeed(options = {}, region = 'amer') {
|
|
590
|
+
const authResult = requireMessageAuth();
|
|
591
|
+
if (!authResult.ok) {
|
|
592
|
+
return authResult;
|
|
593
|
+
}
|
|
594
|
+
const auth = authResult.value;
|
|
595
|
+
const validRegion = validateRegion(region);
|
|
596
|
+
const limit = options.limit ?? DEFAULT_ACTIVITY_LIMIT;
|
|
597
|
+
let url = CHATSVC_API.activityFeed(validRegion);
|
|
598
|
+
url += `?view=msnp24Equivalent&pageSize=${limit}`;
|
|
599
|
+
const response = await httpRequest(url, {
|
|
600
|
+
method: 'GET',
|
|
601
|
+
headers: getSkypeAuthHeaders(auth.skypeToken, auth.authToken),
|
|
602
|
+
});
|
|
603
|
+
if (!response.ok) {
|
|
604
|
+
return response;
|
|
605
|
+
}
|
|
606
|
+
const rawMessages = response.value.data.messages;
|
|
607
|
+
const syncState = response.value.data.syncState;
|
|
608
|
+
if (!Array.isArray(rawMessages)) {
|
|
609
|
+
return ok({
|
|
610
|
+
activities: [],
|
|
611
|
+
syncState,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
const activities = [];
|
|
615
|
+
for (const raw of rawMessages) {
|
|
616
|
+
const msg = raw;
|
|
617
|
+
// Skip control/system messages that aren't relevant
|
|
618
|
+
const messageType = msg.messagetype;
|
|
619
|
+
if (!messageType ||
|
|
620
|
+
messageType.startsWith('Control/') ||
|
|
621
|
+
messageType === 'ThreadActivity/AddMember' ||
|
|
622
|
+
messageType === 'ThreadActivity/DeleteMember') {
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const id = msg.id || msg.originalarrivaltime;
|
|
626
|
+
if (!id)
|
|
627
|
+
continue;
|
|
628
|
+
const content = msg.content || '';
|
|
629
|
+
const contentType = msg.messagetype || 'Text';
|
|
630
|
+
const fromMri = msg.from || '';
|
|
631
|
+
const displayName = msg.imdisplayname || msg.displayName;
|
|
632
|
+
// Safely extract timestamp - returns null if no valid timestamp found
|
|
633
|
+
const timestamp = extractActivityTimestamp(msg);
|
|
634
|
+
if (!timestamp)
|
|
635
|
+
continue;
|
|
636
|
+
const conversationId = msg.conversationid || msg.conversationId;
|
|
637
|
+
const topic = msg.threadtopic || msg.topic;
|
|
638
|
+
// Build activity link if we have the conversation context
|
|
639
|
+
let activityLink;
|
|
640
|
+
if (conversationId && /^\d+$/.test(id)) {
|
|
641
|
+
activityLink = buildMessageLink(conversationId, id);
|
|
642
|
+
}
|
|
643
|
+
const activityType = detectActivityType(msg);
|
|
644
|
+
activities.push({
|
|
645
|
+
id,
|
|
646
|
+
type: activityType,
|
|
647
|
+
content: stripHtml(content),
|
|
648
|
+
contentType,
|
|
649
|
+
sender: {
|
|
650
|
+
mri: fromMri,
|
|
651
|
+
displayName,
|
|
652
|
+
},
|
|
653
|
+
timestamp,
|
|
654
|
+
conversationId,
|
|
655
|
+
topic,
|
|
656
|
+
activityLink,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
// Sort by timestamp (newest first for activity feed)
|
|
660
|
+
activities.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
661
|
+
return ok({
|
|
662
|
+
activities,
|
|
663
|
+
syncState,
|
|
664
|
+
});
|
|
665
|
+
}
|
package/dist/auth/index.d.ts
CHANGED