Notify board members about moved cards according to their Deck setting
Replaces the yes/no notify_email flag with notify_mode:
- off: no mail
- owner: only the board owner, as before (migrated from notify_email=1)
- board: everyone with access to the board, filtered by their own Deck
"Notifications" setting for it (board:<id>:notify-due: off /
assigned / all), mirroring Deck's NotificationHelper rules
Recipients are resolved via PermissionService::findUsers() (owner, ACL,
groups and circles expanded), AssignmentMapper::isUserAssigned() and
IConfig, none of which reads Deck's frozen $userId. If they cannot be
resolved, no mail is sent rather than one to the wrong people.
Moved cards are collected over the whole run and each recipient gets a
single digest mail afterwards, in their own language.
Bumps to 35.2.0 so the migrations run.
This commit is contained in:
1 parent
27717205cd
commit
578b235f08
14 files changed
+579
-100
No files matched your search
@@ -38,6 +38,13 @@ class WorkflowRunner {
|
||||
|
||||
private ?IUser $impersonationRestore = null;
|
||||
|
||||
/**
|
||||
* Moved cards per recipient uid, collected over the whole run.
|
||||
*
|
||||
* @var array<string, list<array{workflow: Workflow, card: Card, targets: array{board: string, sourceStack: string, targetStack: string}}>>
|
||||
*/
|
||||
private array $pendingMails = [];
|
||||
|
||||
public function __construct(
|
||||
private WorkflowMapper $workflowMapper,
|
||||
private DeckIntegrationService $deckService,
|
||||
@@ -56,6 +63,7 @@ class WorkflowRunner {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->pendingMails = [];
|
||||
$byUser = [];
|
||||
$owners = [];
|
||||
foreach ($workflows as $workflow) {
|
||||
@@ -80,6 +88,8 @@ class WorkflowRunner {
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->sendPendingMails();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,10 +221,7 @@ class WorkflowRunner {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolved at most once per run, and only if a card actually moves
|
||||
// and the workflow wants a mail — this is decoration, not a check.
|
||||
$targets = null;
|
||||
|
||||
$moved = [];
|
||||
$now = $this->timeFactory->getDateTime();
|
||||
|
||||
foreach ($cards as $card) {
|
||||
@@ -230,22 +237,96 @@ class WorkflowRunner {
|
||||
'target' => $workflow->getTargetStackId(),
|
||||
]);
|
||||
|
||||
if (!$this->moveCard($workflow, $card)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($workflow->getNotifyEmail()) {
|
||||
$targets ??= $this->deckService->describeTargets(
|
||||
$workflow->getBoardId(),
|
||||
$workflow->getSourceStackId(),
|
||||
$workflow->getTargetStackId(),
|
||||
);
|
||||
$this->mailer->sendCardMovedNotification($user, $workflow, $card, $targets);
|
||||
if ($this->moveCard($workflow, $card)) {
|
||||
$moved[] = $card;
|
||||
}
|
||||
}
|
||||
|
||||
$workflow->setLastRun($this->timeFactory->getDateTime());
|
||||
$this->workflowMapper->update($workflow);
|
||||
|
||||
if ($moved !== []) {
|
||||
$this->queueMails($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.
|
||||
*
|
||||
* 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
|
||||
* rather than people who asked Deck for silence.
|
||||
*
|
||||
* @param Card[] $cards
|
||||
*/
|
||||
private function queueMails(IUser $owner, Workflow $workflow, array $cards): void {
|
||||
$cardIds = array_map(static fn (Card $card): int => (int)$card->getId(), $cards);
|
||||
|
||||
switch ($workflow->getNotifyMode()) {
|
||||
case NotificationPolicy::MODE_OWNER:
|
||||
$recipients = array_fill_keys($cardIds, [$owner->getUID()]);
|
||||
break;
|
||||
case NotificationPolicy::MODE_BOARD:
|
||||
try {
|
||||
$recipients = $this->deckService->findNotificationRecipients($workflow->getBoardId(), $cardIds);
|
||||
} catch (DeckUnavailableException $e) {
|
||||
$this->logger->warning('No mails for workflow {id}: ' . $e->getMessage(), [
|
||||
'app' => 'workflow_deck_automation',
|
||||
'id' => $workflow->getId(),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
$targets = $this->deckService->describeTargets(
|
||||
$workflow->getBoardId(),
|
||||
$workflow->getSourceStackId(),
|
||||
$workflow->getTargetStackId(),
|
||||
);
|
||||
|
||||
foreach ($cards as $card) {
|
||||
foreach ($recipients[(int)$card->getId()] ?? [] as $uid) {
|
||||
$this->pendingMails[$uid][] = [
|
||||
'workflow' => $workflow,
|
||||
'card' => $card,
|
||||
'targets' => $targets,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Runs after the per-user loop, so no impersonation is active any more.
|
||||
*/
|
||||
private function sendPendingMails(): void {
|
||||
$pending = $this->pendingMails;
|
||||
$this->pendingMails = [];
|
||||
|
||||
foreach ($pending as $uid => $entries) {
|
||||
// Same reasoning as in run(): nothing may escape.
|
||||
try {
|
||||
$recipient = $this->userManager->get((string)$uid);
|
||||
if ($recipient === null || !$recipient->isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
$this->mailer->sendCardsMovedNotification($recipient, $entries);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error('Could not send the moved-cards mail to {user}: ' . $e->getMessage(), [
|
||||
'app' => 'workflow_deck_automation',
|
||||
'user' => $uid,
|
||||
'exception' => $e,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,7 +370,7 @@ class WorkflowRunner {
|
||||
'target' => $brokenTarget,
|
||||
]);
|
||||
|
||||
// Sent regardless of the workflow's notifyEmail setting: that flag is
|
||||
// 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.
|
||||
|
||||
Reference in new issue
Block a user