You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CalendarService.js 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. /**
  2. * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
  3. *
  4. * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
  5. *
  6. * @license GNU AGPL version 3 or any later version
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU Affero General Public License as
  10. * published by the Free Software Foundation, either version 3 of the
  11. * License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. */
  21. import { getClient } from '../dav/client'
  22. import ICAL from 'ical.js'
  23. import logger from './logger'
  24. import { parseXML } from 'webdav/dist/node/tools/dav'
  25. import { v4 as uuidv4 } from 'uuid'
  26. export function getEmptySlots() {
  27. return {
  28. MO: [],
  29. TU: [],
  30. WE: [],
  31. TH: [],
  32. FR: [],
  33. SA: [],
  34. SU: [],
  35. }
  36. }
  37. export async function findScheduleInboxAvailability() {
  38. const client = getClient('calendars')
  39. const response = await client.customRequest('inbox', {
  40. method: 'PROPFIND',
  41. data: `<?xml version="1.0"?>
  42. <x0:propfind xmlns:x0="DAV:">
  43. <x0:prop>
  44. <x1:calendar-availability xmlns:x1="urn:ietf:params:xml:ns:caldav"/>
  45. </x0:prop>
  46. </x0:propfind>`,
  47. })
  48. const xml = await parseXML(response.data)
  49. if (!xml) {
  50. return undefined
  51. }
  52. const availability = xml?.multistatus?.response[0]?.propstat?.prop['calendar-availability']
  53. if (!availability) {
  54. return undefined
  55. }
  56. const parsedIcal = ICAL.parse(availability)
  57. const vcalendarComp = new ICAL.Component(parsedIcal)
  58. const vavailabilityComp = vcalendarComp.getFirstSubcomponent('vavailability')
  59. const availableComps = vavailabilityComp.getAllSubcomponents('available')
  60. // Combine all AVAILABLE blocks into a week of slots
  61. const slots = getEmptySlots()
  62. availableComps.forEach((availableComp) => {
  63. const start = availableComp.getFirstProperty('dtstart').getFirstValue().toJSDate()
  64. const end = availableComp.getFirstProperty('dtend').getFirstValue().toJSDate()
  65. const rrule = availableComp.getFirstProperty('rrule')
  66. if (rrule.getFirstValue().freq !== 'WEEKLY') {
  67. logger.warn('rrule not supported', {
  68. rrule: rrule.toICALString(),
  69. })
  70. return
  71. }
  72. rrule.getFirstValue().getComponent('BYDAY').forEach(day => {
  73. slots[day].push({
  74. start,
  75. end,
  76. })
  77. })
  78. })
  79. return {
  80. slots,
  81. }
  82. }
  83. export async function saveScheduleInboxAvailability(slots, timezoneId) {
  84. const all = [...Object.keys(slots).flatMap(dayId => slots[dayId].map(slot => ({
  85. ...slot,
  86. day: dayId,
  87. })))]
  88. const vavailabilityComp = new ICAL.Component('vavailability')
  89. // Deduplicate by start and end time
  90. const deduplicated = all.reduce((acc, slot) => {
  91. const key = [
  92. slot.start.getHours(),
  93. slot.start.getMinutes(),
  94. slot.end.getHours(),
  95. slot.end.getMinutes(),
  96. ].join('-')
  97. return {
  98. ...acc,
  99. [key]: [...(acc[key] ?? []), slot],
  100. }
  101. }, {})
  102. // Create an AVAILABILITY component for every recurring slot
  103. Object.keys(deduplicated).map(key => {
  104. const slots = deduplicated[key]
  105. const start = slots[0].start
  106. const end = slots[0].end
  107. // Combine days but make them also unique
  108. const days = slots.map(slot => slot.day).filter((day, index, self) => self.indexOf(day) === index)
  109. const availableComp = new ICAL.Component('available')
  110. // Define DTSTART and DTEND
  111. // TODO: tz? moment.tz(dateTime, timezone).toDate()
  112. const startTimeProp = availableComp.addPropertyWithValue('dtstart', ICAL.Time.fromJSDate(start, false))
  113. startTimeProp.setParameter('tzid', timezoneId)
  114. const endTimeProp = availableComp.addPropertyWithValue('dtend', ICAL.Time.fromJSDate(end, false))
  115. endTimeProp.setParameter('tzid', timezoneId)
  116. // Add mandatory UID
  117. availableComp.addPropertyWithValue('uid', uuidv4())
  118. // TODO: add optional summary
  119. // Define RRULE
  120. availableComp.addPropertyWithValue('rrule', {
  121. freq: 'WEEKLY',
  122. byday: days,
  123. })
  124. return availableComp
  125. }).map(vavailabilityComp.addSubcomponent.bind(vavailabilityComp))
  126. const vcalendarComp = new ICAL.Component('vcalendar')
  127. vcalendarComp.addSubcomponent(vavailabilityComp)
  128. logger.debug('New availability ical created', {
  129. asObject: vcalendarComp,
  130. asString: vcalendarComp.toString(),
  131. })
  132. const client = getClient('calendars')
  133. await client.customRequest('inbox', {
  134. method: 'PROPPATCH',
  135. data: `<?xml version="1.0"?>
  136. <x0:propertyupdate xmlns:x0="DAV:">
  137. <x0:set>
  138. <x0:prop>
  139. <x1:calendar-availability xmlns:x1="urn:ietf:params:xml:ns:caldav">${vcalendarComp.toString()}</x1:calendar-availability>
  140. </x0:prop>
  141. </x0:set>
  142. </x0:propertyupdate>`,
  143. })
  144. }