diff --git a/src/calendarIntegration/addMeetingLink.ts b/src/calendarIntegration/addMeetingLink.ts index db6625e..f8bef16 100644 --- a/src/calendarIntegration/addMeetingLink.ts +++ b/src/calendarIntegration/addMeetingLink.ts @@ -1,84 +1,171 @@ /* global Office, console */ -import { appendToBody, getMailboxItemSubject, getOrganizer, setLocation } from "../utils/mailbox"; -import { createMeetingSummary as createMeetingSummary } from "./createMeetingSummary"; +import { appendToBody, getBody, getLocation, getMailboxItemSubject, getOrganizer, setLocation } from "../utils/mailbox"; +import { createMeetingSummary } from "./createMeetingSummary"; import { setCustomPropertyAsync, getCustomPropertyAsync } from "../utils/customProperties"; import { showNotification, removeNotification } from "../utils/notifications"; import { isOutlookCalIntegrationEnabled } from "./isOutlookCalIntegrationEnabled"; import { createEvent } from "./createEvent"; import { mailboxItem } from "../commands/commands"; +import { EventResult } from "../types/EventResult"; const defaultSubjectValue = "New Appointment"; +let createdMeeting: EventResult; -export async function addMeetingLink(event: Office.AddinCommands.Event) { +/** + * Checks if the feature is enabled by calling isOutlookCalIntegrationEnabled. + * + * @return {Promise} Whether the feature is enabled or not. + */ +async function isFeatureEnabled(): Promise { + const isEnabled = await isOutlookCalIntegrationEnabled(); + + if (!isEnabled) { + console.log("Outlook calendar integration is disabled for this team. Contact your Wire system administrator."); + removeNotification("wire-for-outlook-disabled"); + showNotification( + "wire-for-outlook-disabled", + "Wire for Outlook is disabled for your team. Please contact your Wire system administrator.", + Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage + ); + return false; + } + return true; +} + +/** + * Fetches custom properties from the mailbox item and sets the createdMeeting object if both wireId and wireLink are present. + * + * @return {Promise} A promise that resolves when the custom properties are fetched and the createdMeeting object is set. + */ +async function fetchCustomProperties(): Promise { + const wireId = await getCustomPropertyAsync(mailboxItem, "wireId"); + const wireLink = await getCustomPropertyAsync(mailboxItem, "wireLink"); + + if (wireId && wireLink) { + createdMeeting = { id: wireId, link: wireLink } as EventResult; + } +} + +/** + * Creates a new meeting by calling the createEvent function with a subject obtained from the mailboxItem. + * If the eventResult is not null, it sets the createdMeeting object to eventResult, updates the meeting details, + * and sets the custom properties wireId and wireLink on the mailboxItem. + * + * @return {Promise} A promise that resolves when the new meeting is created and the custom properties are set. + */ +async function createNewMeeting(): Promise { + removeNotification("adding-wire-meeting"); + showNotification( + "adding-wire-meeting", + "Adding Wire meeting...", + Office.MailboxEnums.ItemNotificationMessageType.ProgressIndicator + ); + + const subject = await getMailboxItemSubject(mailboxItem); + const eventResult = await createEvent(subject || defaultSubjectValue); + + if (eventResult) { + createdMeeting = eventResult; + await updateMeetingDetails(eventResult); + await setCustomPropertyAsync(mailboxItem, "wireId", eventResult.id); + await setCustomPropertyAsync(mailboxItem, "wireLink", eventResult.link); + } + + removeNotification("adding-wire-meeting"); +} + +/** + * Updates the meeting details by setting the location and appending the meeting summary to the body of the mailbox item. + * + * @param {EventResult} eventResult - The event result containing the link for the meeting. + * @return {Promise} A promise that resolves when the meeting details are updated. + */ +async function updateMeetingDetails(eventResult: EventResult): Promise { + getOrganizer(mailboxItem, async (organizer) => { + await setLocation(mailboxItem, eventResult.link, () => {}); + const meetingSummary = createMeetingSummary(eventResult.link, organizer); + await appendToBody(mailboxItem, meetingSummary); + }); +} + +/** + * Handles an existing meeting by updating meeting details and setting custom properties. + * + * @return {Promise} A promise that resolves when the existing meeting is handled. + */ +async function handleExistingMeeting(): Promise { + if (!createdMeeting) { + throw new Error("createdMeeting is undefined"); + } + + const currentBody = await getBody(mailboxItem); + const currentLocation = await getLocation(mailboxItem); + const normalizedCurrentBody = currentBody.replace(/&/g, "&"); + const normalizedMeetingLink = createdMeeting.link?.replace(/&/g, "&"); + + getOrganizer(mailboxItem, async (organizer) => { + if (!currentLocation) { + await setLocation(mailboxItem, createdMeeting.link, () => {}); + } + const meetingSummary = createMeetingSummary(createdMeeting.link, organizer); + if (!normalizedCurrentBody.includes(normalizedMeetingLink)) { + await appendToBody(mailboxItem, meetingSummary); + } + }); + + await setCustomPropertyAsync(mailboxItem, "wireId", createdMeeting.id); + await setCustomPropertyAsync(mailboxItem, "wireLink", createdMeeting.link); +} + +/** + * Adds a meeting link to the Outlook calendar. + * + * @param {Office.AddinCommands.Event} event - The event object. + * @return {Promise} A promise that resolves when the meeting link is added. + */ +async function addMeetingLink(event: Office.AddinCommands.Event): Promise { try { - const isFeatureEnabled = await isOutlookCalIntegrationEnabled(); - - if (!isFeatureEnabled) { - console.log( - "There is no Outlook calendar integration enabled for this team. Please contact your Wire system administrator." - ); - - removeNotification("wire-for-outlook-disabled"); - showNotification( - "wire-for-outlook-disabled", - "Wire for Outlook is disabled for your team. Please contact your Wire system administrator.", - Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage - ); + const isEnabled = await isFeatureEnabled(); + if (!isEnabled) return; + + await fetchCustomProperties(); + if (!createdMeeting) { + await createNewMeeting(); } else { - console.log("Outlook calendar integration feature is enabled for this team."); - - const wireId = await getCustomPropertyAsync(mailboxItem, "wireId"); - if (!wireId) { - console.log("There is no Wire meeting for this Outlook meeting, starting process of creating it..."); - removeNotification("adding-wire-meeting"); - showNotification( - "adding-wire-meeting", - "Adding Wire meeting...", - Office.MailboxEnums.ItemNotificationMessageType.ProgressIndicator - ); - const subject = await getMailboxItemSubject(mailboxItem); - const eventResult = await createEvent(subject || defaultSubjectValue); - if (eventResult) { - getOrganizer(mailboxItem, function (organizer) { - setLocation(mailboxItem, eventResult.link, () => {}); - const meetingSummary = createMeetingSummary(eventResult.link, organizer); - appendToBody(mailboxItem, meetingSummary); - }); - await setCustomPropertyAsync(mailboxItem, "wireId", eventResult.id); - await setCustomPropertyAsync(mailboxItem, "wireLink", eventResult.link); - } - removeNotification("adding-wire-meeting"); - } else { - console.log("Wire meeting is already created for this Outlook meeting"); - removeNotification("wire-meeting-exists"); - showNotification( - "wire-meeting-exists", - "Wire meeting is already created for this Outlook meeting", - Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage - ); - } + await handleExistingMeeting(); } } catch (error) { console.error("Error during adding Wire meeting link", error); + handleAddMeetingLinkError(error); + } finally { + event.completed(); + } +} - removeNotification("adding-wire-meeting"); - removeNotification("adding-wire-meeting-error"); +/** + * Handles errors that occur when adding a meeting link. + * + * @param {Error} error - The error that occurred. + * @return {void} This function does not return anything. + */ +function handleAddMeetingLinkError(error: Error): void { + removeNotification("adding-wire-meeting"); + removeNotification("adding-wire-meeting-error"); - if (error.message.includes("authorization failed")) { - showNotification( - "adding-wire-meeting-error", - "Authorization failed.", - Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage - ); - } else { - showNotification( - "adding-wire-meeting-error", - "There was error while adding wire meeting", - Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage - ); - } + if (error.message.includes("authorization failed")) { + showNotification( + "adding-wire-meeting-error", + "Authorization failed.", + Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage + ); + } else { + showNotification( + "adding-wire-meeting-error", + "There was an error while adding the Wire meeting.", + Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage + ); } - - event.completed(); } + +export { addMeetingLink }; diff --git a/src/calendarIntegration/isOutlookCalIntegrationEnabled.ts b/src/calendarIntegration/isOutlookCalIntegrationEnabled.ts index deaaf9d..7f1f8a4 100644 --- a/src/calendarIntegration/isOutlookCalIntegrationEnabled.ts +++ b/src/calendarIntegration/isOutlookCalIntegrationEnabled.ts @@ -1,34 +1,34 @@ -/* global console */ - -import { FeatureConfigsResponse, Feature } from "../types/FeatureConfigsResponse"; -import { config } from "../utils/config"; -import { fetchWithAuthorizeDialog } from "../wireAuthorize/wireAuthorize"; - -export async function isOutlookCalIntegrationEnabled() { - try { - const response = await fetchWithAuthorizeDialog(new URL(`${config.apiVersion}/feature-configs`, config.apiBaseUrl), { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - - if (response.ok) { - const data: FeatureConfigsResponse = await response.json(); - const outlookCalIntegration: Feature | undefined = data.outlookCalIntegration; - if (outlookCalIntegration && outlookCalIntegration.status === "enabled") { - return true; - } - } else { - const errorMsg = `Error while fetching outlookCalIntegration feature config. Status code: ${response.status}`; - console.error(errorMsg); - throw new Error(errorMsg); - } - } catch (error) { - const errorMsg = `Error while checking outlookCalIntegration feature config: ${error}`; - console.error(errorMsg); - throw new Error(errorMsg); - } - - return false; -} +/* global console */ + +import { FeatureConfigsResponse, Feature } from "../types/FeatureConfigsResponse"; +import { config } from "../utils/config"; +import { fetchWithAuthorizeDialog } from "../wireAuthorize/wireAuthorize"; + +export async function isOutlookCalIntegrationEnabled() { + try { + const response = await fetchWithAuthorizeDialog(new URL(`${config.apiVersion}/feature-configs`, config.apiBaseUrl), { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.ok) { + const data: FeatureConfigsResponse = await response.json(); + const outlookCalIntegration: Feature | undefined = data.outlookCalIntegration; + if (outlookCalIntegration && outlookCalIntegration.status === "enabled") { + return true; + } + } else { + const errorMsg = `Error while fetching outlookCalIntegration feature config. Status code: ${response.status}`; + console.error(errorMsg); + throw new Error(errorMsg); + } + } catch (error) { + const errorMsg = `Error while checking outlookCalIntegration feature config: ${error}`; + console.error(errorMsg); + throw new Error(errorMsg); + } + + return false; +} diff --git a/src/callback/callback.ts b/src/callback/callback.ts index a21d15e..d3eb94f 100644 --- a/src/callback/callback.ts +++ b/src/callback/callback.ts @@ -1,97 +1,97 @@ -/* global Office, window, document, console, fetch, sessionStorage */ - -import { AuthResult } from "../types/AuthResult"; -import { UrlParameters } from "./UrlParameters"; -import { config } from "../utils/config"; - -document.addEventListener( - "DOMContentLoaded", - function () { - Office.onReady((info) => { - if (info.host === Office.HostType.Outlook) { - handleCallback(); - } - }); - }, - false -); - -async function handleCallback(): Promise { - const urlParams = getUrlParameters(); - const { code, receivedState, error } = urlParams; - - if (error) { - console.error("Error in auth flow: ", error); - const authResult = { success: false, error }; - sendMessageToParent(authResult); - return; - } - - const storedCodeVerifier = sessionStorage.getItem("code_verifier"); - const storedState = sessionStorage.getItem("state"); - - if (code && receivedState && storedCodeVerifier) { - if (receivedState !== storedState) { - console.error("State validation failed"); - return; - } - - try { - const authResult = await exchangeCodeForTokens(code, storedCodeVerifier); - sendMessageToParent(authResult); - } catch (error) { - console.error("Error during token exchange:", error); - } - } -} - -function getUrlParameters(): UrlParameters { - const urlParams = new URLSearchParams(window.location.search); - return { - code: urlParams.get("code"), - receivedState: urlParams.get("state"), - error: urlParams.get("error"), - }; -} - -function sendMessageToParent(authResult: AuthResult): void { - Office.onReady(() => { - Office.context.ui.messageParent(JSON.stringify(authResult)); - }); -} - -async function exchangeCodeForTokens(code: string, codeVerifier: string): Promise { - const clientId = config.clientId; - const redirectUri = new URL("/callback.html", config.addInBaseUrl); - const tokenEndpoint = new URL(`${config.apiVersion}/oauth/token`, config.apiBaseUrl); - - const body = getRequestBody(code, clientId, redirectUri, codeVerifier); - - const response = await fetch(tokenEndpoint, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/x-www-form-urlencoded", - }, - body: body.toString(), - }); - - if (response.ok) { - const json = await response.json(); - const { access_token, refresh_token } = json; - return { success: true, access_token, refresh_token }; - } else { - throw new Error("Failed to exchange authorization code for tokens"); - } -} - -function getRequestBody(code: string, clientId: string, redirectUri: URL, codeVerifier: string): URLSearchParams { - const body = new URLSearchParams(); - body.append("grant_type", "authorization_code"); - body.append("client_id", clientId); - body.append("code", code); - body.append("redirect_uri", redirectUri.toString()); - body.append("code_verifier", codeVerifier); - - return body; -} +/* global Office, window, document, console, fetch, sessionStorage */ + +import { AuthResult } from "../types/AuthResult"; +import { UrlParameters } from "./UrlParameters"; +import { config } from "../utils/config"; + +document.addEventListener( + "DOMContentLoaded", + function () { + Office.onReady((info) => { + if (info.host === Office.HostType.Outlook) { + handleCallback(); + } + }); + }, + false +); + +async function handleCallback(): Promise { + const urlParams = getUrlParameters(); + const { code, receivedState, error } = urlParams; + + if (error) { + console.error("Error in auth flow: ", error); + const authResult = { success: false, error }; + sendMessageToParent(authResult); + return; + } + + const storedCodeVerifier = sessionStorage.getItem("code_verifier"); + const storedState = sessionStorage.getItem("state"); + + if (code && receivedState && storedCodeVerifier) { + if (receivedState !== storedState) { + console.error("State validation failed"); + return; + } + + try { + const authResult = await exchangeCodeForTokens(code, storedCodeVerifier); + sendMessageToParent(authResult); + } catch (error) { + console.error("Error during token exchange:", error); + } + } +} + +function getUrlParameters(): UrlParameters { + const urlParams = new URLSearchParams(window.location.search); + return { + code: urlParams.get("code"), + receivedState: urlParams.get("state"), + error: urlParams.get("error"), + }; +} + +function sendMessageToParent(authResult: AuthResult): void { + Office.onReady(() => { + Office.context.ui.messageParent(JSON.stringify(authResult)); + }); +} + +async function exchangeCodeForTokens(code: string, codeVerifier: string): Promise { + const clientId = config.clientId; + const redirectUri = new URL("/callback.html", config.addInBaseUrl); + const tokenEndpoint = new URL(`${config.apiVersion}/oauth/token`, config.apiBaseUrl); + + const body = getRequestBody(code, clientId, redirectUri, codeVerifier); + + const response = await fetch(tokenEndpoint, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", + }, + body: body.toString(), + }); + + if (response.ok) { + const json = await response.json(); + const { access_token, refresh_token } = json; + return { success: true, access_token, refresh_token }; + } else { + throw new Error("Failed to exchange authorization code for tokens"); + } +} + +function getRequestBody(code: string, clientId: string, redirectUri: URL, codeVerifier: string): URLSearchParams { + const body = new URLSearchParams(); + body.append("grant_type", "authorization_code"); + body.append("client_id", clientId); + body.append("code", code); + body.append("redirect_uri", redirectUri.toString()); + body.append("code_verifier", codeVerifier); + + return body; +} diff --git a/src/utils/mailbox.ts b/src/utils/mailbox.ts index 08a771c..e3a0ac3 100644 --- a/src/utils/mailbox.ts +++ b/src/utils/mailbox.ts @@ -32,18 +32,22 @@ export async function getSubject(item, callback) { }); } -export async function getBody(item, callback) { - const { body } = item; +export function getBody(item): Promise { + return new Promise((resolve, reject) => { + const { body } = item; - await body.getAsync(Office.CoercionType.Html, function (asyncResult) { - if (asyncResult.status === Office.AsyncResultStatus.Failed) { - console.error("Failed to get HTML body."); - } else { - callback(asyncResult.value); - } + body.getAsync(Office.CoercionType.Html, function (asyncResult) { + if (asyncResult.status === Office.AsyncResultStatus.Failed) { + console.error("Failed to get HTML body."); + reject(new Error("Failed to get HTML body.")); + } else { + resolve(asyncResult.value); + } + }); }); } + export function setBody(item, newBody) { const { body } = item; const type = { coercionType: Office.CoercionType.Html }; @@ -55,12 +59,31 @@ export function setBody(item, newBody) { }); } -export function appendToBody(item, contentToAppend) { - getBody(item, (currentBody) => { +export async function appendToBody(item, contentToAppend) { + try { + const currentBody = await getBody(item); setBody(item, currentBody + contentToAppend); + } catch (error) { + console.error("Failed to append to body:", error); + } +} + +export async function getLocation(item): Promise { + return new Promise((resolve, reject) => { + const { location } = item; + + location.getAsync(function (asyncResult) { + if (asyncResult.status !== Office.AsyncResultStatus.Succeeded) { + console.error(`Action failed with message ${asyncResult.error.message}`); + reject(new Error(`Failed to get location: ${asyncResult.error.message}`)); + return; + } + resolve(asyncResult.value); + }); }); } + export async function setLocation(item, meetlingLink, callback) { const { location } = item; @@ -69,7 +92,6 @@ export async function setLocation(item, meetlingLink, callback) { console.error(`Action failed with message ${asyncResult.error.message}`); return; } - console.log(`Successfully set location to ${location}`); callback(); }); }