Skip to content

Commit cc0d81e

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 4a72442 commit cc0d81e

6 files changed

Lines changed: 203 additions & 13 deletions

File tree

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@
413413
'OCA\\DAV\\Migration\\Version1034Date20250813093701' => $baseDir . '/../lib/Migration/Version1034Date20250813093701.php',
414414
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => $baseDir . '/../lib/Migration/Version1036Date20251202000000.php',
415415
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => $baseDir . '/../lib/Migration/Version1038Date20260302000000.php',
416+
'OCA\\DAV\\Migration\\Version1038Date20260828000000' => $baseDir . '/../lib/Migration/Version1038Date20260828000000.php',
416417
'OCA\\DAV\\Migration\\Version1039Date20260408000000' => $baseDir . '/../lib/Migration/Version1039Date20260408000000.php',
417418
'OCA\\DAV\\Migration\\Version1040Date20260805000000' => $baseDir . '/../lib/Migration/Version1040Date20260805000000.php',
418419
'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
class ComposerStaticInitDAV
88
{
99
public static $prefixLengthsPsr4 = array (
10-
'O' =>
10+
'O' =>
1111
array (
1212
'OCA\\DAV\\' => 8,
1313
),
1414
);
1515

1616
public static $prefixDirsPsr4 = array (
17-
'OCA\\DAV\\' =>
17+
'OCA\\DAV\\' =>
1818
array (
1919
0 => __DIR__ . '/..' . '/../lib',
2020
),
@@ -428,6 +428,7 @@ class ComposerStaticInitDAV
428428
'OCA\\DAV\\Migration\\Version1034Date20250813093701' => __DIR__ . '/..' . '/../lib/Migration/Version1034Date20250813093701.php',
429429
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => __DIR__ . '/..' . '/../lib/Migration/Version1036Date20251202000000.php',
430430
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => __DIR__ . '/..' . '/../lib/Migration/Version1038Date20260302000000.php',
431+
'OCA\\DAV\\Migration\\Version1038Date20260828000000' => __DIR__ . '/..' . '/../lib/Migration/Version1038Date20260828000000.php',
431432
'OCA\\DAV\\Migration\\Version1039Date20260408000000' => __DIR__ . '/..' . '/../lib/Migration/Version1039Date20260408000000.php',
432433
'OCA\\DAV\\Migration\\Version1040Date20260805000000' => __DIR__ . '/..' . '/../lib/Migration/Version1040Date20260805000000.php',
433434
'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php',

apps/dav/lib/CalDAV/CalDavBackend.php

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use OCP\IConfig;
5151
use OCP\IDBConnection;
5252
use OCP\IUserManager;
53+
use OCP\IUserSession;
5354
use OCP\Security\ISecureRandom;
5455
use Psr\Log\LoggerInterface;
5556
use RuntimeException;
@@ -153,6 +154,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
153154
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
154155
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarm-part-day' => ['default_alarm_pday', 'int'],
155156
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarm-full-day' => ['default_alarm_fday', 'int'],
157+
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}ignore-reminders' => ['ignore_reminders', 'bool'],
156158
];
157159

