diff --git a/config/github-project.php b/config/github-project.php index 539db43..d688b20 100644 --- a/config/github-project.php +++ b/config/github-project.php @@ -13,4 +13,8 @@ ], 'enable_status_comment' => env('GITHUB_PROJECT_ENABLE_STATUS_COMMENT', false), + + 'is_queue_enabled' => env('GITHUB_PROJECT_QUEUE_ENABLED', false), + 'comment_aggregation_cache_key' => env('GITHUB_PROJECT_COMMENT_AGGREGATION_CACHE_KEY', 'github-project-comment-aggregation'), + 'comment_aggregation_time' => env('GITHUB_PROJECT_COMMENT_AGGREGATION_TIME', 20), ]; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4720d94..a648921 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -9,5 +9,8 @@ parameters: - message: '#Cannot cast mixed to string#' path: src/*.php + - message: '#Cannot cast mixed to int#' + path: src/*.php + - message: '#Cannot access offset .+ on mixed#' path: src/*.php diff --git a/resources/views/md/comment.blade.php b/resources/views/md/comment.blade.php index 6e22f42..3c634b6 100644 --- a/resources/views/md/comment.blade.php +++ b/resources/views/md/comment.blade.php @@ -1,19 +1,5 @@ -@php - $fieldData = $payload['changes']['field_value'] ?? null; - - $fieldName = $fieldData['field_name'] ?? 'Unknown Field'; - $fromValue = $fieldData['from']['name'] - ?? $fieldData['from']['title'] - ?? $fieldData['from'] - ?? null; - $toValue = $fieldData['to']['name'] - ?? $fieldData['to']['title'] - ?? $fieldData['to'] - ?? null; -@endphp -@include( - 'github-project::md.field_types.' . $fieldData['field_type'], - compact('fieldName', 'fromValue', 'toValue', 'fieldData') -) - -@include('github-project::md.shared.author', compact('payload')) +@include('github-project::md.shared.content', compact('payload')) +@include('github-project::md.shared.author', [ + 'name' => $payload['sender']['login'] ?? 'Unknown', + 'html_url' => $payload['sender']['html_url'] ?? '#' +]) diff --git a/resources/views/md/shared/author.blade.php b/resources/views/md/shared/author.blade.php index 50b6ad6..237c13d 100644 --- a/resources/views/md/shared/author.blade.php +++ b/resources/views/md/shared/author.blade.php @@ -1 +1,2 @@ -Made changes by: [{{ $payload['sender']['login'] ?? 'Unknown' }}]({{ $payload['sender']['html_url'] ?? '#' }}) + +> Made changes by: [{{ $name }}]({{ $html_url }}) diff --git a/resources/views/md/shared/content.blade.php b/resources/views/md/shared/content.blade.php new file mode 100644 index 0000000..1a284d4 --- /dev/null +++ b/resources/views/md/shared/content.blade.php @@ -0,0 +1,17 @@ +@php + $fieldData = $payload['changes']['field_value'] ?? null; + + $fieldName = $fieldData['field_name'] ?? 'Unknown Field'; + $fromValue = $fieldData['from']['name'] + ?? $fieldData['from']['title'] + ?? $fieldData['from'] + ?? null; + $toValue = $fieldData['to']['name'] + ?? $fieldData['to']['title'] + ?? $fieldData['to'] + ?? null; +@endphp +@include( + 'github-project::md.field_types.' . $fieldData['field_type'], + compact('fieldName', 'fromValue', 'toValue', 'fieldData') +) diff --git a/src/Actions/WebhookAction.php b/src/Actions/WebhookAction.php index 4b978d9..cfd7798 100644 --- a/src/Actions/WebhookAction.php +++ b/src/Actions/WebhookAction.php @@ -2,6 +2,7 @@ namespace CSlant\GitHubProject\Actions; +use CSlant\GitHubProject\Services\GithubService; use CSlant\GitHubProject\Services\WebhookService; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -11,9 +12,12 @@ class WebhookAction { protected WebhookService $webhookService; - public function __construct(WebhookService $webhookService) + protected GithubService $githubService; + + public function __construct(WebhookService $webhookService, GithubService $githubService) { $this->webhookService = $webhookService; + $this->githubService = $githubService; } /** @@ -35,9 +39,7 @@ public function __invoke(): JsonResponse return $validationResponse; } - $message = view('github-project::md.comment', compact('payload'))->render(); - - $this->webhookService->commentOnNode((string) $payload['projects_v2_item']['content_node_id'], $message); + $this->githubService->handleComment($payload); return response()->json(['message' => __('github-project::github-project.success.message')]); } diff --git a/src/Jobs/ProcessAggregatedEvents.php b/src/Jobs/ProcessAggregatedEvents.php new file mode 100644 index 0000000..0977505 --- /dev/null +++ b/src/Jobs/ProcessAggregatedEvents.php @@ -0,0 +1,70 @@ +nodeId = $nodeId; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $commentAggregationCacheKey = config('github-project.comment_aggregation_cache_key')."_{$this->nodeId}"; + + /** @var array $eventMessages */ + $eventMessages = Cache::pull($commentAggregationCacheKey, []); + + $message = $this->aggregateMessages($eventMessages); + $author = Cache::pull($commentAggregationCacheKey.'_author', []); + + Cache::forget($commentAggregationCacheKey); + Cache::forget($commentAggregationCacheKey.'_author'); + + $message .= view( + 'github-project::md.shared.author', + ['name' => $author['name'], 'html_url' => $author['html_url']] + )->render(); + + $githubService = new GithubService; + $githubService->commentOnNode($this->nodeId, $message); + } + + /** + * Aggregate messages from events. + * + * @param array $eventMessages + * + * @return string + */ + protected function aggregateMessages(array $eventMessages): string + { + $messages = array_map(function ($message) { + return $message; + }, $eventMessages); + + return implode("\n", $messages); + } +} diff --git a/src/Jobs/ProcessWebhookEvent.php b/src/Jobs/ProcessWebhookEvent.php new file mode 100644 index 0000000..2f0a89a --- /dev/null +++ b/src/Jobs/ProcessWebhookEvent.php @@ -0,0 +1,61 @@ + */ + protected array $eventData; + + /** + * Create a new job instance. + * + * @param array $eventData + */ + public function __construct(array $eventData) + { + $this->eventData = $eventData; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $nodeId = (string) $this->eventData['projects_v2_item']['content_node_id']; + $commentAggregationCacheKey = config('github-project.comment_aggregation_cache_key')."_{$nodeId}"; + $commentAggregationTime = (int) config('github-project.comment_aggregation_time'); + + $eventMessages = (array) Cache::get($commentAggregationCacheKey, []); + $eventMessages[] = view('github-project::md.shared.content', ['payload' => $this->eventData])->render(); + + Cache::put($commentAggregationCacheKey, $eventMessages, now()->addSeconds($commentAggregationTime + 3)); + + if (!Cache::has($commentAggregationCacheKey.'_author')) { + Cache::put( + $commentAggregationCacheKey.'_author', + [ + 'name' => $this->eventData['sender']['login'], + 'html_url' => $this->eventData['sender']['html_url'], + ], + now()->addSeconds($commentAggregationTime + 3) + ); + } + + if (count($eventMessages) === 1) { + ProcessAggregatedEvents::dispatch($nodeId)->delay(now()->addSeconds($commentAggregationTime)); + } + } +} diff --git a/src/Providers/GithubProjectServiceProvider.php b/src/Providers/GithubProjectServiceProvider.php index 9421d61..0ff1337 100644 --- a/src/Providers/GithubProjectServiceProvider.php +++ b/src/Providers/GithubProjectServiceProvider.php @@ -2,8 +2,6 @@ namespace CSlant\GithubProject\Providers; -use Github\AuthMethod; -use Github\Client; use Illuminate\Support\ServiceProvider; class GithubProjectServiceProvider extends ServiceProvider @@ -30,13 +28,6 @@ public function register(): void $this->registerConfigs(); $this->registerCommands(); - - $this->app->singleton(Client::class, function () { - $client = new Client; - $client->authenticate((string) config('github-project.github.access_token'), null, AuthMethod::ACCESS_TOKEN); - - return $client; - }); } /** diff --git a/src/Services/GithubService.php b/src/Services/GithubService.php new file mode 100644 index 0000000..eb1c0ea --- /dev/null +++ b/src/Services/GithubService.php @@ -0,0 +1,69 @@ +client = $client ?? new Client; + } + + /** + * @param string $contentNodeId + * @param string $message + * + * @return array + */ + public function commentOnNode(string $contentNodeId, string $message): array + { + $query = <<<'GRAPHQL' + mutation($input: AddCommentInput!) { + addComment(input: $input) { + commentEdge { + node { + id + body + } + } + } + } + GRAPHQL; + + $variables = [ + 'input' => [ + 'subjectId' => $contentNodeId, + 'body' => $message, + ], + ]; + + $this->client->authenticate((string) config('github-project.github.access_token'), null, AuthMethod::ACCESS_TOKEN); + + return $this->client->graphql()->execute($query, $variables); + } + + /** + * @param array $payload + * + * @throws \Throwable + */ + public function handleComment(array $payload): void + { + if (config('github-project.is_queue_enabled')) { + ProcessWebhookEvent::dispatch($payload); + + return; + } + + $this->commentOnNode( + (string) $payload['projects_v2_item']['content_node_id'], + view('github-project::md.comment', compact('payload'))->render() + ); + } +} diff --git a/src/Services/WebhookService.php b/src/Services/WebhookService.php index 8a34918..1274e91 100644 --- a/src/Services/WebhookService.php +++ b/src/Services/WebhookService.php @@ -2,19 +2,11 @@ namespace CSlant\GitHubProject\Services; -use Github\Client; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Request; class WebhookService { - protected Client $client; - - public function __construct(Client $client) - { - $this->client = $client; - } - public function eventRequestApproved(Request $request): bool { $event = $request->server->get('HTTP_X_GITHUB_EVENT'); @@ -103,35 +95,4 @@ protected function isStatusCommentEnabled(array $payload): bool return true; } - - /** - * @param string $contentNodeId - * @param string $message - * - * @return array - */ - public function commentOnNode(string $contentNodeId, string $message): array - { - $query = <<<'GRAPHQL' - mutation($input: AddCommentInput!) { - addComment(input: $input) { - commentEdge { - node { - id - body - } - } - } - } - GRAPHQL; - - $variables = [ - 'input' => [ - 'subjectId' => $contentNodeId, - 'body' => $message, - ], - ]; - - return $this->client->graphql()->execute($query, $variables); - } }