Compare commits
1 Commits
main
...
app/vimcal
Author | SHA1 | Date |
---|---|---|
Peer Richelsen | 2f840df54b |
|
@ -20,6 +20,8 @@ export function getIntegrationName(name: string) {
|
|||
return "Huddle01";
|
||||
case "tandem_video":
|
||||
return "Tandem";
|
||||
case "vimcal_calendar":
|
||||
return "Vimcal";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,5 +6,6 @@ export const CALENDAR_INTEGRATIONS_TYPES = {
|
|||
apple: "apple_calendar",
|
||||
caldav: "caldav_calendar",
|
||||
google: "google_calendar",
|
||||
vimcal: "vimcal_calendar",
|
||||
office365: "office365_calendar",
|
||||
};
|
||||
|
|
|
@ -0,0 +1,329 @@
|
|||
import { Credential, Prisma } from "@prisma/client";
|
||||
import { GetTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
|
||||
import { Auth, calendar_v3, google } from "googleapis";
|
||||
|
||||
import { getLocation, getRichDescription } from "@lib/CalEventParser";
|
||||
import { CALENDAR_INTEGRATIONS_TYPES } from "@lib/integrations/calendar/constants/generals";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { EventBusyDate, NewCalendarEventType } from "../constants/types";
|
||||
import { Calendar, CalendarEvent, IntegrationCalendar } from "../interfaces/Calendar";
|
||||
import CalendarService from "./BaseCalendarService";
|
||||
|
||||
const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "";
|
||||
|
||||
export default class VimcalCalendarService implements Calendar {
|
||||
private url = "";
|
||||
private integrationName = "";
|
||||
private auth: { getToken: () => Promise<MyGoogleAuth> };
|
||||
private log: typeof logger;
|
||||
|
||||
constructor(credential: Credential) {
|
||||
this.integrationName = CALENDAR_INTEGRATIONS_TYPES.google;
|
||||
|
||||
this.auth = this.googleAuth(credential);
|
||||
|
||||
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
}
|
||||
|
||||
private googleAuth = (credential: Credential) => {
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(GOOGLE_API_CREDENTIALS).web;
|
||||
|
||||
const myGoogleAuth = new MyGoogleAuth(client_id, client_secret, redirect_uris[0]);
|
||||
|
||||
const googleCredentials = credential.key as Auth.Credentials;
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
|
||||
const isExpired = () => myGoogleAuth.isTokenExpiring();
|
||||
|
||||
const refreshAccessToken = () =>
|
||||
myGoogleAuth
|
||||
.refreshToken(googleCredentials.refresh_token)
|
||||
.then((res: GetTokenResponse) => {
|
||||
const token = res.res?.data;
|
||||
googleCredentials.access_token = token.access_token;
|
||||
googleCredentials.expiry_date = token.expiry_date;
|
||||
return prisma.credential
|
||||
.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: googleCredentials as Prisma.InputJsonValue,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
return myGoogleAuth;
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log.error("Error refreshing google token", err);
|
||||
|
||||
return myGoogleAuth;
|
||||
});
|
||||
|
||||
return {
|
||||
getToken: () => (!isExpired() ? Promise.resolve(myGoogleAuth) : refreshAccessToken()),
|
||||
};
|
||||
};
|
||||
|
||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: getRichDescription(event),
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees.map((attendee) => ({
|
||||
...attendee,
|
||||
responseStatus: "accepted",
|
||||
})),
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = getLocation(event);
|
||||
}
|
||||
|
||||
if (event.conferenceData && event.location === "integrations:google:meet") {
|
||||
payload["conferenceData"] = event.conferenceData;
|
||||
}
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.insert(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
: "primary",
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err || !event?.data) {
|
||||
console.error("There was an error contacting vimcal calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve({
|
||||
uid: "",
|
||||
...event.data,
|
||||
id: event.data.id || "",
|
||||
additionalInfo: {
|
||||
hangoutLink: event.data.hangoutLink || "",
|
||||
},
|
||||
type: "vimcal_calendar",
|
||||
password: "",
|
||||
url: "",
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: getRichDescription(event),
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees,
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = getLocation(event);
|
||||
}
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.update(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
requestBody: payload,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async deleteEvent(uid: string, event: CalendarEvent): Promise<void> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.delete(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
.map((e) => e.externalId);
|
||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
(selectedCalendarIds.length === 0
|
||||
? calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => cals.data.items?.map((cal) => cal.id).filter(Boolean) || [])
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
)
|
||||
.then((calsIds) => {
|
||||
calendar.freebusy.query(
|
||||
{
|
||||
requestBody: {
|
||||
timeMin: dateFrom,
|
||||
timeMax: dateTo,
|
||||
items: calsIds.map((id) => ({ id: id })),
|
||||
},
|
||||
},
|
||||
(err, apires) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
let result: Prisma.PromiseReturnType<CalendarService["getAvailability"]> = [];
|
||||
|
||||
if (apires?.data.calendars) {
|
||||
result = Object.values(apires.data.calendars).reduce((c, i) => {
|
||||
i.busy?.forEach((busyTime) => {
|
||||
c.push({
|
||||
start: busyTime.start || "",
|
||||
end: busyTime.end || "",
|
||||
});
|
||||
});
|
||||
return c;
|
||||
}, [] as typeof result);
|
||||
}
|
||||
resolve(result);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log.error("There was an error contacting google calendar service: ", err);
|
||||
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async listCalendars(): Promise<IntegrationCalendar[]> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
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 ?? "No id",
|
||||
integration: this.integrationName,
|
||||
name: cal.summary ?? "No name",
|
||||
primary: cal.primary ?? false,
|
||||
};
|
||||
return calendar;
|
||||
}) || []
|
||||
);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.log.error("There was an error contacting google calendar service: ", err);
|
||||
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyGoogleAuth extends google.auth.OAuth2 {
|
||||
constructor(client_id: string, client_secret: string, redirect_uri: string) {
|
||||
super(client_id, client_secret, redirect_uri);
|
||||
}
|
||||
|
||||
isTokenExpiring() {
|
||||
return super.isTokenExpiring();
|
||||
}
|
||||
|
||||
async refreshToken(token: string | null | undefined) {
|
||||
return super.refreshToken(token);
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ export type Integration = {
|
|||
installed: boolean;
|
||||
type:
|
||||
| "google_calendar"
|
||||
| "vimcal_calendar"
|
||||
| "office365_calendar"
|
||||
| "zoom_video"
|
||||
| "daily_video"
|
||||
|
@ -38,6 +39,14 @@ export const ALL_INTEGRATIONS = [
|
|||
description: "For personal and business calendars",
|
||||
variant: "calendar",
|
||||
},
|
||||
{
|
||||
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
|
||||
type: "vimcal_calendar",
|
||||
title: "Vimcal",
|
||||
imageSrc: "integrations/vimcal.svg",
|
||||
description: "For personal and business calendars",
|
||||
variant: "calendar",
|
||||
},
|
||||
{
|
||||
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
|
||||
type: "office365_calendar",
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<svg width="254" height="240" viewBox="0 0 254 240" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.266 47.7389C-0.506518 63.4802 -5.61302 85.5234 7.67466 102.453C16.9566 114.278 31.7083 119.283 45.5291 116.697C101.72 62.1957 155.717 44.496 196.987 41.0199C203.368 40.4824 209.439 40.2854 215.159 40.3437C226.469 28.3702 239.202 17.6844 253.142 8.57573L253.244 8.32026C253.244 8.32026 193.032 -5.70712 134.419 2.66537C83.2399 9.976 32.7511 28.0436 14.266 47.7389Z" fill="url(#paint0_linear_777_38125)"/>
|
||||
<path d="M100.398 182.057C99.6098 183.476 98.9017 184.96 98.2816 186.505C90.2655 206.478 99.957 229.168 119.929 237.185C126.539 239.838 133.443 240.552 140.018 239.595C146.597 238.673 153.028 236.054 158.631 231.656C159.799 230.739 160.9 229.77 161.933 228.753C161.005 224.306 160.212 219.801 159.561 215.241C155.498 186.801 157.306 158.876 164.066 132.693C155.119 135.625 146.145 140.323 137.371 146.439C124.112 155.682 111.494 168.049 100.398 182.057Z" fill="url(#paint1_linear_777_38125)"/>
|
||||
<path d="M55.0493 116.603C44.9262 130.351 44.7412 149.673 55.7963 163.758C65.5321 176.161 81.2843 181.061 95.6683 177.569C107.018 163.317 119.957 150.662 133.657 141.111C144.069 133.853 155.025 128.321 166.12 125.299C174.534 97.0249 188.771 70.9751 207.627 48.7561C208.162 48.1248 208.702 47.4965 209.245 46.8715C205.478 46.9526 201.57 47.1516 197.532 47.4917C158.988 50.7382 108.356 66.8644 55.0493 116.603Z" fill="url(#paint2_linear_777_38125)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_777_38125" x1="237.312" y1="28.4175" x2="-48.0846" y2="151.467" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFB6BE"/>
|
||||
<stop offset="0.113339" stop-color="#FA6753"/>
|
||||
<stop offset="0.432815" stop-color="#F50058"/>
|
||||
<stop offset="0.61187" stop-color="#F50081"/>
|
||||
<stop offset="0.77952" stop-color="#C71FD6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_777_38125" x1="237.312" y1="28.4175" x2="-48.0846" y2="151.467" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFB6BE"/>
|
||||
<stop offset="0.113339" stop-color="#FA6753"/>
|
||||
<stop offset="0.432815" stop-color="#F50058"/>
|
||||
<stop offset="0.61187" stop-color="#F50081"/>
|
||||
<stop offset="0.77952" stop-color="#C71FD6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_777_38125" x1="237.312" y1="28.4175" x2="-48.0846" y2="151.467" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFB6BE"/>
|
||||
<stop offset="0.113339" stop-color="#FA6753"/>
|
||||
<stop offset="0.432815" stop-color="#F50058"/>
|
||||
<stop offset="0.61187" stop-color="#F50081"/>
|
||||
<stop offset="0.77952" stop-color="#C71FD6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
Loading…
Reference in New Issue