Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions app/Domain/Notifications/Services/Webhooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,12 @@ private function optedInEndpoints(array $userIds, bool $useCache): array

/**
* Narrows recipients to those who may receive a personal webhook for the
* project right now: webhook enabled with a URL, account active with
* notifications switched on, and access to the project — which must still
* exist. The only place these are checked: the ids include
* mention/collaborator bypasses that skipped the project's member filters,
* and queueToUsers() checks nothing but the opt-in. Access follows the
* project right now: webhook enabled with a URL, account active, and access
* to the project — which must still exist. The email switch
* (zp_user.notifications) is not checked: it governs email only. The only
* place these are checked: the ids include mention/collaborator bypasses
* that skipped the project's member filters, and queueToUsers() checks
* nothing but the opt-in. Access follows the
* project's own rules (team, client, everyone, admin/owner) for the
* recipient, never the session user.
*
Expand Down Expand Up @@ -218,8 +219,8 @@ private function eligibleEndpoints(int $projectId, array $userIds): array

/**
* Whether the user's account currently allows project notifications to reach
* them: it exists, is active, has notifications switched on, and can access
* the project. Re-reads the user row, past the repository's memo.
* their webhook: it exists, is active, and can access the project. The email
* switch plays no part. Re-reads the user row, past the repository's memo.
*
* @param int $userId The recipient.
* @param int $projectId The project the notification belongs to.
Expand All @@ -230,9 +231,7 @@ private function mayReceiveProjectNotifications(int $userId, int $projectId): bo
// with isUserAssignedToProject() below, access is judged by the current role too.
$user = $this->userRepo->getUser($userId, useCache: false);

if ($user === false
|| strtolower((string) ($user['status'] ?? '')) !== 'a'
|| (int) ($user['notifications'] ?? 0) === 0) {
if ($user === false || strtolower((string) ($user['status'] ?? '')) !== 'a') {
return false;
}

Expand Down
73 changes: 56 additions & 17 deletions app/Domain/Projects/Services/Projects.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,9 @@ public function getAllUserInfoToNotify($projectId): array
*
* Channels: queued email, project messengers, mobile push, in-app
* notifications and — last — queued rows for the personal webhooks of
* recipients who opted in (delivered later by the webhook queue).
* recipients who opted in (delivered later by the webhook queue). Webhooks
* ignore the email switch and include the author's own changes (see
* personalWebhookCandidates()).
*
* @param Notification $notification The notification object to send.
*
Expand All @@ -346,19 +348,26 @@ public function notifyProjectUsers(Notification $notification): void
$notification->url['url'] = app()->make(TrustedAppUrl::class)->rebase($notification->url['url']);
}

// Email
$users = $this->getUsersToNotify($notification->projectId);
// One read of the team serves email and personal webhooks.
$activeMembers = array_values(array_filter(
$this->projectRepository->getUsersAssignedToProject($notification->projectId) ?: [],
fn (array $member) => strtolower((string) $member['status']) === 'a'
));
$projectName = $this->getProjectName($notification->projectId);

// Exclude the author
$users = array_filter($users, function ($user) use ($notification) {
return $user != $notification->authorId;
}, ARRAY_FILTER_USE_BOTH);
$users = array_values($users);
// Email goes to members who kept email notifications on, never to the author.
$users = [];
foreach ($activeMembers as $member) {
if ($member['notifications'] != 0 && $member['id'] != $notification->authorId) {
$users[] = $member['id'];
}
}

$webhookCandidates = $this->personalWebhookCandidates($activeMembers, $notification);

// Batch-load notification preferences for all candidate users
// Batch-load notification preferences for every candidate of either channel
$settingKeys = [];
foreach ($users as $userId) {
foreach (array_unique(array_merge($users, $webhookCandidates)) as $userId) {
$settingKeys[] = 'usersettings.'.$userId.'.projectNotificationLevels';
$settingKeys[] = 'usersettings.'.$userId.'.projectMutedNotifications'; // legacy format
$settingKeys[] = 'usersettings.'.$userId.'.notificationEventTypes';
Expand Down Expand Up @@ -489,14 +498,21 @@ public function notifyProjectUsers(Notification $notification): void
*/
self::dispatch_event('notifyProjectUsers', ['type' => 'projectUpdate', 'module' => $notification->module, 'moduleId' => $entityId, 'message' => $notification->message, 'subject' => $notification->subject, 'users' => array_values($filteredUsersToNotify), 'url' => $notification->url['url']], 'leantime.domain.projects.services.projects.notifyProjectUsers');

