fix handling for recurring events (#2455)
This commit is contained in:
		
							parent
							
								
									21d183e661
								
							
						
					
					
						commit
						94f64f9730
					
				
					 2 changed files with 70 additions and 19 deletions
				
			
		| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
/// <reference path="../types/ical.d.ts"/>
 | 
					/// <reference path="../types/ical.d.ts"/>
 | 
				
			||||||
import { Credential, Prisma } from "@prisma/client";
 | 
					import { Credential, Prisma } from "@prisma/client";
 | 
				
			||||||
import dayjs from "dayjs";
 | 
					import dayjs from "dayjs";
 | 
				
			||||||
 | 
					import isBetween from "dayjs/plugin/isBetween";
 | 
				
			||||||
import timezone from "dayjs/plugin/timezone";
 | 
					import timezone from "dayjs/plugin/timezone";
 | 
				
			||||||
import utc from "dayjs/plugin/utc";
 | 
					import utc from "dayjs/plugin/utc";
 | 
				
			||||||
import ICAL from "ical.js";
 | 
					import ICAL from "ical.js";
 | 
				
			||||||
| 
						 | 
					@ -36,6 +37,7 @@ const DEFAULT_CALENDAR_TYPE = "caldav";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dayjs.extend(utc);
 | 
					dayjs.extend(utc);
 | 
				
			||||||
dayjs.extend(timezone);
 | 
					dayjs.extend(timezone);
 | 
				
			||||||
 | 
					dayjs.extend(isBetween);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
 | 
					const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -241,24 +243,63 @@ export default abstract class BaseCalendarService implements Calendar {
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    ).flat();
 | 
					    ).flat();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const events = objects
 | 
					    const events: { start: string; end: string }[] = [];
 | 
				
			||||||
      .filter((e) => !!e.data)
 | 
					
 | 
				
			||||||
      .map((object) => {
 | 
					    objects.forEach((object) => {
 | 
				
			||||||
 | 
					      if (object.data == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const jcalData = ICAL.parse(object.data);
 | 
					      const jcalData = ICAL.parse(object.data);
 | 
				
			||||||
      const vcalendar = new ICAL.Component(jcalData);
 | 
					      const vcalendar = new ICAL.Component(jcalData);
 | 
				
			||||||
      const vevent = vcalendar.getFirstSubcomponent("vevent");
 | 
					      const vevent = vcalendar.getFirstSubcomponent("vevent");
 | 
				
			||||||
      const event = new ICAL.Event(vevent);
 | 
					      const event = new ICAL.Event(vevent);
 | 
				
			||||||
      const vtimezone = vcalendar.getFirstSubcomponent("vtimezone");
 | 
					      const vtimezone = vcalendar.getFirstSubcomponent("vtimezone");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (event.isRecurring()) {
 | 
				
			||||||
 | 
					        let maxIterations = 365;
 | 
				
			||||||
 | 
					        if (["HOURLY", "SECONDLY", "MINUTELY"].includes(event.getRecurrenceTypes())) {
 | 
				
			||||||
 | 
					          console.error(`Won't handle [${event.getRecurrenceTypes()}] recurrence`);
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const start = dayjs(dateFrom);
 | 
				
			||||||
 | 
					        const end = dayjs(dateTo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const iterator = event.iterator();
 | 
				
			||||||
 | 
					        let current;
 | 
				
			||||||
 | 
					        let currentEvent;
 | 
				
			||||||
 | 
					        let currentStart;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        do {
 | 
				
			||||||
 | 
					          maxIterations -= 1;
 | 
				
			||||||
 | 
					          current = iterator.next();
 | 
				
			||||||
 | 
					          currentEvent = event.getOccurrenceDetails(current);
 | 
				
			||||||
 | 
					          // as pointed out in https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.5
 | 
				
			||||||
 | 
					          // recurring events are always in utc
 | 
				
			||||||
 | 
					          currentStart = dayjs(currentEvent.startDate.toJSDate());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (currentStart.isBetween(start, end) === true) {
 | 
				
			||||||
 | 
					            return events.push({
 | 
				
			||||||
 | 
					              start: currentStart.toISOString(),
 | 
				
			||||||
 | 
					              end: dayjs(currentEvent.endDate.toJSDate()).toISOString(),
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } while (maxIterations > 0 && currentStart.isAfter(end) === false);
 | 
				
			||||||
 | 
					        if (maxIterations <= 0) {
 | 
				
			||||||
 | 
					          console.warn("could not find any occurrence for recurring event in 365 iterations");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (vtimezone) {
 | 
					      if (vtimezone) {
 | 
				
			||||||
        const zone = new ICAL.Timezone(vtimezone);
 | 
					        const zone = new ICAL.Timezone(vtimezone);
 | 
				
			||||||
        event.startDate = event.startDate.convertToZone(zone);
 | 
					        event.startDate = event.startDate.convertToZone(zone);
 | 
				
			||||||
        event.endDate = event.endDate.convertToZone(zone);
 | 
					        event.endDate = event.endDate.convertToZone(zone);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return {
 | 
					      return events.push({
 | 
				
			||||||
        start: dayjs(event.startDate.toJSDate()).toISOString(),
 | 
					        start: dayjs(event.startDate.toJSDate()).toISOString(),
 | 
				
			||||||
        end: dayjs(event.endDate.toJSDate()).toISOString(),
 | 
					        end: dayjs(event.endDate.toJSDate()).toISOString(),
 | 
				
			||||||
        };
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Promise.resolve(events);
 | 
					    return Promise.resolve(events);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								packages/types/ical.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								packages/types/ical.d.ts
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -80,6 +80,16 @@ declare module "ical.js" {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public isRecurring(): boolean;
 | 
					    public isRecurring(): boolean;
 | 
				
			||||||
    public iterator(startTime?: Time): RecurExpansion;
 | 
					    public iterator(startTime?: Time): RecurExpansion;
 | 
				
			||||||
 | 
					    public getOccurrenceDetails(occurrence: Time): OccurrenceDetails;
 | 
				
			||||||
 | 
					    public getRecurrenceTypes(): FrequencyValues;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // https://mozilla-comm.github.io/ical.js/api/ICAL.Event.html#.occurrenceDetails
 | 
				
			||||||
 | 
					  interface OccurrenceDetails {
 | 
				
			||||||
 | 
					    recurrenceId: Time;
 | 
				
			||||||
 | 
					    item: Event;
 | 
				
			||||||
 | 
					    startDate: Time;
 | 
				
			||||||
 | 
					    endDate: Time;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export class Property {
 | 
					  export class Property {
 | 
				
			||||||
| 
						 | 
					@ -108,9 +118,9 @@ declare module "ical.js" {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export class Time {
 | 
					  export class Time {
 | 
				
			||||||
    public fromString(str: string): Time;
 | 
					    public static fromString(str: string): Time;
 | 
				
			||||||
    public fromJSDate(aDate: Date | null, useUTC: boolean): Time;
 | 
					    public static fromJSDate(aDate: Date | null, useUTC: boolean): Time;
 | 
				
			||||||
    public fromData(aData: TimeJsonData): Time;
 | 
					    public static fromData(aData: TimeJsonData): Time;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public now(): Time;
 | 
					    public now(): Time;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue