您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

CalendarService.js 5.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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 { getZoneString } from 'icalzone'
  26. import { v4 as uuidv4 } from 'uuid'
  27. /**
  28. *
  29. */
  30. export function getEmptySlots() {
  31. return {
  32. MO: [],
  33. TU: [],
  34. WE: [],
  35. TH: [],
  36. FR: [],
  37. SA: [],
  38. SU: [],
  39. }
  40. }
  41. /**
  42. *
  43. */
  44. export async function findScheduleInboxAvailability() {
  45. const client = getClient('calendars')
  46. const response = await client.customRequest('inbox', {
  47. method: 'PROPFIND',
  48. data: `<?xml version="1.0"?>
  49. <x0:propfind xmlns:x0="DAV:">
  50. <x0:prop>
  51. <x1:calendar-availability xmlns:x1="urn:ietf:params:xml:ns:caldav"/>
  52. </x0:prop>
  53. </x0:propfind>`,
  54. })
  55. const xml = await parseXML(response.data)
  56. if (!xml) {
  57. return undefined
  58. }
  59. const availability = xml?.multistatus?.response[0]?.propstat?.prop['calendar-availability']
  60. if (!availability) {
  61. return undefined
  62. }
  63. const parsedIcal = ICAL.parse(availability)
  64. const vcalendarComp = new ICAL.Component(parsedIcal)
  65. const vavailabilityComp = vcalendarComp.getFirstSubcomponent('vavailability')
  66. let timezoneId
  67. const timezoneComp = vcalendarComp.getFirstSubcomponent('vtimezone')
  68. if (timezoneComp) {
  69. timezoneId = timezoneComp.getFirstProperty('tzid').getFirstValue()
  70. }
  71. const availableComps = vavailabilityComp.getAllSubcomponents('available')
  72. // Combine all AVAILABLE blocks into a week of slots
  73. const slots = getEmptySlots()
  74. availableComps.forEach((availableComp) => {
  75. const start = availableComp.getFirstProperty('dtstart').getFirstValue().toJSDate()
  76. const end = availableComp.getFirstProperty('dtend').getFirstValue().toJSDate()
  77. const rrule = availableComp.getFirstProperty('rrule')
  78. if (rrule.getFirstValue().freq !== 'WEEKLY') {
  79. logger.warn('rrule not supported', {
  80. rrule: rrule.toICALString(),
  81. })
  82. return
  83. }
  84. rrule.getFirstValue().getComponent('BYDAY').forEach(day => {
  85. slots[day].push({
  86. start,
  87. end,
  88. })
  89. })
  90. })
  91. return {
  92. slots,
  93. timezoneId,
  94. }
  95. }
  96. /**
  97. * @param {any} slots -
  98. * @param {any} timezoneId -
  99. */
  100. export async function saveScheduleInboxAvailability(slots, timezoneId) {
  101. const all = [...Object.keys(slots).flatMap(dayId => slots[dayId].map(slot => ({
  102. ...slot,
  103. day: dayId,
  104. })))]
  105. const vcalendarComp = new ICAL.Component('vcalendar')
  106. vcalendarComp.addPropertyWithValue('prodid', 'Nextcloud DAV app')
  107. // Store time zone info
  108. // If possible we use the info from a time zone database
  109. const predefinedTimezoneIcal = getZoneString(timezoneId)
  110. if (predefinedTimezoneIcal) {
  111. const timezoneComp = new ICAL.Component(ICAL.parse(predefinedTimezoneIcal))
  112. vcalendarComp.addSubcomponent(timezoneComp)
  113. } else {
  114. // Fall back to a simple markup
  115. const timezoneComp = new ICAL.Component('vtimezone')
  116. timezoneComp.addPropertyWithValue('tzid', timezoneId)
  117. vcalendarComp.addSubcomponent(timezoneComp)
  118. }
  119. // Store availability info
  120. const vavailabilityComp = new ICAL.Component('vavailability')
  121. // Deduplicate by start and end time
  122. const deduplicated = all.reduce((acc, slot) => {
  123. const key = [
  124. slot.start.getHours(),
  125. slot.start.getMinutes(),
  126. slot.end.getHours(),
  127. slot.end.getMinutes(),
  128. ].join('-')
  129. return {
  130. ...acc,
  131. [key]: [...(acc[key] ?? []), slot],
  132. }
  133. }, {})
  134. // Create an AVAILABILITY component for every recurring slot
  135. Object.keys(deduplicated).map(key => {
  136. const slots = deduplicated[key]
  137. const start = slots[0].start
  138. const end = slots[0].end
  139. // Combine days but make them also unique
  140. const days = slots.map(slot => slot.day).filter((day, index, self) => self.indexOf(day) === index)
  141. const availableComp = new ICAL.Component('available')
  142. // Define DTSTART and DTEND
  143. const startTimeProp = availableComp.addPropertyWithValue('dtstart', ICAL.Time.fromJSDate(start, false))
  144. startTimeProp.setParameter('tzid', timezoneId)
  145. const endTimeProp = availableComp.addPropertyWithValue('dtend', ICAL.Time.fromJSDate(end, false))
  146. endTimeProp.setParameter('tzid', timezoneId)
  147. // Add mandatory UID
  148. availableComp.addPropertyWithValue('uid', uuidv4())
  149. // TODO: add optional summary
  150. // Define RRULE
  151. availableComp.addPropertyWithValue('rrule', {
  152. freq: 'WEEKLY',
  153. byday: days,
  154. })
  155. return availableComp
  156. }).map(vavailabilityComp.addSubcomponent.bind(vavailabilityComp))
  157. vcalendarComp.addSubcomponent(vavailabilityComp)
  158. logger.debug('New availability ical created', {
  159. asObject: vcalendarComp,
  160. asString: vcalendarComp.toString(),
  161. })
  162. const client = getClient('calendars')
  163. await client.customRequest('inbox', {
  164. method: 'PROPPATCH',
  165. data: `<?xml version="1.0"?>
  166. <x0:propertyupdate xmlns:x0="DAV:">
  167. <x0:set>
  168. <x0:prop>
  169. <x1:calendar-availability xmlns:x1="urn:ietf:params:xml:ns:caldav">${vcalendarComp.toString()}</x1:calendar-availability>
  170. </x0:prop>
  171. </x0:set>
  172. </x0:propertyupdate>`,
  173. })
  174. }