diff --git a/app/Actions/ActivityLogger.php b/app/Actions/ActivityLogger.php index 3a329387f..ad4ee7375 100644 --- a/app/Actions/ActivityLogger.php +++ b/app/Actions/ActivityLogger.php @@ -5,6 +5,7 @@ namespace BookStack\Actions; use BookStack\Auth\Permissions\PermissionService; use BookStack\Entities\Models\Entity; use BookStack\Interfaces\Loggable; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Log; class ActivityLogger @@ -35,6 +36,7 @@ class ActivityLogger $activity->save(); $this->setNotification($type); + $this->dispatchWebhooks($type, $detail); } /** @@ -68,7 +70,7 @@ class ActivityLogger /** * Flashes a notification message to the session if an appropriate message is available. */ - protected function setNotification(string $type) + protected function setNotification(string $type): void { $notificationTextKey = 'activities.' . $type . '_notification'; if (trans()->has($notificationTextKey)) { @@ -77,6 +79,21 @@ class ActivityLogger } } + /** + * @param string|Loggable $detail + */ + protected function dispatchWebhooks(string $type, $detail): void + { + $webhooks = Webhook::query()->whereHas('trackedEvents', function(Builder $query) use ($type) { + $query->where('event', '=', $type) + ->orWhere('event', '=', 'all'); + })->get(); + + foreach ($webhooks as $webhook) { + dispatch(new DispatchWebhookJob($webhook, $type, $detail)); + } + } + /** * Log out a failed login attempt, Providing the given username * as part of the message if the '%u' string is used. diff --git a/app/Actions/DispatchWebhookJob.php b/app/Actions/DispatchWebhookJob.php new file mode 100644 index 000000000..4cc749af3 --- /dev/null +++ b/app/Actions/DispatchWebhookJob.php @@ -0,0 +1,120 @@ +webhook = $webhook; + $this->event = $event; + $this->detail = $detail; + $this->initiator = user(); + $this->initiatedTime = time(); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $httpClient = new Client([ + 'timeout' => 3, + 'allow_redirects' => ['strict' => true], + ]); + + $request = new Request('POST', $this->webhook->endpoint, [ + 'Content-Type' => 'application/json' + ], json_encode($this->buildWebhookData())); + + try { + $response = $httpClient->send($request); + if ($response->getStatusCode() >= 400) { + Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with status {$response->getStatusCode()}"); + } + } catch (ClientExceptionInterface $exception) { + Log::error("Received error during webhook call to endpoint {$this->webhook->endpoint}: {$exception->getMessage()}"); + } + } + + protected function buildWebhookData(): array + { + $textParts = [ + $this->initiator->name, + trans('activities.' . $this->event), + ]; + + if ($this->detail instanceof Entity) { + $textParts[] = '"' . $this->detail->name . '"'; + } + + $data = [ + 'event' => $this->event, + 'text' => implode(' ', $textParts), + 'triggered_at' => Carbon::createFromTimestampUTC($this->initiatedTime)->toISOString(), + 'triggered_by' => $this->initiator->attributesToArray(), + 'triggered_by_profile_url' => $this->initiator->getProfileUrl(), + 'webhook_id' => $this->webhook->id, + 'webhook_name' => $this->webhook->name, + ]; + + if (method_exists($this->detail, 'getUrl')) { + $data['url'] = $this->detail->getUrl(); + } + + if ($this->detail instanceof Model) { + $data['related_item'] = $this->detail->attributesToArray(); + } + + return $data; + } +} diff --git a/resources/views/common/activity-item.blade.php b/resources/views/common/activity-item.blade.php index eebfb591a..89d44b152 100644 --- a/resources/views/common/activity-item.blade.php +++ b/resources/views/common/activity-item.blade.php @@ -24,8 +24,6 @@ "{{ $activity->entity->name }}" @endif - @if($activity->extra) "{{ $activity->extra }}" @endif -
@icon('time'){{ $activity->created_at->diffForHumans() }}