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
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace OCA\WorkflowDeckAutomation\Service;
|
||||
|
||||
use OCA\Deck\Db\AssignmentMapper;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
@@ -14,6 +15,7 @@ use OCA\Deck\Service\PermissionService;
|
||||
use OCA\Deck\Service\StackService;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUser;
|
||||
use OCP\Server;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -66,6 +68,7 @@ class DeckIntegrationService {
|
||||
|
||||
public function __construct(
|
||||
private IAppManager $appManager,
|
||||
private IConfig $config,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
@@ -302,6 +305,81 @@ class DeckIntegrationService {
|
||||
return $owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Who should hear about each of the given (moved) cards, according to
|
||||
* every board member's own Deck notification setting for this board —
|
||||
* the "Notifications: none / assigned cards / all cards" menu, stored as
|
||||
* the user preference `deck` / `board:<id>:notify-due`. The decision
|
||||
* itself is NotificationPolicy::shouldNotify(), a copy of Deck's rules.
|
||||
*
|
||||
* Like findStackIds(), assembled only from pieces that take the users
|
||||
* explicitly and never consult Deck's frozen `$userId`:
|
||||
* PermissionService::findUsers() reads the owner and ACL through mappers
|
||||
* and expands groups and circles itself; AssignmentMapper is a plain
|
||||
* mapper that also resolves group/circle assignments; the preference is
|
||||
* read straight from IConfig.
|
||||
*
|
||||
* Throws on any failure, so the caller can log and skip the mail instead
|
||||
* of mailing a half-resolved or wrong set of people.
|
||||
*
|
||||
* @param int[] $cardIds
|
||||
* @return array<int, list<string>> card id => uids to notify
|
||||
* @throws DeckUnavailableException
|
||||
*/
|
||||
public function findNotificationRecipients(int $boardId, array $cardIds): array {
|
||||
try {
|
||||
$board = $this->resolve(BoardMapper::class)->find($boardId, false, true);
|
||||
$owner = $this->unwrapUid($board->getOwner());
|
||||
// Deck notifies the owner of a board nobody else can see about
|
||||
// every card: "assigned" means nothing there.
|
||||
$unshared = count($board->getAcl() ?? []) === 0;
|
||||
|
||||
// `true` bypasses PermissionService's per-process cache, which
|
||||
// would otherwise outlive ACL changes for the whole cron pass.
|
||||
$participants = $this->resolve(PermissionService::class)->findUsers($boardId, true);
|
||||
$assignments = $this->resolve(AssignmentMapper::class);
|
||||
|
||||
$settings = [];
|
||||
foreach ($participants as $uid => $participant) {
|
||||
// Users of other instances (FederatedUser) have no local
|
||||
// account and no preference to read.
|
||||
if (!is_a($participant, 'OCA\Deck\Db\User')) {
|
||||
continue;
|
||||
}
|
||||
// Numeric uids come back as integer array keys.
|
||||
$uid = (string)$uid;
|
||||
$settings[$uid] = (string)$this->config->getUserValue(
|
||||
$uid,
|
||||
self::DECK_APP_ID,
|
||||
'board:' . $boardId . ':notify-due',
|
||||
NotificationPolicy::DECK_DEFAULT,
|
||||
);
|
||||
}
|
||||
|
||||
$recipients = [];
|
||||
foreach ($cardIds as $cardId) {
|
||||
$recipients[$cardId] = [];
|
||||
foreach ($settings as $uid => $setting) {
|
||||
$uid = (string)$uid;
|
||||
$notify = NotificationPolicy::shouldNotify(
|
||||
$setting,
|
||||
$unshared && $uid === $owner,
|
||||
static fn (): bool => (bool)$assignments->isUserAssigned($cardId, $uid),
|
||||
);
|
||||
if ($notify) {
|
||||
$recipients[$cardId][] = $uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (DeckUnavailableException $e) {
|
||||
throw $e;
|
||||
} catch (Throwable $e) {
|
||||
throw new DeckUnavailableException('Could not resolve notification recipients for board ' . $boardId . ': ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return $recipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deck's own exception classes are referenced by name: `is_a()` with a
|
||||
* string simply returns false when the class does not exist, so a missing
|
||||
@@ -368,9 +446,10 @@ class DeckIntegrationService {
|
||||
public function getActiveCardsInStack(int $stackId): array {
|
||||
return $this->call(function () use ($stackId) {
|
||||
// Deliberately not enriched: CardService::enrichCards() only adds
|
||||
// labels, assignees and comment counts — none of which is used —
|
||||
// and runs BoardService::find(), a permission check against
|
||||
// Deck's frozen uid (see beginUserContext()).
|
||||
// labels, assignees and comment counts, and runs
|
||||
// BoardService::find(), a permission check against Deck's frozen
|
||||
// uid (see beginUserContext()). Assignments, where needed for
|
||||
// mails, come from findNotificationRecipients() instead.
|
||||
$cards = $this->resolve(CardMapper::class)->findAllByStack($stackId);
|
||||
|
||||
return array_values(array_filter($cards, static function (Card $card) {
|
||||
|
||||
Reference in new issue
Block a user