Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions apps/dav/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
use OCA\DAV\Events\CardUpdatedEvent;
use OCA\DAV\Events\SubscriptionCreatedEvent;
use OCA\DAV\Events\SubscriptionDeletedEvent;
use OCA\DAV\Listener\OutOfOfficeListener;
use OCP\Accounts\UserUpdatedEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\Events\TrustedServerRemovedEvent;
Expand Down Expand Up @@ -104,9 +103,6 @@
use OCP\Contacts\IManager as IContactsManager;
use OCP\Files\AppData\IAppDataFactory;
use OCP\IUser;
use OCP\User\Events\OutOfOfficeChangedEvent;
use OCP\User\Events\OutOfOfficeClearedEvent;
use OCP\User\Events\OutOfOfficeScheduledEvent;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
Expand Down Expand Up @@ -199,10 +195,6 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(BeforePreferenceDeletedEvent::class, UserPreferenceListener::class);
$context->registerEventListener(BeforePreferenceSetEvent::class, UserPreferenceListener::class);

$context->registerEventListener(OutOfOfficeChangedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeListener::class);

$context->registerNotifierService(Notifier::class);

$context->registerCalendarProvider(CalendarProvider::class);
Expand Down
17 changes: 15 additions & 2 deletions apps/dav/lib/CalDAV/Status/Status.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@
namespace OCA\DAV\CalDAV\Status;

class Status {

public function __construct(private string $status = '', private ?string $message = null, private ?string $customMessage = null){}
public function __construct(private string $status = '', private ?string $message = null, private ?string $customMessage = null, private ?int $timestamp = null, private ?string $customEmoji = null){}

public function getStatus(): string {
return $this->status;
Expand All @@ -53,5 +52,19 @@ public function setCustomMessage(?string $customMessage): void {
$this->customMessage = $customMessage;
}

public function setEndTime(?int $timestamp): void {
$this->timestamp = $timestamp;
}

public function getEndTime(): ?int {
return $this->timestamp;
}

public function getCustomEmoji(): ?string {
return $this->customEmoji;
}

public function setCustomEmoji(?string $emoji): void {
$this->customEmoji = $emoji;
}
}
11 changes: 8 additions & 3 deletions apps/dav/lib/CalDAV/Status/StatusService.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,16 @@ public function processCalendarAvailability(User $user, ?string $availability):
// If there is no FreeBusy property, the time-range is empty and available
// so set the status to online as otherwise we will never recover from a BUSY status
if (count($freeBusyProperties) === 0) {
return new Status(IUserStatus::ONLINE);
return new Status(IUserStatus::ONLINE, IUserStatus::ONLINE);
}

/** @var Property $freeBusyProperty */
$freeBusyProperty = $freeBusyProperties[0];
if (!$freeBusyProperty->offsetExists('FBTYPE')) {
// If there is no FBTYPE, it means it's busy from a regular event
return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY);
$status = new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY);
$status->setCustomEmoji(IUserStatus::MEETING_ICON);
return $status;
}

// If we can't deal with the FBTYPE (custom properties are a possibility)
Expand All @@ -224,8 +226,11 @@ public function processCalendarAvailability(User $user, ?string $availability):
$fbType = $fbTypeParameter->getValue();
switch ($fbType) {
case 'BUSY':
return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, $this->l10n->t('In a meeting'));
$status = new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, $this->l10n->t('In a meeting'));
$status->setCustomEmoji(IUserStatus::MEETING_ICON);
return $status;
case 'BUSY-UNAVAILABLE':
// @todo - the user could also have set the option to set a DND status
return new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_AVAILABILITY);
case 'BUSY-TENTATIVE':
return new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE);
Expand Down
4 changes: 4 additions & 0 deletions apps/dav/lib/Controller/AvailabilitySettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
use OCP\User\IAvailabilityCoordinator;

class AvailabilitySettingsController extends Controller {
public function __construct(
IRequest $request,
private ?string $userId,
private AbsenceService $absenceService,
private IAvailabilityCoordinator $coordinator,
) {
parent::__construct(Application::APP_ID, $request);
}
Expand Down Expand Up @@ -74,6 +76,7 @@ public function updateAbsence(
$status,
$message,
);
$this->coordinator->clearCache($userId);
return new JSONResponse($absence);
}

Expand All @@ -88,6 +91,7 @@ public function clearAbsence(): Response {
}

$this->absenceService->clearAbsence($userId);
$this->coordinator->clearCache($userId);
return new JSONResponse([]);
}

