From b00a37eb17dfba1b6101441ba7aa2785e44506d8 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Sat, 24 Dec 2022 17:44:28 +0100 Subject: [PATCH] Add iCal converter --- app.ts | 2 +- src/models/calendar/events/event.interface.ts | 10 ++ src/models/calendar/events/events.router.ts | 32 ++++- .../calendar/events/icalgenerator.service.ts | 132 ++++++++++++++++++ 4 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 src/models/calendar/events/icalgenerator.service.ts diff --git a/app.ts b/app.ts index 27a9b66..e6d9b05 100644 --- a/app.ts +++ b/app.ts @@ -29,7 +29,7 @@ app.use(express.json()); // Configure CORS let allowedHosts = [ 'https://www.nachklang.art', - 'https://calendar.nachklang.art', + 'https://calendar.nachklang.art' ]; app.use(cors({ origin: function (origin: any, callback: any) { diff --git a/src/models/calendar/events/event.interface.ts b/src/models/calendar/events/event.interface.ts index 90217fe..500720a 100644 --- a/src/models/calendar/events/event.interface.ts +++ b/src/models/calendar/events/event.interface.ts @@ -1,3 +1,13 @@ export interface Event { event_id: number; + calendar_id: number; + uuid: string; + name: string; + description: string; + start_datetime: Date; + end_datetime: Date; + created_date: Date; + location: string; + created_by: string; + url: string; } diff --git a/src/models/calendar/events/events.router.ts b/src/models/calendar/events/events.router.ts index 17c54f1..d383021 100644 --- a/src/models/calendar/events/events.router.ts +++ b/src/models/calendar/events/events.router.ts @@ -4,7 +4,7 @@ import express, {Request, Response} from 'express'; import * as EventService from './events.service'; -import {Event} from './event.interface'; +import * as iCalService from './icalgenerator.service'; /** @@ -13,13 +13,18 @@ import {Event} from './event.interface'; export const eventsRouter = express.Router(); +/** + * Constants + */ +const fileNames = ['Nachklang_calendar', 'Nachklang_internal_calendar', 'Nachklang_management_calendar']; + /** * Controller Definitions */ // POST users/register -eventsRouter.get('/all', async (req: Request, res: Response) => { +eventsRouter.get('/json', async (req: Request, res: Response) => { try { // Get request params let calendarId: number = parseInt(req.query.calendar as string ?? '', 10); @@ -27,10 +32,31 @@ eventsRouter.get('/all', async (req: Request, res: Response) => { // Get events let events = await EventService.getAllEvents(calendarId); - // Send the session details back to the user + // Send the events back res.status(200).send(events); } catch (e: any) { console.log('Error handling a request: ' + e.message); res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); + +eventsRouter.get('/ical', async (req: Request, res: Response) => { + try { + // Get request params + let calendarId: number = parseInt(req.query.calendar as string ?? '', 10); + + // Get events + let events = await EventService.getAllEvents(calendarId); + + // generate file name + let fileName = fileNames[calendarId]; + let file = await iCalService.convertToIcal(events); + + // Send the ical file back + res.set({'Content-Disposition': 'attachment; filename=' + fileName + '.ics'}); + res.status(200).send(file); + } catch (e: any) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); diff --git a/src/models/calendar/events/icalgenerator.service.ts b/src/models/calendar/events/icalgenerator.service.ts new file mode 100644 index 0000000..c2de8c8 --- /dev/null +++ b/src/models/calendar/events/icalgenerator.service.ts @@ -0,0 +1,132 @@ +import {Event} from './event.interface'; + +export const convertToIcal = async (events: Event[]): Promise => { + try { + let ical: iCalFile = {body: []}; + generateHeaderInfo(ical); + generateFooterInfo(ical); + for (let event of events) { + addEventToFile(ical, event); + } + + return serializeIcalFile(ical); + } catch (err) { + throw err; + } +}; + +const serializeIcalFile = (ical: iCalFile): string => { + let returnString = ''; + + returnString += ical.header; + for (let event of ical.body) { + returnString += serializeIcalEvent(event); + } + returnString += ical.footer; + + return returnString; +}; + +const serializeIcalEvent = (icalevent: iCalEvent): string => { + let returnString = ''; + + returnString += icalevent.header; + returnString += 'UID:' + icalevent.uid; + returnString += 'DTSTAMP:' + icalevent.created; + returnString += 'ORGANIZER:' + icalevent.organizer; + returnString += 'DTSTART;TZID=Europe/Berlin:' + icalevent.start; + returnString += 'DTEND;TZID=Europe/Berlin:' + icalevent.end; + returnString += 'SUMMARY:' + icalevent.summary; + returnString += 'DESCRIPTION:' + icalevent.description; + returnString += 'LOCATION:' + icalevent.location; + returnString += 'URL:' + icalevent.url; + returnString += icalevent.footer; + + return returnString; +}; + + +const generateHeaderInfo = (ical: iCalFile) => { + ical.header = 'BEGIN:VCALENDAR\n' + + 'VERSION:2.0\n' + + 'PRODID:-//hacksw/handcal//NONSGML v1.0//EN\n' + + 'CALSCALE:GREGORIAN\n' + + 'BEGIN:VTIMEZONE\n' + + 'TZID:Europe/Berlin\n' + + 'LAST-MODIFIED:20201011T015911Z\n' + + 'TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin\n' + + 'X-LIC-LOCATION:Europe/Berlin\n' + + 'BEGIN:DAYLIGHT\n' + + 'TZNAME:CEST\n' + + 'TZOFFSETFROM:+0100\n' + + 'TZOFFSETTO:+0200\n' + + 'DTSTART:19700329T020000\n' + + 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\n' + + 'END:DAYLIGHT\n' + + 'BEGIN:STANDARD\n' + + 'TZNAME:CET\n' + + 'TZOFFSETFROM:+0200\n' + + 'TZOFFSETTO:+0100\n' + + 'DTSTART:19701025T030000\n' + + 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\n' + + 'END:STANDARD\n' + + 'END:VTIMEZONE\n'; +}; + +const generateFooterInfo = (ical: iCalFile) => { + ical.footer = 'END:VCALENDAR'; +}; + +const addEventToFile = (ical: iCalFile, event: Event) => { + ical.body.push(createIcalEvent(event)); +}; + +const createIcalEvent = (event: Event): iCalEvent => { + return { + header: 'BEGIN:VEVENT\n', + uid: event.uuid + '\n', + created: formatDate(event.created_date) + 'Z\n', + organizer: event.created_by + '\n', + start: formatDate(event.start_datetime) + '\n', + end: formatDate(event.end_datetime) + '\n', + summary: event.name + '\n', + description: event.description + '\n', + location: event.location + '\n', + url: event.url + '\n', + footer: 'END:VEVENT\n' + }; +}; + +const formatDate = (date: Date): string => { + let returnString = ''; + + returnString += date.getFullYear(); + returnString += (date.getMonth() + 1).toString().padStart(2, '0'); // +1 Because JS sucks + returnString += date.getDate().toString().padStart(2, '0'); + returnString += 'T'; + returnString += date.getHours().toString().padStart(2, '0'); + returnString += date.getMinutes().toString().padStart(2, '0'); + returnString += date.getSeconds().toString().padStart(2, '0'); + + return returnString; +}; + +export interface iCalFile { + header?: string; + body: iCalEvent[]; + footer?: string; +} + +export interface iCalEvent { + header: string; + uid: string; + created: string; + organizer: string; + start: string; + end: string; + summary: string; + description: string; + location: string; + url: string; + footer: string; +}