const request = require('request'); const log4js = require('log4js'); const logger = log4js.getLogger('calHandler'); const STRING = require('string'); const util = require('util'); const Elapsed = require('elapsed'); const Sugar = require('sugar'); const { inRange } = require('lodash'); const moment = require('moment'); Sugar.extend(); logger.level = 'error'; moment.updateLocale('en', { 'calendar' : { 'lastDay' : '[Yesterday\n] HH:mm', 'sameDay' : '[Today\n] HH:mm', 'nextDay' : '[Tomorrow\n] HH:mm', 'lastWeek' : '[last] dddd [at] HH:mm', 'nextWeek' : 'dddd[\n] HH:mm', 'sameElse' : 'YYYY-MM-DD' } }); function processICAL(ical) { 'use strict'; logger.info('+ processICAL'); let workingBlock = []; const segments = { 'meetingStartID': 'DTSTART;TZID=Europe/London:', 'meetingStartAlt': 'DTSTART:', 'meetingStartAltOther': 'DTSTART;VALUE=DATE:', 'meetingEndID': 'DTEND;TZID=Europe/London:', 'meetingEndAlt': 'DTEND:', 'meetingEndAltOther': 'DTEND;VALUE=DATE:', 'meetingDescID': 'DESCRIPTION:', 'summaryID': 'SUMMARY:', 'begin': 'BEGIN:VEVENT', 'end': 'END:VEVENT', 'beginAlarm': 'BEGIN:VALARM', 'endAlarm': 'END:VALARM', 'recur': 'RRULE:' }; const rules = ['FREQ', 'WKST', 'UNTIL', 'BYMONTH', 'BYMONTHDAY', 'INTERVAL', 'BYDAY']; function nThDayOfMonth(monthsAhead, wantedDay) { const now = new Date(); for(let t = 0; t < monthsAhead; t++) { } } function processRecurrence(workBlock) { const _workBlock = Object.assign({}, workBlock); const dateLimit = new Date().reset('month').addMonths(2); const recurArray = []; logger.info('---===---'); logger.debug('>> Processing recurrence...'); logger.debug(`Expanding ${_workBlock.summary}`); logger.debug('DateLimit:', dateLimit); // logger.debug('Processing recurrence...'); const weekBits = { 'SU': 0, 'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6 }; const now = new Date(); const day = now.getDate(); const dayNum = now.getDay(); const month = now.getMonth(); const year = now.getFullYear(); const byDayRegEx = /(\d)(\w.)/; const recurSettings = { 'freq': null, 'wkst': null, 'until': null, 'bymonth': null, 'bymonthday': null, 'interval': null, 'byday': null }; const firstSplit = _workBlock.recur.split(';'); for (let t = 0; t < firstSplit.length; t++) { const ws = firstSplit[t].split('='); if (rules.indexOf(ws[0]) > -1) recurSettings[ws[0].toLowerCase()] = ws[1]; } // if all null discard.. if (recurSettings.freq === null && recurSettings.wkst === null && recurSettings.until === null && recurSettings.byday === null && recurSettings.bymonth === null && recurSettings.bymonthday === null && recurSettings.interval === null) return null; if (recurSettings.until !== null) { // have we expired? const _until = Date.create(recurSettings.until).isPast(); if (_until) { logger.warn('EXPIRED!!'); return null; } } if (recurSettings.freq !== null) { // logger.debug(_workBlock); let newStart, newEnd; const origStart = new Date(_workBlock.dtstart); const origEnd = new Date(_workBlock.dtend); const _dstart = new Date(_workBlock.dtstart); const _dend = new Date(_workBlock.dtend); logger.debug('>> origStart', origStart); const _d = origStart.getDate(); const _m = origStart.getMonth(); const _h = origStart.getHours(); const _min = origStart.getMinutes(); const _secs = origStart.getSeconds(); const distance = origEnd - origStart; _workBlock.details = { _d, _m, _h, _min, _secs, distance }; logger.debug('freq:', recurSettings.freq); if (recurSettings.freq === 'YEARLY') { logger.warn('YEARLY'); if (recurSettings.bymonth !== null && recurSettings.bymonthday !== null) { let _yearCount = year; logger.debug('>> Yearly with specific month / day'); let newBlock; newStart = new Date().set({ 'year':_yearCount, 'month': recurSettings.bymonth - 1, 'day': recurSettings.bymonthday, 'hour':_h, 'minutes':_min, 'seconds':_secs }); newEnd = new Date(_dstart).addMilliseconds(distance); do { newBlock = Object.assign({}, _workBlock); newBlock.dstart = new Date(newStart); newBlock.dend = new Date(newEnd); recurArray.push(newBlock); _yearCount++; newStart = new Date().set({ 'year':_yearCount, 'month': recurSettings.bymonth - 1, 'day': recurSettings.bymonthday, 'hour':_h, 'minutes':_min, 'seconds':_secs }); newEnd = new Date(_dstart).addMilliseconds(distance); } while(newStart < dateLimit); } else if (recurSettings.bymonth === null && recurSettings.bymonthday === null) { logger.debug('>> Yearly no specific month / day'); // extract month and year from dtstart let newBlock; do { newBlock = Object.assign({}, _workBlock); newBlock.dtstart = new Date(_dstart); newBlock.dtend = new Date(_dend); // logger.info(newBlock.dtstart.medium()); recurArray.push(newBlock); _dstart.advance({ 'year':1 }); _dend.advance({ 'year':1 }); } while(_dstart < dateLimit); } // logger.info('** recurArray', recurArray); return recurArray; } if (recurSettings.freq === 'MONTHLY' ) { const interval = parseInt(recurSettings.interval, 10) || 1; logger.warn(`MONTHLY - ${interval}`); let newBlock; if (recurSettings.byday === null) do { newBlock = Object.assign({}, _workBlock); newBlock.dtstart = new Date(_dstart); newBlock.dtend = new Date(_dend); // logger.info(newBlock.dtstart.medium()); recurArray.push(newBlock); _dstart.addMonths(interval); _dend.addMonths(interval); } while(_dstart < dateLimit); else { logger.warn('BYDAY!!! ', recurSettings.byday); _dstart.setUTC(true); const byday = byDayRegEx.exec(recurSettings.byday); const dayNumber = weekBits[byday[2]]; const stepCount = parseInt(byday[1], 10) - 1; do { const _findSun = new Date.create(_dstart, { 'fromUTC': true } ).reset('month'); const firstDay = _findSun.getDay(); // find first occurance of the wanted day. if (firstDay !== dayNumber) { const add = (dayNumber - firstDay + 7) ; _findSun.addDays(add).addWeeks(stepCount); } else _findSun.addWeeks(stepCount); newBlock = Object.assign({}, _workBlock); newBlock.dtstart = new Date(_findSun).addMilliseconds(_workBlock.timeStartMS); newBlock.dtend = new Date(_findSun).addMilliseconds(_workBlock.timeEndMS); // logger.info(newBlock.dtstart.medium()); recurArray.push(newBlock); // all done, next. _dstart.reset('month').addDays(32); _dend.reset('month').addDays(32); } while(_dstart < dateLimit); // something } return recurArray; } if (recurSettings.freq === 'WEEKLY') { const interval = parseInt(recurSettings.interval, 10) || 1; logger.warn(`WEEKLY - ${interval}`); let newBlock; do { newBlock = Object.assign({}, _workBlock); newBlock.dtstart = new Date(_dstart); newBlock.dtend = new Date(_dend); // logger.info(newBlock.dtstart.medium()); recurArray.push(newBlock); _dstart.addWeeks(interval); _dend.addWeeks(interval); } while(_dstart < dateLimit); } return recurArray; } // if we get here we've skipped everything just return the _workblock return []; } function processBlock(block) { logger.debug('>> processBlock'); let _wb; const workBlock = { 'summary': '', 'dtstart': null, 'dtend': null, 'description': '', 'timeStart': null, 'timeEnd': null, 'duration': 0, 'combined': '', 'recur': null, 'timeStartMS': null, 'timeEndMS': null, 'ts' : null, 'allday' : false }; let alarmFlag = false, ws, blockStep; for (let step = 0; step < block.length; step++) { blockStep = block[step]; if (blockStep.indexOf(segments.recur) >= 0) workBlock.recur = STRING(block[step].split(segments.recur)[1]).collapseWhitespace().s; // logger.debug(workBlock.recur); if (blockStep.indexOf(segments.summaryID) >= 0) workBlock.summary = STRING(block[step].split(segments.summaryID)[1]).collapseWhitespace().s; if (blockStep.indexOf(segments.meetingStartID) >= 0) { ws = STRING(block[step].split(segments.meetingStartID)[1]).collapseWhitespace().s; // workBlock.dtstart = Date.create(ws); workBlock.dtstart = new Sugar.Date(ws).raw; } if (blockStep.indexOf(segments.meetingEndID) >= 0) { ws = STRING(block[step].split(segments.meetingEndID)[1]).collapseWhitespace().s; // workBlock.dtend = Date.create(ws); workBlock.dtend = new Sugar.Date(ws).raw; } if (blockStep.indexOf(segments.meetingStartAlt) >= 0) { ws = STRING(block[step].split(segments.meetingStartAlt)[1]).collapseWhitespace().s; // console.log('>> ws', ws); // workBlock.dtstart = Date.create(ws); // let d = new Sugar.Date(); workBlock.dtstart = new Sugar.Date(ws).raw; // console.log('>> date', workBlock.dtstart); } if (blockStep.indexOf(segments.meetingEndAlt) >= 0) { ws = STRING(block[step].split(segments.meetingEndAlt)[1]).collapseWhitespace().s; // workBlock.dtend = Date.create(ws); workBlock.dtend = new Sugar.Date(ws).raw; // console.log('>> date', workBlock.dtend); } if (blockStep.indexOf(segments.meetingStartAltOther) >= 0) { ws = STRING(block[step].split(segments.meetingStartAltOther)[1]).collapseWhitespace().s; // workBlock.dtstart = Date.create(ws); workBlock.dtstart = new Sugar.Date(ws).raw; } if (blockStep.indexOf(segments.meetingEndAltOther) >= 0) { ws = STRING(block[step].split(segments.meetingEndAltOther)[1]).collapseWhitespace().s; // console.log('>> ws', ws); // workBlock.dtend = Date.create(ws); workBlock.dtend = new Sugar.Date(ws).raw; } if (blockStep.indexOf(segments.meetingDescID) >= 0) if (!alarmFlag) { workBlock.description = STRING(block[step].split(segments.meetingDescID)[1]).collapseWhitespace().s; } if (blockStep.indexOf(segments.beginAlarm) >= 0) alarmFlag = true; } workBlock.summary = workBlock.summary.replace(/\\/g, ''); workBlock.description = workBlock.description.replace(/\\/g, ''); if (workBlock.dtstart !== null) { // workBlock.timeStart = workBlock.dtstart.format('{24hr}:{mm}:{ss}'); workBlock.timeStart = Sugar.Date(workBlock.dtstart).format('{24hr}:{mm}:{ss}').raw; workBlock.timeStartMS = ((workBlock.dtstart.getHours() * 60 * 60 ) + (workBlock.dtstart.getMinutes() * 60 ) + workBlock.dtstart.getSeconds()) * 1000; // console.log('>> workBlock.timeStart', workBlock.timeStart); workBlock.combined = `${workBlock.timeStart} - '`; workBlock.long = `${Sugar.Date(workBlock.dtstart).format('{Weekday}').raw}, ${workBlock.timeStart} - `; // console.log('>> workBlock.long', workBlock.long); } workBlock.combined = workBlock.combined + workBlock.summary; workBlock.longcombined = workBlock.long + workBlock.summary; if (workBlock.dtend !== null) { workBlock.timeEnd = Sugar.Date(workBlock.dtend).format('{24hr}:{mm}:{ss}').raw; workBlock.timeEndMS = ((workBlock.dtend.getHours() * 60 * 60 ) + (workBlock.dtend.getMinutes() * 60 ) + workBlock.dtend.getSeconds()) * 1000; } if (workBlock.dtstart !== null && workBlock.dtend !== null) { const elapsedTime = new Elapsed(workBlock.dtstart, workBlock.dtend); workBlock.duration = elapsedTime.optimal; workBlock.combined = `${workBlock.combined }, ${ elapsedTime.optimal}`; workBlock.longcombined = `${workBlock.longcombined }, ${ elapsedTime.optimal}`; } workBlock.allday = (workBlock.timeStart === '0:00:00' && workBlock.timeEnd === '0:00:00'); return workBlock; } function buildRecurranceArray(wb) { const _wb = processRecurrence(wb); return _wb; } const lines = ical.split('\r\n'), l = lines.length; let counter = 0; let alarmed = false; while (counter < l) if (lines[counter].indexOf(segments.begin) < 0) counter++; else { let subcounter = 0; const subBlock = []; alarmed = false; while (subcounter < 75) if (lines[counter + subcounter].indexOf(segments.end) < 0) { if (lines[counter + subcounter].indexOf(segments.beginAlarm) > -1) alarmed = true; if (!alarmed) subBlock.push(lines[counter + subcounter]); if (lines[counter + subcounter].indexOf(segments.endAlarm) > -1) alarmed = false; subcounter++; } else break; counter = counter + subcounter; const b = processBlock(subBlock); if (!b.recur ) { if (Array.isArray(b)) logger.error('!returned an array...'); else if (b.dtstart !== null) { b.ts = b.dtstart.format('{X}'); workingBlock.push(b); } } else { // logger.warn('We need to spread the recurrance!!'); // logger.debug(b); let recurBlocks = buildRecurranceArray(b); if (recurBlocks) recurBlocks = recurBlocks.map((item) => { if (item.dtstart) item.ts = item.dtstart.format('{X}'); return item; }); if (recurBlocks && recurBlocks.length > 0) workingBlock = workingBlock.concat(recurBlocks); } } logger.info('- processICAL'); // If (workingBlock.dtstart == null) return {}; return workingBlock; } /* ['https://calendar.google.com/calendar/ical/martind2000%40gmail.com/private-40cfebc9f7dcfa7fde6b9bf2f0092c93/basic.ics', 'https://calendar.google.com/calendar/ical/mt5pgdhknvgoc8usfnrso9vkv0%40group.calendar.google.com/private-58876002af9f302a593acfa6fa792dcf/basic.ics', 'https://www.tripit.com/feed/ical/private/DB96E4BB-94A9BD8F9CC1CF51C6CC0D920840F4F5/tripit.ics', 'https://calendar.google.com/calendar/ical/en.uk%23holiday%40group.v.calendar.google.com/public/basic.ics', 'https://calendar.google.com/calendar/ical/i8dglj12p5nuv20sbjmun5s588%40group.calendar.google.com/private-c8adccb41e56d6a2f285078aaed313f5/basic.ics', 'https://calendar.google.com/calendar/ical/qppj4ebvdur1qui4v0fdpl7l70%40group.calendar.google.com/private-b5071cb2c3fe49544ffbbd08645088f1/basic.ics', 'https://calendar.google.com/calendar/ical/8h3vi3rd5rvpfe11klvgre0q4c%40group.calendar.google.com/private-e9df93163a7046658946be45fb08db6f/basic.ics', 'https://calendar.google.com/calendar/embed?src=family18216414236505453132%40group.calendar.google.com&ctz=Europe%2FLondon'] */ module.exports = { 'calendars': ['https://calendar.google.com/calendar/ical/martind2000%40gmail.com/private-40cfebc9f7dcfa7fde6b9bf2f0092c93/basic.ics', 'https://calendar.google.com/calendar/ical/mt5pgdhknvgoc8usfnrso9vkv0%40group.calendar.google.com/private-58876002af9f302a593acfa6fa792dcf/basic.ics', 'https://www.tripit.com/feed/ical/private/DB96E4BB-94A9BD8F9CC1CF51C6CC0D920840F4F5/tripit.ics', 'https://calendar.google.com/calendar/ical/en.uk%23holiday%40group.v.calendar.google.com/public/basic.ics', 'https://calendar.google.com/calendar/ical/i8dglj12p5nuv20sbjmun5s588%40group.calendar.google.com/private-c8adccb41e56d6a2f285078aaed313f5/basic.ics', 'https://calendar.google.com/calendar/ical/qppj4ebvdur1qui4v0fdpl7l70%40group.calendar.google.com/private-b5071cb2c3fe49544ffbbd08645088f1/basic.ics', 'https://calendar.google.com/calendar/ical/8h3vi3rd5rvpfe11klvgre0q4c%40group.calendar.google.com/private-e9df93163a7046658946be45fb08db6f/basic.ics', 'https://calendar.google.com/calendar/embed?src=family18216414236505453132%40group.calendar.google.com&ctz=Europe%2FLondon'], 'jsonBlock': [], 'getTodaysSimple': function() { 'use strict'; logger.info('+ getTodaysSimple'); const today = { 'entries': [] }; const _td = new Date.create('today'); const _tm = new Date.create('tomorrow') - 1 ; today.entries = this.jsonBlock.filter((item) => { if (!item || !item.dtstart || !item.dtend) return false; return item.dtstart.isBetween(_td, _tm) || item.dtend.isBetween(_td, _tm); }); logger.info('- getTodaysSimple'); return today; }, 'getTomorrow': function() { 'use strict'; logger.info('+ getTomorrow'); const today = { 'entries': [] }; const _tm = new Date.create('tomorrow'); const _da = new Date.create('tomorrow').addDays(1); today.entries = this.jsonBlock.filter((item) => { if (!item || !item.dtstart || !item.dtend) return false; return item.dtstart.isBetween(_tm, _da) || item.dtend.isBetween(_tm, _da); }); logger.info('- getTomorrow'); return today; }, 'getThreeDays': function() { logger.info('+ getThreeDays'); const three = { 'entries': [] }; const _tdms = new Date.create('today').getTime(); const _tmms = new Date.create('tomorrow').addDays(2).getTime() - 1 ; three.entries = this.jsonBlock.filter((item) => { if (!item || !item.dtstart || !item.dtend) return false; return inRange(item.dtstart.getTime(), _tdms, _tmms) || inRange(item.dtend.getTime(), _tdms, _tmms); }); three.entries = three.entries.map( obj => { obj.readDate = moment(obj.dtstart.getTime()).calendar(); if (obj.allday) { let fixReadDate = obj.readDate.split('\n'); if (fixReadDate.length === 2) { obj.readDate = `${fixReadDate[0]}\nAll Day`; } } return obj; }); logger.info('- getThreeDays'); return three; }, 'getWeek': function() { 'use strict'; logger.info('+ getWeek'); const today = { 'entries': [] }; const now = new Date.create('today'); logger.debug('>> now', now); const twoDays = new Date.create('today').addDays(2).beginningOfDay(); logger.debug('>> twoDays', twoDays); const sevenDays = new Date.create('today').addDays(7).beginningOfDay(); logger.debug('>> sevenDays', sevenDays); logger.debug('>> trip', { now, twoDays, sevenDays }); /* for (let t = 0; t < this.jsonBlock.length; t++) // logger.debug('>> between', Sugar.Date(this.jsonBlock[t].dtstart).raw, Sugar.Date(this.jsonBlock[t].dtstart).isBetween(twoDays, sevenDays)); if (Date(this.jsonBlock[t].dtstart).isBetween(twoDays, sevenDays)) today.entries.push(this.jsonBlock[t]);*/ today.entries = this.jsonBlock.filter((item) => { if (!item || !item.dtstart || !item.dtend) return false; return item.dtstart.isBetween(twoDays, sevenDays) || item.dtend.isBetween(twoDays, sevenDays); }); logger.info('- getWeek'); return today; }, 'getTodaysMeetings': function() { 'use strict'; logger.info('+ getTodaysMeetings'); const today = { 'previous': [], 'upcoming': [], 'current': {} }; const now = new Date(); for (let t = 0; t < this.jsonBlock.length; t++) if (Sugar.Date(this.jsonBlock[t].dtstart).isToday().raw) { if (Sugar.Date(this.jsonBlock[t].dtstart).isAfter(now).raw) today.upcoming.push(this.jsonBlock[t]); else today.previous.push(this.jsonBlock[t]); if (now.isBetween(this.jsonBlock[t].dtstart, this.jsonBlock[t].dtend)) today.current = this.jsonBlock[t]; } // logger.debug(today); logger.info('- getTodaysMeetings'); return today; }, 'getSimpleCalV2': function(url, cb) { 'use strict'; const self = this; // Var calJson = []; try { request(url, function(err, res, body) { if (err) { logger.error('Get remote Calendar Request failed'); // Callback.call(null, new Error('Request failed')); return; } self.jsonBlock = processICAL(body); // logger.debug(self.jsonBlock); const st = self.getTodaysSimple(); if (typeof cb === 'function') cb(st); }, function(error, response, body) { if (response.statusCode !== 200) { logger.error(response.statusCode); logger.error(body); } }); } catch (e) { logger.error(e); } }, 'getSimpleCalV3': function(url) { 'use strict'; const self = this; return new Promise(function(resolve, reject) { try { request(url, function(err, res, body) { if (err) // logger.error(err); return reject(err); // Throw err; self.jsonBlock = processICAL(body); logger.debug(self.jsonBlock); const st = self.getTodaysSimple(); return resolve(st); }, function(error, response, body) { if (response.statusCode !== 200) { logger.error(response.statusCode); // logger.error(body); return reject(error); } }); } catch (e) { logger.error(e); return reject(e); } }); // Var calJson = []; }, 'getAdvancedCalV3': function(url) { 'use strict'; const self = this; return new Promise(function(resolve, reject) { try { request(url, function(err, res, body) { if (err) // logger.error(err); return reject(err); // Throw err; self.jsonBlock = processICAL(body); logger.debug('jsonBlock length', self.jsonBlock.length); // logger.debug(self.jsonBlock); const st = self.getTodaysSimple().entries; const tom = self.getTomorrow().entries; const week = self.getWeek().entries; const three = self.getThreeDays().entries; const obj = { 'today': st, 'tomorrow': tom, 'week': week, 'three':three }; // logger.warn(obj); return resolve(obj); }, function(error, response, body) { if (response.statusCode !== 200) { logger.error(response.statusCode); // logger.error(body); return reject(error); } }); } catch (e) { logger.error(e); return reject(e); } }); // Var calJson = []; } /** * Created by Martin on 16/02/2016. */ };