2525 */
2626namespace OCA \DAV \CalDAV \Status ;
2727
28+ use DateTimeImmutable ;
2829use OC \Calendar \CalendarQuery ;
2930use OCA \DAV \CalDAV \CalendarImpl ;
30- use OCA \DAV \CalDAV \FreeBusy \FreeBusyGenerator ;
31- use OCA \DAV \CalDAV \InvitationResponse \InvitationResponseServer ;
32- use OCA \DAV \CalDAV \Schedule \Plugin as SchedulePlugin ;
31+ use OCA \UserStatus \Service \StatusService as UserStatusService ;
32+ use OCP \AppFramework \Db \DoesNotExistException ;
3333use OCP \AppFramework \Utility \ITimeFactory ;
3434use OCP \Calendar \IManager ;
35- use OCP \IL10N ;
35+ use OCP \ICache ;
36+ use OCP \ICacheFactory ;
3637use OCP \IUser as User ;
38+ use OCP \IUserManager ;
39+ use OCP \User \IAvailabilityCoordinator ;
3740use OCP \UserStatus \IUserStatus ;
41+ use Psr \Log \LoggerInterface ;
3842use Sabre \CalDAV \Xml \Property \ScheduleCalendarTransp ;
39- use Sabre \DAV \Exception \NotAuthenticated ;
40- use Sabre \DAVACL \Exception \NeedPrivileges ;
41- use Sabre \DAVACL \Plugin as AclPlugin ;
42- use Sabre \VObject \Component ;
43- use Sabre \VObject \Component \VEvent ;
44- use Sabre \VObject \Parameter ;
45- use Sabre \VObject \Property ;
4643
4744class StatusService {
45+ private ICache $ cache ;
4846 public function __construct (private ITimeFactory $ timeFactory ,
4947 private IManager $ calendarManager ,
50- private InvitationResponseServer $ server ,
51- private IL10N $ l10n ,
52- private FreeBusyGenerator $ generator ) {
48+ private IUserManager $ userManager ,
49+ private UserStatusService $ userStatusService ,
50+ private IAvailabilityCoordinator $ availabilityCoordinator ,
51+ private ICacheFactory $ cacheFactory ,
52+ private LoggerInterface $ logger ) {
53+ $ this ->cache = $ cacheFactory ->createLocal ('CalendarStatusService ' );
5354 }
5455
55- public function processCalendarAvailability (User $ user ): ?Status {
56- $ userId = $ user ->getUID ();
57- $ email = $ user ->getEMailAddress ();
58- if ($ email === null ) {
59- return null ;
56+ public function processCalendarStatus (string $ userId ): void {
57+ $ user = $ this ->userManager ->get ($ userId );
58+ if ($ user === null ) {
59+ return ;
6060 }
6161
62- $ server = $ this ->server ->getServer ();
62+ $ availability = $ this ->availabilityCoordinator ->getCurrentOutOfOfficeData ($ user );
63+ if ($ availability !== null && $ this ->availabilityCoordinator ->isInEffect ($ availability )) {
64+ $ this ->logger ->debug ('An Absence is in effect, skipping calendar status check ' , ['user ' => $ userId ]);
65+ return ;
66+ }
6367
64- /** @var SchedulePlugin $schedulingPlugin */
65- $ schedulingPlugin = $ server ->getPlugin ('caldav-schedule ' );
66- $ caldavNS = '{ ' .$ schedulingPlugin ::NS_CALDAV .'} ' ;
68+ $ calendarEvents = $ this ->cache ->get ($ userId );
69+ if ($ calendarEvents === null ) {
70+ $ calendarEvents = $ this ->getCalendarEvents ($ user );
71+ $ this ->cache ->set ($ userId , $ calendarEvents , 300 );
72+ }
6773
68- /** @var AclPlugin $aclPlugin */
69- $ aclPlugin = $ server -> getPlugin ( ' acl ' );
70- if ( ' mailto: ' === substr ( $ email , 0 , 7 )) {
71- $ email = substr ( $ email , 7 ) ;
74+ if ( empty ( $ calendarEvents )) {
75+ $ this -> userStatusService -> revertUserStatus ( $ userId , IUserStatus:: MESSAGE_CALENDAR_BUSY );
76+ $ this -> logger -> debug ( ' No calendar events found for status check ' , [ ' user ' => $ userId ]);
77+ return ;
7278 }
7379
74- $ result = $ aclPlugin ->principalSearch (
75- ['{http://sabredav.org/ns}email-address ' => $ email ],
76- [
77- '{DAV:}principal-URL ' ,
78- $ caldavNS .'calendar-home-set ' ,
79- $ caldavNS .'schedule-inbox-URL ' ,
80- '{http://sabredav.org/ns}email-address ' ,
81- ]
82- );
80+ $ userStatusTimestamp = null ;
81+ $ currentStatus = null ;
82+ try {
83+ $ currentStatus = $ this ->userStatusService ->findByUserId ($ userId );
84+ $ userStatusTimestamp = $ currentStatus ->getIsUserDefined () ? $ currentStatus ->getStatusTimestamp () : null ;
85+ } catch (DoesNotExistException ) {
86+ }
8387
84- if (!count ($ result ) || !isset ($ result [0 ][200 ][$ caldavNS .'schedule-inbox-URL ' ])) {
85- return null ;
88+ if ($ currentStatus !== null && $ currentStatus ->getMessageId () === IUserStatus::MESSAGE_CALL
89+ || $ currentStatus !== null && $ currentStatus ->getStatus () === IUserStatus::DND
90+ || $ currentStatus !== null && $ currentStatus ->getStatus () === IUserStatus::INVISIBLE ) {
91+ // We don't overwrite the call status, DND status or Invisible status
92+ $ this ->logger ->debug ('Higher priority status detected, skipping calendar status change ' , ['user ' => $ userId ]);
93+ return ;
8694 }
8795
88- $ inboxUrl = $ result [0 ][200 ][$ caldavNS .'schedule-inbox-URL ' ]->getHref ();
96+ // Filter events to see if we have any that apply to the calendar status
97+ $ applicableEvents = array_filter ($ calendarEvents , function (array $ calendarEvent ) use ($ userStatusTimestamp ) {
98+ $ component = $ calendarEvent ['objects ' ][0 ];
99+ if (isset ($ component ['X-NEXTCLOUD-OUT-OF-OFFICE ' ])) {
100+ return false ;
101+ }
102+ if (isset ($ component ['DTSTART ' ]) && $ userStatusTimestamp !== null ) {
103+ /** @var DateTimeImmutable $dateTime */
104+ $ dateTime = $ component ['DTSTART ' ][0 ];
105+ $ timestamp = $ dateTime ->getTimestamp ();
106+ if ($ userStatusTimestamp > $ timestamp ) {
107+ return false ;
108+ }
109+ }
110+ // Ignore events that are transparent
111+ if (isset ($ component ['TRANSP ' ]) && strcasecmp ($ component ['TRANSP ' ][0 ], 'TRANSPARENT ' ) === 0 ) {
112+ return false ;
113+ }
114+ return true ;
115+ });
89116
90- // Do we have permission?
91- try {
92- $ aclPlugin ->checkPrivileges ($ inboxUrl , $ caldavNS .'schedule-query-freebusy ' );
93- } catch (NeedPrivileges | NotAuthenticated $ exception ) {
94- return null ;
117+ if (empty ($ applicableEvents )) {
118+ $ this ->userStatusService ->revertUserStatus ($ userId , IUserStatus::MESSAGE_CALENDAR_BUSY );
119+ $ this ->logger ->debug ('No status relevant events found, skipping calendar status change ' , ['user ' => $ userId ]);
120+ return ;
95121 }
96122
97- $ now = $ this ->timeFactory ->now ();
98- $ calendarTimeZone = $ now ->getTimezone ();
99- $ calendars = $ this ->calendarManager ->getCalendarsForPrincipal ('principals/users/ ' . $ userId );
123+ // One event that fulfills all status conditions is enough
124+ // 1. Not an OOO event
125+ // 2. Current user status was not set after the start of this event
126+ // 3. Event is not set to be transparent
127+ $ count = count ($ applicableEvents );
128+ $ this ->logger ->debug ("Found $ count applicable event(s), changing user status " , ['user ' => $ userId ]);
129+ $ this ->userStatusService ->setUserStatus (
130+ $ userId ,
131+ IUserStatus::AWAY ,
132+ IUserStatus::MESSAGE_CALENDAR_BUSY ,
133+ true
134+ );
135+
136+ }
137+
138+ private function getCalendarEvents (User $ user ): array {
139+ $ calendars = $ this ->calendarManager ->getCalendarsForPrincipal ('principals/users/ ' . $ user ->getUID ());
100140 if (empty ($ calendars )) {
101- return null ;
141+ return [] ;
102142 }
103143
104- $ query = $ this ->calendarManager ->newQuery ('principals/users/ ' . $ userId );
144+ $ query = $ this ->calendarManager ->newQuery ('principals/users/ ' . $ user -> getUID () );
105145 foreach ($ calendars as $ calendarObject ) {
106146 // We can only work with a calendar if it exposes its scheduling information
107147 if (!$ calendarObject instanceof CalendarImpl) {
@@ -114,83 +154,20 @@ public function processCalendarAvailability(User $user): ?Status {
114154 // ignore it for free-busy purposes.
115155 continue ;
116156 }
117-
118- /** @var Component\VTimeZone|null $ctz */
119- $ ctz = $ calendarObject ->getSchedulingTimezone ();
120- if ($ ctz !== null ) {
121- $ calendarTimeZone = $ ctz ->getTimeZone ();
122- }
123157 $ query ->addSearchCalendar ($ calendarObject ->getUri ());
124158 }
125159
126- $ calendarEvents = [];
127- $ dtStart = $ now ;
128- $ dtEnd = \DateTimeImmutable::createFromMutable ($ this ->timeFactory ->getDateTime ('+10 minutes ' ));
160+ $ dtStart = DateTimeImmutable::createFromMutable ($ this ->timeFactory ->getDateTime ());
161+ $ dtEnd = DateTimeImmutable::createFromMutable ($ this ->timeFactory ->getDateTime ('+5 minutes ' ));
129162
130163 // Only query the calendars when there's any to search
131164 if ($ query instanceof CalendarQuery && !empty ($ query ->getCalendarUris ())) {
132165 // Query the next hour
133166 $ query ->setTimerangeStart ($ dtStart );
134167 $ query ->setTimerangeEnd ($ dtEnd );
135- $ calendarEvents = $ this ->calendarManager ->searchForPrincipal ($ query );
168+ return $ this ->calendarManager ->searchForPrincipal ($ query );
136169 }
137170
138- // @todo we can cache that
139- if (empty ($ calendarEvents )) {
140- return null ;
141- }
142-
143- $ calendar = $ this ->generator ->getVCalendar ();
144- foreach ($ calendarEvents as $ calendarEvent ) {
145- $ vEvent = new VEvent ($ calendar , 'VEVENT ' );
146- foreach ($ calendarEvent ['objects ' ] as $ component ) {
147- foreach ($ component as $ key => $ value ) {
148- $ vEvent ->add ($ key , $ value [0 ]);
149- }
150- }
151- $ calendar ->add ($ vEvent );
152- }
153-
154- $ calendar ->METHOD = 'REQUEST ' ;
155-
156- $ this ->generator ->setObjects ($ calendar );
157- $ this ->generator ->setTimeRange ($ dtStart , $ dtEnd );
158- $ this ->generator ->setTimeZone ($ calendarTimeZone );
159- $ result = $ this ->generator ->getResult ();
160-
161- if (!isset ($ result ->VFREEBUSY )) {
162- return null ;
163- }
164-
165- /** @var Component $freeBusyComponent */
166- $ freeBusyComponent = $ result ->VFREEBUSY ;
167- $ freeBusyProperties = $ freeBusyComponent ->select ('FREEBUSY ' );
168- // If there is no FreeBusy property, the time-range is empty and available
169- if (count ($ freeBusyProperties ) === 0 ) {
170- return null ;
171- }
172-
173- /** @var Property $freeBusyProperty */
174- $ freeBusyProperty = $ freeBusyProperties [0 ];
175- if (!$ freeBusyProperty ->offsetExists ('FBTYPE ' )) {
176- // If there is no FBTYPE, it means it's busy from a regular event
177- return new Status (IUserStatus::BUSY , IUserStatus::MESSAGE_CALENDAR_BUSY );
178- }
179-
180- // If we can't deal with the FBTYPE (custom properties are a possibility)
181- // we should ignore it and leave the current status
182- $ fbTypeParameter = $ freeBusyProperty ->offsetGet ('FBTYPE ' );
183- if (!($ fbTypeParameter instanceof Parameter)) {
184- return null ;
185- }
186- $ fbType = $ fbTypeParameter ->getValue ();
187- switch ($ fbType ) {
188- // Ignore BUSY-UNAVAILABLE, that's for the automation
189- case 'BUSY ' :
190- case 'BUSY-TENTATIVE ' :
191- return new Status (IUserStatus::BUSY , IUserStatus::MESSAGE_CALENDAR_BUSY , $ this ->l10n ->t ('In a meeting ' ));
192- default :
193- return null ;
194- }
171+ return [];
195172 }
196173}
0 commit comments