Skip to content

Commit 4a06cd0

Browse files
committed
feat(dav): allow muting reminders per calendar
Add {http://nextcloud.com/ns}ignore-reminders CalDAV property and ignore_reminders DB column to calendars and dav_shares tables to allow per-user per-calendar notification muting. Signed-off-by: SoleroTG <github-29h@solero.quietmail.eu>
1 parent 35606bc commit 4a06cd0

6 files changed

Lines changed: 201 additions & 11 deletions

File tree

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@
390390
'OCA\\DAV\\Migration\\Version1034Date20250813093701' => $baseDir . '/../lib/Migration/Version1034Date20250813093701.php',
391391
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => $baseDir . '/../lib/Migration/Version1036Date20251202000000.php',
392392
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => $baseDir . '/../lib/Migration/Version1038Date20260302000000.php',
393+
'OCA\\DAV\\Migration\\Version1038Date20260828000000' => $baseDir . '/../lib/Migration/Version1038Date20260828000000.php',
393394
'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php',
394395
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php',
395396
'OCA\\DAV\\Paginate\\PaginateCache' => $baseDir . '/../lib/Paginate/PaginateCache.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ class ComposerStaticInitDAV
405405
'OCA\\DAV\\Migration\\Version1034Date20250813093701' => __DIR__ . '/..' . '/../lib/Migration/Version1034Date20250813093701.php',
406406
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => __DIR__ . '/..' . '/../lib/Migration/Version1036Date20251202000000.php',
407407
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => __DIR__ . '/..' . '/../lib/Migration/Version1038Date20260302000000.php',
408+
'OCA\\DAV\\Migration\\Version1038Date20260828000000' => __DIR__ . '/..' . '/../lib/Migration/Version1038Date20260828000000.php',
408409
'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php',
409410
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php',
410411
'OCA\\DAV\\Paginate\\PaginateCache' => __DIR__ . '/..' . '/../lib/Paginate/PaginateCache.php',

apps/dav/lib/CalDAV/CalDavBackend.php

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
use OCP\IConfig;
4949
use OCP\IDBConnection;
5050
use OCP\IUserManager;
51+
use OCP\IUserSession;
5152
use OCP\Security\ISecureRandom;
5253
use Psr\Log\LoggerInterface;
5354
use RuntimeException;
@@ -152,6 +153,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
152153
'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
153154
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
154155
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarm' => ['default_alarm', 'int'],
156+
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}ignore-reminders' => ['ignore_reminders', 'bool'],
155157
];
156158

