2525use OCA \Calendar \Objects \Proposal \ProposalParticipantStatus ;
2626use OCA \Calendar \Objects \Proposal \ProposalResponseObject ;
2727use OCA \Calendar \Objects \Proposal \ProposalVoteCollection ;
28+ use OCA \DAV \CalDAV \InvitationResponse \InvitationResponseServer ;
2829use OCP \Calendar \ICalendar ;
2930use OCP \Calendar \ICalendarIsWritable ;
3031use OCP \Calendar \ICreateFromString ;
@@ -200,7 +201,7 @@ public function createProposal(IUser $user, ProposalObject $proposal): ProposalO
200201 $ this ->generateNotifications ($ user , $ proposal , 'C ' );
201202
202203 // generate iTip for internal participants
203- $ this ->generateIMip ($ user , $ proposal , 'C ' );
204+ $ this ->syncCalendarBlockers ($ user , $ proposal , 'C ' );
204205
205206 return $ proposal ;
206207 }
@@ -267,7 +268,7 @@ public function modifyProposal(IUser $user, ProposalObject $mutatedProposal): Pr
267268 $ this ->generateNotifications ($ user , $ proposal , 'M ' );
268269
269270 // generate iTip for internal participants
270- $ this ->generateIMip ($ user , $ proposal , 'M ' );
271+ $ this ->syncCalendarBlockers ($ user , $ proposal , 'M ' );
271272
272273 return $ proposal ;
273274 }
@@ -291,7 +292,7 @@ public function destroyProposal(IUser $user, int $identifier): void {
291292 $ this ->generateNotifications ($ user , $ proposal , 'D ' );
292293
293294 // generate iTip for internal participants
294- $ this ->generateIMip ($ user , $ proposal , 'D ' );
295+ $ this ->syncCalendarBlockers ($ user , $ proposal , 'D ' );
295296 }
296297
297298 /**
@@ -308,26 +309,8 @@ public function convertProposal(IUser $user, int $proposalId, int $dateId, array
308309 if ($ selectedDate === null ) {
309310 throw new \InvalidArgumentException ('Date not found for proposal ' );
310311 }
311-
312- // retrieve the primary calendar for the user
313- // this condition is just to make psalm happy
314- if (method_exists ($ this ->calendarManager , 'getPrimaryCalendar ' )) {
315- /** @var ICalendar&ICreateFromString|null $userCalendar */
316- $ userCalendar = $ this ->calendarManager ->getPrimaryCalendar ($ user ->getUID ());
317- }
318- if ($ userCalendar !== null && (!$ userCalendar instanceof ICreateFromString || $ userCalendar ->isDeleted ())) {
319- $ userCalendar = null ;
320- }
321- // if no primary calendar is set, use the first useable calendar
322- if ($ userCalendar === null ) {
323- $ userCalendars = $ this ->calendarManager ->getCalendarsForPrincipal ('principals/users/ ' . $ user ->getUID ());
324- foreach ($ userCalendars as $ calendar ) {
325- if ($ calendar instanceof ICreateFromString && $ calendar instanceof ICalendarIsWritable && $ calendar ->isWritable () && !$ calendar ->isDeleted ()) {
326- $ userCalendar = $ calendar ;
327- break ;
328- }
329- }
330- }
312+ // locate users primary calendar
313+ $ userCalendar = $ this ->findPrimaryCalendar ($ user );
331314 if ($ userCalendar === null ) {
332315 throw new \RuntimeException ('Could not find a useable calendar to create a meeting from the selected proposal ' );
333316 }
@@ -593,58 +576,148 @@ private function sendEmailNotifications(IUser $user, ProposalObject $proposal, P
593576
594577 }
595578
596- private function generateIMip (IUser $ user , ProposalObject $ proposal , string $ reason ): void {
597- // if the calendar manager does not have a handleIMip method, we cannot generate iTip messages
598- if (!method_exists ($ this ->calendarManager , 'handleIMip ' )) {
579+ /**
580+ * Create, update, or delete calendar blocker event
581+ */
582+ private function syncCalendarBlockers (IUser $ user , ProposalObject $ proposal , string $ reason ): void {
583+
584+ // if the proposal has no dates or participants, time blockers are not needed
585+ if ($ proposal ->getDates ()->count () === 0 || $ proposal ->getParticipants ()->count () === 0 ) {
599586 return ;
600587 }
601- // if the proposal has no dates or participants, we cannot generate any iTip messages
602- if ($ proposal ->getDates ()->count () === 0 || $ proposal ->getParticipants ()->count () === 0 ) {
588+ $ userCalendarUri = null ;
589+ $ userEventUri = null ;
590+ // if the reason is deletion, remove existing calendar blockers
591+ if ($ reason === 'D ' ) {
592+ $ result = $ this ->findCalendarBlocker ($ user , $ proposal );
593+ if ($ result === null ) {
594+ return ;
595+ }
596+ $ this ->deleteCalendarBlockersOrganizer ($ user , $ result ['calendarUri ' ], $ result ['eventUri ' ], $ proposal );
603597 return ;
604598 }
599+ // if the reason is modification, try to locate existing calendar with blocker
600+ if ($ reason === 'M ' ) {
601+ $ result = $ this ->findCalendarBlocker ($ user , $ proposal );
602+ if ($ result !== null ) {
603+ $ userCalendarUri = $ result ['calendarUri ' ];
604+ $ userEventUri = $ result ['eventUri ' ];
605+ }
606+ }
607+ // if reason is creation, or no existing calendar blocker found, locate primary calendar
608+ if (!isset ($ userCalendarUri ) || $ userCalendarUri === null ) {
609+ $ result = $ this ->findPrimaryCalendar ($ user );
610+ if ($ result !== null ) {
611+ $ userCalendarUri = $ result ->getUri ();
612+ }
613+ }
614+ if ($ userCalendarUri === null ) {
615+ throw new \RuntimeException ('Could not find a useable calendar to create a meeting from the selected proposal ' );
616+ }
617+
618+ $ vObject = $ this ->constructCalendarBlocker ($ user , $ proposal );
619+
620+ $ this ->applyCalendarBlockersOrganizer ($ user , $ userCalendarUri , $ userEventUri , $ vObject );
621+ $ this ->applyCalendarBlockersParticipant ($ user , $ proposal , $ reason , $ userCalendarUri , $ userEventUri , $ vObject );
622+
623+ }
624+
625+ /**
626+ * Construct calendar blocker event
627+ */
628+ private function constructCalendarBlocker (IUser $ user , ProposalObject $ proposal ): VCalendar {
605629 // construct calendar object with events
606- $ template = new VCalendar ();
607- // TODO: change REQUEST to PUBLISH
608- $ template ->add ('METHOD ' , $ reason !== 'D ' ? 'REQUEST ' : 'CANCEL ' );
609- // create a event for each date in the proposal
610- // TODO: should we create a new instance for each date or use a recurrence rule? Like RDATE:19970714T083000Z,19970715T083000Z
630+ $ proposalDates = [];
631+ $ firstProposalDate = null ;
611632 foreach ($ proposal ->getDates ()->sortByDate () as $ proposalDate ) {
612- /** @var VEvent $vEvent */
613- $ vEvent = $ template ->add ('VEVENT ' , []);
614- if (isset ($ baseDate )) {
615- $ vEvent ->add ('RECURRENCE-ID ' , $ baseDate ->getDate ()->format ('Ymd\THis\Z ' ));
616- } else {
617- $ baseDate = $ proposalDate ;
633+ $ date = $ proposalDate ->getDate ();
634+ if ($ firstProposalDate === null ) {
635+ $ firstProposalDate = $ date ;
618636 }
619- $ vEvent ->UID ->setValue ($ proposal ->getUuid ());
620- $ vEvent ->add ('STATUS ' , 'TENTATIVE ' );
621- $ vEvent ->add ('SEQUENCE ' , 1 );
622- $ vEvent ->add ('DTSTART ' , $ proposalDate ->getDate ());
623- $ vEvent ->add ('DURATION ' , "PT {$ proposal ->getDuration ()}M " );
624- $ vEvent ->add ('SUMMARY ' , $ proposal ->getTitle ());
637+ $ proposalDates [] = $ date ->format ('Ymd\THis\Z ' );
638+ }
639+ if ($ firstProposalDate === null ) {
640+ throw new \InvalidArgumentException ('Cannot construct calendar blocker without at least one proposal date ' );
641+ }
642+ $ vObject = new VCalendar ();
643+ /** @var VEvent $vEvent */
644+ $ vEvent = $ vObject ->add ('VEVENT ' , []);
645+ $ vEvent ->UID ->setValue ($ proposal ->getUuid ());
646+ $ vEvent ->add ('STATUS ' , 'TENTATIVE ' );
647+ $ vEvent ->add ('SEQUENCE ' , 1 );
648+ $ vEvent ->add ('DTSTART ' , $ firstProposalDate );
649+ $ vEvent ->add ('DURATION ' , "PT {$ proposal ->getDuration ()}M " );
650+ $ vEvent ->add ('RDATE ' , $ proposalDates );
651+ $ vEvent ->add ('SUMMARY ' , $ this ->l10n ->t ('[Proposed] ' ) . $ proposal ->getTitle ());
652+ if (!empty ($ proposal ->getDescription ())) {
625653 $ vEvent ->add ('DESCRIPTION ' , $ proposal ->getDescription ());
626- $ vEvent ->add ('ORGANIZER ' , 'mailto: ' . $ user ->getEMailAddress (), ['CN ' => $ user ->getDisplayName ()]);
627- // add the participant to the event
628- foreach ($ proposal ->getParticipants () as $ participant ) {
629- $ vEvent ->add ('ATTENDEE ' , 'mailto: ' . $ participant ->getAddress (), [
630- 'CN ' => $ participant ->getName (),
631- 'CUTYPE ' => 'INDIVIDUAL ' ,
632- 'PARTSTAT ' => 'NEEDS-ACTION ' ,
633- 'ROLE ' => 'REQ-PARTICIPANT '
634- ]);
635- }
636654 }
655+ if (!empty ($ proposal ->getLocation ())) {
656+ $ vEvent ->add ('LOCATION ' , $ proposal ->getLocation ());
657+ }
658+ $ vEvent ->add ('ORGANIZER ' , 'mailto: ' . $ user ->getEMailAddress (), ['CN ' => $ user ->getDisplayName ()]);
659+ // add the participant to the event
660+ foreach ($ proposal ->getParticipants () as $ participant ) {
661+ $ vEvent ->add ('ATTENDEE ' , 'mailto: ' . $ participant ->getAddress (), [
662+ 'CN ' => $ participant ->getName (),
663+ 'CUTYPE ' => 'INDIVIDUAL ' ,
664+ 'PARTSTAT ' => 'NEEDS-ACTION ' ,
665+ 'ROLE ' => 'REQ-PARTICIPANT '
666+ ]);
667+ }
668+
669+ return $ vObject ;
670+ }
671+
672+ /**
673+ * Create or update calendar blocker event(s) for organizer
674+ */
675+ private function applyCalendarBlockersOrganizer (IUser $ user , string $ calendarUri , ?string $ eventUri , VCalendar $ vObject ): void {
676+ /** @var \OCA\DAV\CalDAV\CalendarHome $calendarHome */
677+ $ calendarHome = (new InvitationResponseServer (false ))->getServer ()->tree ->getNodeForPath ('/calendars/ ' . $ user ->getUID ());
678+ /** @var \OCA\DAV\CalDAV\Calendar $calendar */
679+ $ calendar = $ calendarHome ->getChild ($ calendarUri );
680+
681+ if ($ eventUri === null ) {
682+ $ calendar ->createFile (
683+ Uuid::v4 ()->toRfc4122 () . '.ics ' ,
684+ $ vObject ->serialize ()
685+ );
686+ } else {
687+ $ event = $ calendar ->getChild ($ eventUri );
688+ $ event ->put ($ vObject ->serialize ());
689+ }
690+ }
691+
692+ /**
693+ * Delete existing calendar blocker event
694+ */
695+ private function deleteCalendarBlockersOrganizer (IUser $ user , string $ calendarUri , string $ eventUri , ProposalObject $ proposal ): void {
696+ /** @var \OCA\DAV\CalDAV\CalendarHome $calendarHome */
697+ $ calendarHome = (new InvitationResponseServer (false ))->getServer ()->tree ->getNodeForPath ('/calendars/ ' . $ user ->getUID ());
698+ /** @var \OCA\DAV\CalDAV\Calendar $calendar */
699+ $ calendar = $ calendarHome ->getChild ($ calendarUri );
700+
701+ $ event = $ calendar ->getChild ($ eventUri );
702+ $ event ->delete ($ event ->getName ());
703+ }
704+
705+ /**
706+ * Create or update calendar blocker event(s) for participant(s)
707+ */
708+ private function applyCalendarBlockersParticipant (IUser $ user , ProposalObject $ proposal , string $ reason , string $ calendarUri , ?string $ eventUri , VCalendar $ vObject ): void {
709+ // if the calendar manager does not have a handleIMip method, we cannot generate iTip messages
710+ if (!method_exists ($ this ->calendarManager , 'handleIMip ' )) {
711+ return ;
712+ }
713+ // TODO: change REQUEST to PUBLISH
714+ $ vObject ->add ('METHOD ' , $ reason !== 'D ' ? 'REQUEST ' : 'CANCEL ' );
637715
638716 foreach ($ proposal ->getParticipants ()->filterByRealm (ProposalParticipantRealm::Internal) as $ participant ) {
639717 $ participantAddress = $ participant ->getAddress ();
640718 if ($ participantAddress === null ) {
641719 continue ;
642720 }
643-
644- // TODO: this is stupid, we send the internal users email address from the UI then convert it back to a user name
645- // should probably be sent from the UI as a user name, or send and store both the user name and email address
646- // maybe send the address as a special schema "local:{user name}/{email address}", this would allow us to later extend this to federated users
647- // with a different special schema like "federated:{user name}@{server}/{email address}"
648721 $ participantUsers = $ this ->userManager ->getByEmail ($ participantAddress );
649722 if ($ participantUsers === []) {
650723 continue ;
@@ -653,14 +726,55 @@ private function generateIMip(IUser $user, ProposalObject $proposal, string $rea
653726 try {
654727 $ this ->calendarManager ->handleIMip (
655728 $ participantUser ->getUID (),
656- $ template ->serialize (),
729+ $ vObject ->serialize (),
657730 $ reason !== 'D ' ? ['absent ' => 'create ' ] : []
658731 );
659732 } catch (Exception $ e ) {
660733 $ this ->logger ->error ($ e ->getMessage (), ['app ' => 'calendar ' , 'exception ' => $ e ]);
661734 }
662735 }
736+ }
737+
738+ /**
739+ * Find the primary calendar for a user, or the first useable calendar
740+ */
741+ private function findPrimaryCalendar (IUser $ user ): ?ICreateFromString {
742+ // retrieve the primary calendar for the user
743+ // this condition is just to make psalm happy
744+ if (method_exists ($ this ->calendarManager , 'getPrimaryCalendar ' )) {
745+ /** @var ICalendar&ICreateFromString|null $userCalendar */
746+ $ userCalendar = $ this ->calendarManager ->getPrimaryCalendar ($ user ->getUID ());
747+ }
748+ if ($ userCalendar !== null && (!$ userCalendar instanceof ICreateFromString || $ userCalendar ->isDeleted ())) {
749+ $ userCalendar = null ;
750+ }
751+ // if no primary calendar is set, use the first useable calendar
752+ if ($ userCalendar === null ) {
753+ $ userCalendars = $ this ->calendarManager ->getCalendarsForPrincipal ('principals/users/ ' . $ user ->getUID ());
754+ foreach ($ userCalendars as $ calendar ) {
755+ if ($ calendar instanceof ICreateFromString && $ calendar instanceof ICalendarIsWritable && $ calendar ->isWritable () && !$ calendar ->isDeleted ()) {
756+ $ userCalendar = $ calendar ;
757+ break ;
758+ }
759+ }
760+ }
761+ return $ userCalendar ;
762+ }
663763
764+ /**
765+ * Find existing calendar blocker event
766+ *
767+ * @return array{calendarUri: string, eventUri: string}|null
768+ */
769+ private function findCalendarBlocker (IUser $ user , ProposalObject $ proposal ): ?array {
770+ $ userCalendars = $ this ->calendarManager ->getCalendarsForPrincipal ('principals/users/ ' . $ user ->getUID ());
771+ foreach ($ userCalendars as $ calendar ) {
772+ $ result = $ calendar ->search ('' , [], ['uid ' => (string )$ proposal ->getUuid ()]);
773+ if (isset ($ result [0 ])) {
774+ return ['calendarUri ' => $ calendar ->getUri (), 'eventUri ' => $ result [0 ]['uri ' ]];
775+ }
776+ }
777+ return null ;
664778 }
665779
666780}
0 commit comments