Expand Down
8 changes: 5 additions & 3 deletions apps/dav/lib/Db/Absence.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,18 @@ public function __construct() {
$this->addType('message', 'string');
}

public function toOutOufOfficeData(IUser $user): IOutOfOfficeData {
public function toOutOufOfficeData(IUser $user, ?string $timezone): IOutOfOfficeData {
if ($user->getUID() !== $this->getUserId()) {
throw new InvalidArgumentException("The user doesn't match the user id of this absence! Expected " . $this->getUserId() . ", got " . $user->getUID());
}
if ($this->getId() === null) {
throw new Exception('Creating out-of-office data without ID');
}

$startDate = new DateTimeImmutable($this->getFirstDay());
$endDate = new DateTimeImmutable($this->getLastDay());
$tz = new \DateTimeZone($timezone ?? 'UTC');
$startDate = new \DateTime($this->getFirstDay(), $tz);
$endDate = new \DateTime($this->getLastDay(), $tz);
$endDate->setTime(23, 59);
return new OutOfOfficeData(
(string)$this->getId(),
$user,
Expand Down
20 changes: 20 additions & 0 deletions apps/dav/lib/Db/AbsenceMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use Sabre\CalDAV\Schedule\Plugin;

/**
* @template-extends QBMapper<Absence>
Expand Down Expand Up @@ -78,4 +79,23 @@ public function deleteByUserId(string $userId): void {
);
$qb->executeStatement();
}

public function getAvailability(string $userId): ?string {
$propertyPath = 'calendars/' . $userId . '/inbox';
$propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability';

$query = $this->db->getQueryBuilder();
$query->select('propertyvalue')
->from('properties')
->where($query->expr()->eq('userid', $query->createNamedParameter($userId)))
->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($propertyPath)))
->andWhere($query->expr()->eq('propertyname', $query->createNamedParameter($propertyName)))
->setMaxResults(1);

$result = $query->executeQuery();
$property = $result->fetchOne();
$result->closeCursor();

return ($property === false ? null : $property);
}
}
14 changes: 11 additions & 3 deletions apps/dav/lib/Listener/OutOfOfficeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\ServerFactory;
use OCA\UserStatus\Service\StatusService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use OCP\User\Events\OutOfOfficeChangedEvent;
use OCP\User\Events\OutOfOfficeClearedEvent;
use OCP\User\Events\OutOfOfficeScheduledEvent;
use OCP\User\IOutOfOfficeData;
use OCP\UserStatus\IUserStatus;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\NotFound;
use Sabre\VObject\Component\VCalendar;
Expand All @@ -52,9 +54,12 @@
* @template-implements IEventListener<OutOfOfficeScheduledEvent|OutOfOfficeChangedEvent|OutOfOfficeClearedEvent>
*/
class OutOfOfficeListener implements IEventListener {
public function __construct(private ServerFactory $serverFactory,
private IConfig $appConfig,
private LoggerInterface $logger) {
public function __construct(
private ServerFactory $serverFactory,
private IConfig $appConfig,
private LoggerInterface $logger,
private StatusService $statusService,
) {
}

public function handle(Event $event): void {
Expand Down Expand Up @@ -121,7 +126,10 @@ public function handle(Event $event): void {
$oldEvent->delete();
} catch (NotFound) {
// The user must have deleted it or the default calendar changed -> ignore
return;
}

$this->statusService->revertUserStatus($userId);
}
}

Expand Down
88 changes: 85 additions & 3 deletions apps/dav/lib/Service/AbsenceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,33 @@
namespace OCA\DAV\Service;

use InvalidArgumentException;
use OC\User\OutOfOfficeData;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\CalendarImpl;
use OCA\DAV\Db\Absence;
use OCA\DAV\Db\AbsenceMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\ICalendar;
use OCP\Calendar\IManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IUserManager;
use OCP\User\Events\OutOfOfficeChangedEvent;
use OCP\User\Events\OutOfOfficeClearedEvent;
use OCP\User\Events\OutOfOfficeScheduledEvent;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VTimeZone;
use Sabre\VObject\Reader;

class AbsenceService {
public function __construct(
private AbsenceMapper $absenceMapper,
private IEventDispatcher $eventDispatcher,
private IUserManager $userManager,
private ITimeFactory $timeFactory,
private IConfig $appConfig,
private IManager $calendarManager,
) {
}

Expand Down Expand Up @@ -78,14 +91,17 @@ public function createOrUpdateAbsence(

if ($absence->getId() === null) {
$persistedAbsence = $this->absenceMapper->insert($absence);
$timezone = $this->getAbsenceTimezone($userId);
$this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent(
$persistedAbsence->toOutOufOfficeData($user)
$persistedAbsence->toOutOufOfficeData($user, $timezone)
));
return $persistedAbsence;
}

$timezone = $this->getAbsenceTimezone($userId);

$this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent(
$absence->toOutOufOfficeData($user)
$absence->toOutOufOfficeData($user, $timezone)
));
return $this->absenceMapper->update($absence);
}
Expand All @@ -106,8 +122,74 @@ public function clearAbsence(string $userId): void {
if ($user === null) {
throw new InvalidArgumentException("User $userId does not exist");
}
$eventData = $absence->toOutOufOfficeData($user);
$timezone = $this->getAbsenceTimezone($userId);
$eventData = $absence->toOutOufOfficeData($user, $timezone);
$this->eventDispatcher->dispatchTyped(new OutOfOfficeClearedEvent($eventData));
}

public function getAbsence(string $userId): ?Absence {
try {
return $this->absenceMapper->findByUserId($userId);
} catch (DoesNotExistException $e) {
return null;
}
}

public function isInEffect(OutOfOfficeData $absence): bool {
$now = $this->timeFactory->getTime();
return $absence->getStartDate() <= $now && $absence->getEndDate() >= $now;
}

/**
* Get a users calendar timezone or null if no calendar timezones exist
*
* @param string $userId
* @return string|null
*/
public function getAbsenceTimezone(string $userId): ?string {
$availability = $this->absenceMapper->getAvailability($userId);
if(!empty($availability)) {
/** @var VCalendar $vCalendar */
$vCalendar = Reader::read($availability);
/** @var VTimeZone $vTimezone */
$vTimezone = $vCalendar->VTIMEZONE;
// Sabre has a fallback to date_default_timezone_get
return $vTimezone->getTimeZone()->getName();
}

$principal = 'principals/users/' . $userId;
$uri = $this->appConfig->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
$calendars = $this->calendarManager->getCalendarsForPrincipal($principal);

$tz = null;
$personal = array_filter($calendars, function (ICalendar $calendar) use ($uri) {
return $calendar->getUri() === $uri && $calendar->isDeleted() === false;
});

if(!empty($personal)) {
$personal = array_pop($personal);
$tz = $personal instanceof CalendarImpl ? $personal->getSchedulingTimezone() : null;
}

if($tz !== null) {
return $tz->getTimeZone()->getName();
}

// No timezone in the personal calendar or no personal calendar
// Loop through all calendars until we find a timezone.
/** @var CalendarImpl $calendar */
foreach ($calendars as $calendar) {
if($calendar->isDeleted() === true) {
continue;
}
$tz = $calendar->getSchedulingTimezone();
if($tz !== null) {
break;
}
}

return $tz?->getTimeZone()->getName();

}
}

6 changes: 3 additions & 3 deletions apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ public function testAvailabilityAndSearchCalendarsStatusOnline(): void {
->willReturn($result);

$status = $this->service->processCalendarAvailability($user, $availability->serialize());
$this->assertEquals(new Status(IUserStatus::ONLINE), $status);
$this->assertEquals(new Status(IUserStatus::ONLINE, IUserStatus::ONLINE), $status);
}

public function testAvailabilityAndSearchCalendarsStatusBusyNoFBType(): void {
Expand Down Expand Up @@ -970,7 +970,7 @@ public function testAvailabilityAndSearchCalendarsStatusBusyNoFBType(): void {
->willReturn($result);

$status = $this->service->processCalendarAvailability($user, $availability->serialize());
$this->assertEquals(new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY), $status);
$this->assertEquals(new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, customEmoji: IUserStatus::MEETING_ICON), $status);
}

public function testAvailabilityAndSearchCalendarsStatusBusy(): void {
Expand Down Expand Up @@ -1098,7 +1098,7 @@ public function testAvailabilityAndSearchCalendarsStatusBusy(): void {
->willReturn('In a meeting');

$status = $this->service->processCalendarAvailability($user, $availability->serialize());
$this->assertEquals(new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, 'In a meeting'), $status);
$this->assertEquals(new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, 'In a meeting', customEmoji: IUserStatus::MEETING_ICON), $status);
}

public function testAvailabilityAndSearchCalendarsStatusBusyUnavailable(): void {
Expand Down
Loading