UNPKG

ical-toolkit

Version:

ICal generator/updater/parser with Timezone/DST, Alams, Organizers, Events, etc. support.

379 lines (326 loc) 11.3 kB
/** * ICS Builders * */ var crypto = require('crypto'), path = require('path'); function ICSFileBuilder() { this.calscale = 'GREGORIAN'; this.version = '2.0'; this.calname = null; this.method = 'PUBLISH'; this.prodid = 'node-ical-toolkit'; this.timezone = null; this.tzid = null; this.events = []; this.additionalTags = {}; this.spacers = true; this.NEWLINE_CHAR = '\r\n'; this.throwError = false; this.ignoreTZIDMismatch = true; } ICSFileBuilder.prototype.toString = function () { var lines = []; lines.push('BEGIN:VCALENDAR'); if (this.version) lines.push('VERSION:' + this.version.toString().trim()); if (this.calscale) lines.push('CALSCALE:' + this.calscale.toString().trim()); if (this.calname) lines.push('X-WR-CALNAME:' + this.calname.toString().trim()); if (this.method) lines.push('METHOD:' + this.method.toString().trim()); if (this.prodid) lines.push('PRODID:' + this.prodid.toString().trim()); if (this.timezone) lines.push('X-WR-TIMEZONE:' + this.timezone.toString().trim()); if (this.tzid) { var tzData; try { tzData = require(path.join(__dirname, '../timezones/database', this.tzid.toString().toLowerCase().replace(/\//g, '-'))); if (!tzData || !tzData.TZID) throw new Error('Timezone not found! Please check!'); } catch (c) { if (this.ignoreTZIDMismatch) { tzData = { "VTIMEZONE": { "TZID": this.tzid }, "TZID": this.tzid }; } else { c = new Error('Unable to process TZID provided! ' + c.message); if (this.throwError) throw c; else return c; } } if (this.spacers)lines.push(''); lines.push('BEGIN:VTIMEZONE'); for (var key in tzData.VTIMEZONE) { if (tzData.VTIMEZONE.hasOwnProperty(key)) { if (typeof tzData.VTIMEZONE[key] == 'string' || tzData.VTIMEZONE[key] instanceof String) { lines.push(key + ':' + tzData.VTIMEZONE[key].toString().trim()); } else { var ref = tzData.VTIMEZONE[key]; if (ref instanceof Array) ref = ref[0]; if (this.spacers)lines.push(''); lines.push('BEGIN:' + key); for (var prop in ref) { if (ref.hasOwnProperty(prop)) { if (typeof ref[prop] == 'string' || ref[prop] instanceof String) { lines.push(prop + ':' + ref[prop].toString().trim()); } else { //todo move this to recursive generic method. bad code, i know. //No inner tz tags supported. Invalid data. Ignore. } } } lines.push('END:' + key); if (this.spacers)lines.push(''); } } } lines.push('END:VTIMEZONE'); if (this.spacers)lines.push(''); } for (var idx = 0; idx < this.events.length; idx++) { var _event = this._checkAndBuildEventObject(this.events[idx]); if (_event instanceof Error) { if (this.throwError) throw _event; else return _event; } if (this.spacers)lines.push(''); lines.push('BEGIN:VEVENT'); lines.push('UID:' + _event.uid); lines.push('DTSTAMP:' + _formatDate(_event.stamp)); if (_event.transp) lines.push('TRANSP:' + _event.transp); if (_event.allDay) { lines.push('DTSTART;VALUE=DATE:' + _formatDate(_event.start, true)); lines.push('DTEND;VALUE=DATE:' + _formatDate(_event.end, true)); } else { lines.push('DTSTART:' + _formatDate(_event.start, false, _event.floating)); lines.push('DTEND:' + _formatDate(_event.end, false, _event.floating)); } lines.push('SUMMARY:' + _escape(_event.summary)); lines.push('SEQUENCE:' + _event.sequence); if (_event.location) lines.push('LOCATION:' + _escape(_event.location)); if (_event.description) lines.push('DESCRIPTION:' + _escape(_event.description)); if (_event.url) lines.push('URL;VALUE=URI:' + _event.url); if (_event.status) lines.push('STATUS:' + _event.status.toUpperCase()); if (_event.organizer) lines.push('ORGANIZER;' + (!!_event.organizer.sentBy ? ('SENT-BY="MAILTO:' + _event.organizer.sentBy + '":') : '') + 'CN="' + _event.organizer.name.replace(/"/g, '\\"') + '":mailto:' + _event.organizer.email); for (var ac = 0; ac < _event.attendees.length; ac++) { if (_event.attendees[ac] instanceof Error) { if (this.throwError) throw _event.attendees[ac]; else return _event.attendees[ac]; } else { lines.push('ATTENDEE;ROLE=' + _event.attendees[ac].role + ';PARTSTAT=' + _event.attendees[ac].status + (_event.attendees[ac].rsvp ? ';RSVP=TRUE' : '') + ';CN=' + _event.attendees[ac].name + ':MAILTO:' + _event.attendees[ac].email); } } for (var i = 0; i < _event.alarms.length; i++) { if (this.spacers)lines.push(''); lines.push('BEGIN:VALARM'); lines.push('TRIGGER:-PT' + _event.alarms[i] + 'M'); lines.push('ACTION:DISPLAY'); lines.push('END:VALARM'); if (this.spacers)lines.push(''); } if (_event.repeating) { var rrlue = 'RRULE:FREQ=' + _event.repeating.freq; if (_event.repeating.count) { rrlue += ';COUNT=' + _event.repeating.count; } if (_event.repeating.interval) { rrlue += ';INTERVAL=' + _event.repeating.interval; } if (_event.repeating.until) { rrlue += ';UNTIL=' + _formatDate(_event.repeating.until); } lines.push(rrlue); } for (var additionalProp in _event.additionalTags) { if (_event.additionalTags.hasOwnProperty(additionalProp)) { lines.push(additionalProp + ':' + _event.additionalTags[additionalProp]); } } lines.push('END:VEVENT'); if (this.spacers)lines.push(''); } if (this.additionalTags) { for (additionalProp in this.additionalTags) { if (this.additionalTags.hasOwnProperty(additionalProp)) { lines.push(additionalProp + ':' + this.additionalTags[additionalProp]); } } } lines.push('END:VCALENDAR'); return lines.join(this.NEWLINE_CHAR); }; ICSFileBuilder.prototype._checkAndBuildEventObject = function (rawEvent) { var event = {}, allowedMethods = [ 'PUBLISH', 'REQUEST', 'REPLY', 'ADD', 'CANCEL', 'REFRESH', 'COUNTER', 'DECLINECOUNTER' ], allowedRepeatingFreq = [ 'SECONDLY', 'MINUTELY', 'HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY' ], allowedStatuses = [ 'CONFIRMED', 'TENTATIVE', 'CANCELLED' ]; if (!rawEvent || typeof rawEvent !== 'object') { return new Error('event is not an object.'); } // Date Start if (!rawEvent.start) { return new Error('event.start is a mandatory item.'); } if (!(rawEvent.start instanceof Date)) { return new Error('event.start must be a Date Object.'); } event.start = rawEvent.start; // Date Stop if (!rawEvent.end) { return new Error('event.end is a mandatory item.'); } if (!(rawEvent.end instanceof Date)) { return new Error('event.end must be a Date Object.'); } event.end = rawEvent.end; //Alarms event.alarms = rawEvent.alarms || []; //Additional tags event.additionalTags = rawEvent.additionalTags || {}; // UID event.uid = rawEvent.uid || crypto.randomBytes(4).toString('hex').toString(36); //Update sequence event.sequence = rawEvent.sequence || 0; // Repeating Event if (rawEvent.repeating) { event.repeating = {}; if (!rawEvent.repeating.freq || allowedRepeatingFreq.indexOf(rawEvent.repeating.freq.toUpperCase()) === -1) { return new Error('event.repeating.freq is a mandatory item, and must be one of the following: ' + allowedRepeatingFreq.join(', ')); } event.repeating.freq = rawEvent.repeating.freq; if (rawEvent.repeating.count) { if (!isFinite(rawEvent.repeating.count)) { return new Error('event.repeating.count must be a Number.'); } event.repeating.count = rawEvent.repeating.count; } if (rawEvent.repeating.interval) { if (!isFinite(rawEvent.repeating.interval)) { return new Error('event.repeating.interval must be a Number.'); } event.repeating.interval = rawEvent.repeating.interval; } if (rawEvent.repeating.until) { if (!(rawEvent.repeating.until instanceof Date)) { return new Error('event.repeating.until must be a Date Object.'); } event.repeating.until = rawEvent.repeating.until; } } // allDay flag event.allDay = !!rawEvent.allDay; // Date Stamp if (rawEvent.stamp && !(rawEvent.stamp instanceof Date)) { return new Error('event.stamp must be a Date Object.'); } event.stamp = rawEvent.stamp || new Date(); // Floating times event.floating = rawEvent.floating || false; // Summary if (!rawEvent.summary) { return new Error('event.summary is a mandatory item.'); } event.summary = rawEvent.summary; // Location event.location = rawEvent.location || null; // Description event.description = rawEvent.description || null; //attendees event.attendees = []; (rawEvent.attendees || []).forEach(function (att) { if (att.name && att.email) { event.attendees.push({ name: att.name, email: att.email, role: (att.role || 'REQ-PARTICIPANT').toUpperCase(), status: (att.status || 'NEEDS-ACTION').toUpperCase(), rsvp: !!att.rsvp }); } else { event.attendees.push(new Error('Invalid attendee data, name and email is required: ' + JSON.stringify({input: att}))); } }); // Organizer event.organizer = null; if (rawEvent.organizer) { if (!rawEvent.organizer.name) { return new Error('event.organizer.name is empty.'); } if (!rawEvent.organizer.email) { return new Error('event.organizer.email is empty.'); } event.organizer = { name: rawEvent.organizer.name, email: rawEvent.organizer.email }; if (rawEvent.organizer.sentBy) event.organizer.sentBy = rawEvent.organizer.sentBy; } // Method if (rawEvent.method && allowedMethods.indexOf(rawEvent.method.toUpperCase()) === -1) { return new Error('event.method must be one of the following: ' + allowedMethods.join(', ')); } event.method = rawEvent.method; // Status if (rawEvent.status && allowedStatuses.indexOf(rawEvent.status.toUpperCase()) === -1) { return new Error('event.status must be one of the following: ' + allowedStatuses.join(', ')); } event.status = rawEvent.status; // URL event.url = rawEvent.url || null; //Transp prop event.transp = rawEvent.transp; return event; }; /** * Export builder * */ exports.createIcsFileBuilder = function () { return new ICSFileBuilder(); }; //Utils function _formatDate(d, dateonly, floating) { var s; function pad(i) { return (i < 10 ? '0' : '') + i; } s = d.getUTCFullYear(); s += pad(d.getUTCMonth() + 1); s += pad(d.getUTCDate()); if (!dateonly) { s += 'T'; s += pad(d.getUTCHours()); s += pad(d.getUTCMinutes()); s += pad(d.getUTCSeconds()); if (!floating) { s += 'Z'; } } return s; } function _escape(str) { return str.replace(/[\\;,\n]/g, function (match) { if (match === '\n') { return '\\n'; } return '\\' + match; }); }