// Personal webhooks go last, to the same filtered recipients as email (relevance,
// category, mentions, collaborators). This only queues one row per recipient: the
// scheduler's WebhookQueue posts later, so no endpoint is contacted during this
// request. Queueing reads only the recipients' opt-in; Webhooks checks each queued
// recipient's opt-in, account and project access itself when the row is posted. The
// catch is a last guard so this step can never break the dispatch path.
// Personal webhooks go last, filtered like email (relevance, category, mentions,
// collaborators) but from their own candidates: see personalWebhookCandidates(). This
// only queues one row per recipient: the scheduler's WebhookQueue posts later, so no
// endpoint is contacted during this request. Queueing reads only the recipients'
// opt-in; Webhooks checks each queued recipient's opt-in, account and project access
// itself when the row is posted. The catch is a last guard so this step can never
// break the dispatch path.
$webhookRecipients = $this->filterUsersByProjectRelevance($webhookCandidates, $notification, $preloadedSettings);
$webhookRecipients = $this->filterUsersByEventType($webhookRecipients, $notification->module, $preloadedSettings);
// The author is a candidate already, so a mention or collaboration never brings them back past their own filters.
$webhookRecipients = $this->addBypassRecipients($webhookRecipients, $mentionedUserIds, $notification->authorId);
$webhookRecipients = $this->addBypassRecipients($webhookRecipients, $collaboratorIds, $notification->authorId);

try {
$this->webhookService->queueToUsers($notification, $users);
$this->webhookService->queueToUsers($notification, $webhookRecipients);
} catch (\Throwable $e) {
Log::warning('Personal webhook dispatch failed', ['exception' => get_class($e)]);
}
Expand Down Expand Up @@ -777,6 +793,29 @@ private function extractCollaboratorIds(Notification $notification): array
return array_values(array_unique($ids));
}

/**
* Who may get a notification on their personal webhook before the relevance and category
* filters: every active team member and the author. Unlike email, the email switch
* (zp_user.notifications) plays no part, so a user can move their notifications from email to
* a webhook; and the author's own changes are included, since a webhook usually feeds an
* integration that keeps a full record of the user's work.
*
* @param array<int, array<string, mixed>> $activeMembers Active users assigned to the project.
* @param Notification $notification The notification being dispatched.
* @return array<int, int> Candidate user ids, without repeats.
*/
private function personalWebhookCandidates(array $activeMembers, Notification $notification): array
{
$candidates = array_map(fn (array $member) => (int) $member['id'], $activeMembers);

$authorId = (int) $notification->authorId;
if ($authorId > 0) {
$candidates[] = $authorId;
}

return array_values(array_unique($candidates));
}

