msteams-mcp 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of msteams-mcp might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/__fixtures__/api-responses.d.ts +254 -0
- package/dist/__fixtures__/api-responses.js +245 -0
- package/dist/api/calendar-api.d.ts +66 -0
- package/dist/api/calendar-api.js +179 -0
- package/dist/api/chatsvc-api.d.ts +352 -0
- package/dist/api/chatsvc-api.js +1100 -0
- package/dist/api/csa-api.d.ts +64 -0
- package/dist/api/csa-api.js +200 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.js +7 -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 +7 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/session-store.d.ts +87 -0
- package/dist/auth/session-store.js +230 -0
- package/dist/auth/token-extractor.d.ts +185 -0
- package/dist/auth/token-extractor.js +674 -0
- package/dist/auth/token-refresh.d.ts +25 -0
- package/dist/auth/token-refresh.js +85 -0
- package/dist/browser/auth.d.ts +53 -0
- package/dist/browser/auth.js +603 -0
- package/dist/browser/context.d.ts +40 -0
- package/dist/browser/context.js +122 -0
- package/dist/constants.d.ts +104 -0
- package/dist/constants.js +195 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -0
- package/dist/research/auth-research.d.ts +10 -0
- package/dist/research/auth-research.js +175 -0
- package/dist/research/explore.d.ts +11 -0
- package/dist/research/explore.js +270 -0
- package/dist/research/search-research.d.ts +17 -0
- package/dist/research/search-research.js +317 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.js +295 -0
- package/dist/test/debug-search.d.ts +10 -0
- package/dist/test/debug-search.js +147 -0
- package/dist/test/mcp-harness.d.ts +17 -0
- package/dist/test/mcp-harness.js +474 -0
- package/dist/tools/auth-tools.d.ts +26 -0
- package/dist/tools/auth-tools.js +191 -0
- package/dist/tools/index.d.ts +56 -0
- package/dist/tools/index.js +34 -0
- package/dist/tools/meeting-tools.d.ts +33 -0
- package/dist/tools/meeting-tools.js +64 -0
- package/dist/tools/message-tools.d.ts +269 -0
- package/dist/tools/message-tools.js +856 -0
- package/dist/tools/people-tools.d.ts +46 -0
- package/dist/tools/people-tools.js +112 -0
- package/dist/tools/registry.d.ts +23 -0
- package/dist/tools/registry.js +63 -0
- package/dist/tools/search-tools.d.ts +91 -0
- package/dist/tools/search-tools.js +222 -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/server.d.ts +27 -0
- package/dist/types/server.js +7 -0
- package/dist/types/teams.d.ts +85 -0
- package/dist/types/teams.js +4 -0
- package/dist/utils/api-config.d.ts +103 -0
- package/dist/utils/api-config.js +158 -0
- package/dist/utils/auth-guards.d.ts +67 -0
- package/dist/utils/auth-guards.js +147 -0
- package/dist/utils/http.d.ts +29 -0
- package/dist/utils/http.js +112 -0
- package/dist/utils/parsers.d.ts +247 -0
- package/dist/utils/parsers.js +731 -0
- package/dist/utils/parsers.test.d.ts +7 -0
- package/dist/utils/parsers.test.js +511 -0
- package/package.json +62 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API response fixtures for testing.
|
|
3
|
+
*
|
|
4
|
+
* These are based on real API response structures documented in docs/API-REFERENCE.md.
|
|
5
|
+
* They represent the shape of data returned by Teams/Substrate APIs.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Substrate v2 search result item.
|
|
9
|
+
* From: POST https://substrate.office.com/searchservice/api/v2/query
|
|
10
|
+
*/
|
|
11
|
+
export const searchResultItem = {
|
|
12
|
+
Id: 'AAMkAGE1OWFlZjc0LWYxMjQtNGM1Mi05NzJlLTU0MTU2ZGU1OGM1YQBGAAAAAACaT2h4EH4ZT5pQgKA-example',
|
|
13
|
+
ReferenceId: 'abc123-def456.1000.1',
|
|
14
|
+
HitHighlightedSummary: 'Let me check the <c0>budget</c0> report for Q3',
|
|
15
|
+
Summary: 'Let me check the budget report for Q3',
|
|
16
|
+
Source: {
|
|
17
|
+
DateTimeReceived: '2026-01-20T14:30:00.000Z', // = 1768919400000
|
|
18
|
+
From: {
|
|
19
|
+
EmailAddress: {
|
|
20
|
+
Name: 'Smith, John',
|
|
21
|
+
Address: 'john.smith@company.com',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
ChannelName: 'General',
|
|
25
|
+
TeamName: 'Finance Team',
|
|
26
|
+
Extensions: {
|
|
27
|
+
SkypeSpaces_ConversationPost_Extension_SkypeGroupId: '19:abcdef123456@thread.tacv2',
|
|
28
|
+
},
|
|
29
|
+
// Top-level post: messageid matches DateTimeReceived timestamp
|
|
30
|
+
ClientConversationId: '19:abcdef123456@thread.tacv2;messageid=1768919400000',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Search result with HTML content that needs stripping.
|
|
35
|
+
*/
|
|
36
|
+
export const searchResultWithHtml = {
|
|
37
|
+
Id: 'AAMkBGFiY2RlZg',
|
|
38
|
+
ReferenceId: 'xyz789.1000.1',
|
|
39
|
+
HitHighlightedSummary: '<p>Meeting <strong>notes</strong> from & yesterday's call</p><br/><div>Action items:</div>',
|
|
40
|
+
Source: {
|
|
41
|
+
DateTimeReceived: '2026-01-21T09:00:00.000Z',
|
|
42
|
+
From: 'Jane Doe',
|
|
43
|
+
ClientThreadId: '19:meeting123@thread.v2',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Minimal search result with only required fields.
|
|
48
|
+
*/
|
|
49
|
+
export const searchResultMinimal = {
|
|
50
|
+
Id: 'minimal-id',
|
|
51
|
+
HitHighlightedSummary: 'A short message here',
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Search result too short to be valid (content < 5 chars).
|
|
55
|
+
*/
|
|
56
|
+
export const searchResultTooShort = {
|
|
57
|
+
Id: 'short-id',
|
|
58
|
+
HitHighlightedSummary: 'Hi',
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Full EntitySets response structure from v2 query.
|
|
62
|
+
*/
|
|
63
|
+
export const searchEntitySetsResponse = {
|
|
64
|
+
EntitySets: [
|
|
65
|
+
{
|
|
66
|
+
ResultSets: [
|
|
67
|
+
{
|
|
68
|
+
Total: 4307,
|
|
69
|
+
Results: [
|
|
70
|
+
searchResultItem,
|
|
71
|
+
searchResultWithHtml,
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Person suggestion from Substrate suggestions API.
|
|
80
|
+
* From: POST https://substrate.office.com/search/api/v1/suggestions
|
|
81
|
+
*/
|
|
82
|
+
export const personSuggestion = {
|
|
83
|
+
Id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890@company.onmicrosoft.com',
|
|
84
|
+
MRI: '8:orgid:a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
85
|
+
DisplayName: 'Smith, John',
|
|
86
|
+
GivenName: 'John',
|
|
87
|
+
Surname: 'Smith',
|
|
88
|
+
EmailAddresses: ['john.smith@company.com'],
|
|
89
|
+
CompanyName: 'Acme Corp',
|
|
90
|
+
Department: 'Engineering',
|
|
91
|
+
JobTitle: 'Senior Engineer',
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Person with minimal info using a proper GUID (no optional fields).
|
|
95
|
+
*/
|
|
96
|
+
export const personMinimal = {
|
|
97
|
+
Id: 'b1c2d3e4-f5a6-7890-bcde-1234567890ab',
|
|
98
|
+
DisplayName: 'Jane Doe',
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Person with base64-encoded GUID (real-world API response format).
|
|
102
|
+
* The base64 '93qkaTtFGWpUHjyRafgdhg==' decodes to GUID '69a47af7-453b-6a19-541e-3c9169f81d86'.
|
|
103
|
+
*/
|
|
104
|
+
export const personWithBase64Id = {
|
|
105
|
+
Id: '93qkaTtFGWpUHjyRafgdhg==',
|
|
106
|
+
MRI: '8:orgid:93qkaTtFGWpUHjyRafgdhg==',
|
|
107
|
+
DisplayName: 'Rob MacDonald',
|
|
108
|
+
EmailAddresses: ['rob@company.com'],
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Groups response from suggestions API.
|
|
112
|
+
*/
|
|
113
|
+
export const peopleGroupsResponse = {
|
|
114
|
+
Groups: [
|
|
115
|
+
{
|
|
116
|
+
Suggestions: [
|
|
117
|
+
personSuggestion,
|
|
118
|
+
personMinimal,
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* JWT payload with full user info.
|
|
125
|
+
*/
|
|
126
|
+
export const jwtPayloadFull = {
|
|
127
|
+
oid: 'user-object-id-guid',
|
|
128
|
+
name: 'Macdonald, Rob',
|
|
129
|
+
upn: 'rob.macdonald@company.com',
|
|
130
|
+
preferred_username: 'rob@company.com',
|
|
131
|
+
email: 'rob.m@personal.com',
|
|
132
|
+
given_name: 'Rob',
|
|
133
|
+
family_name: 'Macdonald',
|
|
134
|
+
tid: 'tenant-id-guid',
|
|
135
|
+
exp: 1705850000,
|
|
136
|
+
iat: 1705846400,
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* JWT payload with minimal info (only required fields).
|
|
140
|
+
*/
|
|
141
|
+
export const jwtPayloadMinimal = {
|
|
142
|
+
oid: 'another-user-guid',
|
|
143
|
+
name: 'Alice Smith',
|
|
144
|
+
exp: 1705850000,
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* JWT payload for name parsing tests - "Surname, GivenName" format.
|
|
148
|
+
*/
|
|
149
|
+
export const jwtPayloadCommaName = {
|
|
150
|
+
oid: 'comma-name-user',
|
|
151
|
+
name: 'Jones, David',
|
|
152
|
+
upn: 'david.jones@company.com',
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* JWT payload for name parsing tests - "GivenName Surname" format.
|
|
156
|
+
*/
|
|
157
|
+
export const jwtPayloadSpaceName = {
|
|
158
|
+
oid: 'space-name-user',
|
|
159
|
+
name: 'Sarah Connor',
|
|
160
|
+
upn: 'sarah.connor@company.com',
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Channel thread reply - messageid differs from message timestamp.
|
|
164
|
+
* The parent post was at 1768919400000 (2026-01-20T14:30:00.000Z),
|
|
165
|
+
* this reply is at 1768921200000 (2026-01-20T15:00:00.000Z).
|
|
166
|
+
*/
|
|
167
|
+
export const searchResultThreadReply = {
|
|
168
|
+
Id: 'AAMkThreadReply',
|
|
169
|
+
ReferenceId: 'thread-reply.1000.1',
|
|
170
|
+
HitHighlightedSummary: 'Thanks for the update on the budget!',
|
|
171
|
+
Source: {
|
|
172
|
+
DateTimeReceived: '2026-01-20T15:00:00.000Z', // = 1768921200000
|
|
173
|
+
From: {
|
|
174
|
+
EmailAddress: {
|
|
175
|
+
Name: 'Doe, Jane',
|
|
176
|
+
Address: 'jane.doe@company.com',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
ChannelName: 'General',
|
|
180
|
+
TeamName: 'Finance Team',
|
|
181
|
+
Extensions: {
|
|
182
|
+
SkypeSpaces_ConversationPost_Extension_SkypeGroupId: '19:abcdef123456@thread.tacv2',
|
|
183
|
+
},
|
|
184
|
+
// Parent message ID (1768919400000) differs from this message's timestamp (1768921200000)
|
|
185
|
+
ClientConversationId: '19:abcdef123456@thread.tacv2;messageid=1768919400000',
|
|
186
|
+
ClientThreadId: '19:abcdef123456@thread.tacv2',
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Message source with explicit message ID.
|
|
191
|
+
*/
|
|
192
|
+
export const sourceWithMessageId = {
|
|
193
|
+
MessageId: '1705760000000',
|
|
194
|
+
DateTimeReceived: '2026-01-20T12:00:00.000Z',
|
|
195
|
+
ClientConversationId: '19:thread@tacv2',
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Message source with ID in ClientConversationId.
|
|
199
|
+
*/
|
|
200
|
+
export const sourceWithConvIdMessageId = {
|
|
201
|
+
DateTimeReceived: '2026-01-20T12:00:00.000Z',
|
|
202
|
+
ClientConversationId: '19:thread@tacv2;messageid=1705770000000',
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Thread message from chatsvc API.
|
|
206
|
+
* From: GET /api/chatsvc/{region}/v1/users/ME/conversations/{id}/messages
|
|
207
|
+
*/
|
|
208
|
+
export const threadMessage = {
|
|
209
|
+
id: '1705760000000',
|
|
210
|
+
content: '<p>Hello team!</p>',
|
|
211
|
+
messagetype: 'RichText/Html',
|
|
212
|
+
contenttype: 'text',
|
|
213
|
+
from: '8:orgid:user-guid-123',
|
|
214
|
+
imdisplayname: 'John Smith',
|
|
215
|
+
originalarrivaltime: '2026-01-20T12:00:00.000Z',
|
|
216
|
+
composetime: '2026-01-20T11:59:58.000Z',
|
|
217
|
+
clientmessageid: 'client-msg-123',
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Favorites folder response.
|
|
221
|
+
* From: POST /api/csa/{region}/api/v1/teams/users/me/conversationFolders
|
|
222
|
+
*/
|
|
223
|
+
export const favoritesFolderResponse = {
|
|
224
|
+
folderHierarchyVersion: 1705850000000,
|
|
225
|
+
conversationFolders: [
|
|
226
|
+
{
|
|
227
|
+
id: 'tenant-guid~user-guid~Favorites',
|
|
228
|
+
sortType: 'UserDefinedCustomOrder',
|
|
229
|
+
name: 'Favorites',
|
|
230
|
+
folderType: 'Favorites',
|
|
231
|
+
conversationFolderItems: [
|
|
232
|
+
{
|
|
233
|
+
conversationId: '19:abc@thread.tacv2',
|
|
234
|
+
createdTime: 1705700000000,
|
|
235
|
+
lastUpdatedTime: 1705800000000,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
conversationId: '19:xyz@thread.v2',
|
|
239
|
+
createdTime: 1705600000000,
|
|
240
|
+
lastUpdatedTime: 1705750000000,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar API client for meeting operations.
|
|
3
|
+
*
|
|
4
|
+
* Handles calls to teams.microsoft.com/api/mt/part/{region}/v2.1/me/calendars endpoints.
|
|
5
|
+
*/
|
|
6
|
+
import { type Result } from '../types/result.js';
|
|
7
|
+
/** Organiser information for a meeting. */
|
|
8
|
+
export interface MeetingOrganizer {
|
|
9
|
+
name: string;
|
|
10
|
+
email: string;
|
|
11
|
+
}
|
|
12
|
+
/** A calendar meeting/event. */
|
|
13
|
+
export interface Meeting {
|
|
14
|
+
/** Unique identifier for the meeting. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Meeting subject/title. */
|
|
17
|
+
subject: string;
|
|
18
|
+
/** Start time (ISO 8601). */
|
|
19
|
+
startTime: string;
|
|
20
|
+
/** End time (ISO 8601). */
|
|
21
|
+
endTime: string;
|
|
22
|
+
/** Meeting organiser. */
|
|
23
|
+
organizer: MeetingOrganizer;
|
|
24
|
+
/** Location (room name or text). */
|
|
25
|
+
location?: string;
|
|
26
|
+
/** Whether this is a Teams online meeting. */
|
|
27
|
+
isOnlineMeeting: boolean;
|
|
28
|
+
/** Teams join URL (if online meeting). */
|
|
29
|
+
joinUrl?: string;
|
|
30
|
+
/** Meeting chat thread ID (for use with teams_get_thread). */
|
|
31
|
+
threadId?: string;
|
|
32
|
+
/** Your RSVP status. */
|
|
33
|
+
myResponse: 'None' | 'Accepted' | 'Tentative' | 'Declined';
|
|
34
|
+
/** Calendar show-as status. */
|
|
35
|
+
showAs: 'Free' | 'Busy' | 'Tentative' | 'OutOfOffice' | 'Unknown';
|
|
36
|
+
/** Whether you're the organiser. */
|
|
37
|
+
isOrganizer: boolean;
|
|
38
|
+
/** Event type (Single, Occurrence, Exception, SeriesMaster). */
|
|
39
|
+
eventType: string;
|
|
40
|
+
}
|
|
41
|
+
/** Options for fetching calendar view. */
|
|
42
|
+
export interface CalendarViewOptions {
|
|
43
|
+
/** Start of date range (ISO 8601). Defaults to now. */
|
|
44
|
+
startDate?: string;
|
|
45
|
+
/** End of date range (ISO 8601). Defaults to 7 days from now. */
|
|
46
|
+
endDate?: string;
|
|
47
|
+
/** Maximum results to return. */
|
|
48
|
+
limit?: number;
|
|
49
|
+
}
|
|
50
|
+
/** Response from getting calendar view. */
|
|
51
|
+
export interface CalendarViewResult {
|
|
52
|
+
/** Number of meetings returned. */
|
|
53
|
+
count: number;
|
|
54
|
+
/** List of meetings. */
|
|
55
|
+
meetings: Meeting[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Gets meetings from the user's calendar for a date range.
|
|
59
|
+
*
|
|
60
|
+
* The region and partition are extracted from the user's session (DISCOVER-REGION-GTM),
|
|
61
|
+
* so we always use the correct endpoint without guessing.
|
|
62
|
+
*
|
|
63
|
+
* @param options - Options for filtering meetings
|
|
64
|
+
* @returns List of meetings
|
|
65
|
+
*/
|
|
66
|
+
export declare function getCalendarView(options?: CalendarViewOptions): Promise<Result<CalendarViewResult>>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar API client for meeting operations.
|
|
3
|
+
*
|
|
4
|
+
* Handles calls to teams.microsoft.com/api/mt/part/{region}/v2.1/me/calendars endpoints.
|
|
5
|
+
*/
|
|
6
|
+
import { httpRequest } from '../utils/http.js';
|
|
7
|
+
import { CALENDAR_API, getTeamsHeaders } from '../utils/api-config.js';
|
|
8
|
+
import { ok, err } from '../types/result.js';
|
|
9
|
+
import { ErrorCode, createError } from '../types/errors.js';
|
|
10
|
+
import { requireCalendarAuth } from '../utils/auth-guards.js';
|
|
11
|
+
import { extractRegionConfig } from '../auth/token-extractor.js';
|
|
12
|
+
import { DEFAULT_MEETING_LIMIT, DEFAULT_MEETING_DAYS_AHEAD, } from '../constants.js';
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
// Helpers
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* Parses a raw API meeting response into our Meeting type.
|
|
18
|
+
*/
|
|
19
|
+
function parseMeeting(raw) {
|
|
20
|
+
// Extract thread ID from skypeTeamsData if present
|
|
21
|
+
let threadId;
|
|
22
|
+
const skypeTeamsData = raw.skypeTeamsData;
|
|
23
|
+
if (skypeTeamsData) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(skypeTeamsData);
|
|
26
|
+
threadId = parsed.cid;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Ignore parsing errors
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Map response type to our enum values
|
|
33
|
+
const rawResponse = raw.myResponseType;
|
|
34
|
+
let myResponse = 'None';
|
|
35
|
+
if (rawResponse === 'Accepted' || rawResponse === 'Organizer') {
|
|
36
|
+
myResponse = 'Accepted';
|
|
37
|
+
}
|
|
38
|
+
else if (rawResponse === 'Tentative' || rawResponse === 'TentativelyAccepted') {
|
|
39
|
+
myResponse = 'Tentative';
|
|
40
|
+
}
|
|
41
|
+
else if (rawResponse === 'Declined') {
|
|
42
|
+
myResponse = 'Declined';
|
|
43
|
+
}
|
|
44
|
+
// Map showAs values
|
|
45
|
+
const rawShowAs = raw.showAs;
|
|
46
|
+
let showAs = 'Unknown';
|
|
47
|
+
if (rawShowAs === 'Free') {
|
|
48
|
+
showAs = 'Free';
|
|
49
|
+
}
|
|
50
|
+
else if (rawShowAs === 'Busy') {
|
|
51
|
+
showAs = 'Busy';
|
|
52
|
+
}
|
|
53
|
+
else if (rawShowAs === 'Tentative') {
|
|
54
|
+
showAs = 'Tentative';
|
|
55
|
+
}
|
|
56
|
+
else if (rawShowAs === 'Oof' || rawShowAs === 'OutOfOffice') {
|
|
57
|
+
showAs = 'OutOfOffice';
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
id: raw.objectId,
|
|
61
|
+
subject: raw.subject || '(No subject)',
|
|
62
|
+
startTime: raw.startTime,
|
|
63
|
+
endTime: raw.endTime,
|
|
64
|
+
organizer: {
|
|
65
|
+
name: raw.organizerName || 'Unknown',
|
|
66
|
+
email: raw.organizerAddress || '',
|
|
67
|
+
},
|
|
68
|
+
location: raw.location,
|
|
69
|
+
isOnlineMeeting: raw.isOnlineMeeting === true,
|
|
70
|
+
joinUrl: raw.skypeTeamsMeetingUrl,
|
|
71
|
+
threadId,
|
|
72
|
+
myResponse,
|
|
73
|
+
showAs,
|
|
74
|
+
isOrganizer: raw.isOrganizer === true,
|
|
75
|
+
eventType: raw.eventType || 'Single',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Builds the select fields for the calendar API.
|
|
80
|
+
*/
|
|
81
|
+
function getSelectFields() {
|
|
82
|
+
return [
|
|
83
|
+
'cleanGlobalObjectId',
|
|
84
|
+
'endTime',
|
|
85
|
+
'eventTimeZone',
|
|
86
|
+
'eventType',
|
|
87
|
+
'hasAttachments',
|
|
88
|
+
'iCalUid',
|
|
89
|
+
'isAllDayEvent',
|
|
90
|
+
'isAppointment',
|
|
91
|
+
'isCancelled',
|
|
92
|
+
'isOnlineMeeting',
|
|
93
|
+
'isOrganizer',
|
|
94
|
+
'isPrivate',
|
|
95
|
+
'lastModifiedTime',
|
|
96
|
+
'location',
|
|
97
|
+
'myResponseType',
|
|
98
|
+
'objectId',
|
|
99
|
+
'organizerAddress',
|
|
100
|
+
'organizerName',
|
|
101
|
+
'schedulingServiceUpdateUrl',
|
|
102
|
+
'showAs',
|
|
103
|
+
'skypeTeamsData',
|
|
104
|
+
'skypeTeamsMeetingUrl',
|
|
105
|
+
'startTime',
|
|
106
|
+
'subject',
|
|
107
|
+
].join(',');
|
|
108
|
+
}
|
|
109
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
// API Functions
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
/**
|
|
113
|
+
* Gets meetings from the user's calendar for a date range.
|
|
114
|
+
*
|
|
115
|
+
* The region and partition are extracted from the user's session (DISCOVER-REGION-GTM),
|
|
116
|
+
* so we always use the correct endpoint without guessing.
|
|
117
|
+
*
|
|
118
|
+
* @param options - Options for filtering meetings
|
|
119
|
+
* @returns List of meetings
|
|
120
|
+
*/
|
|
121
|
+
export async function getCalendarView(options = {}) {
|
|
122
|
+
const authResult = requireCalendarAuth();
|
|
123
|
+
if (!authResult.ok) {
|
|
124
|
+
return authResult;
|
|
125
|
+
}
|
|
126
|
+
const { skypeToken, spacesToken } = authResult.value;
|
|
127
|
+
// Get the user's region/partition from session discovery config
|
|
128
|
+
const regionConfig = extractRegionConfig();
|
|
129
|
+
if (!regionConfig) {
|
|
130
|
+
return err(createError(ErrorCode.AUTH_REQUIRED, 'Could not determine region. Please run teams_login to authenticate.', { suggestions: ['Call teams_login to authenticate'] }));
|
|
131
|
+
}
|
|
132
|
+
// Calculate default date range
|
|
133
|
+
const now = new Date();
|
|
134
|
+
const defaultEnd = new Date(now);
|
|
135
|
+
defaultEnd.setDate(defaultEnd.getDate() + DEFAULT_MEETING_DAYS_AHEAD);
|
|
136
|
+
const startDate = options.startDate || now.toISOString();
|
|
137
|
+
const endDate = options.endDate || defaultEnd.toISOString();
|
|
138
|
+
const limit = options.limit || DEFAULT_MEETING_LIMIT;
|
|
139
|
+
// Build query parameters
|
|
140
|
+
const params = new URLSearchParams({
|
|
141
|
+
startDate,
|
|
142
|
+
endDate,
|
|
143
|
+
'$top': limit.toString(),
|
|
144
|
+
'$count': 'true',
|
|
145
|
+
'$skip': '0',
|
|
146
|
+
'$orderby': 'startTime asc',
|
|
147
|
+
// Filter out appointments (blocks), all-day events, and cancelled meetings
|
|
148
|
+
'$filter': 'isAppointment eq false and isAllDayEvent eq false and isCancelled eq false',
|
|
149
|
+
'$select': getSelectFields(),
|
|
150
|
+
});
|
|
151
|
+
// Use the exact region-partition from session discovery
|
|
152
|
+
// Some tenants use partitioned URLs (/api/mt/part/amer-02), others don't (/api/mt/emea)
|
|
153
|
+
const calendarUrl = CALENDAR_API.calendarView(regionConfig.regionPartition, regionConfig.hasPartition, regionConfig.teamsBaseUrl);
|
|
154
|
+
const url = `${calendarUrl}?${params.toString()}`;
|
|
155
|
+
const response = await httpRequest(url, {
|
|
156
|
+
method: 'GET',
|
|
157
|
+
headers: {
|
|
158
|
+
...getTeamsHeaders(regionConfig.teamsBaseUrl),
|
|
159
|
+
'Authentication': `skypetoken=${skypeToken}`,
|
|
160
|
+
'Authorization': `Bearer ${spacesToken}`,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
return response;
|
|
165
|
+
}
|
|
166
|
+
const data = response.value.data;
|
|
167
|
+
const rawMeetings = data.value;
|
|
168
|
+
if (!rawMeetings || !Array.isArray(rawMeetings)) {
|
|
169
|
+
return ok({
|
|
170
|
+
count: 0,
|
|
171
|
+
meetings: [],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const meetings = rawMeetings.map(parseMeeting);
|
|
175
|
+
return ok({
|
|
176
|
+
count: meetings.length,
|
|
177
|
+
meetings,
|
|
178
|
+
});
|
|
179
|
+
}
|