diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 648a4a7b5f..791235c5e6 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -1,16 +1,18 @@ import EventOrganizerMail from "./emails/EventOrganizerMail"; import EventAttendeeMail from "./emails/EventAttendeeMail"; -import {v5 as uuidv5} from 'uuid'; -import short from 'short-uuid'; +import { v5 as uuidv5 } from "uuid"; +import short from "short-uuid"; import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail"; import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail"; const translator = short(); -const {google} = require('googleapis'); +const { google } = require("googleapis"); const googleAuth = () => { - const {client_secret, client_id, redirect_uris} = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web; + const { client_secret, client_id, redirect_uris } = JSON.parse( + process.env.GOOGLE_API_CREDENTIALS + ).web; return new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); }; @@ -31,36 +33,41 @@ function handleErrorsRaw(response) { } const o365Auth = (credential) => { + const isExpired = (expiryDate) => expiryDate < +new Date(); - const isExpired = (expiryDate) => expiryDate < +(new Date()); - - const refreshAccessToken = (refreshToken) => fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { - method: 'POST', - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: new URLSearchParams({ - 'scope': 'User.Read Calendars.Read Calendars.ReadWrite', - 'client_id': process.env.MS_GRAPH_CLIENT_ID, - 'refresh_token': refreshToken, - 'grant_type': 'refresh_token', - 'client_secret': process.env.MS_GRAPH_CLIENT_SECRET, - }) - }) - .then(handleErrorsJson) - .then((responseBody) => { - credential.key.access_token = responseBody.access_token; - credential.key.expiry_date = Math.round((+(new Date()) / 1000) + responseBody.expires_in); - return credential.key.access_token; + const refreshAccessToken = (refreshToken) => + fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + scope: "User.Read Calendars.Read Calendars.ReadWrite", + client_id: process.env.MS_GRAPH_CLIENT_ID, + refresh_token: refreshToken, + grant_type: "refresh_token", + client_secret: process.env.MS_GRAPH_CLIENT_SECRET, + }), }) + .then(handleErrorsJson) + .then((responseBody) => { + credential.key.access_token = responseBody.access_token; + credential.key.expiry_date = Math.round( + +new Date() / 1000 + responseBody.expires_in + ); + return credential.key.access_token; + }); return { - getToken: () => !isExpired(credential.key.expiry_date) ? Promise.resolve(credential.key.access_token) : refreshAccessToken(credential.key.refresh_token) + getToken: () => + !isExpired(credential.key.expiry_date) + ? Promise.resolve(credential.key.access_token) + : refreshAccessToken(credential.key.refresh_token), }; }; interface Person { - name?: string, - email: string, - timeZone: string + name?: string; + email: string; + timeZone: string; } interface CalendarEvent { @@ -72,13 +79,18 @@ interface CalendarEvent { location?: string; organizer: Person; attendees: Person[]; -}; + conferenceData?: ConferenceData; +} + +interface ConferenceData { + createRequest: any; +} interface IntegrationCalendar { - integration: string; - primary: boolean; - externalId: string; - name: string; + integration: string; + primary: boolean; + externalId: string; + name: string; } interface CalendarApiAdapter { @@ -88,26 +100,28 @@ interface CalendarApiAdapter { deleteEvent(uid: String); - getAvailability(dateFrom, dateTo, selectedCalendars: IntegrationCalendar[]): Promise; + getAvailability( + dateFrom, + dateTo, + selectedCalendars: IntegrationCalendar[] + ): Promise; - listCalendars(): Promise; + listCalendars(): Promise; } const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { - const auth = o365Auth(credential); const translateEvent = (event: CalendarEvent) => { - let optional = {}; if (event.location) { - optional.location = {displayName: event.location}; + optional.location = { displayName: event.location }; } return { subject: event.title, body: { - contentType: 'HTML', + contentType: "HTML", content: event.description, }, start: { @@ -118,284 +132,370 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { dateTime: event.endTime, timeZone: event.organizer.timeZone, }, - attendees: event.attendees.map(attendee => ({ + attendees: event.attendees.map((attendee) => ({ emailAddress: { address: attendee.email, - name: attendee.name + name: attendee.name, }, - type: "required" + type: "required", })), - ...optional - } + ...optional, + }; }; - const integrationType = "office365_calendar"; + const integrationType = "office365_calendar"; - function listCalendars(): Promise { - return auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendars', { - method: 'get', - headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json' - }, - }).then(handleErrorsJson) - .then(responseBody => { - return responseBody.value.map(cal => { - const calendar: IntegrationCalendar = { - externalId: cal.id, integration: integrationType, name: cal.name, primary: cal.isDefaultCalendar - } - return calendar; - }); - }) - ) - } - - return { - getAvailability: (dateFrom, dateTo, selectedCalendars) => { - const filter = "?$filter=start/dateTime ge '" + dateFrom + "' and end/dateTime le '" + dateTo + "'" - return auth.getToken().then( - (accessToken) => { - const selectedCalendarIds = selectedCalendars.filter(e => e.integration === integrationType).map(e => e.externalId); - if (selectedCalendarIds.length == 0 && selectedCalendars.length > 0){ - // Only calendars of other integrations selected - return Promise.resolve([]); - } - - return (selectedCalendarIds.length == 0 - ? listCalendars().then(cals => cals.map(e => e.externalId)) - : Promise.resolve(selectedCalendarIds).then(x => x)).then((ids: string[]) => { - const urls = ids.map(calendarId => 'https://graph.microsoft.com/v1.0/me/calendars/' + calendarId + '/events' + filter) - return Promise.all(urls.map(url => fetch(url, { - method: 'get', - headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Prefer': 'outlook.timezone="Etc/GMT"' - } - }) - .then(handleErrorsJson) - .then(responseBody => responseBody.value.map((evt) => ({ - start: evt.start.dateTime + 'Z', - end: evt.end.dateTime + 'Z' - })) - ))).then(results => results.reduce((acc, events) => acc.concat(events), [])) - }) - } - ).catch((err) => { - console.log(err); - }); + function listCalendars(): Promise { + return auth.getToken().then((accessToken) => + fetch("https://graph.microsoft.com/v1.0/me/calendars", { + method: "get", + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", }, - createEvent: (event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events', { - method: 'POST', - headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(translateEvent(event)) - }).then(handleErrorsJson).then((responseBody) => ({ + }) + .then(handleErrorsJson) + .then((responseBody) => { + return responseBody.value.map((cal) => { + const calendar: IntegrationCalendar = { + externalId: cal.id, + integration: integrationType, + name: cal.name, + primary: cal.isDefaultCalendar, + }; + return calendar; + }); + }) + ); + } + + return { + getAvailability: (dateFrom, dateTo, selectedCalendars) => { + const filter = + "?$filter=start/dateTime ge '" + + dateFrom + + "' and end/dateTime le '" + + dateTo + + "'"; + return auth + .getToken() + .then((accessToken) => { + const selectedCalendarIds = selectedCalendars + .filter((e) => e.integration === integrationType) + .map((e) => e.externalId); + if (selectedCalendarIds.length == 0 && selectedCalendars.length > 0) { + // Only calendars of other integrations selected + return Promise.resolve([]); + } + + return ( + selectedCalendarIds.length == 0 + ? listCalendars().then((cals) => cals.map((e) => e.externalId)) + : Promise.resolve(selectedCalendarIds).then((x) => x) + ).then((ids: string[]) => { + const urls = ids.map( + (calendarId) => + "https://graph.microsoft.com/v1.0/me/calendars/" + + calendarId + + "/events" + + filter + ); + return Promise.all( + urls.map((url) => + fetch(url, { + method: "get", + headers: { + Authorization: "Bearer " + accessToken, + Prefer: 'outlook.timezone="Etc/GMT"', + }, + }) + .then(handleErrorsJson) + .then((responseBody) => + responseBody.value.map((evt) => ({ + start: evt.start.dateTime + "Z", + end: evt.end.dateTime + "Z", + })) + ) + ) + ).then((results) => + results.reduce((acc, events) => acc.concat(events), []) + ); + }); + }) + .catch((err) => { + console.log(err); + }); + }, + createEvent: (event: CalendarEvent) => + auth.getToken().then((accessToken) => + fetch("https://graph.microsoft.com/v1.0/me/calendar/events", { + method: "POST", + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(translateEvent(event)), + }) + .then(handleErrorsJson) + .then((responseBody) => ({ ...responseBody, disableConfirmationEmail: true, - }))), - deleteEvent: (uid: String) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events/' + uid, { - method: 'DELETE', - headers: { - 'Authorization': 'Bearer ' + accessToken - } - }).then(handleErrorsRaw)), - updateEvent: (uid: String, event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events/' + uid, { - method: 'PATCH', - headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(translateEvent(event)) - }).then(handleErrorsRaw)), - listCalendars - } + })) + ), + deleteEvent: (uid: String) => + auth.getToken().then((accessToken) => + fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, { + method: "DELETE", + headers: { + Authorization: "Bearer " + accessToken, + }, + }).then(handleErrorsRaw) + ), + updateEvent: (uid: String, event: CalendarEvent) => + auth.getToken().then((accessToken) => + fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, { + method: "PATCH", + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(translateEvent(event)), + }).then(handleErrorsRaw) + ), + listCalendars, + }; }; const GoogleCalendar = (credential): CalendarApiAdapter => { - const myGoogleAuth = googleAuth(); - myGoogleAuth.setCredentials(credential.key); - const integrationType = "google_calendar"; + const myGoogleAuth = googleAuth(); + myGoogleAuth.setCredentials(credential.key); + const integrationType = "google_calendar"; - return { - getAvailability: (dateFrom, dateTo, selectedCalendars) => new Promise((resolve, reject) => { - const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); - calendar.calendarList - .list() - .then(cals => { - const filteredItems = cals.data.items.filter(i => selectedCalendars.findIndex(e => e.externalId === i.id) > -1) - if (filteredItems.length == 0 && selectedCalendars.length > 0){ - // Only calendars of other integrations selected - resolve([]); - } - calendar.freebusy.query({ - requestBody: { - timeMin: dateFrom, - timeMax: dateTo, - items: filteredItems.length > 0 ? filteredItems : cals.data.items - } - }, (err, apires) => { - if (err) { - reject(err); - } - - resolve( - Object.values(apires.data.calendars).flatMap( - (item) => item["busy"] - ) - ) - }); - }) - .catch((err) => { - reject(err); - }); - - }), - createEvent: (event: CalendarEvent) => new Promise((resolve, reject) => { - const payload = { - summary: event.title, - description: event.description, - start: { - dateTime: event.startTime, - timeZone: event.organizer.timeZone, - }, - end: { - dateTime: event.endTime, - timeZone: event.organizer.timeZone, - }, - attendees: event.attendees, - reminders: { - useDefault: false, - overrides: [ - {'method': 'email', 'minutes': 60} - ], - }, - }; - - if (event.location) { - payload['location'] = event.location; - } - - const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); - calendar.events.insert({ - auth: myGoogleAuth, - calendarId: 'primary', - resource: payload, - }, function (err, event) { - if (err) { - console.log('There was an error contacting the Calendar service: ' + err); - return reject(err); - } - return resolve(event.data); - }); - }), - updateEvent: (uid: String, event: CalendarEvent) => new Promise((resolve, reject) => { - const payload = { - summary: event.title, - description: event.description, - start: { - dateTime: event.startTime, - timeZone: event.organizer.timeZone, - }, - end: { - dateTime: event.endTime, - timeZone: event.organizer.timeZone, - }, - attendees: event.attendees, - reminders: { - useDefault: false, - overrides: [ - {'method': 'email', 'minutes': 60} - ], - }, - }; - - if (event.location) { - payload['location'] = event.location; - } - - const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); - calendar.events.update({ - auth: myGoogleAuth, - calendarId: 'primary', - eventId: uid, - sendNotifications: true, - sendUpdates: 'all', - resource: payload - }, function (err, event) { + return { + getAvailability: (dateFrom, dateTo, selectedCalendars) => + new Promise((resolve, reject) => { + const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); + calendar.calendarList + .list() + .then((cals) => { + const filteredItems = cals.data.items.filter( + (i) => + selectedCalendars.findIndex((e) => e.externalId === i.id) > -1 + ); + if (filteredItems.length == 0 && selectedCalendars.length > 0) { + // Only calendars of other integrations selected + resolve([]); + } + calendar.freebusy.query( + { + requestBody: { + timeMin: dateFrom, + timeMax: dateTo, + items: + filteredItems.length > 0 ? filteredItems : cals.data.items, + }, + }, + (err, apires) => { if (err) { - console.log('There was an error contacting the Calendar service: ' + err); - return reject(err); - } - return resolve(event.data); - }); - }), - deleteEvent: (uid: String) => new Promise( (resolve, reject) => { - const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); - calendar.events.delete({ - auth: myGoogleAuth, - calendarId: 'primary', - eventId: uid, - sendNotifications: true, - sendUpdates: 'all', - }, function (err, event) { - if (err) { - console.log('There was an error contacting the Calendar service: ' + err); - return reject(err); - } - return resolve(event.data); - }); - }), - listCalendars: () => new Promise((resolve, reject) => { - const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); - calendar.calendarList - .list() - .then(cals => { - resolve(cals.data.items.map(cal => { - const calendar: IntegrationCalendar = { - externalId: cal.id, integration: integrationType, name: cal.summary, primary: cal.primary - } - return calendar; - })) - }) - .catch((err) => { reject(err); - }); - }) - }; + } + + resolve( + Object.values(apires.data.calendars).flatMap( + (item) => item["busy"] + ) + ); + } + ); + }) + .catch((err) => { + reject(err); + }); + }), + createEvent: (event: CalendarEvent) => + new Promise((resolve, reject) => { + const payload = { + summary: event.title, + description: event.description, + start: { + dateTime: event.startTime, + timeZone: event.organizer.timeZone, + }, + end: { + dateTime: event.endTime, + timeZone: event.organizer.timeZone, + }, + attendees: event.attendees, + reminders: { + useDefault: false, + overrides: [{ method: "email", minutes: 60 }], + }, + }; + + if (event.location) { + payload["location"] = event.location; + } + + if (event.conferenceData) { + payload["conferenceData"] = event.conferenceData; + } + + const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); + calendar.events.insert( + { + auth: myGoogleAuth, + calendarId: "primary", + resource: payload, + conferenceDataVersion: 1, + }, + function (err, event) { + if (err) { + console.log( + "There was an error contacting the Calendar service: " + err + ); + return reject(err); + } + return resolve(event.data); + } + ); + }), + updateEvent: (uid: String, event: CalendarEvent) => + new Promise((resolve, reject) => { + const payload = { + summary: event.title, + description: event.description, + start: { + dateTime: event.startTime, + timeZone: event.organizer.timeZone, + }, + end: { + dateTime: event.endTime, + timeZone: event.organizer.timeZone, + }, + attendees: event.attendees, + reminders: { + useDefault: false, + overrides: [{ method: "email", minutes: 60 }], + }, + }; + + if (event.location) { + payload["location"] = event.location; + } + + const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); + calendar.events.update( + { + auth: myGoogleAuth, + calendarId: "primary", + eventId: uid, + sendNotifications: true, + sendUpdates: "all", + resource: payload, + }, + function (err, event) { + if (err) { + console.log( + "There was an error contacting the Calendar service: " + err + ); + return reject(err); + } + return resolve(event.data); + } + ); + }), + deleteEvent: (uid: String) => + new Promise((resolve, reject) => { + const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); + calendar.events.delete( + { + auth: myGoogleAuth, + calendarId: "primary", + eventId: uid, + sendNotifications: true, + sendUpdates: "all", + }, + function (err, event) { + if (err) { + console.log( + "There was an error contacting the Calendar service: " + err + ); + return reject(err); + } + return resolve(event.data); + } + ); + }), + listCalendars: () => + new Promise((resolve, reject) => { + const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); + calendar.calendarList + .list() + .then((cals) => { + resolve( + cals.data.items.map((cal) => { + const calendar: IntegrationCalendar = { + externalId: cal.id, + integration: integrationType, + name: cal.summary, + primary: cal.primary, + }; + return calendar; + }) + ); + }) + .catch((err) => { + reject(err); + }); + }), + }; }; // factory -const calendars = (withCredentials): CalendarApiAdapter[] => withCredentials.map((cred) => { - switch (cred.type) { - case 'google_calendar': - return GoogleCalendar(cred); - case 'office365_calendar': - return MicrosoftOffice365Calendar(cred); - default: - return; // unknown credential, could be legacy? In any case, ignore - } -}).filter(Boolean); +const calendars = (withCredentials): CalendarApiAdapter[] => + withCredentials + .map((cred) => { + switch (cred.type) { + case "google_calendar": + return GoogleCalendar(cred); + case "office365_calendar": + return MicrosoftOffice365Calendar(cred); + default: + return; // unknown credential, could be legacy? In any case, ignore + } + }) + .filter(Boolean); -const getBusyCalendarTimes = (withCredentials, dateFrom, dateTo, selectedCalendars) => Promise.all( - calendars(withCredentials).map(c => c.getAvailability(dateFrom, dateTo, selectedCalendars)) -).then( - (results) => { - return results.reduce((acc, availability) => acc.concat(availability), []) - } -); +const getBusyCalendarTimes = ( + withCredentials, + dateFrom, + dateTo, + selectedCalendars +) => + Promise.all( + calendars(withCredentials).map((c) => + c.getAvailability(dateFrom, dateTo, selectedCalendars) + ) + ).then((results) => { + return results.reduce((acc, availability) => acc.concat(availability), []); + }); -const listCalendars = (withCredentials) => Promise.all( - calendars(withCredentials).map(c => c.listCalendars()) -).then( - (results) => results.reduce((acc, calendars) => acc.concat(calendars), []) -); +const listCalendars = (withCredentials) => + Promise.all(calendars(withCredentials).map((c) => c.listCalendars())).then( + (results) => results.reduce((acc, calendars) => acc.concat(calendars), []) + ); -const createEvent = async (credential, calEvent: CalendarEvent): Promise => { - const uid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); +const createEvent = async ( + credential, + calEvent: CalendarEvent +): Promise => { + const uid: string = translator.fromUUID( + uuidv5(JSON.stringify(calEvent), uuidv5.URL) + ); - const creationResult = credential ? await calendars([credential])[0].createEvent(calEvent) : null; + const creationResult = credential + ? await calendars([credential])[0].createEvent(calEvent) + : null; const organizerMail = new EventOrganizerMail(calEvent, uid); const attendeeMail = new EventAttendeeMail(calEvent, uid); @@ -407,14 +507,22 @@ const createEvent = async (credential, calEvent: CalendarEvent): Promise => return { uid, - createdEvent: creationResult + createdEvent: creationResult, }; }; -const updateEvent = async (credential, uidToUpdate: String, calEvent: CalendarEvent): Promise => { - const newUid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); +const updateEvent = async ( + credential, + uidToUpdate: String, + calEvent: CalendarEvent +): Promise => { + const newUid: string = translator.fromUUID( + uuidv5(JSON.stringify(calEvent), uuidv5.URL) + ); - const updateResult = credential ? await calendars([credential])[0].updateEvent(uidToUpdate, calEvent) : null; + const updateResult = credential + ? await calendars([credential])[0].updateEvent(uidToUpdate, calEvent) + : null; const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid); const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid); @@ -426,7 +534,7 @@ const updateEvent = async (credential, uidToUpdate: String, calEvent: CalendarEv return { uid: newUid, - updatedEvent: updateResult + updatedEvent: updateResult, }; }; @@ -438,4 +546,12 @@ const deleteEvent = (credential, uid: String): Promise => { return Promise.resolve({}); }; -export {getBusyCalendarTimes, createEvent, updateEvent, deleteEvent, CalendarEvent, listCalendars, IntegrationCalendar}; +export { + getBusyCalendarTimes, + createEvent, + updateEvent, + deleteEvent, + CalendarEvent, + listCalendars, + IntegrationCalendar, +}; diff --git a/lib/location.ts b/lib/location.ts index b1ec56af04..b27f497702 100644 --- a/lib/location.ts +++ b/lib/location.ts @@ -2,5 +2,6 @@ export enum LocationType { InPerson = 'inPerson', Phone = 'phone', + GoogleMeet = 'integrations:google:meet' } diff --git a/package.json b/package.json index 750bd7be96..31e86ca42c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dayjs": "^1.10.4", "googleapis": "^67.1.1", "ics": "^2.27.0", + "lodash.merge": "^4.6.2", "next": "^10.2.0", "next-auth": "^3.13.2", "next-transpile-modules": "^7.0.0", diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index 98b475c813..31b7f23ab6 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -45,9 +45,10 @@ export default function Book(props) { const locationLabels = { [LocationType.InPerson]: 'In-person meeting', [LocationType.Phone]: 'Phone call', + [LocationType.GoogleMeet]: 'Google Meet', }; - const bookingHandler = event => { + const bookingHandler = (event) => { event.preventDefault(); let notes = ""; @@ -81,7 +82,19 @@ export default function Book(props) { }; if (selectedLocation) { - payload['location'] = selectedLocation === LocationType.Phone ? event.target.phone.value : locationInfo(selectedLocation).address; + switch (selectedLocation) { + case LocationType.Phone: + payload['location'] = event.target.phone.value + break + + case LocationType.InPerson: + payload['location'] = locationInfo(selectedLocation).address + break + + case LocationType.GoogleMeet: + payload['location'] = LocationType.GoogleMeet + break + } } telemetry.withJitsu(jitsu => jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters())); @@ -98,7 +111,12 @@ export default function Book(props) { let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=${!!rescheduleUid}&name=${payload.name}`; if (payload['location']) { - successUrl += "&location=" + encodeURIComponent(payload['location']); + if (payload['location'].includes('integration')) { + successUrl += "&location=" + encodeURIComponent("Web conferencing details to follow."); + } + else { + successUrl += "&location=" + encodeURIComponent(payload['location']); + } } router.push(successUrl); diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index 0c43637f19..d10358e440 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -7,9 +7,30 @@ import short from 'short-uuid'; import {createMeeting, updateMeeting} from "../../../lib/videoClient"; import EventAttendeeMail from "../../../lib/emails/EventAttendeeMail"; import {getEventName} from "../../../lib/event"; - +import { LocationType } from '../../../lib/location'; +import merge from "lodash.merge" const translator = short(); +interface p { + location: string +} + +const getLocationRequestFromIntegration = ({location}: p) => { + if (location === LocationType.GoogleMeet.valueOf()) { + const requestId = uuidv5(location, uuidv5.URL) + + return { + conferenceData: { + createRequest: { + requestId: requestId + } + } + } + } + + return null +} + export default async function handler(req: NextApiRequest, res: NextApiResponse) { const {user} = req.query; @@ -43,19 +64,38 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } }); - const evt: CalendarEvent = { + let rawLocation = req.body.location + + let evt: CalendarEvent = { type: selectedEventType.title, title: getEventName(req.body.name, selectedEventType.title, selectedEventType.eventName), description: req.body.notes, startTime: req.body.start, endTime: req.body.end, - location: req.body.location, organizer: {email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone}, attendees: [ {email: req.body.email, name: req.body.name, timeZone: req.body.timeZone} ] }; + // If phone or inPerson use raw location + // set evt.location to req.body.location + if (!rawLocation.includes('integration')) { + evt.location = rawLocation + } + + + // If location is set to an integration location + // Build proper transforms for evt object + // Extend evt object with those transformations + if (rawLocation.includes('integration')) { + let maybeLocationRequestObject = getLocationRequestFromIntegration({ + location: rawLocation + }) + + evt = merge(evt, maybeLocationRequestObject) + } + const eventType = await prisma.eventType.findFirst({ where: { userId: currentUser.id, diff --git a/pages/availability/event/[type].tsx b/pages/availability/event/[type].tsx index 9518dadce7..1ad8824a22 100644 --- a/pages/availability/event/[type].tsx +++ b/pages/availability/event/[type].tsx @@ -1,8 +1,8 @@ import Head from 'next/head'; import Link from 'next/link'; -import {useRouter} from 'next/router'; -import {useRef, useState} from 'react'; -import Select, {OptionBase} from 'react-select'; +import { useRouter } from 'next/router'; +import { useRef, useState, useEffect } from 'react'; +import Select, { OptionBase } from 'react-select'; import prisma from '../../../lib/prisma'; import {LocationType} from '../../../lib/location'; import Shell from '../../../components/Shell'; @@ -33,6 +33,7 @@ export default function EventType(props) { const [ selectedInputOption, setSelectedInputOption ] = useState(inputOptions[0]); const [ locations, setLocations ] = useState(props.eventType.locations || []); const [customInputs, setCustomInputs] = useState(props.eventType.customInputs.sort((a, b) => a.id - b.id) || []); + const locationOptions = props.locationOptions const titleRef = useRef(); const slugRef = useRef(); @@ -81,12 +82,6 @@ export default function EventType(props) { router.push('/availability'); } - // TODO: Tie into translations instead of abstracting to locations.ts - const locationOptions: OptionBase[] = [ - { value: LocationType.InPerson, label: 'In-person meeting' }, - { value: LocationType.Phone, label: 'Phone call', }, - ]; - const openLocationModal = (type: LocationType) => { setSelectedLocation(locationOptions.find( (option) => option.value === type)); setShowLocationModal(true); @@ -124,6 +119,10 @@ export default function EventType(props) { return (

Calendso will ask your invitee to enter a phone number before scheduling.

) + case LocationType.GoogleMeet: + return ( +

Calendso will provide a Google Meet location.

+ ) } return null; }; @@ -234,6 +233,12 @@ export default function EventType(props) { Phone call )} + {location.type === LocationType.GoogleMeet && ( +
+ + Google Meet +
+ )}