/**
* Adds bypass recipients (e.g. mentions, collaborators) to a recipient list.
*
Expand Down
2 changes: 1 addition & 1 deletion app/Language/ar-SA.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2619,7 +2619,7 @@ label.notifications_my_work_description="المهام المسندة إليك و
label.notifications_muted="مكتوم"
label.notifications_muted_description="لا إشعارات باستثناء الإشارات المباشرة (@)"
label.webhook_notifications="ويب هوك شخصي"
label.webhook_notifications_description="أرسل إشعاراتك إلى نقطة نهاية HTTPS خاصة بك. لكل إشعار مشروع يحق لك تلقيه، يرسل Leantime طلب HTTP POST واحدًا بمحتوى JSON إلى هذا الرابط. تُرسل الطلبات في الخلفية وقد يستغرق وصولها عدة دقائق، ولا تُعاد محاولة الطلب الذي يفشل. تحدد إعداداتك في تبويب الإشعارات (تلقي الإشعارات والفئات ومستويات كل مشروع) الإشعاراتِ المؤهلة. لا يرسل الويب هوك إلا عندما يكون مفعّلاً ومحددًا له رابط. يبقى رابطك محفوظًا أثناء تعطيل الويب هوك — امسح الرابط لإزالته."
label.webhook_notifications_description="أرسل إشعاراتك إلى نقطة نهاية HTTPS خاصة بك. لكل إشعار مشروع يحق لك تلقيه، يرسل Leantime طلب HTTP POST واحدًا بمحتوى JSON إلى هذا الرابط. تُرسل الطلبات في الخلفية وقد يستغرق وصولها عدة دقائق، ولا تُعاد محاولة الطلب الذي يفشل. تحدد الفئات ومستويات كل مشروع في تبويب الإشعارات الإشعاراتِ المؤهلة، بما في ذلك التغييرات التي تجريها بنفسك. لا يؤدي إيقاف إشعارات البريد الإلكتروني إلى إيقاف الويب هوك. لا يرسل الويب هوك إلا عندما يكون مفعّلاً ومحددًا له رابط. يبقى رابطك محفوظًا أثناء تعطيل الويب هوك — امسح الرابط لإزالته."
label.enable_webhook_notifications="تفعيل إشعارات الويب هوك"
label.webhook_url_hint="HTTPS فقط. اتركه فارغًا لإزالة الويب هوك."
label.default_notification_types="أنواع الإشعارات الافتراضية للمستخدمين الجدد"
Expand Down
2 changes: 1 addition & 1 deletion app/Language/cs-CZ.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2622,7 +2622,7 @@ label.notifications_my_work_description="Pouze úkoly přiřazené vám, vaše p
label.notifications_muted="Ztlumeno"
label.notifications_muted_description="Žádná oznámení kromě přímých @zmínek"
label.webhook_notifications="Osobní webhook"
label.webhook_notifications_description="Posílejte svá oznámení na vlastní HTTPS endpoint. Pro každé oznámení projektu, které byste měli dostat, odešle Leantime na tuto URL jeden požadavek HTTP POST s tělem ve formátu JSON. Požadavky se odesílají na pozadí a jejich doručení může trvat několik minut; neúspěšný požadavek se neopakuje. O tom, která oznámení se odešlou, rozhodují vaše nastavení na kartě Oznámení (příjem oznámení, kategorie a úrovně jednotlivých projektů). Webhook odesílá pouze tehdy, je-li zapnutý a je nastavena URL. Vaše URL zůstane uložená i při vypnutém webhooku — chcete-li ji odstranit, vymažte ji."
label.webhook_notifications_description="Posílejte svá oznámení na vlastní HTTPS endpoint. Pro každé oznámení projektu, které byste měli dostat, odešle Leantime na tuto URL jeden požadavek HTTP POST s tělem ve formátu JSON. Požadavky se odesílají na pozadí a jejich doručení může trvat několik minut; neúspěšný požadavek se neopakuje. O tom, která oznámení se odešlou, rozhodují kategorie a úrovně jednotlivých projektů na kartě Oznámení; odesílají se i změny, které provedete sami. Vypnutí e-mailových oznámení webhook nezastaví. Webhook odesílá pouze tehdy, je-li zapnutý a je nastavena URL. Vaše URL zůstane uložená i při vypnutém webhooku — chcete-li ji odstranit, vymažte ji."
label.enable_webhook_notifications="Zapnout oznámení přes webhook"
label.webhook_url_hint="Pouze HTTPS. Ponecháním prázdného pole webhook odstraníte."
label.default_notification_types="Výchozí typy oznámení pro nové uživatele"
Expand Down
4 changes: 2 additions & 2 deletions app/Language/da-DK.ini
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ label.project_assignment="Projekt Tildeling"
label.overview="Overblik"
label.active="Aktiv"
label.inactive="Inaktiv"
label.receive_notifications="Modtag notifikationer"
label.receive_notifications="Modtag notifikationer på e-mail"
label.upload="Upload"
label.ondate="på"
label.label="Mærkat"
Expand Down Expand Up @@ -2621,7 +2621,7 @@ label.notifications_my_work_description="Kun opgaver tildelt dig, dine elementer
label.notifications_muted="Lyd slået fra"
label.notifications_muted_description="Ingen notifikationer undtagen direkte @omtaler"
label.webhook_notifications="Personlig webhook"
label.webhook_notifications_description="Send dine notifikationer til dit eget HTTPS-endpoint. For hver projektnotifikation, du er berettiget til, sender Leantime én HTTP POST-anmodning med en JSON-body til denne URL. Anmodninger sendes i baggrunden og kan tage flere minutter om at nå frem; en anmodning, der fejler, bliver ikke forsøgt igen. Dine indstillinger under fanen Notifikationer (modtagelse af notifikationer, kategorier og niveauer pr. projekt) afgør, hvilke notifikationer der er berettigede. Webhooken sender kun, mens den er slået til, og en URL er angivet. Din URL forbliver gemt, mens webhooken er slået fra — ryd URL'en for at fjerne den."
label.webhook_notifications_description="Send dine notifikationer til dit eget HTTPS-endpoint. For hver projektnotifikation, du er berettiget til, sender Leantime én HTTP POST-anmodning med en JSON-body til denne URL. Anmodninger sendes i baggrunden og kan tage flere minutter om at nå frem; en anmodning, der fejler, bliver ikke forsøgt igen. Kategorierne og niveauerne pr. projekt under fanen Notifikationer afgør, hvilke notifikationer der er berettigede, også ændringer du selv foretager. Hvis du slår e-mailnotifikationer fra, stopper webhooken ikke. Webhooken sender kun, mens den er slået til, og en URL er angivet. Din URL forbliver gemt, mens webhooken er slået fra — ryd URL'en for at fjerne den."
label.enable_webhook_notifications="Slå webhook-notifikationer til"
label.webhook_url_hint="Kun HTTPS. Lad stå tomt for at fjerne webhooken."
label.default_notification_types="Standardnotifikationstyper for nye brugere"
Expand Down
4 changes: 2 additions & 2 deletions app/Language/de-CH.ini
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,7 @@ label.project_assignment = "Projekte zuweisen"
label.overview = "Übersicht"
label.active = "aktiv"
label.inactive = "inaktiv"
label.receive_notifications = "Benachrichtigungen erhalten"
label.receive_notifications = "E-Mail-Benachrichtigungen erhalten"
label.upload = "Hochladen"
label.ondate = "am"
label.label = "Etikett"
Expand Down Expand Up @@ -1844,7 +1844,7 @@ label.notifications_my_work_description="Nur Ihnen zugewiesene Aufgaben, Ihre El
label.notifications_muted="Stummgeschaltet"
label.notifications_muted_description="Keine Benachrichtigungen ausser direkten @Erwähnungen"
label.webhook_notifications="Persönlicher Webhook"
label.webhook_notifications_description="Senden Sie Ihre Benachrichtigungen an Ihren eigenen HTTPS-Endpunkt. Für jede Projektbenachrichtigung, die für Sie infrage kommt, sendet Leantime eine HTTP-POST-Anfrage mit einem JSON-Body an diese URL. Die Anfragen werden im Hintergrund gesendet und können einige Minuten bis zur Zustellung benötigen; fehlgeschlagene Anfragen werden nicht wiederholt. Welche Benachrichtigungen infrage kommen, bestimmen Ihre Einstellungen im Tab Benachrichtigungen (Benachrichtigungsempfang, Kategorien und Stufen pro Projekt). Der Webhook sendet nur, solange er aktiviert und eine URL gesetzt ist. Ihre URL bleibt gespeichert, während der Webhook deaktiviert ist – leeren Sie die URL, um sie zu entfernen."
label.webhook_notifications_description="Senden Sie Ihre Benachrichtigungen an Ihren eigenen HTTPS-Endpunkt. Für jede Projektbenachrichtigung, die für Sie infrage kommt, sendet Leantime eine HTTP-POST-Anfrage mit einem JSON-Body an diese URL. Die Anfragen werden im Hintergrund gesendet und können einige Minuten bis zur Zustellung benötigen; fehlgeschlagene Anfragen werden nicht wiederholt. Welche Benachrichtigungen infrage kommen, bestimmen die Kategorien und Stufen pro Projekt im Tab Benachrichtigungen, auch für Ihre eigenen Änderungen. Wenn Sie E-Mail-Benachrichtigungen ausschalten, sendet der Webhook trotzdem. Der Webhook sendet nur, solange er aktiviert und eine URL gesetzt ist. Ihre URL bleibt gespeichert, während der Webhook deaktiviert ist – leeren Sie die URL, um sie zu entfernen."
label.enable_webhook_notifications="Webhook-Benachrichtigungen aktivieren"
label.webhook_url_hint="Nur HTTPS. Leer lassen, um den Webhook zu entfernen."
label.default_notification_types="Standard-Benachrichtigungsarten für neue Benutzer"
Expand Down
4 changes: 2 additions & 2 deletions app/Language/de-DE-inf.ini
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ label.project_assignment="Projekte zuweisen"
label.overview="Übersicht"
label.active="aktiv"
label.inactive="inaktiv"
label.receive_notifications="Benachrichtigungen erhalten"
label.receive_notifications="E-Mail-Benachrichtigungen erhalten"
label.upload="Hochladen"
label.ondate="am"
label.label="Etikett"
Expand Down Expand Up @@ -2621,7 +2621,7 @@ label.notifications_my_work_description="Nur dir zugewiesene Aufgaben, deine Ele
label.notifications_muted="Stummgeschaltet"
label.notifications_muted_description="Keine Benachrichtigungen außer direkten @Erwähnungen"
label.webhook_notifications="Persönlicher Webhook"
label.webhook_notifications_description="Sende deine Benachrichtigungen an deinen eigenen HTTPS-Endpunkt. Für jede Projektbenachrichtigung, die für dich infrage kommt, sendet Leantime eine HTTP-POST-Anfrage mit einem JSON-Body an diese URL. Die Anfragen werden im Hintergrund gesendet und können einige Minuten bis zur Zustellung benötigen; fehlgeschlagene Anfragen werden nicht wiederholt. Welche Benachrichtigungen infrage kommen, bestimmen deine Einstellungen im Tab Benachrichtigungen (Benachrichtigungsempfang, Kategorien und Stufen pro Projekt). Der Webhook sendet nur, solange er aktiviert und eine URL gesetzt ist. Deine URL bleibt gespeichert, während der Webhook deaktiviert ist – leere die URL, um sie zu entfernen."
label.webhook_notifications_description="Sende deine Benachrichtigungen an deinen eigenen HTTPS-Endpunkt. Für jede Projektbenachrichtigung, die für dich infrage kommt, sendet Leantime eine HTTP-POST-Anfrage mit einem JSON-Body an diese URL. Die Anfragen werden im Hintergrund gesendet und können einige Minuten bis zur Zustellung benötigen; fehlgeschlagene Anfragen werden nicht wiederholt. Welche Benachrichtigungen infrage kommen, bestimmen die Kategorien und Stufen pro Projekt im Tab Benachrichtigungen, auch für deine eigenen Änderungen. Wenn du E-Mail-Benachrichtigungen ausschaltest, sendet der Webhook trotzdem. Der Webhook sendet nur, solange er aktiviert und eine URL gesetzt ist. Deine URL bleibt gespeichert, während der Webhook deaktiviert ist – leere die URL, um sie zu entfernen."
label.enable_webhook_notifications="Webhook-Benachrichtigungen aktivieren"
label.webhook_url_hint="Nur HTTPS. Leer lassen, um den Webhook zu entfernen."
label.default_notification_types="Standard-Benachrichtigungsarten für neue Benutzer"
Expand Down
Loading
Loading