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

+146
View File
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace OCA\WorkflowDeckAutomation\Notification;
use OCA\WorkflowDeckAutomation\AppInfo\Application;
use OCA\WorkflowDeckAutomation\Service\NotificationMailer;
use OCA\WorkflowDeckAutomation\Service\NotificationPusher;
use OCP\IURLGenerator;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
use OCP\Notification\UnknownNotificationException;
/**
* Turns the values NotificationPusher stored into text. German only, like
* the rest of the app — the language Nextcloud asks for is ignored.
*
* Deliberately asks Deck nothing: the stored titles are the ones from the
* moment the card moved, and a Deck lookup here would run as whoever
* happens to fetch notifications (see CLAUDE.md on Deck's frozen uid).
*/
class Notifier implements INotifier {
public function __construct(
private IURLGenerator $urlGenerator,
) {
}
public function getID(): string {
return Application::APP_ID;
}
public function getName(): string {
return 'Deck Workflow-Automatisierung';
}
public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getApp() !== Application::APP_ID) {
throw new UnknownNotificationException();
}
$notification->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath(Application::APP_ID, 'app-dark.svg')));
$params = $notification->getSubjectParameters();
switch ($notification->getSubject()) {
case NotificationPusher::SUBJECT_CARD_MOVED:
$this->prepareCardMoved($notification, $params);
return $notification;
case NotificationPusher::SUBJECT_WORKFLOW_DISABLED:
$this->prepareWorkflowDisabled($notification, $params);
return $notification;
default:
throw new UnknownNotificationException();
}
}
private function prepareCardMoved(INotification $notification, array $params): void {
$cardId = $notification->getObjectId();
$boardId = (string)($params['boardId'] ?? '');
$boardTitle = (string)($params['boardTitle'] ?? '');
$targetStackTitle = (string)($params['targetStackTitle'] ?? '');
$cardUrl = $this->urlGenerator->getAbsoluteURL('/apps/deck/board/' . $boardId . '/card/' . $cardId);
// Every title is user input and goes in as a rich object, never into
// the template itself — a title containing "{…}" would otherwise be
// read as a placeholder.
$objects = [
'deck-card' => [
'type' => 'deck-card',
'id' => $cardId,
'name' => (string)($params['cardTitle'] ?? ''),
'boardname' => $boardTitle,
'stackname' => $targetStackTitle,
'link' => $cardUrl,
],
'deck-board' => [
'type' => 'deck-board',
'id' => $boardId,
'name' => $boardTitle,
'link' => $this->urlGenerator->getAbsoluteURL('/apps/deck/board/' . $boardId),
],
'source-stack' => [
'type' => 'highlight',
'id' => (string)($params['sourceStackId'] ?? ''),
'name' => (string)($params['sourceStackTitle'] ?? ''),
],
'target-stack' => [
'type' => 'highlight',
'id' => (string)($params['targetStackId'] ?? ''),
'name' => $targetStackTitle,
],
];
$notification->setLink($cardUrl);
self::setText(
$notification,
'{deck-card} wurde nach {target-stack} verschoben',
'Die Karte auf {deck-board} ist überfällig und wurde automatisch von {source-stack} nach {target-stack} verschoben.',
$objects,
);
}
private function prepareWorkflowDisabled(INotification $notification, array $params): void {
$settingsUrl = $this->urlGenerator->getAbsoluteURL('/settings/user/' . Application::APP_ID);
$objects = [
'workflow' => [
'type' => 'highlight',
'id' => $notification->getObjectId(),
'name' => (string)($params['workflowTitle'] ?? ''),
'link' => $settingsUrl,
],
];
$notification->setLink($settingsUrl);
self::setText(
$notification,
'Workflow {workflow} wurde deaktiviert',
sprintf(
'Er wurde automatisch ausgeschaltet, weil %s, und verschiebt keine Karten mehr. Lösche ihn oder stelle ein vorhandenes Board und vorhandene Stapel ein, um ihn wieder zu aktivieren.',
NotificationMailer::describeBrokenTarget((string)($params['brokenTarget'] ?? '')),
),
$objects,
);
}
/**
* Sets the rich texts and their plain fallback (used by clients without
* rich text support), with titles in German quotes there.
*
* @param array<string, array{name: string}> $objects
*/
private static function setText(INotification $notification, string $subject, string $message, array $objects): void {
$plain = [];
foreach ($objects as $key => $object) {
$plain['{' . $key . '}'] = '„' . $object['name'] . '“';
}
// strtr() replaces in a single pass, so a title that itself contains
// a placeholder is left as it is.
$notification->setParsedSubject(strtr($subject, $plain))
->setRichSubject($subject, $objects)
->setParsedMessage(strtr($message, $plain))
->setRichMessage($message, $objects);
}
}