This commit is contained in:
Adrian Hopek 2022-04-25 13:23:49 +02:00
parent 851a52fe32
commit 25816cc47a
28 changed files with 531 additions and 239 deletions

View File

@ -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));
}); });
} }

View 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();
}
}

View 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;
}
}

View 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;
}
}

View 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());
}
}

View File

@ -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);
}
} }

View File

@ -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();
} }
} }

View File

@ -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",
];
} }
} }

View File

@ -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),
); );
} }
} }

View File

@ -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;
} }
} }

View File

@ -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"),
); );
} }
} }

View File

@ -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();
}
}

View File

@ -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",
];
} }
} }

View 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 [];
}
}

View File

@ -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),
]); ]);
} }
} }

View 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";
}
}

View 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, "<@", "|");
}
}

View 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();
}
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Slack;
use Spatie\SlashCommand\Exceptions\SlackSlashCommandException;
class UserNotFoundException extends SlackSlashCommandException
{
}

View File

@ -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();

View File

@ -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(),
]); ]);
} }

View File

@ -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()

View File

@ -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.", [

View File

@ -62,7 +62,8 @@
"extra": { "extra": {
"laravel": { "laravel": {
"dont-discover": [ "dont-discover": [
"laravel/telescope" "laravel/telescope",
"spatie/laravel-slack-slash-command"
] ]
} }
}, },

View File

@ -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,
], ],
]; ];

View File

@ -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"
} }

View File

@ -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);

View File

@ -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();