*/ public function getDashboardEvents(string $userId): array { $principaluri = 'principals/users/' . $userId; $calendars = $this->calendarManager->getCalendarsForPrincipal($principaluri); if (count($calendars) === 0) { return []; } // Only use personal calendars // Events for shared calendars where you are an ATTENDEE will be in your personal calendar $calendars = array_filter($calendars, static function (ICalendar $calendar) { if ($calendar->getUri() === 'contact_birthdays') { // The birthday calendar does not contain events with a location matching a talk room. return false; } if (method_exists($calendar, 'isShared')) { return $calendar->isShared() === false; } return true; }); $userTimezone = $this->dateTimeZone->getTimezone(); // Midnight for the current user so we also include ongoing events (might be all day events) $start = $this->timeFactory->getDateTime()->setTimezone($userTimezone)->setTime(0, 0); $start = $start->setTimezone(new \DateTimeZone('UTC')); $end = clone($start); $end = $end->add(\DateInterval::createFromDateString('1 week')); $options = [ 'timerange' => [ 'start' => $start, 'end' => $end, ], ]; $pattern = '/call/'; $searchProperties = ['LOCATION']; $events = []; /** @var ICalendar $calendar */ foreach ($calendars as $calendar) { $searchResult = $calendar->search($pattern, $searchProperties, $options, 100); foreach ($searchResult as $calendarEvent) { // Find first recurrence in the future $event = null; $dashboardEvent = new Event(); foreach ($calendarEvent['objects'] as $object) { if (!isset($object['DTEND'][0])) { // Don't show events without end since they should not take up any time // @link https://www.kanzaki.com/docs/ical/vevent.html continue; } $dashboardEvent->setStart(\DateTime::createFromImmutable($object['DTSTART'][0])->setTimezone($userTimezone)->getTimestamp()); $dashboardEvent->setEnd(\DateTime::createFromImmutable($object['DTEND'][0])->setTimezone($userTimezone)->getTimestamp()); // Filter out events in the past if ($dashboardEvent->getEnd() <= $this->timeFactory->getDateTime('now', $userTimezone)->getTimestamp()) { continue; } $event = $object; break; } $location = $event['LOCATION'][0] ?? null; if ($event === null || $location === null) { continue; } if (isset($event['STATUS']) && $event['STATUS'][0] === 'CANCELLED') { continue; } try { $token = $this->roomService->parseRoomTokenFromUrl($location); // Already returns public / open conversations $room = $this->manager->getRoomForUserByToken($token, $userId); } catch (RoomNotFoundException) { $this->logger->debug("Room for url $location not found in dashboard service"); continue; } $dashboardEvent->setRoomToken($token); $dashboardEvent->setRoomType($room->getType()); $dashboardEvent->setRoomName($room->getName()); $dashboardEvent->setRoomDisplayName($room->getDisplayName($userId)); if (isset($event['ATTENDEE'])) { $dashboardEvent->generateAttendance($event['ATTENDEE']); } $dashboardEvent->setEventName($event['SUMMARY'][0] ?? ''); $dashboardEvent->setEventDescription($event['DESCRIPTION'][0] ?? null); if (isset($event['ATTACH'])) { $dashboardEvent->handleCalendarAttachments($calendar->getUri(), $event['ATTACH']); } if (isset($events[$dashboardEvent->generateEventIdentifier()])) { /** @var Event $existing */ $existing = $events[$dashboardEvent->generateEventIdentifier()]; $existing->addCalendar($calendar->getUri(), $calendar->getDisplayName(), $calendar->getDisplayColor()); // Merge attachments $existing->mergeAttachments($dashboardEvent); // If original SUMMARY is empty, use the duplicate content if it exists if ($existing->getEventDescription() === null) { $existing->setEventDescription($dashboardEvent->getEventDescription() ?? ''); } // We continue here as the same event already exists in a different calendar $events[$existing->generateEventIdentifier()] = $existing; continue; } $dashboardEvent->addCalendar($calendar->getUri(), $calendar->getDisplayName(), $calendar->getDisplayColor()); $dashboardEvent->setRoomAvatarVersion($this->avatarService->getAvatarVersion($room)); $dashboardEvent->setRoomActiveSince($room->getActiveSince()?->getTimestamp()); $objectId = base64_encode($this->urlGenerator->getWebroot() . '/remote.php/dav/calendars/' . $userId . '/' . $calendar->getUri() . '/' . $calendarEvent['uri']); if (isset($event['RECURRENCE-ID'])) { $dashboardEvent->setEventLink( $this->urlGenerator->linkToRouteAbsolute( 'calendar.view.indexdirect.edit', [ 'objectId' => $objectId, 'recurrenceId' => $event['RECURRENCE-ID'][0], ] ) ); } else { $dashboardEvent->setEventLink( $this->urlGenerator->linkToRouteAbsolute('calendar.view.indexdirect.edit', ['objectId' => $objectId]) ); } $events[$dashboardEvent->generateEventIdentifier()] = $dashboardEvent; if (count($events) === 10) { break; } } } if (empty($events)) { return $events; } usort($events, static function (Event $a, Event $b) { return $a->getStart() - $b->getStart(); }); return array_map(static function (Event $event) { return $event->jsonSerialize(); }, array_slice($events, 0, 10)); } /** * @param string $userId * @param Room $room * @return list */ public function getMutualEvents(string $userId, Room $room): array { if ($room->getType() !== Room::TYPE_ONE_TO_ONE) { throw new InvalidRoomException(); } try { $userIds = json_decode($room->getName(), false, 512, JSON_THROW_ON_ERROR); } catch (\JsonException) { throw new InvalidRoomException(); } $participants = array_filter($userIds, static function (string $participantId) use ($userId) { return $participantId !== $userId; }); if (count($participants) !== 1) { throw new InvalidRoomException(); } $otherParticipant = $this->userManager->get(array_pop($participants)); if ($otherParticipant === null) { // Change to correct exception throw new ParticipantNotFoundException(); } $pattern = $otherParticipant->getEMailAddress(); if ($pattern === null) { return []; } $principaluri = 'principals/users/' . $userId; $calendars = $this->calendarManager->getCalendarsForPrincipal($principaluri); if (count($calendars) === 0) { return []; } // Only use personal calendars $calendars = array_filter($calendars, static function (ICalendar $calendar) { if (method_exists($calendar, 'isShared')) { return $calendar->isShared() === false; } return true; }); $start = $this->timeFactory->getDateTime(); $end = clone($start); $end = $end->add(\DateInterval::createFromDateString('1 week')); $options = [ 'timerange' => [ 'start' => $start, 'end' => $end, ], ]; $userTimezone = $this->dateTimeZone->getTimezone(); $searchProperties = ['ATTENDEE', 'ORGANIZER']; $events = []; /** @var ICalendar $calendar */ foreach ($calendars as $calendar) { $searchResult = $calendar->search($pattern, $searchProperties, $options); foreach ($searchResult as $calendarEvent) { // Find first recurrence in the future $event = null; $dashboardEvent = new Event(); foreach ($calendarEvent['objects'] as $object) { $dashboardEvent->setStart(\DateTime::createFromImmutable($object['DTSTART'][0])->setTimezone($userTimezone)->getTimestamp()); $dashboardEvent->setEnd(\DateTime::createFromImmutable($object['DTEND'][0])->setTimezone($userTimezone)->getTimestamp()); if ($dashboardEvent->getStart() >= $start->getTimestamp()) { $event = $object; break; } } if ($event === null) { continue; } if (!isset($event['ORGANIZER']) && !isset($event['ATTENDEE'])) { // Don't show events without attendees continue; } if (!$dashboardEvent->isOrganizer($event['ORGANIZER'], $otherParticipant->getEMailAddress()) && !$dashboardEvent->isAttendee($event['ATTENDEE'], $otherParticipant->getEMailAddress())) { // Due to a bug in the caldav search, we will get a search result for recurring events // even if the pattern does not match the current recurrence // So make sure that $otherParticipant is an attendee on the current event continue; } $dashboardEvent->generateAttendance($event['ATTENDEE']); $dashboardEvent->setEventName($event['SUMMARY'][0] ?? ''); $dashboardEvent->setEventDescription($event['DESCRIPTION'][0] ?? null); $dashboardEvent->addCalendar($calendar->getUri(), $calendar->getDisplayName(), $calendar->getDisplayColor()); $location = $event['LOCATION'][0] ?? null; if ($location !== null && str_contains($location, '/call/') === true) { try { $token = $this->roomService->parseRoomTokenFromUrl($location); // Already returns public / open conversations $eventRoom = $this->manager->getRoomForUserByToken($token, $userId); } catch (RoomNotFoundException) { $this->logger->debug("Room for url $location not found in dashboard service"); continue; } $dashboardEvent->setRoomType($eventRoom->getType()); $dashboardEvent->setRoomName($eventRoom->getName()); $dashboardEvent->setRoomToken($eventRoom->getToken()); $dashboardEvent->setRoomDisplayName($eventRoom->getDisplayName($userId)); $dashboardEvent->setRoomAvatarVersion($this->avatarService->getAvatarVersion($eventRoom)); $dashboardEvent->setRoomActiveSince($eventRoom->getActiveSince()?->getTimestamp()); } if (isset($event['ATTACH'])) { $dashboardEvent->handleCalendarAttachments($calendar->getUri(), $event['ATTACH']); } $objectId = base64_encode($this->urlGenerator->getWebroot() . '/remote.php/dav/calendars/' . $userId . '/' . $calendar->getUri() . '/' . $calendarEvent['uri']); if (isset($event['RECURRENCE-ID'])) { $dashboardEvent->setEventLink( $this->urlGenerator->linkToRouteAbsolute( 'calendar.view.indexdirect.edit', [ 'objectId' => $objectId, 'recurrenceId' => $event['RECURRENCE-ID'][0], ] ) ); } else { $dashboardEvent->setEventLink( $this->urlGenerator->linkToRouteAbsolute('calendar.view.indexdirect.edit', ['objectId' => $objectId]) ); } $events[] = $dashboardEvent; } } if (empty($events)) { return $events; } usort($events, static function (Event $a, Event $b) { return $a->getStart() - $b->getStart(); }); return array_map(static function (Event $event) { return $event->jsonSerialize(); }, array_slice($events, 0, 3)); } }