158160
/**
@@ -220,6 +222,7 @@ public function __construct(
220222
private FederatedCalendarMapper $federatedCalendarMapper,
221223
ICacheFactory $cacheFactory,
222224
private bool $legacyEndpoint = false,
225+
private ?IUserSession $userSession = null,
223226
) {
224227
$this->publishStatusCache = $cacheFactory->createInMemory();
225228
}
@@ -383,6 +386,9 @@ public function getCalendarsForUser($principalUri) {
383386

384387
$fields = array_column($this->propertyMap, 0);
385388
$fields = array_map(function (string $field) {
389+
if ($field === 'ignore_reminders') {
390+
return 's.ignore_reminders';
391+
}
386392
return 'a.' . $field;
387393
}, $fields);
388394
$fields[] = 'a.id';
@@ -514,6 +520,12 @@ public function getUsersOwnCalendars($principalUri) {
514520
*/
515521
public function getPublicCalendars() {
516522
$fields = array_column($this->propertyMap, 0);
523+
$fields = array_map(function (string $field) {
524+
if ($field === 'ignore_reminders') {
525+
return 's.ignore_reminders';
526+
}
527+
return 'a.' . $field;
528+
}, $fields);
517529
$fields[] = 'a.id';
518530
$fields[] = 'a.uri';
519531
$fields[] = 'a.synctoken';
@@ -572,6 +584,12 @@ public function getPublicCalendars() {
572584
*/
573585
public function getPublicCalendar($uri) {
574586
$fields = array_column($this->propertyMap, 0);
587+
$fields = array_map(function (string $field) {
588+
if ($field === 'ignore_reminders') {
589+
return 's.ignore_reminders';
590+
}
591+
return 'a.' . $field;
592+
}, $fields);
575593
$fields[] = 'a.id';
576594
$fields[] = 'a.uri';
577595
$fields[] = 'a.synctoken';
@@ -905,14 +923,55 @@ public function updateCalendar($calendarId, PropPatch $propPatch) {
905923
break;
906924
}
907925
}
908-
[$calendarData, $shares] = $this->atomic(function () use ($calendarId, $newValues) {
909-
$query = $this->db->getQueryBuilder();
910-
$query->update('calendars');
911-
foreach ($newValues as $fieldName => $value) {
912-
$query->set($fieldName, $query->createNamedParameter($value));
926+
[$calendarData, $shares] = $this->atomic(function () use ($calendarId, &$newValues) {
927+
if (isset($newValues['ignore_reminders'])) {
928+
$rawVal = $newValues['ignore_reminders'];
929+
if (\is_bool($rawVal)) {
930+
$ignoreRemindersVal = $rawVal ? 1 : 0;
931+
} elseif (\is_string($rawVal)) {
932+
$ignoreRemindersVal = \in_array(strtolower(trim($rawVal)), ['1', 'true', 'yes'], true) ? 1 : 0;
933+
} else {
934+
$ignoreRemindersVal = (int)(bool)$rawVal;
935+
}
936+
937+
$user = $this->userSession?->getUser();
938+
if ($user !== null) {
939+
$principalUri = 'principals/users/' . $user->getUID();
940+
$principals = $this->principalBackend->getGroupMembership($principalUri, true);
941+
$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUri));
942+
$principals[] = $principalUri;
943+
944+
$qbCheck = $this->db->getQueryBuilder();
945+
$qbCheck->select('id')
946+
->from('dav_shares')
947+
->where($qbCheck->expr()->eq('resourceid', $qbCheck->createNamedParameter($calendarId)))
948+
->andWhere($qbCheck->expr()->in('principaluri', $qbCheck->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY)))
949+
->andWhere($qbCheck->expr()->eq('type', $qbCheck->createNamedParameter('calendar')));
950+
$shareId = $qbCheck->executeQuery()->fetchOne();
951+
if ($shareId !== false) {
952+
$qbUpdate = $this->db->getQueryBuilder();
953+
$qbUpdate->update('dav_shares')
954+
->set('ignore_reminders', $qbUpdate->createNamedParameter($ignoreRemindersVal))
955+
->where($qbUpdate->expr()->eq('id', $qbUpdate->createNamedParameter($shareId)));
956+
$qbUpdate->executeStatement();
957+
unset($newValues['ignore_reminders']);
958+
} else {
959+
$newValues['ignore_reminders'] = $ignoreRemindersVal;
960+
}
961+
} else {
962+
$newValues['ignore_reminders'] = $ignoreRemindersVal;
963+
}
964+
}
965+
966+
if (!empty($newValues)) {
967+
$query = $this->db->getQueryBuilder();
968+
$query->update('calendars');
969+
foreach ($newValues as $fieldName => $value) {
970+
$query->set($fieldName, $query->createNamedParameter($value));
971+
}
972+
$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
973+
$query->executeStatement();
913974
}
914-
$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
915-
$query->executeStatement();
916975

917976
$this->addChanges($calendarId, [''], 2);
918977

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

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OCA\DAV\Connector\Sabre\Principal;
1616
use OCP\AppFramework\Utility\ITimeFactory;
1717
use OCP\IConfig;
18+
use OCP\IDBConnection;
1819
use OCP\IGroup;
1920
use OCP\IGroupManager;
2021
use OCP\IUser;
@@ -58,6 +59,7 @@ public function __construct(
5859
private IConfig $config,
5960
private LoggerInterface $logger,
6061
private Principal $principalConnector,
62+
private ?IDBConnection $db = null,
6163
) {
6264
}
6365

@@ -123,15 +125,30 @@ public function processReminders() :void {
123125
continue;
124126
}
125127

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

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

137154
$userPrincipalEmailAddresses = [];
@@ -834,4 +851,35 @@ private function getCalendarTimeZone(int $calendarid): DateTimeZone {
834851
$vtimezone = $vtimezoneObj->VTIMEZONE;
835852
return $vtimezone->getTimeZone();
836853
}
854+
855+
private function isReminderIgnored(int $calendarId, string $principalUri): bool {
856+
if ($this->db === null) {
857+
return false;
858+
}
859+
860+
// 1. Check dav_shares for sharee setting
861+
$qbShares = $this->db->getQueryBuilder();
862+
$qbShares->select('ignore_reminders')
863+
->from('dav_shares')
864+
->where($qbShares->expr()->eq('resourceid', $qbShares->createNamedParameter($calendarId)))
865+
->andWhere($qbShares->expr()->eq('principaluri', $qbShares->createNamedParameter($principalUri)))
866+
->andWhere($qbShares->expr()->eq('type', $qbShares->createNamedParameter('calendar')));
867+
$ignoredShare = $qbShares->executeQuery()->fetchOne();
868+
if ($ignoredShare !== false) {
869+
return (bool)$ignoredShare;
870+
}
871+
872+
// 2. Check calendars table for owner setting
873+
$qbCal = $this->db->getQueryBuilder();
874+
$qbCal->select('ignore_reminders')
875+
->from('calendars')
876+
->where($qbCal->expr()->eq('id', $qbCal->createNamedParameter($calendarId)))
877+
->andWhere($qbCal->expr()->eq('principaluri', $qbCal->createNamedParameter($principalUri)));
878+
$ignoredCal = $qbCal->executeQuery()->fetchOne();
879+
if ($ignoredCal !== false) {
880+
return (bool)$ignoredCal;
881+
}
882+
883+
return false;
884+
}
837885
}
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
@@ -2477,4 +2477,35 @@ public function testGetFederatedCalendarByUriHidesPendingCalendar(): void {
24772477

24782478
$this->assertNull($this->backend->getFederatedCalendarByUri(self::UNIT_TEST_USER, 'federated-cal'));
24792479
}
2480+
2481+
public function testIgnoreRemindersProperty(): void {
2482+
$calendarId = $this->backend->createCalendar(self::UNIT_TEST_USER, 'IgnoreRemindersTest', []);
2483+
2484+
// Default should be false
2485+
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
2486+
$this->assertFalse((bool)($calendars[0]['{http://nextcloud.com/ns}ignore-reminders'] ?? false));
2487+
2488+
// Update to true ('1')
2489+
$patch = new PropPatch([
2490+
'{http://nextcloud.com/ns}ignore-reminders' => '1'
2491+
]);
2492+
$this->backend->updateCalendar($calendarId, $patch);
2493+
$patch->commit();
2494+
2495+
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
2496+
$this->assertTrue((bool)($calendars[0]['{http://nextcloud.com/ns}ignore-reminders'] ?? false));
2497+
2498+
// Update to false ('0')
2499+
$patch = new PropPatch([
2500+
'{http://nextcloud.com/ns}ignore-reminders' => '0'
2501+
]);
2502+
$this->backend->updateCalendar($calendarId, $patch);
2503+
$patch->commit();
2504+
2505+
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
2506+
$this->assertFalse((bool)($calendars[0]['{http://nextcloud.com/ns}ignore-reminders'] ?? false));
2507+
2508+
// Clean up
2509+
$this->backend->deleteCalendar($calendars[0]['id'], true);
2510+
}
24802511
}

0 commit comments

Comments
 (0)