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.
- package/README.md +229 -0
- package/dist/__fixtures__/api-responses.d.ts +228 -0
- package/dist/__fixtures__/api-responses.js +217 -0
- package/dist/api/chatsvc-api.d.ts +171 -0
- package/dist/api/chatsvc-api.js +459 -0
- package/dist/api/csa-api.d.ts +44 -0
- package/dist/api/csa-api.js +148 -0
- package/dist/api/index.d.ts +6 -0
- package/dist/api/index.js +6 -0
- package/dist/api/substrate-api.d.ts +50 -0
- package/dist/api/substrate-api.js +305 -0
- package/dist/auth/crypto.d.ts +32 -0
- package/dist/auth/crypto.js +66 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/session-store.d.ts +82 -0
- package/dist/auth/session-store.js +136 -0
- package/dist/auth/token-extractor.d.ts +69 -0
- package/dist/auth/token-extractor.js +330 -0
- package/dist/browser/auth.d.ts +43 -0
- package/dist/browser/auth.js +232 -0
- package/dist/browser/context.d.ts +40 -0
- package/dist/browser/context.js +121 -0
- package/dist/browser/session.d.ts +34 -0
- package/dist/browser/session.js +92 -0
- package/dist/constants.d.ts +54 -0
- package/dist/constants.js +72 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -0
- package/dist/research/explore.d.ts +11 -0
- package/dist/research/explore.js +267 -0
- package/dist/research/search-research.d.ts +17 -0
- package/dist/research/search-research.js +317 -0
- package/dist/server.d.ts +64 -0
- package/dist/server.js +291 -0
- package/dist/teams/api-interceptor.d.ts +54 -0
- package/dist/teams/api-interceptor.js +391 -0
- package/dist/teams/direct-api.d.ts +321 -0
- package/dist/teams/direct-api.js +1305 -0
- package/dist/teams/messages.d.ts +14 -0
- package/dist/teams/messages.js +142 -0
- package/dist/teams/search.d.ts +40 -0
- package/dist/teams/search.js +458 -0
- package/dist/test/cli.d.ts +12 -0
- package/dist/test/cli.js +328 -0
- package/dist/test/debug-search.d.ts +10 -0
- package/dist/test/debug-search.js +147 -0
- package/dist/test/manual-test.d.ts +11 -0
- package/dist/test/manual-test.js +160 -0
- package/dist/test/mcp-harness.d.ts +17 -0
- package/dist/test/mcp-harness.js +427 -0
- package/dist/tools/auth-tools.d.ts +26 -0
- package/dist/tools/auth-tools.js +127 -0
- package/dist/tools/index.d.ts +45 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/message-tools.d.ts +139 -0
- package/dist/tools/message-tools.js +433 -0
- package/dist/tools/people-tools.d.ts +46 -0
- package/dist/tools/people-tools.js +123 -0
- package/dist/tools/registry.d.ts +23 -0
- package/dist/tools/registry.js +61 -0
- package/dist/tools/search-tools.d.ts +79 -0
- package/dist/tools/search-tools.js +168 -0
- package/dist/types/errors.d.ts +58 -0
- package/dist/types/errors.js +132 -0
- package/dist/types/result.d.ts +43 -0
- package/dist/types/result.js +51 -0
- package/dist/types/teams.d.ts +79 -0
- package/dist/types/teams.js +5 -0
- package/dist/utils/api-config.d.ts +66 -0
- package/dist/utils/api-config.js +113 -0
- package/dist/utils/auth-guards.d.ts +29 -0
- package/dist/utils/auth-guards.js +54 -0
- package/dist/utils/http.d.ts +29 -0
- package/dist/utils/http.js +111 -0
- package/dist/utils/parsers.d.ts +187 -0
- package/dist/utils/parsers.js +574 -0
- package/dist/utils/parsers.test.d.ts +7 -0
- package/dist/utils/parsers.test.js +360 -0
- 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, '&')
|
|
416
|
+
.replace(/</g, '<')
|
|
417
|
+
.replace(/>/g, '>')
|
|
418
|
+
.replace(/"/g, '"');
|
|
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>>;
|