Zomars/refactor emails followup (#1216)
This commit is contained in:
parent
d76ef4a007
commit
ec2acedf34
10 changed files with 417 additions and 166 deletions
|
@ -8,7 +8,7 @@
|
|||
NEXT_PUBLIC_LICENSE_CONSENT=''
|
||||
|
||||
# DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>/<db-name>'
|
||||
DATABASE_URL="postgresql://postgres:@localhost:5432/calendso?schema=public"
|
||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
|
||||
GOOGLE_API_CREDENTIALS='secret'
|
||||
|
||||
|
|
257
@types/ical.d.ts
vendored
Normal file
257
@types/ical.d.ts
vendored
Normal file
|
@ -0,0 +1,257 @@
|
|||
// SPDX-FileCopyrightText: © 2019 EteSync Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// https://github.com/mozilla-comm/ical.js/issues/367#issuecomment-568493517
|
||||
declare module "ical.js" {
|
||||
function parse(input: string): any[];
|
||||
|
||||
export class helpers {
|
||||
public updateTimezones(vcal: Component): Component;
|
||||
}
|
||||
|
||||
class Component {
|
||||
public fromString(str: string): Component;
|
||||
|
||||
public name: string;
|
||||
|
||||
constructor(jCal: any[] | string, parent?: Component);
|
||||
|
||||
public toJSON(): any[];
|
||||
|
||||
public getFirstSubcomponent(name?: string): Component | null;
|
||||
public getAllSubcomponents(name?: string): Component[];
|
||||
|
||||
public getFirstPropertyValue<T = any>(name?: string): T;
|
||||
|
||||
public getFirstProperty(name?: string): Property;
|
||||
public getAllProperties(name?: string): Property[];
|
||||
|
||||
public addProperty(property: Property): Property;
|
||||
public addPropertyWithValue(name: string, value: string | number | Record<string, unknown>): Property;
|
||||
|
||||
public hasProperty(name?: string): boolean;
|
||||
|
||||
public updatePropertyWithValue(name: string, value: string | number | Record<string, unknown>): Property;
|
||||
|
||||
public removeAllProperties(name?: string): boolean;
|
||||
|
||||
public addSubcomponent(component: Component): Component;
|
||||
}
|
||||
|
||||
export class Event {
|
||||
public uid: string;
|
||||
public summary: string;
|
||||
public startDate: Time;
|
||||
public endDate: Time;
|
||||
public description: string;
|
||||
public location: string;
|
||||
public attendees: Property[];
|
||||
/**
|
||||
* The sequence value for this event. Used for scheduling.
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof Event
|
||||
*/
|
||||
public sequence: number;
|
||||
/**
|
||||
* The duration. This can be the result directly from the property, or the
|
||||
* duration calculated from start date and end date. Setting the property
|
||||
* will remove any `dtend` properties.
|
||||
*
|
||||
* @type {Duration}
|
||||
* @memberof Event
|
||||
*/
|
||||
public duration: Duration;
|
||||
/**
|
||||
* The organizer value as an uri. In most cases this is a mailto: uri,
|
||||
* but it can also be something else, like urn:uuid:...
|
||||
*/
|
||||
public organizer: string;
|
||||
/** The sequence value for this event. Used for scheduling */
|
||||
public sequence: number;
|
||||
/** The recurrence id for this event */
|
||||
public recurrenceId: Time;
|
||||
|
||||
public component: Component;
|
||||
|
||||
public constructor(
|
||||
component?: Component | null,
|
||||
options?: { strictExceptions: boolean; exepctions: Array<Component | Event> }
|
||||
);
|
||||
|
||||
public isRecurring(): boolean;
|
||||
public iterator(startTime?: Time): RecurExpansion;
|
||||
}
|
||||
|
||||
export class Property {
|
||||
public name: string;
|
||||
public type: string;
|
||||
|
||||
constructor(jCal: any[] | string, parent?: Component);
|
||||
|
||||
public getFirstValue<T = any>(): T;
|
||||
public getValues<T = any>(): T[];
|
||||
|
||||
public setParameter(name: string, value: string | string[]): void;
|
||||
public setValue(value: string | Record<string, unknown>): void;
|
||||
public setValues(values: (string | Record<string, unknown>)[]): void;
|
||||
public toJSON(): any;
|
||||
}
|
||||
|
||||
interface TimeJsonData {
|
||||
year?: number;
|
||||
month?: number;
|
||||
day?: number;
|
||||
hour?: number;
|
||||
minute?: number;
|
||||
second?: number;
|
||||
isDate?: boolean;
|
||||
}
|
||||
|
||||
export class Time {
|
||||
public fromString(str: string): Time;
|
||||
public fromJSDate(aDate: Date | null, useUTC: boolean): Time;
|
||||
public fromData(aData: TimeJsonData): Time;
|
||||
|
||||
public now(): Time;
|
||||
|
||||
public isDate: boolean;
|
||||
public timezone: string;
|
||||
public zone: Timezone;
|
||||
|
||||
public year: number;
|
||||
public month: number;
|
||||
public day: number;
|
||||
public hour: number;
|
||||
public minute: number;
|
||||
public second: number;
|
||||
|
||||
constructor(data?: TimeJsonData);
|
||||
public compare(aOther: Time): number;
|
||||
|
||||
public clone(): Time;
|
||||
public convertToZone(zone: Timezone): Time;
|
||||
|
||||
public adjust(
|
||||
aExtraDays: number,
|
||||
aExtraHours: number,
|
||||
aExtraMinutes: number,
|
||||
aExtraSeconds: number,
|
||||
aTimeopt?: Time
|
||||
): void;
|
||||
|
||||
public addDuration(aDuration: Duration): void;
|
||||
public subtractDateTz(aDate: Time): Duration;
|
||||
|
||||
public toUnixTime(): number;
|
||||
public toJSDate(): Date;
|
||||
public toJSON(): TimeJsonData;
|
||||
public get icaltype(): "date" | "date-time";
|
||||
}
|
||||
|
||||
export class Duration {
|
||||
public weeks: number;
|
||||
public days: number;
|
||||
public hours: number;
|
||||
public minutes: number;
|
||||
public seconds: number;
|
||||
public isNegative: boolean;
|
||||
public icalclass: string;
|
||||
public icaltype: string;
|
||||
}
|
||||
|
||||
export class RecurExpansion {
|
||||
public complete: boolean;
|
||||
public dtstart: Time;
|
||||
public last: Time;
|
||||
public next(): Time;
|
||||
public fromData(options);
|
||||
public toJSON();
|
||||
constructor(options: {
|
||||
/** Start time of the event */
|
||||
dtstart: Time;
|
||||
/** Component for expansion, required if not resuming. */
|
||||
component?: Component;
|
||||
});
|
||||
}
|
||||
|
||||
export class Timezone {
|
||||
public utcTimezone: Timezone;
|
||||
public localTimezone: Timezone;
|
||||
public convert_time(tt: Time, fromZone: Timezone, toZone: Timezone): Time;
|
||||
|
||||
public tzid: string;
|
||||
public component: Component;
|
||||
|
||||
constructor(
|
||||
data:
|
||||
| Component
|
||||
| {
|
||||
component: string | Component;
|
||||
tzid?: string;
|
||||
location?: string;
|
||||
tznames?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export class TimezoneService {
|
||||
public get(tzid: string): Timezone | null;
|
||||
public has(tzid: string): boolean;
|
||||
public register(tzid: string, zone: Timezone | Component);
|
||||
public remove(tzid: string): Timezone | null;
|
||||
}
|
||||
|
||||
export type FrequencyValues =
|
||||
| "YEARLY"
|
||||
| "MONTHLY"
|
||||
| "WEEKLY"
|
||||
| "DAILY"
|
||||
| "HOURLY"
|
||||
| "MINUTELY"
|
||||
| "SECONDLY";
|
||||
|
||||
export enum WeekDay {
|
||||
SU = 1,
|
||||
MO,
|
||||
TU,
|
||||
WE,
|
||||
TH,
|
||||
FR,
|
||||
SA,
|
||||
}
|
||||
|
||||
export class RecurData {
|
||||
public freq?: FrequencyValues;
|
||||
public interval?: number;
|
||||
public wkst?: WeekDay;
|
||||
public until?: Time;
|
||||
public count?: number;
|
||||
public bysecond?: number[] | number;
|
||||
public byminute?: number[] | number;
|
||||
public byhour?: number[] | number;
|
||||
public byday?: string[] | string;
|
||||
public bymonthday?: number[] | number;
|
||||
public byyearday?: number[] | number;
|
||||
public byweekno?: number[] | number;
|
||||
public bymonth?: number[] | number;
|
||||
public bysetpos?: number[] | number;
|
||||
}
|
||||
|
||||
export class RecurIterator {
|
||||
public next(): Time;
|
||||
}
|
||||
|
||||
export class Recur {
|
||||
constructor(data?: RecurData);
|
||||
public until: Time | null;
|
||||
public freq: FrequencyValues;
|
||||
public count: number | null;
|
||||
|
||||
public clone(): Recur;
|
||||
public toJSON(): Omit<RecurData, "until"> & { until?: string };
|
||||
public iterator(startTime?: Time): RecurIterator;
|
||||
public isByCount(): boolean;
|
||||
}
|
||||
}
|
|
@ -19,6 +19,8 @@ import {
|
|||
import logger from "@lib/logger";
|
||||
import { VideoCallData } from "@lib/videoClient";
|
||||
|
||||
import { Ensure } from "./types/utils";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["[lib] calendarClient"] });
|
||||
|
||||
export type Person = { name: string; email: string; timeZone: string };
|
||||
|
@ -61,7 +63,7 @@ export interface CalendarEvent {
|
|||
paymentInfo?: PaymentInfo | null;
|
||||
}
|
||||
|
||||
export interface IntegrationCalendar extends Partial<SelectedCalendar> {
|
||||
export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "externalId"> {
|
||||
primary?: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
@ -89,11 +91,9 @@ function getCalendarAdapterOrNull(credential: Credential): CalendarApiAdapter |
|
|||
case "office365_calendar":
|
||||
return Office365CalendarApiAdapter(credential);
|
||||
case "caldav_calendar":
|
||||
// FIXME types wrong & type casting should not be needed
|
||||
return new CalDavCalendar(credential) as never as CalendarApiAdapter;
|
||||
return new CalDavCalendar(credential);
|
||||
case "apple_calendar":
|
||||
// FIXME types wrong & type casting should not be needed
|
||||
return new AppleCalendar(credential) as never as CalendarApiAdapter;
|
||||
return new AppleCalendar(credential);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -24,8 +24,6 @@ dayjs.extend(utc);
|
|||
|
||||
const log = logger.getChildLogger({ prefix: ["[[lib] apple calendar"] });
|
||||
|
||||
type EventBusyDate = Record<"start" | "end", Date>;
|
||||
|
||||
export class AppleCalendar implements CalendarApiAdapter {
|
||||
private url: string;
|
||||
private credentials: Record<string, string>;
|
||||
|
@ -34,7 +32,7 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
|
||||
constructor(credential: Credential) {
|
||||
const decryptedCredential = JSON.parse(
|
||||
symmetricDecrypt(credential.key, process.env.CALENDSO_ENCRYPTION_KEY)
|
||||
symmetricDecrypt(credential.key as string, process.env.CALENDSO_ENCRYPTION_KEY!)
|
||||
);
|
||||
const username = decryptedCredential.username;
|
||||
const password = decryptedCredential.password;
|
||||
|
@ -52,12 +50,12 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
});
|
||||
}
|
||||
|
||||
convertDate(date: string): number[] {
|
||||
convertDate(date: string): [number, number, number] {
|
||||
return dayjs(date)
|
||||
.utc()
|
||||
.toArray()
|
||||
.slice(0, 6)
|
||||
.map((v, i) => (i === 1 ? v + 1 : v));
|
||||
.map((v, i) => (i === 1 ? v + 1 : v)) as [number, number, number];
|
||||
}
|
||||
|
||||
getDuration(start: string, end: string): DurationObject {
|
||||
|
@ -70,11 +68,11 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
return attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));
|
||||
}
|
||||
|
||||
async createEvent(event: CalendarEvent): Promise<Record<string, unknown>> {
|
||||
async createEvent(event: CalendarEvent) {
|
||||
try {
|
||||
const calendars = await this.listCalendars();
|
||||
const uid = uuidv4();
|
||||
const { error, value: iCalString } = await createEvent({
|
||||
const { error, value: iCalString } = createEvent({
|
||||
uid,
|
||||
startInputType: "utc",
|
||||
start: this.convertDate(event.startTime),
|
||||
|
@ -86,15 +84,9 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
attendees: this.getAttendees(event.attendees),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
log.debug("Error creating iCalString");
|
||||
return {};
|
||||
}
|
||||
if (error) throw new Error("Error creating iCalString");
|
||||
|
||||
if (!iCalString) {
|
||||
log.debug("Error creating iCalString");
|
||||
return {};
|
||||
}
|
||||
if (!iCalString) throw new Error("Error creating iCalString");
|
||||
|
||||
await Promise.all(
|
||||
calendars.map((calendar) => {
|
||||
|
@ -112,6 +104,9 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
return {
|
||||
uid,
|
||||
id: uid,
|
||||
type: "apple_calendar",
|
||||
password: "",
|
||||
url: "",
|
||||
};
|
||||
} catch (reason) {
|
||||
console.error(reason);
|
||||
|
@ -132,7 +127,7 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
const { error, value: iCalString } = await createEvent({
|
||||
const { error, value: iCalString } = createEvent({
|
||||
uid,
|
||||
startInputType: "utc",
|
||||
start: this.convertDate(event.startTime),
|
||||
|
@ -201,11 +196,7 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[]) {
|
||||
try {
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
|
@ -229,8 +220,8 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
ids.map(async (calId) => {
|
||||
return (await this.getEvents(calId, dateFrom, dateTo)).map((event) => {
|
||||
return {
|
||||
start: event.startDate,
|
||||
end: event.endDate,
|
||||
start: event.startDate.toISOString(),
|
||||
end: event.endDate.toISOString(),
|
||||
};
|
||||
});
|
||||
})
|
||||
|
@ -272,8 +263,8 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
calId: string,
|
||||
dateFrom: string | null,
|
||||
dateTo: string | null,
|
||||
objectUrls: string[] | null
|
||||
): Promise<unknown[]> {
|
||||
objectUrls?: string[] | null
|
||||
) {
|
||||
try {
|
||||
const objects = await fetchCalendarObjects({
|
||||
calendar: {
|
||||
|
@ -290,54 +281,48 @@ export class AppleCalendar implements CalendarApiAdapter {
|
|||
headers: this.headers,
|
||||
});
|
||||
|
||||
const events =
|
||||
objects &&
|
||||
objects?.length > 0 &&
|
||||
objects
|
||||
.map((object) => {
|
||||
if (object?.data) {
|
||||
const jcalData = ICAL.parse(object.data);
|
||||
const vcalendar = new ICAL.Component(jcalData);
|
||||
const vevent = vcalendar.getFirstSubcomponent("vevent");
|
||||
const event = new ICAL.Event(vevent);
|
||||
const events = objects
|
||||
.filter((e) => !!e.data)
|
||||
.map((object) => {
|
||||
const jcalData = ICAL.parse(object.data);
|
||||
const vcalendar = new ICAL.Component(jcalData);
|
||||
const vevent = vcalendar.getFirstSubcomponent("vevent");
|
||||
const event = new ICAL.Event(vevent);
|
||||
|
||||
const calendarTimezone = vcalendar.getFirstSubcomponent("vtimezone")
|
||||
? vcalendar.getFirstSubcomponent("vtimezone").getFirstPropertyValue("tzid")
|
||||
: "";
|
||||
const calendarTimezone =
|
||||
vcalendar.getFirstSubcomponent("vtimezone")?.getFirstPropertyValue("tzid") || "";
|
||||
|
||||
const startDate = calendarTimezone
|
||||
? dayjs(event.startDate).tz(calendarTimezone)
|
||||
: new Date(event.startDate.toUnixTime() * 1000);
|
||||
const endDate = calendarTimezone
|
||||
? dayjs(event.endDate).tz(calendarTimezone)
|
||||
: new Date(event.endDate.toUnixTime() * 1000);
|
||||
const startDate = calendarTimezone
|
||||
? dayjs(event.startDate.toJSDate()).tz(calendarTimezone)
|
||||
: new Date(event.startDate.toUnixTime() * 1000);
|
||||
const endDate = calendarTimezone
|
||||
? dayjs(event.endDate.toJSDate()).tz(calendarTimezone)
|
||||
: new Date(event.endDate.toUnixTime() * 1000);
|
||||
|
||||
return {
|
||||
uid: event.uid,
|
||||
etag: object.etag,
|
||||
url: object.url,
|
||||
summary: event.summary,
|
||||
description: event.description,
|
||||
location: event.location,
|
||||
sequence: event.sequence,
|
||||
startDate,
|
||||
endDate,
|
||||
duration: {
|
||||
weeks: event.duration.weeks,
|
||||
days: event.duration.days,
|
||||
hours: event.duration.hours,
|
||||
minutes: event.duration.minutes,
|
||||
seconds: event.duration.seconds,
|
||||
isNegative: event.duration.isNegative,
|
||||
},
|
||||
organizer: event.organizer,
|
||||
attendees: event.attendees.map((a) => a.getValues()),
|
||||
recurrenceId: event.recurrenceId,
|
||||
timezone: calendarTimezone,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter((e) => e != null);
|
||||
return {
|
||||
uid: event.uid,
|
||||
etag: object.etag,
|
||||
url: object.url,
|
||||
summary: event.summary,
|
||||
description: event.description,
|
||||
location: event.location,
|
||||
sequence: event.sequence,
|
||||
startDate,
|
||||
endDate,
|
||||
duration: {
|
||||
weeks: event.duration.weeks,
|
||||
days: event.duration.days,
|
||||
hours: event.duration.hours,
|
||||
minutes: event.duration.minutes,
|
||||
seconds: event.duration.seconds,
|
||||
isNegative: event.duration.isNegative,
|
||||
},
|
||||
organizer: event.organizer,
|
||||
attendees: event.attendees.map((a) => a.getValues()),
|
||||
recurrenceId: event.recurrenceId,
|
||||
timezone: calendarTimezone,
|
||||
};
|
||||
});
|
||||
|
||||
return events;
|
||||
} catch (reason) {
|
||||
|
|
|
@ -24,8 +24,6 @@ dayjs.extend(utc);
|
|||
|
||||
const log = logger.getChildLogger({ prefix: ["[lib] caldav"] });
|
||||
|
||||
type EventBusyDate = Record<"start" | "end", Date>;
|
||||
|
||||
export class CalDavCalendar implements CalendarApiAdapter {
|
||||
private url: string;
|
||||
private credentials: Record<string, string>;
|
||||
|
@ -34,7 +32,7 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
|
||||
constructor(credential: Credential) {
|
||||
const decryptedCredential = JSON.parse(
|
||||
symmetricDecrypt(credential.key, process.env.CALENDSO_ENCRYPTION_KEY)
|
||||
symmetricDecrypt(credential.key as string, process.env.CALENDSO_ENCRYPTION_KEY!)
|
||||
);
|
||||
const username = decryptedCredential.username;
|
||||
const url = decryptedCredential.url;
|
||||
|
@ -53,12 +51,12 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
});
|
||||
}
|
||||
|
||||
convertDate(date: string): number[] {
|
||||
convertDate(date: string): [number, number, number] {
|
||||
return dayjs(date)
|
||||
.utc()
|
||||
.toArray()
|
||||
.slice(0, 6)
|
||||
.map((v, i) => (i === 1 ? v + 1 : v));
|
||||
.map((v, i) => (i === 1 ? v + 1 : v)) as [number, number, number];
|
||||
}
|
||||
|
||||
getDuration(start: string, end: string): DurationObject {
|
||||
|
@ -71,15 +69,14 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
return attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));
|
||||
}
|
||||
|
||||
async createEvent(event: CalendarEvent): Promise<Record<string, unknown>> {
|
||||
async createEvent(event: CalendarEvent) {
|
||||
try {
|
||||
const calendars = await this.listCalendars();
|
||||
const uid = uuidv4();
|
||||
|
||||
const { error, value: iCalString } = await createEvent({
|
||||
const { error, value: iCalString } = createEvent({
|
||||
uid,
|
||||
startInputType: "utc",
|
||||
// FIXME types
|
||||
start: this.convertDate(event.startTime),
|
||||
duration: this.getDuration(event.startTime, event.endTime),
|
||||
title: event.title,
|
||||
|
@ -89,15 +86,9 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
attendees: this.getAttendees(event.attendees),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
log.debug("Error creating iCalString");
|
||||
return {};
|
||||
}
|
||||
if (error) throw new Error("Error creating iCalString");
|
||||
|
||||
if (!iCalString) {
|
||||
log.debug("Error creating iCalString");
|
||||
return {};
|
||||
}
|
||||
if (!iCalString) throw new Error("Error creating iCalString");
|
||||
|
||||
await Promise.all(
|
||||
calendars.map((calendar) => {
|
||||
|
@ -115,6 +106,9 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
return {
|
||||
uid,
|
||||
id: uid,
|
||||
type: "caldav_calendar",
|
||||
password: "",
|
||||
url: "",
|
||||
};
|
||||
} catch (reason) {
|
||||
log.error(reason);
|
||||
|
@ -138,7 +132,6 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
const { error, value: iCalString } = await createEvent({
|
||||
uid,
|
||||
startInputType: "utc",
|
||||
// FIXME - types wrong
|
||||
start: this.convertDate(event.startTime),
|
||||
duration: this.getDuration(event.startTime, event.endTime),
|
||||
title: event.title,
|
||||
|
@ -205,12 +198,7 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME - types wrong
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
async getAvailability(dateFrom: string, dateTo: string, selectedCalendars: IntegrationCalendar[]) {
|
||||
try {
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
|
@ -235,8 +223,8 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
ids.map(async (calId) => {
|
||||
return (await this.getEvents(calId, dateFrom, dateTo)).map((event) => {
|
||||
return {
|
||||
start: event.startDate,
|
||||
end: event.endDate,
|
||||
start: event.startDate.toISOString(),
|
||||
end: event.endDate.toISOString(),
|
||||
};
|
||||
});
|
||||
})
|
||||
|
@ -274,7 +262,7 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
async getEvents(calId: string, dateFrom: string | null, dateTo: string | null): Promise<unknown[]> {
|
||||
async getEvents(calId: string, dateFrom: string | null, dateTo: string | null) {
|
||||
try {
|
||||
const objects = await fetchCalendarObjects({
|
||||
calendar: {
|
||||
|
@ -295,50 +283,47 @@ export class CalDavCalendar implements CalendarApiAdapter {
|
|||
}
|
||||
|
||||
const events = objects
|
||||
.filter((e) => !!e.data)
|
||||
.map((object) => {
|
||||
if (object?.data) {
|
||||
const jcalData = ICAL.parse(object.data);
|
||||
const vcalendar = new ICAL.Component(jcalData);
|
||||
const vevent = vcalendar.getFirstSubcomponent("vevent");
|
||||
const event = new ICAL.Event(vevent);
|
||||
const jcalData = ICAL.parse(object.data);
|
||||
const vcalendar = new ICAL.Component(jcalData);
|
||||
const vevent = vcalendar.getFirstSubcomponent("vevent");
|
||||
const event = new ICAL.Event(vevent);
|
||||
|
||||
const calendarTimezone = vcalendar.getFirstSubcomponent("vtimezone")
|
||||
? vcalendar.getFirstSubcomponent("vtimezone").getFirstPropertyValue("tzid")
|
||||
: "";
|
||||
const calendarTimezone =
|
||||
vcalendar.getFirstSubcomponent("vtimezone")?.getFirstPropertyValue("tzid") || "";
|
||||
|
||||
const startDate = calendarTimezone
|
||||
? dayjs(event.startDate).tz(calendarTimezone)
|
||||
: new Date(event.startDate.toUnixTime() * 1000);
|
||||
const endDate = calendarTimezone
|
||||
? dayjs(event.endDate).tz(calendarTimezone)
|
||||
: new Date(event.endDate.toUnixTime() * 1000);
|
||||
const startDate = calendarTimezone
|
||||
? dayjs(event.startDate.toJSDate()).tz(calendarTimezone)
|
||||
: new Date(event.startDate.toUnixTime() * 1000);
|
||||
const endDate = calendarTimezone
|
||||
? dayjs(event.endDate.toJSDate()).tz(calendarTimezone)
|
||||
: new Date(event.endDate.toUnixTime() * 1000);
|
||||
|
||||
return {
|
||||
uid: event.uid,
|
||||
etag: object.etag,
|
||||
url: object.url,
|
||||
summary: event.summary,
|
||||
description: event.description,
|
||||
location: event.location,
|
||||
sequence: event.sequence,
|
||||
startDate,
|
||||
endDate,
|
||||
duration: {
|
||||
weeks: event.duration.weeks,
|
||||
days: event.duration.days,
|
||||
hours: event.duration.hours,
|
||||
minutes: event.duration.minutes,
|
||||
seconds: event.duration.seconds,
|
||||
isNegative: event.duration.isNegative,
|
||||
},
|
||||
organizer: event.organizer,
|
||||
attendees: event.attendees.map((a) => a.getValues()),
|
||||
recurrenceId: event.recurrenceId,
|
||||
timezone: calendarTimezone,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter((e) => e != null);
|
||||
return {
|
||||
uid: event.uid,
|
||||
etag: object.etag,
|
||||
url: object.url,
|
||||
summary: event.summary,
|
||||
description: event.description,
|
||||
location: event.location,
|
||||
sequence: event.sequence,
|
||||
startDate,
|
||||
endDate,
|
||||
duration: {
|
||||
weeks: event.duration.weeks,
|
||||
days: event.duration.days,
|
||||
hours: event.duration.hours,
|
||||
minutes: event.duration.minutes,
|
||||
seconds: event.duration.seconds,
|
||||
isNegative: event.duration.isNegative,
|
||||
},
|
||||
organizer: event.organizer,
|
||||
attendees: event.attendees.map((a) => a.getValues()),
|
||||
recurrenceId: event.recurrenceId,
|
||||
timezone: calendarTimezone,
|
||||
};
|
||||
});
|
||||
|
||||
return events;
|
||||
} catch (reason) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { CalendarApiAdapter, CalendarEvent, IntegrationCalendar } from "@lib/cal
|
|||
import prisma from "@lib/prisma";
|
||||
|
||||
export interface ConferenceData {
|
||||
createRequest: calendar_v3.Schema$CreateConferenceRequest;
|
||||
createRequest?: calendar_v3.Schema$CreateConferenceRequest;
|
||||
}
|
||||
|
||||
const googleAuth = (credential: Credential) => {
|
||||
|
@ -91,7 +91,19 @@ export const GoogleCalendarApiAdapter = (credential: Credential): CalendarApiAda
|
|||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(Object.values(apires.data.calendars).flatMap((item) => item["busy"]));
|
||||
let result: Prisma.PromiseReturnType<CalendarApiAdapter["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);
|
||||
}
|
||||
);
|
||||
})
|
||||
|
@ -146,7 +158,14 @@ export const GoogleCalendarApiAdapter = (credential: Credential): CalendarApiAda
|
|||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event.data);
|
||||
return resolve({
|
||||
...event.data,
|
||||
id: event.data.id || "",
|
||||
hangoutLink: event.data.hangoutLink || "",
|
||||
type: "google_calendar",
|
||||
password: "",
|
||||
url: "",
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
|
|
10
package.json
10
package.json
|
@ -11,7 +11,9 @@
|
|||
"db-migrate": "yarn prisma migrate dev",
|
||||
"db-seed": "yarn ts-node scripts/seed.ts",
|
||||
"db-nuke": "docker-compose down --volumes --remove-orphans",
|
||||
"dx": "cross-env BASE_URL=http://localhost:3000 JWT_SECRET=secret DATABASE_URL=postgresql://postgres:@localhost:5450/calendso run-s db-up db-migrate db-seed dev",
|
||||
"db-setup": "run-s db-up db-migrate db-seed",
|
||||
"db-reset": "run-s db-nuke db-setup",
|
||||
"dx": "env-cmd run-s db-setup dev",
|
||||
"test": "jest",
|
||||
"test-playwright": "jest --config jest.playwright.config.js",
|
||||
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
||||
|
@ -91,8 +93,8 @@
|
|||
"react-use-intercom": "1.4.0",
|
||||
"short-uuid": "^4.2.0",
|
||||
"stripe": "^8.191.0",
|
||||
"tsdav": "^1.1.5",
|
||||
"superjson": "1.8.0",
|
||||
"tsdav": "^1.1.5",
|
||||
"tslog": "^3.2.1",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.8.2"
|
||||
|
@ -117,7 +119,7 @@
|
|||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"autoprefixer": "^10.3.1",
|
||||
"babel-jest": "^27.3.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"env-cmd": "10.1.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
|
@ -149,4 +151,4 @@
|
|||
"prisma format"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import dayjs from "dayjs";
|
||||
import { kont } from "kont";
|
||||
|
||||
import { loginProvider } from "./lib/loginProvider";
|
||||
|
@ -35,14 +34,11 @@ describe("webhooks", () => {
|
|||
// page contains the url
|
||||
await expect(page).toHaveSelector(`text='${webhookReceiver.url}'`);
|
||||
|
||||
// --- go to tomorrow in the pro user's "30min"-event
|
||||
const tomorrow = dayjs().add(1, "day");
|
||||
const tomorrowFormatted = tomorrow.format("YYYY-MM-DDZZ");
|
||||
|
||||
await page.goto(`http://localhost:3000/pro/30min?date=${encodeURIComponent(tomorrowFormatted)}`);
|
||||
|
||||
// click first time available
|
||||
await page.click("[data-testid=time]");
|
||||
// --- Book the first available day next month in the pro user's "30min"-event
|
||||
await page.goto(`http://localhost:3000/pro/30min`);
|
||||
await page.click('[data-testid="incrementMonth"]');
|
||||
await page.click('[data-testid="day"]');
|
||||
await page.click('[data-testid="time"]');
|
||||
|
||||
// --- fill form
|
||||
await page.fill('[name="name"]', "Test Testson");
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"@types/*.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -3152,6 +3152,11 @@ commander@^3.0.2:
|
|||
resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
|
||||
integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==
|
||||
|
||||
commander@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
|
||||
|
||||
commander@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
|
||||
|
@ -3273,13 +3278,6 @@ create-require@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||
|
||||
cross-env@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
|
||||
integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.1"
|
||||
|
||||
cross-fetch@3.1.4:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39"
|
||||
|
@ -3298,7 +3296,7 @@ cross-spawn@^6.0.5:
|
|||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||
|
@ -3702,6 +3700,14 @@ enquirer@^2.3.5, enquirer@^2.3.6:
|
|||
dependencies:
|
||||
ansi-colors "^4.1.1"
|
||||
|
||||
env-cmd@10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/env-cmd/-/env-cmd-10.1.0.tgz#c7f5d3b550c9519f137fdac4dd8fb6866a8c8c4b"
|
||||
integrity sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==
|
||||
dependencies:
|
||||
commander "^4.0.0"
|
||||
cross-spawn "^7.0.0"
|
||||
|
||||
errno@^0.1.1, errno@~0.1.1:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
|
||||
|
|
Loading…
Reference in a new issue