wip
This commit is contained in:
		| @@ -9,13 +9,13 @@ use Illuminate\Notifications\ChannelManager; | |||||||
| use Illuminate\Support\Carbon; | use Illuminate\Support\Carbon; | ||||||
| use Illuminate\Support\Facades\Notification; | use Illuminate\Support\Facades\Notification; | ||||||
| use Illuminate\Support\ServiceProvider; | use Illuminate\Support\ServiceProvider; | ||||||
| use Toby\Domain\Slack\Channels\SlackApiChannel; | use Toby\Domain\Slack\SlackApiChannel; | ||||||
|  |  | ||||||
| class AppServiceProvider extends ServiceProvider | 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)); |             $service->extend("slack", fn(Application $app) => $app->make(SlackApiChannel::class)); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								app/Domain/DailySummaryRetriever.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/Domain/DailySummaryRetriever.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | declare(strict_types=1); | ||||||
|  |  | ||||||
|  | namespace Toby\Domain; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Carbon; | ||||||
|  | use Illuminate\Support\Collection; | ||||||
|  | use Toby\Domain\Enums\VacationType; | ||||||
|  | use Toby\Eloquent\Models\User; | ||||||
|  | use Toby\Eloquent\Models\Vacation; | ||||||
|  |  | ||||||
|  | class DailySummaryRetriever | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         protected VacationTypeConfigRetriever $configRetriever, | ||||||
|  |     ) {} | ||||||
|  |  | ||||||
|  |     public function getAbsences(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 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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								app/Domain/Notifications/KeyHasBeenGivenNotification.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/Domain/Notifications/KeyHasBeenGivenNotification.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | declare(strict_types=1); | ||||||
|  |  | ||||||
|  | namespace Toby\Domain\Notifications; | ||||||
|  |  | ||||||
|  | use Illuminate\Bus\Queueable; | ||||||
|  | use Illuminate\Notifications\Notification; | ||||||
|  | use Toby\Eloquent\Models\User; | ||||||
|  |  | ||||||
|  | class KeyHasBeenGivenNotification extends Notification | ||||||
|  | { | ||||||
|  |     use Queueable; | ||||||
|  |  | ||||||
|  |     public function __construct( | ||||||
|  |         protected User $sender, | ||||||
|  |         protected User $recipient, | ||||||
|  |     ) {} | ||||||
|  |  | ||||||
|  |     public function via(): array | ||||||
|  |     { | ||||||
|  |         return ["slack"]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function toSlack($notifiable): string | ||||||
|  |     { | ||||||
|  |         return __(":sender gives key no :key to :recipient", [ | ||||||
|  |             "sender" => $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; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								app/Domain/Notifications/KeyHasBeenTakenNotification.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/Domain/Notifications/KeyHasBeenTakenNotification.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | declare(strict_types=1); | ||||||
|  |  | ||||||
|  | namespace Toby\Domain\Notifications; | ||||||
|  |  | ||||||
|  | use Illuminate\Bus\Queueable; | ||||||
|  | use Illuminate\Notifications\Notification; | ||||||
|  | use Toby\Eloquent\Models\User; | ||||||
|  |  | ||||||
|  | class KeyHasBeenTakenNotification extends Notification | ||||||
|  | { | ||||||
|  |     use Queueable; | ||||||
|  |  | ||||||
|  |     public function __construct( | ||||||
|  |         protected User $recipient, | ||||||
|  |         protected User $sender, | ||||||
|  |     ) {} | ||||||
|  |  | ||||||
|  |     public function via(): array | ||||||
|  |     { | ||||||
|  |         return ["slack"]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function toSlack($notifiable): string | ||||||
|  |     { | ||||||
|  |         return __(":recipient takes key no :key from :sender", [ | ||||||
|  |             "recipient" => $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; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								app/Domain/Slack/Controller.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								app/Domain/Slack/Controller.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | declare(strict_types=1); | ||||||
|  |  | ||||||
|  | namespace Toby\Domain\Slack; | ||||||
|  |  | ||||||
|  | use Exception; | ||||||
|  | use Illuminate\Http\Request as IlluminateRequest; | ||||||
|  | use Illuminate\Http\Response as IlluminateResponse; | ||||||
|  | use Illuminate\Support\Collection; | ||||||
|  | use Illuminate\Validation\ValidationException; | ||||||
|  | use Spatie\SlashCommand\Attachment; | ||||||
|  | use Spatie\SlashCommand\Controller as SlackController; | ||||||
|  | use Spatie\SlashCommand\Exceptions\RequestCouldNotBeHandled; | ||||||
|  | use Spatie\SlashCommand\Exceptions\SlackSlashCommandException; | ||||||
|  | use Spatie\SlashCommand\Response; | ||||||
|  |  | ||||||
|  | class Controller extends SlackController | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * @throws RequestCouldNotBeHandled | ||||||
|  |      */ | ||||||
|  |     public function getResponse(IlluminateRequest $request): IlluminateResponse | ||||||
|  |     { | ||||||
|  |         $this->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()); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -4,48 +4,32 @@ declare(strict_types=1); | |||||||
|  |  | ||||||
| namespace Toby\Domain\Slack\Handlers; | namespace Toby\Domain\Slack\Handlers; | ||||||
|  |  | ||||||
| use Illuminate\Support\Collection; |  | ||||||
| use Spatie\SlashCommand\Attachment; | use Spatie\SlashCommand\Attachment; | ||||||
| use Spatie\SlashCommand\AttachmentField; | use Spatie\SlashCommand\Handlers\BaseHandler; | ||||||
| use Spatie\SlashCommand\Handlers\CatchAll as BaseCatchAllHandler; |  | ||||||
| use Spatie\SlashCommand\Handlers\SignatureHandler; |  | ||||||
| use Spatie\SlashCommand\Request; | use Spatie\SlashCommand\Request; | ||||||
| use Spatie\SlashCommand\Response; | 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 |     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 ?? ""); |         return $this->respondToSlack(":x: Nie rozpoznaję tej komendy. Lista wszystkich komend:") | ||||||
|  |             ->withAttachment( | ||||||
|         $alternativeHandlers = $this->findAlternativeHandlers($command); |                 Attachment::create() | ||||||
|  |                     ->setColor("danger") | ||||||
|         if ($alternativeHandlers->count()) { |                     ->useMarkdown() | ||||||
|             $response->withAttachment($this->getCommandListAttachment($alternativeHandlers)); |                     ->setFields($attachmentFields), | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if ($this->containsHelpHandler($alternativeHandlers)) { |  | ||||||
|             $response->withAttachment(Attachment::create() |  | ||||||
|                 ->setText("Aby wyświetlić wszystkie komendy, napisz: `/toby pomoc`") |  | ||||||
|             ); |             ); | ||||||
|         } |  | ||||||
|  |  | ||||||
|         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); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -5,70 +5,52 @@ declare(strict_types=1); | |||||||
| namespace Toby\Domain\Slack\Handlers; | namespace Toby\Domain\Slack\Handlers; | ||||||
|  |  | ||||||
| use Illuminate\Support\Carbon; | use Illuminate\Support\Carbon; | ||||||
| use Illuminate\Support\Collection; |  | ||||||
| use Spatie\SlashCommand\Attachment; | use Spatie\SlashCommand\Attachment; | ||||||
| use Spatie\SlashCommand\Request; | use Spatie\SlashCommand\Request; | ||||||
| use Spatie\SlashCommand\Response; | use Spatie\SlashCommand\Response; | ||||||
| use Spatie\SlashCommand\Handlers\SignatureHandler; | use Toby\Domain\DailySummaryRetriever; | ||||||
| use Toby\Domain\Enums\VacationType; | use Toby\Domain\Slack\SignatureHandler; | ||||||
| use Toby\Domain\VacationTypeConfigRetriever; |  | ||||||
| use Toby\Eloquent\Models\User; | use Toby\Eloquent\Models\User; | ||||||
| use Toby\Eloquent\Models\Vacation; | use Toby\Eloquent\Models\Vacation; | ||||||
|  |  | ||||||
| class DailySummary extends SignatureHandler | class DailySummary extends SignatureHandler | ||||||
| { | { | ||||||
|     protected $signature = "toby dzisiaj"; |     protected $signature = "toby dzisiaj"; | ||||||
|  |     protected $description = "Codzienne podsumowanie"; | ||||||
|     protected $description = "Podsumowanie"; |  | ||||||
|  |  | ||||||
|     public function handle(Request $request): Response |     public function handle(Request $request): Response | ||||||
|     { |     { | ||||||
|         $configRetriever = app(VacationTypeConfigRetriever::class); |         $dailySummaryRetriever = app()->make(DailySummaryRetriever::class); | ||||||
|  |  | ||||||
|         $now = Carbon::today(); |         $now = Carbon::today(); | ||||||
|  |  | ||||||
|         /** @var Collection $absences */ |         $absences = $dailySummaryRetriever->getAbsences($now) | ||||||
|         $absences = Vacation::query() |  | ||||||
|             ->with(["user", "vacationRequest"]) |  | ||||||
|             ->whereDate("date", $now) |  | ||||||
|             ->approved() |  | ||||||
|             ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => $configRetriever->isVacation($type))) |  | ||||||
|             ->get() |  | ||||||
|             ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); |             ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); | ||||||
|  |  | ||||||
|         /** @var Collection $remoteDays */ |         $remoteDays = $dailySummaryRetriever->getRemoteDays($now) | ||||||
|         $remoteDays = Vacation::query() |  | ||||||
|             ->with(["user", "vacationRequest"]) |  | ||||||
|             ->whereDate("date", $now) |  | ||||||
|             ->approved() |  | ||||||
|             ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => !$configRetriever->isVacation($type))) |  | ||||||
|             ->get() |  | ||||||
|             ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); |             ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); | ||||||
|  |  | ||||||
|         $birthdays = User::query() |         $birthdays = $dailySummaryRetriever->getBirthdays($now) | ||||||
|             ->whereRelation("profile", "birthday", $now) |  | ||||||
|             ->get() |  | ||||||
|             ->map(fn(User $user) => $user->profile->full_name); |             ->map(fn(User $user) => $user->profile->full_name); | ||||||
|  |  | ||||||
|         $absencesAttachment = Attachment::create() |         $absencesAttachment = Attachment::create() | ||||||
|             ->setTitle("Nieobecności :palm_tree:") |             ->setTitle("Nieobecności :palm_tree:") | ||||||
|             ->setColor('#eab308') |             ->setColor("#eab308") | ||||||
|             ->setText($absences->isNotEmpty() ? $absences->implode("\n") : "Wszyscy dzisiaj pracują :muscle:"); |             ->setText($absences->isNotEmpty() ? $absences->implode("\n") : "Wszyscy dzisiaj pracują :muscle:"); | ||||||
|  |  | ||||||
|         $remoteAttachment = Attachment::create() |         $remoteAttachment = Attachment::create() | ||||||
|             ->setTitle("Praca zdalna :house_with_garden:") |             ->setTitle("Praca zdalna :house_with_garden:") | ||||||
|             ->setColor('#d946ef') |             ->setColor("#d946ef") | ||||||
|             ->setText($remoteDays->isNotEmpty() ? $remoteDays->implode("\n") : "Wszyscy dzisiaj są w biurze :boom:"); |             ->setText($remoteDays->isNotEmpty() ? $remoteDays->implode("\n") : "Wszyscy dzisiaj są w biurze :boom:"); | ||||||
|  |  | ||||||
|         $birthdayAttachment = Attachment::create() |         $birthdayAttachment = Attachment::create() | ||||||
|             ->setTitle("Urodziny :birthday:") |             ->setTitle("Urodziny :birthday:") | ||||||
|             ->setColor('#3C5F97') |             ->setColor("#3C5F97") | ||||||
|             ->setText($birthdays->isNotEmpty() ? $birthdays->implode("\n") : "Dzisiaj nikt nie ma urodzin :cry:"); |             ->setText($birthdays->isNotEmpty() ? $birthdays->implode("\n") : "Dzisiaj nikt nie ma urodzin :cry:"); | ||||||
|  |  | ||||||
|         return $this->respondToSlack("Podsumowanie dla dnia {$now->toDisplayString()}") |         return $this->respondToSlack("Podsumowanie dla dnia {$now->toDisplayString()}") | ||||||
|             ->withAttachment($absencesAttachment) |             ->withAttachment($absencesAttachment) | ||||||
|             ->withAttachment($remoteAttachment) |             ->withAttachment($remoteAttachment) | ||||||
|             ->withAttachment($birthdayAttachment) |             ->withAttachment($birthdayAttachment); | ||||||
|             ->displayResponseToEveryoneOnChannel(); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -4,46 +4,69 @@ declare(strict_types=1); | |||||||
|  |  | ||||||
| namespace Toby\Domain\Slack\Handlers; | namespace Toby\Domain\Slack\Handlers; | ||||||
|  |  | ||||||
| use Illuminate\Support\Str; | use Illuminate\Validation\ValidationException; | ||||||
| use Spatie\SlashCommand\Request; | use Spatie\SlashCommand\Request; | ||||||
| use Spatie\SlashCommand\Response; | 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\Key; | ||||||
| use Toby\Eloquent\Models\User; |  | ||||||
|  |  | ||||||
| class GiveKeysTo extends SignatureHandler | 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 |     public function handle(Request $request): Response | ||||||
|     { |     { | ||||||
|         $to = $this->getArgument('użytkownik'); |         ["user" => $from] = $this->validate(); | ||||||
|  |  | ||||||
|         $id = Str::between($to, "@", "|"); |         $authUser = $this->findUserBySlackIdOrFail($request->userId); | ||||||
|  |         $user = $this->findUserBySlackId($from); | ||||||
|         $authUser = $this->findUserBySlackId($request->userId); |  | ||||||
|         $user = $this->findUserBySlackId($id); |  | ||||||
|  |  | ||||||
|         /** @var Key $key */ |         /** @var Key $key */ | ||||||
|         $key = $authUser->keys()->first(); |         $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->user()->associate($user); | ||||||
|  |  | ||||||
|         $key->save(); |         $key->save(); | ||||||
|  |  | ||||||
|         return $this->respondToSlack("<@{$authUser->profile->slack_id}> daje klucz nr {$key->id} użytkownikowi <@{$user->profile->slack_id}>") |         $key->notify(new KeyHasBeenGivenNotification($authUser, $user)); | ||||||
|             ->displayResponseToEveryoneOnChannel(); |  | ||||||
|  |         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 */ |         return [ | ||||||
|         $user = User::query() |             "user" => ["required", new SlackUserExistsRule()], | ||||||
|             ->whereRelation("profile", "slack_id", $slackId) |         ]; | ||||||
|             ->first(); |     } | ||||||
|  |  | ||||||
|         return $user; |     protected function getMessages(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             "user.required" => "Musisz podać użytkownika, któremu chcesz przekazać klucze", | ||||||
|  |         ]; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -4,42 +4,31 @@ declare(strict_types=1); | |||||||
|  |  | ||||||
| namespace Toby\Domain\Slack\Handlers; | namespace Toby\Domain\Slack\Handlers; | ||||||
|  |  | ||||||
| use Illuminate\Support\Collection; |  | ||||||
| use Spatie\SlashCommand\Attachment; | 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\Request; | ||||||
| use Spatie\SlashCommand\Response; | 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 $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 |     public function handle(Request $request): Response | ||||||
|     { |     { | ||||||
|         $handlers = $this->findAvailableHandlers(); |         $handlers = $this->findAvailableHandlers(); | ||||||
|  |  | ||||||
|         return $this->displayListOfAllCommands($handlers); |         $attachmentFields = $this->mapHandlersToAttachments($handlers); | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected function displayListOfAllCommands(Collection $handlers): Response |         return $this->respondToSlack("Dostępne komendy:") | ||||||
|     { |  | ||||||
|         $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') |  | ||||||
|             ->withAttachment( |             ->withAttachment( | ||||||
|                 Attachment::create() |                 Attachment::create() | ||||||
|                     ->setColor('good') |                     ->setColor("good") | ||||||
|                     ->setFields($attachmentFields) |                     ->useMarkdown() | ||||||
|  |                     ->setFields($attachmentFields), | ||||||
|             ); |             ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -7,22 +7,31 @@ namespace Toby\Domain\Slack\Handlers; | |||||||
| use Illuminate\Support\Carbon; | use Illuminate\Support\Carbon; | ||||||
| use Spatie\SlashCommand\Request; | use Spatie\SlashCommand\Request; | ||||||
| use Spatie\SlashCommand\Response; | use Spatie\SlashCommand\Response; | ||||||
| use Spatie\SlashCommand\Handlers\SignatureHandler; |  | ||||||
| use Toby\Domain\Actions\VacationRequest\CreateAction; | use Toby\Domain\Actions\VacationRequest\CreateAction; | ||||||
| use Toby\Domain\Enums\VacationType; | 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\User; | ||||||
| use Toby\Eloquent\Models\YearPeriod; | use Toby\Eloquent\Models\YearPeriod; | ||||||
|  |  | ||||||
| class HomeOffice extends SignatureHandler | class HomeOffice extends SignatureHandler | ||||||
| { | { | ||||||
|     protected $signature = "toby zdalnie {kiedy?}"; |     use FindsUserBySlackId; | ||||||
|     protected $description = "Pracuj zdalnie wybranego dnia (domyślnie dzisiaj)"; |  | ||||||
|  |     protected $signature = "toby zdalnie"; | ||||||
|  |     protected $description = "Pracuj dzisiaj zdalnie"; | ||||||
|  |  | ||||||
|     public function handle(Request $request): Response |     public function handle(Request $request): Response | ||||||
|     { |     { | ||||||
|         $date = $this->getDateFromArgument($this->getArgument('kiedy') ?? "dzisiaj"); |  | ||||||
|         $user = $this->findUserBySlackId($request->userId); |         $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); |         $yearPeriod = YearPeriod::findByYear($date->year); | ||||||
|  |  | ||||||
|         app(CreateAction::class)->execute([ |         app(CreateAction::class)->execute([ | ||||||
| @@ -33,27 +42,5 @@ class HomeOffice extends SignatureHandler | |||||||
|             "year_period_id" => $yearPeriod->id, |             "year_period_id" => $yearPeriod->id, | ||||||
|             "flow_skipped" => false, |             "flow_skipped" => false, | ||||||
|         ], $user); |         ], $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; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -7,13 +7,12 @@ namespace Toby\Domain\Slack\Handlers; | |||||||
| use Spatie\SlashCommand\Attachment; | use Spatie\SlashCommand\Attachment; | ||||||
| use Spatie\SlashCommand\Request; | use Spatie\SlashCommand\Request; | ||||||
| use Spatie\SlashCommand\Response; | use Spatie\SlashCommand\Response; | ||||||
| use Spatie\SlashCommand\Handlers\SignatureHandler; | use Toby\Domain\Slack\SignatureHandler; | ||||||
| use Toby\Eloquent\Models\Key; | use Toby\Eloquent\Models\Key; | ||||||
|  |  | ||||||
| class KeyList extends SignatureHandler | class KeyList extends SignatureHandler | ||||||
| { | { | ||||||
|     protected $signature = "toby klucze"; |     protected $signature = "toby klucze"; | ||||||
|  |  | ||||||
|     protected $description = "Lista wszystkich kluczy"; |     protected $description = "Lista wszystkich kluczy"; | ||||||
|  |  | ||||||
|     public function handle(Request $request): Response |     public function handle(Request $request): Response | ||||||
| @@ -23,11 +22,11 @@ class KeyList extends SignatureHandler | |||||||
|             ->get() |             ->get() | ||||||
|             ->map(fn(Key $key) => "Klucz nr {$key->id} - <@{$key->user->profile->slack_id}>"); |             ->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( |             ->withAttachment( | ||||||
|                 Attachment::create() |                 Attachment::create() | ||||||
|                     ->setColor('#3C5F97') |                     ->setColor("#3C5F97") | ||||||
|                     ->setText($keys->implode("\n")) |                     ->setText($keys->isNotEmpty() ? $keys->implode("\n") : "Nie ma żadnych kluczy w tobym"), | ||||||
|             ); |             ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,24 +0,0 @@ | |||||||
| <?php |  | ||||||
|  |  | ||||||
| declare(strict_types=1); |  | ||||||
|  |  | ||||||
| namespace Toby\Domain\Slack\Handlers; |  | ||||||
|  |  | ||||||
| use Spatie\SlashCommand\Request; |  | ||||||
| use Spatie\SlashCommand\Response; |  | ||||||
| use Spatie\SlashCommand\Handlers\SignatureHandler; |  | ||||||
|  |  | ||||||
| class SaySomething extends SignatureHandler |  | ||||||
| { |  | ||||||
|     protected $signature = "toby powiedz {zdanie}"; |  | ||||||
|  |  | ||||||
|     protected $description = "Powiedz zdanie"; |  | ||||||
|  |  | ||||||
|     public function handle(Request $request): Response |  | ||||||
|     { |  | ||||||
|         $sentence = $this->getArgument("zdanie"); |  | ||||||
|  |  | ||||||
|         return $this->respondToSlack($sentence) |  | ||||||
|             ->displayResponseToEveryoneOnChannel(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -4,46 +4,68 @@ declare(strict_types=1); | |||||||
|  |  | ||||||
| namespace Toby\Domain\Slack\Handlers; | namespace Toby\Domain\Slack\Handlers; | ||||||
|  |  | ||||||
| use Illuminate\Support\Str; | use Illuminate\Validation\ValidationException; | ||||||
| use Spatie\SlashCommand\Request; | use Spatie\SlashCommand\Request; | ||||||
| use Spatie\SlashCommand\Response; | 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\Key; | ||||||
| use Toby\Eloquent\Models\User; |  | ||||||
|  |  | ||||||
| class TakeKeysFrom extends SignatureHandler | 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"; |     protected $description = "Zabierz klucze wskazanemu użytkownikowi"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @throws UserNotFoundException|ValidationException | ||||||
|  |      */ | ||||||
|     public function handle(Request $request): Response |     public function handle(Request $request): Response | ||||||
|     { |     { | ||||||
|         $from = $this->getArgument("użytkownik"); |         ["user" => $from] = $this->validate(); | ||||||
|  |  | ||||||
|         $id = Str::between($from, "@", "|"); |         $authUser = $this->findUserBySlackIdOrFail($request->userId); | ||||||
|  |         $user = $this->findUserBySlackId($from); | ||||||
|         $authUser = $this->findUserBySlackId($request->userId); |  | ||||||
|         $user = $this->findUserBySlackId($id); |  | ||||||
|  |  | ||||||
|         /** @var Key $key */ |         /** @var Key $key */ | ||||||
|         $key = $user->keys()->first(); |         $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->user()->associate($authUser); | ||||||
|  |  | ||||||
|         $key->save(); |         $key->save(); | ||||||
|  |  | ||||||
|         return $this->respondToSlack("<@{$authUser->profile->slack_id}> zabiera klucz nr {$key->id} użytkownikowi <@{$user->profile->slack_id}>") |         $key->notify(new KeyHasBeenTakenNotification($authUser, $user)); | ||||||
|             ->displayResponseToEveryoneOnChannel(); |  | ||||||
|  |         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 */ |         return [ | ||||||
|         $user = User::query() |             "user" => ["required", new SlackUserExistsRule()], | ||||||
|             ->whereRelation("profile", "slack_id", $slackId) |         ]; | ||||||
|             ->first(); |     } | ||||||
|  |  | ||||||
|         return $user; |     protected function getMessages(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             "user.required" => "Musisz podać użytkownika, któremu chcesz zabrać klucze", | ||||||
|  |         ]; | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										26
									
								
								app/Domain/Slack/SignatureHandler.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/Domain/Slack/SignatureHandler.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | declare(strict_types=1); | ||||||
|  |  | ||||||
|  | namespace Toby\Domain\Slack; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\Validator; | ||||||
|  | use Spatie\SlashCommand\Handlers\SignatureHandler as BaseSignatureHandler; | ||||||
|  |  | ||||||
|  | abstract class SignatureHandler extends BaseSignatureHandler | ||||||
|  | { | ||||||
|  |     public function validate() | ||||||
|  |     { | ||||||
|  |         return Validator::validate($this->getArguments(), $this->getRules(), $this->getMessages()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected function getRules(): array | ||||||
|  |     { | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected function getMessages(): array | ||||||
|  |     { | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -2,7 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| declare(strict_types=1); | declare(strict_types=1); | ||||||
| 
 | 
 | ||||||
| namespace Toby\Domain\Slack\Channels; | namespace Toby\Domain\Slack; | ||||||
| 
 | 
 | ||||||
| use Illuminate\Http\Client\Response; | use Illuminate\Http\Client\Response; | ||||||
| use Illuminate\Notifications\Notification; | use Illuminate\Notifications\Notification; | ||||||
| @@ -14,12 +14,12 @@ class SlackApiChannel | |||||||
|     { |     { | ||||||
|         $baseUrl = config("services.slack.url"); |         $baseUrl = config("services.slack.url"); | ||||||
|         $url = "{$baseUrl}/chat.postMessage"; |         $url = "{$baseUrl}/chat.postMessage"; | ||||||
|         $channel = $notifiable->routeNotificationFor('slack', $notification); |         $channel = $notifiable->routeNotificationFor("slack", $notification); | ||||||
| 
 | 
 | ||||||
|         return Http::withToken(config("services.slack.client_token")) |         return Http::withToken(config("services.slack.client_token")) | ||||||
|             ->post($url, [ |             ->post($url, [ | ||||||
|                 "channel" => $channel, |                 "channel" => $channel, | ||||||
|                 "text" => $notification->toSlack(), |                 "text" => $notification->toSlack($notifiable), | ||||||
|             ]); |             ]); | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										24
									
								
								app/Domain/Slack/SlackUserExistsRule.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/Domain/Slack/SlackUserExistsRule.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | declare(strict_types=1); | ||||||
|  |  | ||||||
|  | namespace Toby\Domain\Slack; | ||||||
|  |  | ||||||
|  | use Illuminate\Contracts\Validation\Rule; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | use Toby\Eloquent\Models\Profile; | ||||||
|  |  | ||||||
|  | class SlackUserExistsRule implements Rule | ||||||
|  | { | ||||||
|  |     public function passes($attribute, $value): bool | ||||||
|  |     { | ||||||
|  |         $slackId = Str::between($value, "<@", "|"); | ||||||
|  |  | ||||||
|  |         return Profile::query()->where("slack_id", $slackId)->exists(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function message(): string | ||||||
|  |     { | ||||||
|  |         return "Użytkownik :input nie istnieje w tobym"; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										43
									
								
								app/Domain/Slack/Traits/FindsUserBySlackId.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/Domain/Slack/Traits/FindsUserBySlackId.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | declare(strict_types=1); | ||||||
|  |  | ||||||
|  | namespace Toby\Domain\Slack\Traits; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | use Toby\Domain\Slack\UserNotFoundException; | ||||||
|  | use Toby\Eloquent\Models\User; | ||||||
|  |  | ||||||
|  | trait FindsUserBySlackId | ||||||
|  | { | ||||||
|  |     protected function findUserBySlackId(string $slackId): ?User | ||||||
|  |     { | ||||||
|  |         $id = $this->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, "<@", "|"); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								app/Domain/Slack/Traits/ListsHandlers.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/Domain/Slack/Traits/ListsHandlers.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | declare(strict_types=1); | ||||||
|  |  | ||||||
|  | namespace Toby\Domain\Slack\Traits; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Collection; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | use Spatie\SlashCommand\AttachmentField; | ||||||
|  | use Spatie\SlashCommand\Handlers\SignatureHandler; | ||||||
|  | use Spatie\SlashCommand\Handlers\SignatureParts; | ||||||
|  | use Spatie\SlashCommand\HandlesSlashCommand; | ||||||
|  |  | ||||||
|  | trait ListsHandlers | ||||||
|  | { | ||||||
|  |     protected function findAvailableHandlers(): Collection | ||||||
|  |     { | ||||||
|  |         return collect(config("laravel-slack-slash-command.handlers")) | ||||||
|  |             ->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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								app/Domain/Slack/UserNotFoundException.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/Domain/Slack/UserNotFoundException.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | declare(strict_types=1); | ||||||
|  |  | ||||||
|  | namespace Toby\Domain\Slack; | ||||||
|  |  | ||||||
|  | use Spatie\SlashCommand\Exceptions\SlackSlashCommandException; | ||||||
|  |  | ||||||
|  | class UserNotFoundException extends SlackSlashCommandException | ||||||
|  | { | ||||||
|  | } | ||||||
| @@ -8,6 +8,7 @@ use Database\Factories\KeyFactory; | |||||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||||
| use Illuminate\Database\Eloquent\Model; | use Illuminate\Database\Eloquent\Model; | ||||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||||
|  | use Illuminate\Notifications\Notifiable; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @property int $id |  * @property int $id | ||||||
| @@ -16,6 +17,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; | |||||||
| class Key extends Model | class Key extends Model | ||||||
| { | { | ||||||
|     use HasFactory; |     use HasFactory; | ||||||
|  |     use Notifiable; | ||||||
|  |  | ||||||
|     protected $guarded = []; |     protected $guarded = []; | ||||||
|  |  | ||||||
| @@ -24,6 +26,11 @@ class Key extends Model | |||||||
|         return $this->belongsTo(User::class); |         return $this->belongsTo(User::class); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public function routeNotificationForSlack(): string | ||||||
|  |     { | ||||||
|  |         return config("services.slack.default_channel"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     protected static function newFactory(): KeyFactory |     protected static function newFactory(): KeyFactory | ||||||
|     { |     { | ||||||
|         return KeyFactory::new(); |         return KeyFactory::new(); | ||||||
|   | |||||||
| @@ -7,11 +7,9 @@ namespace Toby\Infrastructure\Console\Commands; | |||||||
| use Carbon\CarbonInterface; | use Carbon\CarbonInterface; | ||||||
| use Illuminate\Console\Command; | use Illuminate\Console\Command; | ||||||
| use Illuminate\Support\Carbon; | use Illuminate\Support\Carbon; | ||||||
| use Illuminate\Support\Collection; |  | ||||||
| use Illuminate\Support\Facades\Http; | use Illuminate\Support\Facades\Http; | ||||||
| use Spatie\SlashCommand\Attachment; | use Spatie\SlashCommand\Attachment; | ||||||
| use Toby\Domain\Enums\VacationType; | use Toby\Domain\DailySummaryRetriever; | ||||||
| use Toby\Domain\VacationTypeConfigRetriever; |  | ||||||
| use Toby\Eloquent\Models\Holiday; | use Toby\Eloquent\Models\Holiday; | ||||||
| use Toby\Eloquent\Models\User; | use Toby\Eloquent\Models\User; | ||||||
| use Toby\Eloquent\Models\Vacation; | use Toby\Eloquent\Models\Vacation; | ||||||
| @@ -21,7 +19,7 @@ class SendDailySummaryToSlack extends Command | |||||||
|     protected $signature = "toby:slack:daily-summary {--f|force}"; |     protected $signature = "toby:slack:daily-summary {--f|force}"; | ||||||
|     protected $description = "Sent daily summary to slack"; |     protected $description = "Sent daily summary to slack"; | ||||||
|  |  | ||||||
|     public function handle(VacationTypeConfigRetriever $configRetriever): void |     public function handle(DailySummaryRetriever $dailySummaryRetriever): void | ||||||
|     { |     { | ||||||
|         $now = Carbon::today(); |         $now = Carbon::today(); | ||||||
|  |  | ||||||
| @@ -29,42 +27,28 @@ class SendDailySummaryToSlack extends Command | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** @var Collection $absences */ |         $absences = $dailySummaryRetriever->getAbsences($now) | ||||||
|         $absences = Vacation::query() |  | ||||||
|             ->with(["user", "vacationRequest"]) |  | ||||||
|             ->whereDate("date", $now) |  | ||||||
|             ->approved() |  | ||||||
|             ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => $configRetriever->isVacation($type))) |  | ||||||
|             ->get() |  | ||||||
|             ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); |             ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); | ||||||
|  |  | ||||||
|         /** @var Collection $remoteDays */ |         $remoteDays = $dailySummaryRetriever->getRemoteDays($now) | ||||||
|         $remoteDays = Vacation::query() |  | ||||||
|             ->with(["user", "vacationRequest"]) |  | ||||||
|             ->whereDate("date", $now) |  | ||||||
|             ->approved() |  | ||||||
|             ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => !$configRetriever->isVacation($type))) |  | ||||||
|             ->get() |  | ||||||
|             ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); |             ->map(fn(Vacation $vacation) => $vacation->user->profile->full_name); | ||||||
|  |  | ||||||
|         $birthdays = User::query() |         $birthdays = $dailySummaryRetriever->getBirthdays($now) | ||||||
|             ->whereRelation("profile", "birthday", $now) |  | ||||||
|             ->get() |  | ||||||
|             ->map(fn(User $user) => $user->profile->full_name); |             ->map(fn(User $user) => $user->profile->full_name); | ||||||
|  |  | ||||||
|         $absencesAttachment = Attachment::create() |         $absencesAttachment = Attachment::create() | ||||||
|             ->setTitle("Nieobecności :palm_tree:") |             ->setTitle("Nieobecności :palm_tree:") | ||||||
|             ->setColor('#eab308') |             ->setColor("#eab308") | ||||||
|             ->setText($absences->isNotEmpty() ? $absences->implode("\n") : "Wszyscy dzisiaj pracują :muscle:"); |             ->setText($absences->isNotEmpty() ? $absences->implode("\n") : "Wszyscy dzisiaj pracują :muscle:"); | ||||||
|  |  | ||||||
|         $remoteAttachment = Attachment::create() |         $remoteAttachment = Attachment::create() | ||||||
|             ->setTitle("Praca zdalna :house_with_garden:") |             ->setTitle("Praca zdalna :house_with_garden:") | ||||||
|             ->setColor('#d946ef') |             ->setColor("#d946ef") | ||||||
|             ->setText($remoteDays->isNotEmpty() ? $remoteDays->implode("\n") : "Wszyscy dzisiaj są w biurze :boom:"); |             ->setText($remoteDays->isNotEmpty() ? $remoteDays->implode("\n") : "Wszyscy dzisiaj są w biurze :boom:"); | ||||||
|  |  | ||||||
|         $birthdayAttachment = Attachment::create() |         $birthdayAttachment = Attachment::create() | ||||||
|             ->setTitle("Urodziny :birthday:") |             ->setTitle("Urodziny :birthday:") | ||||||
|             ->setColor('#3C5F97') |             ->setColor("#3C5F97") | ||||||
|             ->setText($birthdays->isNotEmpty() ? $birthdays->implode("\n") : "Dzisiaj nikt nie ma urodzin :cry:"); |             ->setText($birthdays->isNotEmpty() ? $birthdays->implode("\n") : "Dzisiaj nikt nie ma urodzin :cry:"); | ||||||
|  |  | ||||||
|         $baseUrl = config("services.slack.url"); |         $baseUrl = config("services.slack.url"); | ||||||
| @@ -74,8 +58,8 @@ class SendDailySummaryToSlack extends Command | |||||||
|             ->post($url, [ |             ->post($url, [ | ||||||
|                 "channel" => config("services.slack.default_channel"), |                 "channel" => config("services.slack.default_channel"), | ||||||
|                 "text" => "Podsumowanie dla dnia {$now->toDisplayString()}", |                 "text" => "Podsumowanie dla dnia {$now->toDisplayString()}", | ||||||
|                 'attachments' => collect([$absencesAttachment, $remoteAttachment, $birthdayAttachment])->map( |                 "attachments" => collect([$absencesAttachment, $remoteAttachment, $birthdayAttachment])->map( | ||||||
|                     fn(Attachment $attachment) => $attachment->toArray() |                     fn(Attachment $attachment) => $attachment->toArray(), | ||||||
|                 )->toArray(), |                 )->toArray(), | ||||||
|             ]); |             ]); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -7,12 +7,11 @@ namespace Toby\Infrastructure\Http\Controllers; | |||||||
| use Illuminate\Http\Request; | use Illuminate\Http\Request; | ||||||
| use Illuminate\Support\Carbon; | use Illuminate\Support\Carbon; | ||||||
| use Inertia\Response; | use Inertia\Response; | ||||||
| use Toby\Domain\Enums\VacationType; | use Toby\Domain\DailySummaryRetriever; | ||||||
| use Toby\Domain\UserVacationStatsRetriever; | use Toby\Domain\UserVacationStatsRetriever; | ||||||
| use Toby\Domain\VacationRequestStatesRetriever; | use Toby\Domain\VacationRequestStatesRetriever; | ||||||
| use Toby\Domain\VacationTypeConfigRetriever; | use Toby\Domain\VacationTypeConfigRetriever; | ||||||
| use Toby\Eloquent\Helpers\YearPeriodRetriever; | use Toby\Eloquent\Helpers\YearPeriodRetriever; | ||||||
| use Toby\Eloquent\Models\Vacation; |  | ||||||
| use Toby\Eloquent\Models\VacationRequest; | use Toby\Eloquent\Models\VacationRequest; | ||||||
| use Toby\Infrastructure\Http\Resources\HolidayResource; | use Toby\Infrastructure\Http\Resources\HolidayResource; | ||||||
| use Toby\Infrastructure\Http\Resources\VacationRequestResource; | use Toby\Infrastructure\Http\Resources\VacationRequestResource; | ||||||
| @@ -25,24 +24,14 @@ class DashboardController extends Controller | |||||||
|         YearPeriodRetriever $yearPeriodRetriever, |         YearPeriodRetriever $yearPeriodRetriever, | ||||||
|         UserVacationStatsRetriever $vacationStatsRetriever, |         UserVacationStatsRetriever $vacationStatsRetriever, | ||||||
|         VacationTypeConfigRetriever $configRetriever, |         VacationTypeConfigRetriever $configRetriever, | ||||||
|  |         DailySummaryRetriever $dailySummaryRetriever, | ||||||
|     ): Response { |     ): Response { | ||||||
|         $user = $request->user(); |         $user = $request->user(); | ||||||
|         $now = Carbon::now(); |         $now = Carbon::now(); | ||||||
|         $yearPeriod = $yearPeriodRetriever->selected(); |         $yearPeriod = $yearPeriodRetriever->selected(); | ||||||
|  |  | ||||||
|         $absences = Vacation::query() |         $absences = $dailySummaryRetriever->getAbsences($now); | ||||||
|             ->with(["user", "vacationRequest"]) |         $remoteDays = $dailySummaryRetriever->getRemoteDays($now); | ||||||
|             ->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(); |  | ||||||
|  |  | ||||||
|         if ($user->can("listAll", VacationRequest::class)) { |         if ($user->can("listAll", VacationRequest::class)) { | ||||||
|             $vacationRequests = $yearPeriod->vacationRequests() |             $vacationRequests = $yearPeriod->vacationRequests() | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ use Illuminate\Auth\Access\AuthorizationException; | |||||||
| use Illuminate\Http\Request; | use Illuminate\Http\Request; | ||||||
| use Inertia\Response; | use Inertia\Response; | ||||||
| use Symfony\Component\HttpFoundation\RedirectResponse; | use Symfony\Component\HttpFoundation\RedirectResponse; | ||||||
|  | use Toby\Domain\Notifications\KeyHasBeenGivenNotification; | ||||||
|  | use Toby\Domain\Notifications\KeyHasBeenTakenNotification; | ||||||
| use Toby\Eloquent\Models\Key; | use Toby\Eloquent\Models\Key; | ||||||
| use Toby\Eloquent\Models\User; | use Toby\Eloquent\Models\User; | ||||||
| use Toby\Infrastructure\Http\Requests\GiveKeyRequest; | use Toby\Infrastructure\Http\Requests\GiveKeyRequest; | ||||||
| @@ -60,6 +62,8 @@ class KeysController extends Controller | |||||||
|  |  | ||||||
|         $key->save(); |         $key->save(); | ||||||
|  |  | ||||||
|  |         $key->notify(new KeyHasBeenTakenNotification($request->user(), $previousUser)); | ||||||
|  |  | ||||||
|         return redirect() |         return redirect() | ||||||
|             ->back() |             ->back() | ||||||
|             ->with("success", __("Key no :number has been taken from :user.", [ |             ->with("success", __("Key no :number has been taken from :user.", [ | ||||||
| @@ -81,6 +85,8 @@ class KeysController extends Controller | |||||||
|  |  | ||||||
|         $key->save(); |         $key->save(); | ||||||
|  |  | ||||||
|  |         $key->notify(new KeyHasBeenGivenNotification($request->user(), $recipient)); | ||||||
|  |  | ||||||
|         return redirect() |         return redirect() | ||||||
|             ->back() |             ->back() | ||||||
|             ->with("success", __("Key no :number has been given to :user.", [ |             ->with("success", __("Key no :number has been given to :user.", [ | ||||||
|   | |||||||
| @@ -62,7 +62,8 @@ | |||||||
|     "extra": { |     "extra": { | ||||||
|         "laravel": { |         "laravel": { | ||||||
|             "dont-discover": [ |             "dont-discover": [ | ||||||
|                 "laravel/telescope" |                 "laravel/telescope", | ||||||
|  |                 "spatie/laravel-slack-slash-command" | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -8,21 +8,18 @@ use Toby\Domain\Slack\Handlers\GiveKeysTo; | |||||||
| use Toby\Domain\Slack\Handlers\Help; | use Toby\Domain\Slack\Handlers\Help; | ||||||
| use Toby\Domain\Slack\Handlers\HomeOffice; | use Toby\Domain\Slack\Handlers\HomeOffice; | ||||||
| use Toby\Domain\Slack\Handlers\KeyList; | use Toby\Domain\Slack\Handlers\KeyList; | ||||||
| use Toby\Domain\Slack\Handlers\SaySomething; |  | ||||||
| use Toby\Domain\Slack\Handlers\TakeKeysFrom; | use Toby\Domain\Slack\Handlers\TakeKeysFrom; | ||||||
|  |  | ||||||
| return [ | return [ | ||||||
|     'url' => 'api/slack', |     "signing_secret" => env("SLACK_SIGNING_SECRET"), | ||||||
|     'signing_secret' => env('SLACK_SIGNING_SECRET'), |     "verify_with_signing" => true, | ||||||
|     'verify_with_signing' => true, |     "handlers" => [ | ||||||
|     'handlers' => [ |  | ||||||
|         TakeKeysFrom::class, |         TakeKeysFrom::class, | ||||||
|         GiveKeysTo::class, |         GiveKeysTo::class, | ||||||
|         KeyList::class, |         KeyList::class, | ||||||
|         HomeOffice::class, |         HomeOffice::class, | ||||||
|         DailySummary::class, |         DailySummary::class, | ||||||
|         SaySomething::class, |  | ||||||
|         Help::class, |         Help::class, | ||||||
|         CatchAll::class |         CatchAll::class, | ||||||
|     ], |     ], | ||||||
| ]; | ]; | ||||||
|   | |||||||
| @@ -23,11 +23,11 @@ | |||||||
|   "cancelled": "anulowany", |   "cancelled": "anulowany", | ||||||
|   "rejected": "odrzucony", |   "rejected": "odrzucony", | ||||||
|   "approved": "zatwierdzony", |   "approved": "zatwierdzony", | ||||||
|   "You have pending vacation request in this range.": "Masz oczekujący 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 urlopowy 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 limit has been exceeded.": "Limit urlopu został przekroczony.", | ||||||
|   "Vacation needs minimum one day.": "Urlop musi być co najmniej na jeden dzień.", |   "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 created.": "Użytkownik został utworzony.", | ||||||
|   "User has been updated.": "Użytkownik został zaktualizowany.", |   "User has been updated.": "Użytkownik został zaktualizowany.", | ||||||
|   "User has been deleted.": "Użytkownik został usunięty.", |   "User has been deleted.": "Użytkownik został usunięty.", | ||||||
| @@ -37,11 +37,11 @@ | |||||||
|   "Holiday has been deleted.": "Dzień wolny został usunięty.", |   "Holiday has been deleted.": "Dzień wolny został usunięty.", | ||||||
|   "Selected year period has been changed.": "Wybrany rok został zmieniony.", |   "Selected year period has been changed.": "Wybrany rok został zmieniony.", | ||||||
|   "Vacation limits have been updated.": "Limity urlopów zostały zaktualizowane.", |   "Vacation limits have been updated.": "Limity urlopów zostały zaktualizowane.", | ||||||
|   "Vacation request has been created.": "Wniosek urlopowy został utworzony.", |   "Vacation request has been created.": "Wniosek został utworzony.", | ||||||
|   "Vacation request has been accepted.": "Wniosek urlopowy został zaakceptowany.", |   "Vacation request has been accepted.": "Wniosek został zaakceptowany.", | ||||||
|   "Vacation request has been approved.": "Wniosek urlopowy został zatwierdzony.", |   "Vacation request has been approved.": "Wniosek został zatwierdzony.", | ||||||
|   "Vacation request has been rejected.": "Wniosek urlopowy został odrzucony.", |   "Vacation request has been rejected.": "Wniosek został odrzucony.", | ||||||
|   "Vacation request has been cancelled.": "Wniosek urlopowy został anulowany.", |   "Vacation request has been cancelled.": "Wniosek został anulowany.", | ||||||
|   "Sum:": "Suma:", |   "Sum:": "Suma:", | ||||||
|   "Date": "Data", |   "Date": "Data", | ||||||
|   "Day of week": "Dzień tygodnia", |   "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 created.": "Klucz nr :number został utworzony.", | ||||||
|   "Key no :number has been deleted.": "Klucz nr :number został usunięty.", |   "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 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" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,11 +3,14 @@ | |||||||
| declare(strict_types=1); | declare(strict_types=1); | ||||||
|  |  | ||||||
| use Illuminate\Support\Facades\Route; | 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\CalculateUserUnavailableDaysController; | ||||||
| use Toby\Infrastructure\Http\Controllers\Api\CalculateUserVacationStatsController; | use Toby\Infrastructure\Http\Controllers\Api\CalculateUserVacationStatsController; | ||||||
| use Toby\Infrastructure\Http\Controllers\Api\CalculateVacationDaysController; | use Toby\Infrastructure\Http\Controllers\Api\CalculateVacationDaysController; | ||||||
| use Toby\Infrastructure\Http\Controllers\Api\GetAvailableVacationTypesController; | use Toby\Infrastructure\Http\Controllers\Api\GetAvailableVacationTypesController; | ||||||
|  |  | ||||||
|  | Route::post("slack", [SlackController::class, "getResponse"]); | ||||||
|  |  | ||||||
| Route::middleware("auth:sanctum")->group(function (): void { | Route::middleware("auth:sanctum")->group(function (): void { | ||||||
|     Route::post("vacation/calculate-days", CalculateVacationDaysController::class); |     Route::post("vacation/calculate-days", CalculateVacationDaysController::class); | ||||||
|     Route::post("vacation/calculate-stats", CalculateUserVacationStatsController::class); |     Route::post("vacation/calculate-stats", CalculateUserVacationStatsController::class); | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ declare(strict_types=1); | |||||||
| namespace Tests\Feature; | namespace Tests\Feature; | ||||||
|  |  | ||||||
| use Illuminate\Foundation\Testing\DatabaseMigrations; | use Illuminate\Foundation\Testing\DatabaseMigrations; | ||||||
|  | use Illuminate\Support\Facades\Notification; | ||||||
| use Inertia\Testing\AssertableInertia as Assert; | use Inertia\Testing\AssertableInertia as Assert; | ||||||
| use Tests\FeatureTestCase; | use Tests\FeatureTestCase; | ||||||
| use Toby\Eloquent\Models\Key; | use Toby\Eloquent\Models\Key; | ||||||
| @@ -14,6 +15,13 @@ class KeyTest extends FeatureTestCase | |||||||
| { | { | ||||||
|     use DatabaseMigrations; |     use DatabaseMigrations; | ||||||
|  |  | ||||||
|  |     protected function setUp(): void | ||||||
|  |     { | ||||||
|  |         parent::setUp(); | ||||||
|  |  | ||||||
|  |         Notification::fake(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public function testUserCanSeeKeyList(): void |     public function testUserCanSeeKeyList(): void | ||||||
|     { |     { | ||||||
|         Key::factory()->count(10)->create(); |         Key::factory()->count(10)->create(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user