From 25816cc47ae710b990db9e1211ce063f724b9e9d Mon Sep 17 00:00:00 2001 From: Adrian Hopek Date: Mon, 25 Apr 2022 13:23:49 +0200 Subject: [PATCH] wip --- .../Providers/AppServiceProvider.php | 6 +- app/Domain/DailySummaryRetriever.php | 45 ++++++++++++++ .../KeyHasBeenGivenNotification.php | 42 +++++++++++++ .../KeyHasBeenTakenNotification.php | 42 +++++++++++++ app/Domain/Slack/Controller.php | 55 +++++++++++++++++ app/Domain/Slack/Handlers/CatchAll.php | 52 ++++++---------- app/Domain/Slack/Handlers/DailySummary.php | 40 ++++--------- app/Domain/Slack/Handlers/GiveKeysTo.php | 59 +++++++++++++------ app/Domain/Slack/Handlers/Help.php | 33 ++++------- app/Domain/Slack/Handlers/HomeOffice.php | 39 ++++-------- app/Domain/Slack/Handlers/KeyList.php | 9 ++- app/Domain/Slack/Handlers/SaySomething.php | 24 -------- app/Domain/Slack/Handlers/TakeKeysFrom.php | 56 ++++++++++++------ app/Domain/Slack/SignatureHandler.php | 26 ++++++++ .../Slack/{Channels => }/SlackApiChannel.php | 6 +- app/Domain/Slack/SlackUserExistsRule.php | 24 ++++++++ .../Slack/Traits/FindsUserBySlackId.php | 43 ++++++++++++++ app/Domain/Slack/Traits/ListsHandlers.php | 45 ++++++++++++++ app/Domain/Slack/UserNotFoundException.php | 11 ++++ app/Eloquent/Models/Key.php | 7 +++ .../Commands/SendDailySummaryToSlack.php | 36 ++++------- .../Http/Controllers/DashboardController.php | 19 ++---- .../Http/Controllers/KeysController.php | 6 ++ composer.json | 3 +- config/laravel-slack-slash-command.php | 11 ++-- resources/lang/pl.json | 20 ++++--- routes/api.php | 3 + tests/Feature/KeyTest.php | 8 +++ 28 files changed, 531 insertions(+), 239 deletions(-) create mode 100644 app/Domain/DailySummaryRetriever.php create mode 100644 app/Domain/Notifications/KeyHasBeenGivenNotification.php create mode 100644 app/Domain/Notifications/KeyHasBeenTakenNotification.php create mode 100644 app/Domain/Slack/Controller.php delete mode 100644 app/Domain/Slack/Handlers/SaySomething.php create mode 100644 app/Domain/Slack/SignatureHandler.php rename app/Domain/Slack/{Channels => }/SlackApiChannel.php (77%) create mode 100644 app/Domain/Slack/SlackUserExistsRule.php create mode 100644 app/Domain/Slack/Traits/FindsUserBySlackId.php create mode 100644 app/Domain/Slack/Traits/ListsHandlers.php create mode 100644 app/Domain/Slack/UserNotFoundException.php diff --git a/app/Architecture/Providers/AppServiceProvider.php b/app/Architecture/Providers/AppServiceProvider.php index 37f7884..094200f 100644 --- a/app/Architecture/Providers/AppServiceProvider.php +++ b/app/Architecture/Providers/AppServiceProvider.php @@ -9,13 +9,13 @@ use Illuminate\Notifications\ChannelManager; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Notification; use Illuminate\Support\ServiceProvider; -use Toby\Domain\Slack\Channels\SlackApiChannel; +use Toby\Domain\Slack\SlackApiChannel; class AppServiceProvider extends ServiceProvider { - public function register() + public function register(): void { - Notification::resolved(function (ChannelManager $service) { + Notification::resolved(function (ChannelManager $service): void { $service->extend("slack", fn(Application $app) => $app->make(SlackApiChannel::class)); }); } diff --git a/app/Domain/DailySummaryRetriever.php b/app/Domain/DailySummaryRetriever.php new file mode 100644 index 0000000..465cdb4 --- /dev/null +++ b/app/Domain/DailySummaryRetriever.php @@ -0,0 +1,45 @@ +with(["user", "vacationRequest"]) + ->whereDate("date", $date) + ->approved() + ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => $this->configRetriever->isVacation($type))) + ->get(); + } + + public function getRemoteDays(Carbon $date): Collection + { + return Vacation::query() + ->with(["user", "vacationRequest"]) + ->whereDate("date", $date) + ->approved() + ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => !$this->configRetriever->isVacation($type))) + ->get(); + } + + public function getBirthdays(Carbon $date): Collection + { + return User::query() + ->whereRelation("profile", "birthday", $date) + ->get(); + } +} diff --git a/app/Domain/Notifications/KeyHasBeenGivenNotification.php b/app/Domain/Notifications/KeyHasBeenGivenNotification.php new file mode 100644 index 0000000..84bf706 --- /dev/null +++ b/app/Domain/Notifications/KeyHasBeenGivenNotification.php @@ -0,0 +1,42 @@ + $this->getName($this->sender), + "recipient" => $this->getName($this->recipient), + "key" => $notifiable->id, + ]); + } + + protected function getName(User $user): string + { + if ($user->profile->slack_id !== null) { + return "<@{$user->profile->slack_id}>"; + } + + return $user->profile->full_name; + } +} diff --git a/app/Domain/Notifications/KeyHasBeenTakenNotification.php b/app/Domain/Notifications/KeyHasBeenTakenNotification.php new file mode 100644 index 0000000..d22467d --- /dev/null +++ b/app/Domain/Notifications/KeyHasBeenTakenNotification.php @@ -0,0 +1,42 @@ + $this->getName($this->recipient), + "sender" => $this->getName($this->sender), + "key" => $notifiable->id, + ]); + } + + protected function getName(User $user): string + { + if ($user->profile->slack_id !== null) { + return "<@{$user->profile->slack_id}>"; + } + + return $user->profile->full_name; + } +} diff --git a/app/Domain/Slack/Controller.php b/app/Domain/Slack/Controller.php new file mode 100644 index 0000000..78cb4de --- /dev/null +++ b/app/Domain/Slack/Controller.php @@ -0,0 +1,55 @@ +guardAgainstInvalidRequest($request); + + $handler = $this->determineHandler(); + + try { + $response = $handler->handle($this->request); + } catch (SlackSlashCommandException $exception) { + $response = $exception->getResponse($this->request); + } catch (ValidationException $exception) { + $response = $this->prepareValidationResponse($exception); + } catch (Exception $exception) { + $response = $this->convertToResponse($exception); + } + + return $response->getIlluminateResponse(); + } + + protected function prepareValidationResponse(ValidationException $exception): Response + { + $errors = (new Collection($exception->errors())) + ->map( + fn(array $message) => Attachment::create() + ->setColor("danger") + ->setText($message[0]), + ); + + return Response::create($this->request) + ->withText(":x: Komenda `/{$this->request->command} {$this->request->text}` jest niepoprawna:") + ->withAttachments($errors->all()); + } +} \ No newline at end of file diff --git a/app/Domain/Slack/Handlers/CatchAll.php b/app/Domain/Slack/Handlers/CatchAll.php index c38930f..a2516b5 100644 --- a/app/Domain/Slack/Handlers/CatchAll.php +++ b/app/Domain/Slack/Handlers/CatchAll.php @@ -4,48 +4,32 @@ declare(strict_types=1); namespace Toby\Domain\Slack\Handlers; -use Illuminate\Support\Collection; use Spatie\SlashCommand\Attachment; -use Spatie\SlashCommand\AttachmentField; -use Spatie\SlashCommand\Handlers\CatchAll as BaseCatchAllHandler; -use Spatie\SlashCommand\Handlers\SignatureHandler; +use Spatie\SlashCommand\Handlers\BaseHandler; use Spatie\SlashCommand\Request; use Spatie\SlashCommand\Response; +use Toby\Domain\Slack\Traits\ListsHandlers; -class CatchAll extends BaseCatchAllHandler +class CatchAll extends BaseHandler { + use ListsHandlers; + + public function canHandle(Request $request): bool + { + return true; + } + public function handle(Request $request): Response { - $response = $this->respondToSlack("Nie rozpoznaję tej komendy: `/{$request->command} {$request->text}`"); + $handlers = $this->findAvailableHandlers(); + $attachmentFields = $this->mapHandlersToAttachments($handlers); - [$command] = explode(' ', $this->request->text ?? ""); - - $alternativeHandlers = $this->findAlternativeHandlers($command); - - if ($alternativeHandlers->count()) { - $response->withAttachment($this->getCommandListAttachment($alternativeHandlers)); - } - - if ($this->containsHelpHandler($alternativeHandlers)) { - $response->withAttachment(Attachment::create() - ->setText("Aby wyświetlić wszystkie komendy, napisz: `/toby pomoc`") + return $this->respondToSlack(":x: Nie rozpoznaję tej komendy. Lista wszystkich komend:") + ->withAttachment( + Attachment::create() + ->setColor("danger") + ->useMarkdown() + ->setFields($attachmentFields), ); - } - - return $response; - } - - protected function getCommandListAttachment(Collection $handlers): Attachment - { - $attachmentFields = $handlers - ->map(function (SignatureHandler $handler) { - return AttachmentField::create($handler->getFullCommand(), $handler->getDescription()); - }) - ->all(); - - return Attachment::create() - ->setColor('warning') - ->setTitle('Czy miałeś na myśli:') - ->setFields($attachmentFields); } } \ No newline at end of file diff --git a/app/Domain/Slack/Handlers/DailySummary.php b/app/Domain/Slack/Handlers/DailySummary.php index b3ae2be..af62bb8 100644 --- a/app/Domain/Slack/Handlers/DailySummary.php +++ b/app/Domain/Slack/Handlers/DailySummary.php @@ -5,70 +5,52 @@ declare(strict_types=1); namespace Toby\Domain\Slack\Handlers; use Illuminate\Support\Carbon; -use Illuminate\Support\Collection; use Spatie\SlashCommand\Attachment; use Spatie\SlashCommand\Request; use Spatie\SlashCommand\Response; -use Spatie\SlashCommand\Handlers\SignatureHandler; -use Toby\Domain\Enums\VacationType; -use Toby\Domain\VacationTypeConfigRetriever; +use Toby\Domain\DailySummaryRetriever; +use Toby\Domain\Slack\SignatureHandler; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\Vacation; class DailySummary extends SignatureHandler { protected $signature = "toby dzisiaj"; - - protected $description = "Podsumowanie"; + protected $description = "Codzienne podsumowanie"; public function handle(Request $request): Response { - $configRetriever = app(VacationTypeConfigRetriever::class); + $dailySummaryRetriever = app()->make(DailySummaryRetriever::class); $now = Carbon::today(); - /** @var Collection $absences */ - $absences = Vacation::query() - ->with(["user", "vacationRequest"]) - ->whereDate("date", $now) - ->approved() - ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => $configRetriever->isVacation($type))) - ->get() + $absences = $dailySummaryRetriever->getAbsences($now) ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); - /** @var Collection $remoteDays */ - $remoteDays = Vacation::query() - ->with(["user", "vacationRequest"]) - ->whereDate("date", $now) - ->approved() - ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => !$configRetriever->isVacation($type))) - ->get() + $remoteDays = $dailySummaryRetriever->getRemoteDays($now) ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); - $birthdays = User::query() - ->whereRelation("profile", "birthday", $now) - ->get() + $birthdays = $dailySummaryRetriever->getBirthdays($now) ->map(fn(User $user) => $user->profile->full_name); $absencesAttachment = Attachment::create() ->setTitle("Nieobecności :palm_tree:") - ->setColor('#eab308') + ->setColor("#eab308") ->setText($absences->isNotEmpty() ? $absences->implode("\n") : "Wszyscy dzisiaj pracują :muscle:"); $remoteAttachment = Attachment::create() ->setTitle("Praca zdalna :house_with_garden:") - ->setColor('#d946ef') + ->setColor("#d946ef") ->setText($remoteDays->isNotEmpty() ? $remoteDays->implode("\n") : "Wszyscy dzisiaj są w biurze :boom:"); $birthdayAttachment = Attachment::create() ->setTitle("Urodziny :birthday:") - ->setColor('#3C5F97') + ->setColor("#3C5F97") ->setText($birthdays->isNotEmpty() ? $birthdays->implode("\n") : "Dzisiaj nikt nie ma urodzin :cry:"); return $this->respondToSlack("Podsumowanie dla dnia {$now->toDisplayString()}") ->withAttachment($absencesAttachment) ->withAttachment($remoteAttachment) - ->withAttachment($birthdayAttachment) - ->displayResponseToEveryoneOnChannel(); + ->withAttachment($birthdayAttachment); } } \ No newline at end of file diff --git a/app/Domain/Slack/Handlers/GiveKeysTo.php b/app/Domain/Slack/Handlers/GiveKeysTo.php index bfaf900..049551e 100644 --- a/app/Domain/Slack/Handlers/GiveKeysTo.php +++ b/app/Domain/Slack/Handlers/GiveKeysTo.php @@ -4,46 +4,69 @@ declare(strict_types=1); namespace Toby\Domain\Slack\Handlers; -use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; use Spatie\SlashCommand\Request; use Spatie\SlashCommand\Response; -use Spatie\SlashCommand\Handlers\SignatureHandler; +use Toby\Domain\Notifications\KeyHasBeenGivenNotification; +use Toby\Domain\Slack\SignatureHandler; +use Toby\Domain\Slack\SlackUserExistsRule; +use Toby\Domain\Slack\Traits\FindsUserBySlackId; +use Toby\Domain\Slack\UserNotFoundException; use Toby\Eloquent\Models\Key; -use Toby\Eloquent\Models\User; class GiveKeysTo extends SignatureHandler { - protected $signature = "toby klucze:dla {użytkownik}"; + use FindsUserBySlackId; - protected $description = "Daj klucze wskazanemu użytkownikowi"; + protected $signature = "toby klucze:dla {user}"; + protected $description = "Przekaż klucze wskazanemu użytkownikowi"; + /** + * @throws UserNotFoundException + * @throws ValidationException + */ public function handle(Request $request): Response { - $to = $this->getArgument('użytkownik'); + ["user" => $from] = $this->validate(); - $id = Str::between($to, "@", "|"); - - $authUser = $this->findUserBySlackId($request->userId); - $user = $this->findUserBySlackId($id); + $authUser = $this->findUserBySlackIdOrFail($request->userId); + $user = $this->findUserBySlackId($from); /** @var Key $key */ $key = $authUser->keys()->first(); + if (!$key) { + throw ValidationException::withMessages(["key" => "Nie masz żadnego klucza do przekazania"]); + } + + if ($user->is($authUser)) { + throw ValidationException::withMessages([ + "key" => "Nie możesz przekazać sobie kluczy :dzban:", + ]); + } + $key->user()->associate($user); $key->save(); - return $this->respondToSlack("<@{$authUser->profile->slack_id}> daje klucz nr {$key->id} użytkownikowi <@{$user->profile->slack_id}>") - ->displayResponseToEveryoneOnChannel(); + $key->notify(new KeyHasBeenGivenNotification($authUser, $user)); + + return $this->respondToSlack( + ":white_check_mark: Klucz nr {$key->id} został przekazany użytkownikowi <@{$user->profile->slack_id}>", + ); } - protected function findUserBySlackId(string $slackId): User + protected function getRules(): array { - /** @var User $user */ - $user = User::query() - ->whereRelation("profile", "slack_id", $slackId) - ->first(); + return [ + "user" => ["required", new SlackUserExistsRule()], + ]; + } - return $user; + protected function getMessages(): array + { + return [ + "user.required" => "Musisz podać użytkownika, któremu chcesz przekazać klucze", + ]; } } \ No newline at end of file diff --git a/app/Domain/Slack/Handlers/Help.php b/app/Domain/Slack/Handlers/Help.php index 45fc7bf..1407c50 100644 --- a/app/Domain/Slack/Handlers/Help.php +++ b/app/Domain/Slack/Handlers/Help.php @@ -4,42 +4,31 @@ declare(strict_types=1); namespace Toby\Domain\Slack\Handlers; -use Illuminate\Support\Collection; use Spatie\SlashCommand\Attachment; -use Spatie\SlashCommand\AttachmentField; -use Spatie\SlashCommand\Handlers\Help as BaseHelpHandler; -use Spatie\SlashCommand\Handlers\SignatureHandler; use Spatie\SlashCommand\Request; use Spatie\SlashCommand\Response; +use Toby\Domain\Slack\SignatureHandler; +use Toby\Domain\Slack\Traits\ListsHandlers; -class Help extends BaseHelpHandler +class Help extends SignatureHandler { + use ListsHandlers; + protected $signature = "toby pomoc"; - protected $description = "Wyświetl wszystkie dostępne komendy tobiego"; + protected $description = "Wyświetl wszystkie dostępne komendy"; public function handle(Request $request): Response { $handlers = $this->findAvailableHandlers(); - return $this->displayListOfAllCommands($handlers); - } + $attachmentFields = $this->mapHandlersToAttachments($handlers); - protected function displayListOfAllCommands(Collection $handlers): Response - { - $attachmentFields = $handlers - ->sort(function (SignatureHandler $handlerA, SignatureHandler $handlerB) { - return strcmp($handlerA->getFullCommand(), $handlerB->getFullCommand()); - }) - ->map(function (SignatureHandler $handler) { - return AttachmentField::create("/{$handler->getSignature()}", $handler->getDescription()); - }) - ->all(); - - return $this->respondToSlack('Dostępne komendy') + return $this->respondToSlack("Dostępne komendy:") ->withAttachment( Attachment::create() - ->setColor('good') - ->setFields($attachmentFields) + ->setColor("good") + ->useMarkdown() + ->setFields($attachmentFields), ); } } \ No newline at end of file diff --git a/app/Domain/Slack/Handlers/HomeOffice.php b/app/Domain/Slack/Handlers/HomeOffice.php index 87270d8..437cff0 100644 --- a/app/Domain/Slack/Handlers/HomeOffice.php +++ b/app/Domain/Slack/Handlers/HomeOffice.php @@ -7,22 +7,31 @@ namespace Toby\Domain\Slack\Handlers; use Illuminate\Support\Carbon; use Spatie\SlashCommand\Request; use Spatie\SlashCommand\Response; -use Spatie\SlashCommand\Handlers\SignatureHandler; use Toby\Domain\Actions\VacationRequest\CreateAction; use Toby\Domain\Enums\VacationType; +use Toby\Domain\Slack\SignatureHandler; +use Toby\Domain\Slack\Traits\FindsUserBySlackId; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\YearPeriod; class HomeOffice extends SignatureHandler { - protected $signature = "toby zdalnie {kiedy?}"; - protected $description = "Pracuj zdalnie wybranego dnia (domyślnie dzisiaj)"; + use FindsUserBySlackId; + + protected $signature = "toby zdalnie"; + protected $description = "Pracuj dzisiaj zdalnie"; public function handle(Request $request): Response { - $date = $this->getDateFromArgument($this->getArgument('kiedy') ?? "dzisiaj"); $user = $this->findUserBySlackId($request->userId); + $this->createRemoteday($user, Carbon::today()); + + return $this->respondToSlack(":white_check_mark: Pracujesz dzisiaj zdalnie"); + } + + protected function createRemoteday(User $user, Carbon $date): void + { $yearPeriod = YearPeriod::findByYear($date->year); app(CreateAction::class)->execute([ @@ -33,27 +42,5 @@ class HomeOffice extends SignatureHandler "year_period_id" => $yearPeriod->id, "flow_skipped" => false, ], $user); - - return $this->respondToSlack("Praca zdalna dnia {$date->toDisplayString()} została utworzona pomyślnie.") - ->displayResponseToEveryoneOnChannel(); - } - - protected function getDateFromArgument(string $argument): Carbon - { - return match ($argument) { - "dzisiaj" => Carbon::today(), - "jutro" => Carbon::tomorrow(), - default => Carbon::create($argument), - }; - } - - protected function findUserBySlackId(string $slackId): User - { - /** @var User $user */ - $user = User::query() - ->whereRelation("profile", "slack_id", $slackId) - ->first(); - - return $user; } } \ No newline at end of file diff --git a/app/Domain/Slack/Handlers/KeyList.php b/app/Domain/Slack/Handlers/KeyList.php index ac60a93..70ed4a7 100644 --- a/app/Domain/Slack/Handlers/KeyList.php +++ b/app/Domain/Slack/Handlers/KeyList.php @@ -7,13 +7,12 @@ namespace Toby\Domain\Slack\Handlers; use Spatie\SlashCommand\Attachment; use Spatie\SlashCommand\Request; use Spatie\SlashCommand\Response; -use Spatie\SlashCommand\Handlers\SignatureHandler; +use Toby\Domain\Slack\SignatureHandler; use Toby\Eloquent\Models\Key; class KeyList extends SignatureHandler { protected $signature = "toby klucze"; - protected $description = "Lista wszystkich kluczy"; public function handle(Request $request): Response @@ -23,11 +22,11 @@ class KeyList extends SignatureHandler ->get() ->map(fn(Key $key) => "Klucz nr {$key->id} - <@{$key->user->profile->slack_id}>"); - return $this->respondToSlack("Lista kluczy") + return $this->respondToSlack("Lista kluczy :key:") ->withAttachment( Attachment::create() - ->setColor('#3C5F97') - ->setText($keys->implode("\n")) + ->setColor("#3C5F97") + ->setText($keys->isNotEmpty() ? $keys->implode("\n") : "Nie ma żadnych kluczy w tobym"), ); } } \ No newline at end of file diff --git a/app/Domain/Slack/Handlers/SaySomething.php b/app/Domain/Slack/Handlers/SaySomething.php deleted file mode 100644 index 4099172..0000000 --- a/app/Domain/Slack/Handlers/SaySomething.php +++ /dev/null @@ -1,24 +0,0 @@ -getArgument("zdanie"); - - return $this->respondToSlack($sentence) - ->displayResponseToEveryoneOnChannel(); - } -} \ No newline at end of file diff --git a/app/Domain/Slack/Handlers/TakeKeysFrom.php b/app/Domain/Slack/Handlers/TakeKeysFrom.php index eed3a89..de6aa85 100644 --- a/app/Domain/Slack/Handlers/TakeKeysFrom.php +++ b/app/Domain/Slack/Handlers/TakeKeysFrom.php @@ -4,46 +4,68 @@ declare(strict_types=1); namespace Toby\Domain\Slack\Handlers; -use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; use Spatie\SlashCommand\Request; use Spatie\SlashCommand\Response; -use Spatie\SlashCommand\Handlers\SignatureHandler; +use Toby\Domain\Notifications\KeyHasBeenTakenNotification; +use Toby\Domain\Slack\SignatureHandler; +use Toby\Domain\Slack\SlackUserExistsRule; +use Toby\Domain\Slack\Traits\FindsUserBySlackId; +use Toby\Domain\Slack\UserNotFoundException; use Toby\Eloquent\Models\Key; -use Toby\Eloquent\Models\User; class TakeKeysFrom extends SignatureHandler { - protected $signature = "toby klucze:od {użytkownik}"; + use FindsUserBySlackId; + protected $signature = "toby klucze:od {user}"; protected $description = "Zabierz klucze wskazanemu użytkownikowi"; + /** + * @throws UserNotFoundException|ValidationException + */ public function handle(Request $request): Response { - $from = $this->getArgument("użytkownik"); + ["user" => $from] = $this->validate(); - $id = Str::between($from, "@", "|"); - - $authUser = $this->findUserBySlackId($request->userId); - $user = $this->findUserBySlackId($id); + $authUser = $this->findUserBySlackIdOrFail($request->userId); + $user = $this->findUserBySlackId($from); /** @var Key $key */ $key = $user->keys()->first(); + if (!$key) { + throw ValidationException::withMessages([ + "key" => "Użytkownik <@{$user->profile->slack_id}> nie ma żadnych kluczy", + ]); + } + + if ($key->user()->is($authUser)) { + throw ValidationException::withMessages([ + "key" => "Nie możesz zabrać sobie kluczy :dzban:", + ]); + } + $key->user()->associate($authUser); $key->save(); - return $this->respondToSlack("<@{$authUser->profile->slack_id}> zabiera klucz nr {$key->id} użytkownikowi <@{$user->profile->slack_id}>") - ->displayResponseToEveryoneOnChannel(); + $key->notify(new KeyHasBeenTakenNotification($authUser, $user)); + + return $this->respondToSlack(":white_check_mark: Klucz nr {$key->id} został zabrany użytkownikowi <@{$user->profile->slack_id}>"); } - protected function findUserBySlackId(string $slackId): User + protected function getRules(): array { - /** @var User $user */ - $user = User::query() - ->whereRelation("profile", "slack_id", $slackId) - ->first(); + return [ + "user" => ["required", new SlackUserExistsRule()], + ]; + } - return $user; + protected function getMessages(): array + { + return [ + "user.required" => "Musisz podać użytkownika, któremu chcesz zabrać klucze", + ]; } } \ No newline at end of file diff --git a/app/Domain/Slack/SignatureHandler.php b/app/Domain/Slack/SignatureHandler.php new file mode 100644 index 0000000..b7d4ddf --- /dev/null +++ b/app/Domain/Slack/SignatureHandler.php @@ -0,0 +1,26 @@ +getArguments(), $this->getRules(), $this->getMessages()); + } + + protected function getRules(): array + { + return []; + } + + protected function getMessages(): array + { + return []; + } +} \ No newline at end of file diff --git a/app/Domain/Slack/Channels/SlackApiChannel.php b/app/Domain/Slack/SlackApiChannel.php similarity index 77% rename from app/Domain/Slack/Channels/SlackApiChannel.php rename to app/Domain/Slack/SlackApiChannel.php index 6b40cb0..9b66640 100644 --- a/app/Domain/Slack/Channels/SlackApiChannel.php +++ b/app/Domain/Slack/SlackApiChannel.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Toby\Domain\Slack\Channels; +namespace Toby\Domain\Slack; use Illuminate\Http\Client\Response; use Illuminate\Notifications\Notification; @@ -14,12 +14,12 @@ class SlackApiChannel { $baseUrl = config("services.slack.url"); $url = "{$baseUrl}/chat.postMessage"; - $channel = $notifiable->routeNotificationFor('slack', $notification); + $channel = $notifiable->routeNotificationFor("slack", $notification); return Http::withToken(config("services.slack.client_token")) ->post($url, [ "channel" => $channel, - "text" => $notification->toSlack(), + "text" => $notification->toSlack($notifiable), ]); } } \ No newline at end of file diff --git a/app/Domain/Slack/SlackUserExistsRule.php b/app/Domain/Slack/SlackUserExistsRule.php new file mode 100644 index 0000000..f0176ba --- /dev/null +++ b/app/Domain/Slack/SlackUserExistsRule.php @@ -0,0 +1,24 @@ +where("slack_id", $slackId)->exists(); + } + + public function message(): string + { + return "Użytkownik :input nie istnieje w tobym"; + } +} \ No newline at end of file diff --git a/app/Domain/Slack/Traits/FindsUserBySlackId.php b/app/Domain/Slack/Traits/FindsUserBySlackId.php new file mode 100644 index 0000000..15514f9 --- /dev/null +++ b/app/Domain/Slack/Traits/FindsUserBySlackId.php @@ -0,0 +1,43 @@ +prepareSlackIdFromString($slackId); + + /** @var User $user */ + $user = User::query() + ->whereRelation("profile", "slack_id", $id) + ->first(); + + return $user; + } + + /** + * @throws UserNotFoundException + */ + protected function findUserBySlackIdOrFail(string $slackId): ?User + { + $user = $this->findUserBySlackId($slackId); + + if (!$user) { + throw new UserNotFoundException("Użytkownik {$slackId} nie istnieje w tobym"); + } + + return $user; + } + + protected function prepareSlackIdFromString(string $slackId): string + { + return Str::between($slackId, "<@", "|"); + } +} \ No newline at end of file diff --git a/app/Domain/Slack/Traits/ListsHandlers.php b/app/Domain/Slack/Traits/ListsHandlers.php new file mode 100644 index 0000000..08895f9 --- /dev/null +++ b/app/Domain/Slack/Traits/ListsHandlers.php @@ -0,0 +1,45 @@ +map(fn(string $handlerClassName) => new $handlerClassName($this->request)) + ->filter(fn(HandlesSlashCommand $handler) => $handler instanceof SignatureHandler) + ->filter(function (SignatureHandler $handler) { + $signatureParts = new SignatureParts($handler->getSignature()); + + return Str::is($signatureParts->getSlashCommandName(), $this->request->command); + }); + } + + protected function mapHandlersToAttachments(Collection $handlers): array + { + return $handlers + ->sort( + fn(SignatureHandler $handlerA, SignatureHandler $handlerB) => strcmp( + $handlerA->getFullCommand(), + $handlerB->getFullCommand(), + ), + ) + ->map( + fn(SignatureHandler $handler) => AttachmentField::create( + $handler->getDescription(), + "`/{$handler->getSignature()}`", + ), + ) + ->all(); + } +} \ No newline at end of file diff --git a/app/Domain/Slack/UserNotFoundException.php b/app/Domain/Slack/UserNotFoundException.php new file mode 100644 index 0000000..1d60d66 --- /dev/null +++ b/app/Domain/Slack/UserNotFoundException.php @@ -0,0 +1,11 @@ +belongsTo(User::class); } + public function routeNotificationForSlack(): string + { + return config("services.slack.default_channel"); + } + protected static function newFactory(): KeyFactory { return KeyFactory::new(); diff --git a/app/Infrastructure/Console/Commands/SendDailySummaryToSlack.php b/app/Infrastructure/Console/Commands/SendDailySummaryToSlack.php index 3959be0..c487c1f 100644 --- a/app/Infrastructure/Console/Commands/SendDailySummaryToSlack.php +++ b/app/Infrastructure/Console/Commands/SendDailySummaryToSlack.php @@ -7,11 +7,9 @@ namespace Toby\Infrastructure\Console\Commands; use Carbon\CarbonInterface; use Illuminate\Console\Command; use Illuminate\Support\Carbon; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; use Spatie\SlashCommand\Attachment; -use Toby\Domain\Enums\VacationType; -use Toby\Domain\VacationTypeConfigRetriever; +use Toby\Domain\DailySummaryRetriever; use Toby\Eloquent\Models\Holiday; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\Vacation; @@ -21,7 +19,7 @@ class SendDailySummaryToSlack extends Command protected $signature = "toby:slack:daily-summary {--f|force}"; protected $description = "Sent daily summary to slack"; - public function handle(VacationTypeConfigRetriever $configRetriever): void + public function handle(DailySummaryRetriever $dailySummaryRetriever): void { $now = Carbon::today(); @@ -29,42 +27,28 @@ class SendDailySummaryToSlack extends Command return; } - /** @var Collection $absences */ - $absences = Vacation::query() - ->with(["user", "vacationRequest"]) - ->whereDate("date", $now) - ->approved() - ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => $configRetriever->isVacation($type))) - ->get() + $absences = $dailySummaryRetriever->getAbsences($now) ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); - /** @var Collection $remoteDays */ - $remoteDays = Vacation::query() - ->with(["user", "vacationRequest"]) - ->whereDate("date", $now) - ->approved() - ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => !$configRetriever->isVacation($type))) - ->get() + $remoteDays = $dailySummaryRetriever->getRemoteDays($now) ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); - $birthdays = User::query() - ->whereRelation("profile", "birthday", $now) - ->get() + $birthdays = $dailySummaryRetriever->getBirthdays($now) ->map(fn(User $user) => $user->profile->full_name); $absencesAttachment = Attachment::create() ->setTitle("Nieobecności :palm_tree:") - ->setColor('#eab308') + ->setColor("#eab308") ->setText($absences->isNotEmpty() ? $absences->implode("\n") : "Wszyscy dzisiaj pracują :muscle:"); $remoteAttachment = Attachment::create() ->setTitle("Praca zdalna :house_with_garden:") - ->setColor('#d946ef') + ->setColor("#d946ef") ->setText($remoteDays->isNotEmpty() ? $remoteDays->implode("\n") : "Wszyscy dzisiaj są w biurze :boom:"); $birthdayAttachment = Attachment::create() ->setTitle("Urodziny :birthday:") - ->setColor('#3C5F97') + ->setColor("#3C5F97") ->setText($birthdays->isNotEmpty() ? $birthdays->implode("\n") : "Dzisiaj nikt nie ma urodzin :cry:"); $baseUrl = config("services.slack.url"); @@ -74,8 +58,8 @@ class SendDailySummaryToSlack extends Command ->post($url, [ "channel" => config("services.slack.default_channel"), "text" => "Podsumowanie dla dnia {$now->toDisplayString()}", - 'attachments' => collect([$absencesAttachment, $remoteAttachment, $birthdayAttachment])->map( - fn(Attachment $attachment) => $attachment->toArray() + "attachments" => collect([$absencesAttachment, $remoteAttachment, $birthdayAttachment])->map( + fn(Attachment $attachment) => $attachment->toArray(), )->toArray(), ]); } diff --git a/app/Infrastructure/Http/Controllers/DashboardController.php b/app/Infrastructure/Http/Controllers/DashboardController.php index 33bf1aa..9f164a9 100644 --- a/app/Infrastructure/Http/Controllers/DashboardController.php +++ b/app/Infrastructure/Http/Controllers/DashboardController.php @@ -7,12 +7,11 @@ namespace Toby\Infrastructure\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Inertia\Response; -use Toby\Domain\Enums\VacationType; +use Toby\Domain\DailySummaryRetriever; use Toby\Domain\UserVacationStatsRetriever; use Toby\Domain\VacationRequestStatesRetriever; use Toby\Domain\VacationTypeConfigRetriever; use Toby\Eloquent\Helpers\YearPeriodRetriever; -use Toby\Eloquent\Models\Vacation; use Toby\Eloquent\Models\VacationRequest; use Toby\Infrastructure\Http\Resources\HolidayResource; use Toby\Infrastructure\Http\Resources\VacationRequestResource; @@ -25,24 +24,14 @@ class DashboardController extends Controller YearPeriodRetriever $yearPeriodRetriever, UserVacationStatsRetriever $vacationStatsRetriever, VacationTypeConfigRetriever $configRetriever, + DailySummaryRetriever $dailySummaryRetriever, ): Response { $user = $request->user(); $now = Carbon::now(); $yearPeriod = $yearPeriodRetriever->selected(); - $absences = Vacation::query() - ->with(["user", "vacationRequest"]) - ->whereDate("date", $now) - ->approved() - ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => $configRetriever->isVacation($type))) - ->get(); - - $remoteDays = Vacation::query() - ->with(["user", "vacationRequest"]) - ->whereDate("date", $now) - ->approved() - ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => !$configRetriever->isVacation($type))) - ->get(); + $absences = $dailySummaryRetriever->getAbsences($now); + $remoteDays = $dailySummaryRetriever->getRemoteDays($now); if ($user->can("listAll", VacationRequest::class)) { $vacationRequests = $yearPeriod->vacationRequests() diff --git a/app/Infrastructure/Http/Controllers/KeysController.php b/app/Infrastructure/Http/Controllers/KeysController.php index 096e540..d71c177 100644 --- a/app/Infrastructure/Http/Controllers/KeysController.php +++ b/app/Infrastructure/Http/Controllers/KeysController.php @@ -8,6 +8,8 @@ use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\Request; use Inertia\Response; use Symfony\Component\HttpFoundation\RedirectResponse; +use Toby\Domain\Notifications\KeyHasBeenGivenNotification; +use Toby\Domain\Notifications\KeyHasBeenTakenNotification; use Toby\Eloquent\Models\Key; use Toby\Eloquent\Models\User; use Toby\Infrastructure\Http\Requests\GiveKeyRequest; @@ -60,6 +62,8 @@ class KeysController extends Controller $key->save(); + $key->notify(new KeyHasBeenTakenNotification($request->user(), $previousUser)); + return redirect() ->back() ->with("success", __("Key no :number has been taken from :user.", [ @@ -81,6 +85,8 @@ class KeysController extends Controller $key->save(); + $key->notify(new KeyHasBeenGivenNotification($request->user(), $recipient)); + return redirect() ->back() ->with("success", __("Key no :number has been given to :user.", [ diff --git a/composer.json b/composer.json index 71cb14b..36d9d98 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,8 @@ "extra": { "laravel": { "dont-discover": [ - "laravel/telescope" + "laravel/telescope", + "spatie/laravel-slack-slash-command" ] } }, diff --git a/config/laravel-slack-slash-command.php b/config/laravel-slack-slash-command.php index 51ea7d4..23eb170 100644 --- a/config/laravel-slack-slash-command.php +++ b/config/laravel-slack-slash-command.php @@ -8,21 +8,18 @@ use Toby\Domain\Slack\Handlers\GiveKeysTo; use Toby\Domain\Slack\Handlers\Help; use Toby\Domain\Slack\Handlers\HomeOffice; use Toby\Domain\Slack\Handlers\KeyList; -use Toby\Domain\Slack\Handlers\SaySomething; use Toby\Domain\Slack\Handlers\TakeKeysFrom; return [ - 'url' => 'api/slack', - 'signing_secret' => env('SLACK_SIGNING_SECRET'), - 'verify_with_signing' => true, - 'handlers' => [ + "signing_secret" => env("SLACK_SIGNING_SECRET"), + "verify_with_signing" => true, + "handlers" => [ TakeKeysFrom::class, GiveKeysTo::class, KeyList::class, HomeOffice::class, DailySummary::class, - SaySomething::class, Help::class, - CatchAll::class + CatchAll::class, ], ]; diff --git a/resources/lang/pl.json b/resources/lang/pl.json index f755a40..192ce08 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -23,11 +23,11 @@ "cancelled": "anulowany", "rejected": "odrzucony", "approved": "zatwierdzony", - "You have pending vacation request in this range.": "Masz oczekujący wniosek urlopowy w tym zakresie dat.", - "You have approved vacation request in this range.": "Masz zaakceptowany wniosek urlopowy w tym zakresie dat.", + "You have pending vacation request in this range.": "Masz oczekujący wniosek w tym zakresie dat.", + "You have approved vacation request in this range.": "Masz zaakceptowany wniosek w tym zakresie dat.", "Vacation limit has been exceeded.": "Limit urlopu został przekroczony.", "Vacation needs minimum one day.": "Urlop musi być co najmniej na jeden dzień.", - "The vacation request cannot be created at the turn of the year.": "Wniosek urlopowy nie może zostać złożony na przełomie roku.", + "The vacation request cannot be created at the turn of the year.": "Wniosek nie może zostać złożony na przełomie roku.", "User has been created.": "Użytkownik został utworzony.", "User has been updated.": "Użytkownik został zaktualizowany.", "User has been deleted.": "Użytkownik został usunięty.", @@ -37,11 +37,11 @@ "Holiday has been deleted.": "Dzień wolny został usunięty.", "Selected year period has been changed.": "Wybrany rok został zmieniony.", "Vacation limits have been updated.": "Limity urlopów zostały zaktualizowane.", - "Vacation request has been created.": "Wniosek urlopowy został utworzony.", - "Vacation request has been accepted.": "Wniosek urlopowy został zaakceptowany.", - "Vacation request has been approved.": "Wniosek urlopowy został zatwierdzony.", - "Vacation request has been rejected.": "Wniosek urlopowy został odrzucony.", - "Vacation request has been cancelled.": "Wniosek urlopowy został anulowany.", + "Vacation request has been created.": "Wniosek został utworzony.", + "Vacation request has been accepted.": "Wniosek został zaakceptowany.", + "Vacation request has been approved.": "Wniosek został zatwierdzony.", + "Vacation request has been rejected.": "Wniosek został odrzucony.", + "Vacation request has been cancelled.": "Wniosek został anulowany.", "Sum:": "Suma:", "Date": "Data", "Day of week": "Dzień tygodnia", @@ -71,5 +71,7 @@ "Key no :number has been created.": "Klucz nr :number został utworzony.", "Key no :number has been deleted.": "Klucz nr :number został usunięty.", "Key no :number has been taken from :user.": "Klucz nr :number został zabrany użytkownikowi :user.", - "Key no :number has been given to :user.": "Klucz nr :number został przekazany użytkownikowi :user." + "Key no :number has been given to :user.": "Klucz nr :number został przekazany użytkownikowi :user.", + ":sender gives key no :key to :recipient": ":sender przekazuje klucz nr :key :recipient", + ":recipient takes key no :key from :sender": ":recipient zabiera klucz nr :key :sender" } diff --git a/routes/api.php b/routes/api.php index d76094c..9d8bace 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,11 +3,14 @@ declare(strict_types=1); use Illuminate\Support\Facades\Route; +use Toby\Domain\Slack\Controller as SlackController; use Toby\Infrastructure\Http\Controllers\Api\CalculateUserUnavailableDaysController; use Toby\Infrastructure\Http\Controllers\Api\CalculateUserVacationStatsController; use Toby\Infrastructure\Http\Controllers\Api\CalculateVacationDaysController; use Toby\Infrastructure\Http\Controllers\Api\GetAvailableVacationTypesController; +Route::post("slack", [SlackController::class, "getResponse"]); + Route::middleware("auth:sanctum")->group(function (): void { Route::post("vacation/calculate-days", CalculateVacationDaysController::class); Route::post("vacation/calculate-stats", CalculateUserVacationStatsController::class); diff --git a/tests/Feature/KeyTest.php b/tests/Feature/KeyTest.php index e9c3ddc..2b1c329 100644 --- a/tests/Feature/KeyTest.php +++ b/tests/Feature/KeyTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Tests\Feature; use Illuminate\Foundation\Testing\DatabaseMigrations; +use Illuminate\Support\Facades\Notification; use Inertia\Testing\AssertableInertia as Assert; use Tests\FeatureTestCase; use Toby\Eloquent\Models\Key; @@ -14,6 +15,13 @@ class KeyTest extends FeatureTestCase { use DatabaseMigrations; + protected function setUp(): void + { + parent::setUp(); + + Notification::fake(); + } + public function testUserCanSeeKeyList(): void { Key::factory()->count(10)->create();