diff --git a/apps/teams-test-app/e2e-test-data/presence.json b/apps/teams-test-app/e2e-test-data/presence.json index 04d19a9408..6b94f6b484 100644 --- a/apps/teams-test-app/e2e-test-data/presence.json +++ b/apps/teams-test-app/e2e-test-data/presence.json @@ -4,7 +4,7 @@ { "feature": { "id": "presence", - "version": 1 + "version": 2 }, "testCases": [ { @@ -74,6 +74,40 @@ "upn": " " }, "expectedTestAppValue": "Error: Error: Error code: 4000, message: UPN cannot be null or empty" + }, + { + "title": "setPresence API Call - Set Out of Office", + "type": "callResponse", + "boxSelector": "#box_setPresence", + "inputValue": { + "status": "OutOfOffice", + "customMessage": "On vacation", + "outOfOfficeDetails": { + "startTime": "2024-03-20T00:00:00Z", + "endTime": "2024-03-27T00:00:00Z", + "message": "Annual leave - back next week" + } + }, + "expectedAlertValue": "setPresence called with status: OutOfOffice and message: On vacation\nOOF from 3/20/2024 to 3/27/2024", + "expectedTestAppValue": "Presence set successfully" + }, + { + "title": "getPresence API Call - Get OOF User", + "type": "callResponse", + "boxSelector": "#box_getPresence", + "inputValue": { + "upn": "oof.user@contoso.com" + }, + "expectedAlertValue": "getPresence called for user: oof.user@contoso.com", + "expectedTestAppValue": { + "status": "OutOfOffice", + "customMessage": "Mock presence status", + "outOfOfficeDetails": { + "startTime": "__ANY_STRING__", + "endTime": "__ANY_STRING__", + "message": "On vacation until next week" + } + } } ] } diff --git a/packages/teams-js/src/public/presence.ts b/packages/teams-js/src/public/presence.ts index 446c0b873e..25cd32d267 100644 --- a/packages/teams-js/src/public/presence.ts +++ b/packages/teams-js/src/public/presence.ts @@ -40,6 +40,31 @@ export enum PresenceStatus { * User is offline and cannot be contacted */ Offline = 'Offline', + + /** + * User is out of office + */ + OutOfOffice = 'OutOfOffice', +} + +/** + * Out of office details for a user + */ +export interface OutOfOfficeDetails { + /** + * Start time of OOF period (ISO string) + */ + startTime: string; + + /** + * End time of OOF period (ISO string) + */ + endTime: string; + + /** + * OOF message to display + */ + message: string; } /** @@ -55,6 +80,12 @@ export interface UserPresence { * Optional custom status message */ customMessage?: string; + + /** + * Optional out of office details + * Only present when status is OutOfOffice + */ + outOfOfficeDetails?: OutOfOfficeDetails; } /** @@ -80,6 +111,12 @@ export interface SetPresenceParams { * Optional custom status message */ customMessage?: string; + + /** + * Optional out of office details + * Only valid when status is OutOfOffice + */ + outOfOfficeDetails?: OutOfOfficeDetails; } /** @@ -87,7 +124,31 @@ export interface SetPresenceParams { */ class UserPresenceResponseHandler extends ResponseHandler { public validate(response: UserPresence): boolean { - return response !== undefined && Object.values(PresenceStatus).includes(response.status); + if (response === undefined || !Object.values(PresenceStatus).includes(response.status)) { + return false; + } + + // Validate OOF details if present + if (response.outOfOfficeDetails) { + if (response.status !== PresenceStatus.OutOfOffice) { + return false; // OOF details only valid with OOF status + } + + const { startTime, endTime, message } = response.outOfOfficeDetails; + if (!startTime || !endTime || !message || typeof message !== 'string') { + return false; + } + + // Validate date strings + try { + new Date(startTime).toISOString(); + new Date(endTime).toISOString(); + } catch { + return false; + } + } + + return true; } public deserialize(response: UserPresence): UserPresence { @@ -167,6 +228,7 @@ export function getPresence(params: GetPresenceParams): Promise { * - The library has not been initialized * - The status parameter is invalid * - The custom message parameter is invalid + * - The out of office details are invalid */ export function setPresence(params: SetPresenceParams): Promise { ensureInitialized(runtime, FrameContexts.content); @@ -177,6 +239,7 @@ export function setPresence(params: SetPresenceParams): Promise { validateStatus(params.status); validateCustomMessage(params.customMessage); + validateOutOfOfficeDetails(params.status, params.outOfOfficeDetails); return callFunctionInHostAndHandleResponse( 'presence.setPresence', @@ -235,3 +298,53 @@ function validateCustomMessage(customMessage: unknown): void { throw new Error(`Error code: ${ErrorCode.INVALID_ARGUMENTS}, message: Custom message must be a string`); } } + +/** + * Validates out of office details if provided + * @param status Current presence status + * @param details Out of office details to validate + * @throws Error if details are invalid + */ +function validateOutOfOfficeDetails(status: PresenceStatus, details?: OutOfOfficeDetails): void { + if (!details) { + if (status === PresenceStatus.OutOfOffice) { + throw new Error( + `Error code: ${ErrorCode.INVALID_ARGUMENTS}, ` + + 'message: Out of office details required when status is OutOfOffice', + ); + } + return; + } + + if (status !== PresenceStatus.OutOfOffice) { + throw new Error( + `Error code: ${ErrorCode.INVALID_ARGUMENTS}, ` + + 'message: Out of office details only valid when status is OutOfOffice', + ); + } + + const { startTime, endTime, message } = details; + + if (!startTime || !endTime || !message) { + throw new Error( + `Error code: ${ErrorCode.INVALID_ARGUMENTS}, ` + + 'message: Out of office details must include startTime, endTime, and message', + ); + } + + if (typeof message !== 'string') { + throw new Error(`Error code: ${ErrorCode.INVALID_ARGUMENTS}, message: Out of office message must be a string`); + } + + try { + const start = new Date(startTime); + const end = new Date(endTime); + if (end <= start) { + throw new Error( + `Error code: ${ErrorCode.INVALID_ARGUMENTS}, ` + 'message: Out of office end time must be after start time', + ); + } + } catch { + throw new Error(`Error code: ${ErrorCode.INVALID_ARGUMENTS}, message: Invalid date format for out of office times`); + } +}