Send moved-card notices as Nextcloud notifications too
Build package / xml-lint (push) Successful in 33s
Build package / php-lint (8.5) (push) Successful in 1m48s
Build package / php-lint (8.4) (push) Successful in 1m50s
Build package / unit-tests (push) Successful in 1m52s
Build package / package (push) Successful in 1m38s

Each workflow now has a notify channel (mail, notification or both) next
to its recipient mode. Notifications go out per card through
OCP\Notification\IManager, so they show up in the bell and as push on the
mobile and desktop clients. Without the notifications app nothing is sent.
The one-off "workflow disabled" notice is sent as a notification as well.

Existing workflows keep mail (migration Version1004). Bumps to 35.3.0 so
the migration runs.
This commit is contained in:
Patrick Niebeling committed 2026-09-30 12:31:01 +02:00
1 parent 35903772eb
commit 7612344726
19 files changed
+585 -70

No files matched your search

+45 -22
View File
@@ -39,16 +39,19 @@ class WorkflowRunner {
private ?IUser $impersonationRestore = null;
/**
* Moved cards per recipient uid, collected over the whole run.
* Moved cards per recipient uid, collected over the whole run. Each
* entry's workflow says whether it goes out by mail, as a Nextcloud
* notification, or both.
*
* @var array<string, list<array{workflow: Workflow, card: Card, targets: array{board: string, sourceStack: string, targetStack: string}}>>
*/
private array $pendingMails = [];
private array $pendingNotices = [];
public function __construct(
private WorkflowMapper $workflowMapper,
private DeckIntegrationService $deckService,
private NotificationMailer $mailer,
private NotificationPusher $pusher,
private IUserManager $userManager,
private IUserSession $userSession,
private IRootFolder $rootFolder,
@@ -63,7 +66,7 @@ class WorkflowRunner {
return;
}
$this->pendingMails = [];
$this->pendingNotices = [];
$byUser = [];
$owners = [];
foreach ($workflows as $workflow) {
@@ -89,7 +92,7 @@ class WorkflowRunner {
}
}
$this->sendPendingMails();
$this->sendPendingNotices();
}
/**
@@ -246,22 +249,22 @@ class WorkflowRunner {
$this->workflowMapper->update($workflow);
if ($moved !== []) {
$this->queueMails($user, $workflow, $moved);
$this->queueNotices($user, $workflow, $moved);
}
}
/**
* Works out who hears about the moved cards and parks them in
* $pendingMails; sendPendingMails() delivers them once per recipient at
* the end of the run.
* $pendingNotices; sendPendingNotices() delivers them at the end of the
* run. The recipients are the same for mail and notification.
*
* Mails are decoration, not part of the move: nothing in here may fail
* the workflow, and if Deck cannot say who wants a mail, nobody gets one
* Notices are decoration, not part of the move: nothing in here may fail
* the workflow, and if Deck cannot say who wants one, nobody gets one
* rather than people who asked Deck for silence.
*
* @param Card[] $cards
*/
private function queueMails(IUser $owner, Workflow $workflow, array $cards): void {
private function queueNotices(IUser $owner, Workflow $workflow, array $cards): void {
$cardIds = array_map(static fn (Card $card): int => (int)$card->getId(), $cards);
switch ($workflow->getNotifyMode()) {
@@ -272,7 +275,7 @@ class WorkflowRunner {
try {
$recipients = $this->deckService->findNotificationRecipients($workflow->getBoardId(), $cardIds);
} catch (DeckUnavailableException $e) {
$this->logger->warning('No mails for workflow {id}: ' . $e->getMessage(), [
$this->logger->warning('No mails or notifications for workflow {id}: ' . $e->getMessage(), [
'app' => 'workflow_deck_automation',
'id' => $workflow->getId(),
]);
@@ -291,7 +294,7 @@ class WorkflowRunner {
foreach ($cards as $card) {
foreach ($recipients[(int)$card->getId()] ?? [] as $uid) {
$this->pendingMails[$uid][] = [
$this->pendingNotices[$uid][] = [
'workflow' => $workflow,
'card' => $card,
'targets' => $targets,
@@ -303,13 +306,15 @@ class WorkflowRunner {
/**
* One mail per recipient and run, however many cards and workflows it
* covers — a board with twenty overdue cards and fifteen members would
* otherwise send three hundred mails at once.
* otherwise send three hundred mails at once. Nextcloud notifications
* go out per card instead: they are grouped in the bell anyway, and each
* one links its card.
*
* Runs after the per-user loop, so no impersonation is active any more.
*/
private function sendPendingMails(): void {
$pending = $this->pendingMails;
$this->pendingMails = [];
private function sendPendingNotices(): void {
$pending = $this->pendingNotices;
$this->pendingNotices = [];
foreach ($pending as $uid => $entries) {
// Same reasoning as in run(): nothing may escape.
@@ -318,9 +323,24 @@ class WorkflowRunner {
if ($recipient === null || !$recipient->isEnabled()) {
continue;
}
$this->mailer->sendCardsMovedNotification($recipient, $entries);
$mailEntries = array_values(array_filter(
$entries,
static fn (array $entry): bool => NotificationPolicy::sendsMail((string)$entry['workflow']->getNotifyChannel()),
));
$notificationEntries = array_values(array_filter(
$entries,
static fn (array $entry): bool => NotificationPolicy::sendsNotification((string)$entry['workflow']->getNotifyChannel()),
));
if ($mailEntries !== []) {
$this->mailer->sendCardsMovedNotification($recipient, $mailEntries);
}
if ($notificationEntries !== []) {
$this->pusher->sendCardsMovedNotification($recipient, $notificationEntries);
}
} catch (Throwable $e) {
$this->logger->error('Could not send the moved-cards mail to {user}: ' . $e->getMessage(), [
$this->logger->error('Could not notify {user} about moved cards: ' . $e->getMessage(), [
'app' => 'workflow_deck_automation',
'user' => $uid,
'exception' => $e,
@@ -370,11 +390,14 @@ class WorkflowRunner {
'target' => $brokenTarget,
]);
// Sent to the owner regardless of the workflow's notifyMode: that is
// about moved cards, this is a one-off notice that the automation the
// user configured has been switched off. It stays one-off because a
// disabled workflow is not picked up again.
// Sent to the owner regardless of the workflow's notifyMode and
// notifyChannel: those are about moved cards, this is a one-off notice
// that the automation the user configured has been switched off — by
// mail as always, and in the bell too if the notifications app is
// there. It stays one-off because a disabled workflow is not picked
// up again.
$this->mailer->sendWorkflowDisabledNotification($user, $workflow, $brokenTarget);
$this->pusher->sendWorkflowDisabledNotification($user, $workflow, $brokenTarget);
}
private function moveCard(Workflow $workflow, Card $card): bool {