157159
/**
@@ -219,6 +221,7 @@ public function __construct(
219221
private FederatedCalendarMapper $federatedCalendarMapper,
220222
ICacheFactory $cacheFactory,
221223
private bool $legacyEndpoint = false,
224+
private ?IUserSession $userSession = null,
222225
) {
223226
$this->publishStatusCache = $cacheFactory->createInMemory();
224227
}
@@ -381,6 +384,9 @@ public function getCalendarsForUser($principalUri) {
381384

382385
$fields = array_column($this->propertyMap, 0);
383386
$fields = array_map(function (string $field) {
387+
if ($field === 'ignore_reminders') {
388+
return 's.ignore_reminders';
389+
}
384390
return 'a.' . $field;
385391
}, $fields);
386392
$fields[] = 'a.id';
@@ -512,6 +518,12 @@ public function getUsersOwnCalendars($principalUri) {
512518
*/
513519
public function getPublicCalendars() {
514520
$fields = array_column($this->propertyMap, 0);
521+
$fields = array_map(function (string $field) {
522+
if ($field === 'ignore_reminders') {
523+
return 's.ignore_reminders';
524+
}
525+
return 'a.' . $field;
526+
}, $fields);
515527
$fields[] = 'a.id';
516528
$fields[] = 'a.uri';
517529
$fields[] = 'a.synctoken';
@@ -570,6 +582,12 @@ public function getPublicCalendars() {
570582
*/
571583
public function getPublicCalendar($uri) {
572584
$fields = array_column($this->propertyMap, 0);
585+
$fields = array_map(function (string $field) {
586+
if ($field === 'ignore_reminders') {
587+
return 's.ignore_reminders';
588+
}
589+
return 'a.' . $field;
590+
}, $fields);
573591
$fields[] = 'a.id';
574592
$fields[] = 'a.uri';
575593
$fields[] = 'a.synctoken';
@@ -901,14 +919,55 @@ public function updateCalendar($calendarId, PropPatch $propPatch) {
901919
break;
902920
}
903921
}
904-
[$calendarData, $shares] = $this->atomic(function () use ($calendarId, $newValues) {
905-
$query = $this->db->getQueryBuilder();
906-
$query->update('calendars');
907-
foreach ($newValues as $fieldName => $value) {
908-
$query->set($fieldName, $query->createNamedParameter($value));
922+
[$calendarData, $shares] = $this->atomic(function () use ($calendarId, &$newValues) {
923+
if (isset($newValues['ignore_reminders'])) {
924+
$rawVal = $newValues['ignore_reminders'];
925+
if (\is_bool($rawVal)) {
926+
$ignoreRemindersVal = $rawVal ? 1 : 0;
927+
} elseif (\is_string($rawVal)) {
928+
$ignoreRemindersVal = \in_array(strtolower(trim($rawVal)), ['1', 'true', 'yes'], true) ? 1 : 0;
929+
} else {
930+
$ignoreRemindersVal = (int)(bool)$rawVal;
931+
}
932+
933+
$user = $this->userSession?->getUser();
934+
if ($user !== null) {
935+
$principalUri = 'principals/users/' . $user->getUID();
936+
$principals = $this->principalBackend->getGroupMembership($principalUri, true);
937+
$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUri));
938+
$principals[] = $principalUri;
939+
940+
$qbCheck = $this->db->getQueryBuilder();
941+
$qbCheck->select('id')
942+
->from('dav_shares')
943+
->where($qbCheck->expr()->eq('resourceid', $qbCheck->createNamedParameter($calendarId)))
944+
->andWhere($qbCheck->expr()->in('principaluri', $qbCheck->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY)))
945+
->andWhere($qbCheck->expr()->eq('type', $qbCheck->createNamedParameter('calendar')));
946+
$shareId = $qbCheck->executeQuery()->fetchOne();
947+
if ($shareId !== false) {
948+
$qbUpdate = $this->db->getQueryBuilder();
949+
$qbUpdate->update('dav_shares')
950+
->set('ignore_reminders', $qbUpdate->createNamedParameter($ignoreRemindersVal))
951+
->where($qbUpdate->expr()->eq('id', $qbUpdate->createNamedParameter($shareId)));
952+
$qbUpdate->executeStatement();
953+
unset($newValues['ignore_reminders']);
954+
} else {
955+
$newValues['ignore_reminders'] = $ignoreRemindersVal;
956+
}
957+
} else {
958+
$newValues['ignore_reminders'] = $ignoreRemindersVal;
959+
}
960+
}
961+
962+
if (!empty($newValues)) {
963+
$query = $this->db->getQueryBuilder();
964+
$query->update('calendars');
965+
foreach ($newValues as $fieldName => $value) {
966+
$query->set($fieldName, $query->createNamedParameter($value));
967+
}
968+
$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
969+
$query->executeStatement();
909970
}
910-
$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
911-
$query->executeStatement();
912971

913972
$this->addChanges($calendarId, [''], 2);
914973

apps/dav/lib/CalDAV/Reminder/ReminderService.php

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use OCA\DAV\Connector\Sabre\Principal;
1515
use OCP\AppFramework\Utility\ITimeFactory;
1616
use OCP\IConfig;
17+
use OCP\IDBConnection;
1718
use OCP\IGroup;
1819
use OCP\IGroupManager;
1920
use OCP\IUser;
@@ -57,6 +58,7 @@ public function __construct(
5758
private IConfig $config,
5859
private LoggerInterface $logger,
5960
private Principal $principalConnector,
61+
private ?IDBConnection $db = null,
6062
) {
6163
}
6264

@@ -122,15 +124,30 @@ public function processReminders() :void {
122124
continue;
123125
}
124126

125-
if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedUsers', 'yes') === 'no') {
127+
if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedUsers', 'yes') === 'yes') {
126128
$users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
127129
} else {
128130
$users = [];
129131
}
130132

131-
$user = $this->getUserFromPrincipalURI($reminder['principaluri']);
132-
if ($user) {
133-
$users[] = $user;
133+
$ownerUser = $this->getUserFromPrincipalURI($reminder['principaluri']);
134+
if ($ownerUser !== null) {
135+
$users[] = $ownerUser;
136+
}
137+
138+
// Filter out any users who have muted reminders for this calendar
139+
$users = array_values(array_filter($users, function (IUser $u) use ($reminder): bool {
140+
$principalUri = 'principals/users/' . $u->getUID();
141+
return !$this->isReminderIgnored((int)$reminder['calendar_id'], $principalUri);
142+
}));
143+
144+
if (count($users) === 0) {
145+
$this->logger->debug('Reminder {id} is ignored by all recipient users for calendar {calendarId}', [
146+
'id' => $reminder['id'],
147+
'calendarId' => $reminder['calendar_id'],
148+
]);
149+
$this->deleteOrProcessNext($reminder, $vevent);
150+
continue;
134151
}
135152

136153
$userPrincipalEmailAddresses = [];
@@ -833,4 +850,35 @@ private function getCalendarTimeZone(int $calendarid): DateTimeZone {
833850
$vtimezone = $vtimezoneObj->VTIMEZONE;
834851
return $vtimezone->getTimeZone();
835852
}
853+
854+
private function isReminderIgnored(int $calendarId, string $principalUri): bool {
855+
if ($this->db === null) {
856+
return false;
857+
}
858+
859+
// 1. Check dav_shares for sharee setting
860+
$qbShares = $this->db->getQueryBuilder();
861+
$qbShares->select('ignore_reminders')
862+
->from('dav_shares')
863+
->where($qbShares->expr()->eq('resourceid', $qbShares->createNamedParameter($calendarId)))
864+
->andWhere($qbShares->expr()->eq('principaluri', $qbShares->createNamedParameter($principalUri)))
865+
->andWhere($qbShares->expr()->eq('type', $qbShares->createNamedParameter('calendar')));
866+
$ignoredShare = $qbShares->executeQuery()->fetchOne();
867+
if ($ignoredShare !== false) {
868+
return (bool)$ignoredShare;
869+
}
870+
871+
// 2. Check calendars table for owner setting
872+
$qbCal = $this->db->getQueryBuilder();
873+
$qbCal->select('ignore_reminders')
874+
->from('calendars')
875+
->where($qbCal->expr()->eq('id', $qbCal->createNamedParameter($calendarId)))
876+
->andWhere($qbCal->expr()->eq('principaluri', $qbCal->createNamedParameter($principalUri)));
877+
$ignoredCal = $qbCal->executeQuery()->fetchOne();
878+
if ($ignoredCal !== false) {
879+
return (bool)$ignoredCal;
880+
}
881+
882+
return false;
883+
}
836884
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\DAV\Migration;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\Migration\Attributes\AddColumn;
16+
use OCP\Migration\Attributes\ColumnType;
17+
use OCP\Migration\IOutput;
18+
use OCP\Migration\SimpleMigrationStep;
19+
use Override;
20+
21+
#[AddColumn(table: 'calendars', name: 'ignore_reminders', type: ColumnType::BOOLEAN)]
22+
#[AddColumn(table: 'dav_shares', name: 'ignore_reminders', type: ColumnType::BOOLEAN)]
23+
class Version1038Date20260828000000 extends SimpleMigrationStep {
24+
#[Override]
25+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
26+
/** @var ISchemaWrapper $schema */
27+
$schema = $schemaClosure();
28+
$modified = false;
29+
30+
$calendarsTable = $schema->getTable('calendars');
31+
if (!$calendarsTable->hasColumn('ignore_reminders')) {
32+
$calendarsTable->addColumn('ignore_reminders', Types::BOOLEAN, [
33+
'notnull' => false,
34+
'default' => false,
35+
]);
36+
$modified = true;
37+
}
38+
39+
$davSharesTable = $schema->getTable('dav_shares');
40+
if (!$davSharesTable->hasColumn('ignore_reminders')) {
41+
$davSharesTable->addColumn('ignore_reminders', Types::BOOLEAN, [
42+
'notnull' => false,
43+
'default' => false,
44+
]);
45+
$modified = true;
46+
}
47+
48+
return $modified ? $schema : null;
49+
}
50+
}

apps/dav/tests/unit/CalDAV/CalDavBackendTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,4 +1920,35 @@ public function testDefaultAlarmProperty(): void {
19201920
// Clean up
19211921
$this->backend->deleteCalendar($calendars[0]['id'], true);
19221922
}
1923+
1924+
public function testIgnoreRemindersProperty(): void {
1925+
$calendarId = $this->backend->createCalendar(self::UNIT_TEST_USER, 'IgnoreRemindersTest', []);
1926+
1927+
// Default should be false
1928+
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
1929+
$this->assertFalse((bool)($calendars[0]['{http://nextcloud.com/ns}ignore-reminders'] ?? false));
1930+
1931+
// Update to true ('1')
1932+
$patch = new PropPatch([
1933+
'{http://nextcloud.com/ns}ignore-reminders' => '1'
1934+
]);
1935+
$this->backend->updateCalendar($calendarId, $patch);
1936+
$patch->commit();
1937+
1938+
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
1939+
$this->assertTrue((bool)($calendars[0]['{http://nextcloud.com/ns}ignore-reminders'] ?? false));
1940+
1941+
// Update to false ('0')
1942+
$patch = new PropPatch([
1943+
'{http://nextcloud.com/ns}ignore-reminders' => '0'
1944+
]);
1945+
$this->backend->updateCalendar($calendarId, $patch);
1946+
$patch->commit();
1947+
1948+
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
1949+
$this->assertFalse((bool)($calendars[0]['{http://nextcloud.com/ns}ignore-reminders'] ?? false));
1950+
1951+
// Clean up
1952+
$this->backend->deleteCalendar($calendars[0]['id'], true);
1953+
}
19231954
}

0 commit comments

Comments
 (0)