This commit is contained in:
Adrian Hopek
2022-04-26 12:46:18 +02:00
parent cbfc2b0c45
commit 7f5e462e4d
54 changed files with 361 additions and 211 deletions

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Channels;
use Illuminate\Http\Client\Response;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Http;
use Toby\Domain\Notifications\Notifiable;
class SlackApiChannel
{
public function send(Notifiable $notifiable, Notification $notification): Response
{
$baseUrl = $this->getBaseUrl();
$url = "{$baseUrl}/chat.postMessage";
$channel = $notifiable->routeNotificationFor("slack", $notification);
return Http::withToken($this->getClientToken())
->post($url, [
"channel" => $channel,
"text" => $notification->toSlack($notifiable),
]);
}
protected function getClientToken(): string
{
return config("services.slack.client_token");
}
protected function getBaseUrl(): string
{
return config("services.slack.url");
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\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\InvalidRequest;
use Spatie\SlashCommand\Exceptions\RequestCouldNotBeHandled;
use Spatie\SlashCommand\Exceptions\SlackSlashCommandException;
use Spatie\SlashCommand\Response;
class Controller extends SlackController
{
/**
* @throws InvalidRequest|RequestCouldNotBeHandled
*/
public function getResponse(IlluminateRequest $request): IlluminateResponse
{
$this->verifyWithSigning($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 => Attachment::create()
->setColor("danger")
->setText($message[0]),
);
return Response::create($this->request)
->withText(":x: Polecenie `/{$this->request->command} {$this->request->text}` jest niepoprawna:")
->withAttachments($errors->all());
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Elements;
use Illuminate\Support\Collection;
use Toby\Eloquent\Models\Vacation;
class AbsencesAttachment extends ListAttachment
{
public function __construct(Collection $absences)
{
parent::__construct();
$this
->setTitle("Nieobecności :palm_tree:")
->setColor("#eab308")
->setItems($absences->map(fn(Vacation $vacation): string => $vacation->user->profile->full_name))
->setEmptyText("Wszyscy dzisiaj pracują :muscle:");
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Elements;
use Illuminate\Contracts\Support\Arrayable;
use Spatie\SlashCommand\Attachment as BaseAttachment;
class Attachment extends BaseAttachment implements Arrayable
{
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Elements;
use Illuminate\Support\Collection;
use Toby\Eloquent\Models\User;
class BirthdaysAttachment extends ListAttachment
{
public function __construct(Collection $birthdays)
{
parent::__construct();
$this
->setTitle("Urodziny :birthday:")
->setColor("#3c5f97")
->setItems($birthdays->map(fn(User $user): string => $user->profile->full_name))
->setEmptyText("Dzisiaj nikt nie ma urodzin :cry:");
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Elements;
use Illuminate\Support\Collection;
use Toby\Eloquent\Models\Key;
class KeysAttachment extends ListAttachment
{
public function __construct(Collection $keys)
{
parent::__construct();
$this
->setColor("#3c5f97")
->setItems($keys->map(fn(Key $key): string => "Klucz nr {$key->id} - <@{$key->user->profile->slack_id}>"))
->setEmptyText("Nie ma żadnych kluczy w tobym");
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Elements;
use Illuminate\Support\Collection;
class ListAttachment extends Attachment
{
protected Collection $items;
protected string $emptyText = "";
public function setItems(Collection $items): static
{
$this->items = $items;
return $this;
}
public function setEmptyText(string $emptyText): static
{
$this->emptyText = $emptyText;
return $this;
}
public function toArray(): array
{
$fields = parent::toArray();
return array_merge($fields, [
"text" => $this->items->isNotEmpty() ? $this->items->implode("\n") : $this->emptyText,
]);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Elements;
use Illuminate\Support\Collection;
use Toby\Eloquent\Models\Vacation;
class RemotesAttachment extends ListAttachment
{
public function __construct(Collection $remoteDays)
{
parent::__construct();
$this
->setTitle("Praca zdalna :house_with_garden:")
->setColor("#527aba")
->setItems($remoteDays->map(fn(Vacation $vacation): string => $vacation->user->profile->full_name))
->setEmptyText("Wszyscy dzisiaj są w biurze :boom:");
}
}

View File

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

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Handlers;
use Spatie\SlashCommand\Handlers\BaseHandler;
use Spatie\SlashCommand\Request;
use Spatie\SlashCommand\Response;
use Toby\Infrastructure\Slack\Elements\Attachment;
use Toby\Infrastructure\Slack\Traits\ListsHandlers;
class CatchAll extends BaseHandler
{
use ListsHandlers;
public function canHandle(Request $request): bool
{
return true;
}
public function handle(Request $request): Response
{
$handlers = $this->findAvailableHandlers();
$attachmentFields = $this->mapHandlersToAttachments($handlers);
return $this->respondToSlack(":x: Nie rozpoznaję polecenia. Lista wszystkich poleceń:")
->withAttachment(
Attachment::create()
->setColor("danger")
->useMarkdown()
->setFields($attachmentFields),
);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Handlers;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Spatie\SlashCommand\Request;
use Spatie\SlashCommand\Response;
use Toby\Domain\DailySummaryRetriever;
use Toby\Infrastructure\Slack\Elements\AbsencesAttachment;
use Toby\Infrastructure\Slack\Elements\BirthdaysAttachment;
use Toby\Infrastructure\Slack\Elements\RemotesAttachment;
class DailySummary extends SignatureHandler
{
protected $signature = "toby dzisiaj";
protected $description = "Codzienne podsumowanie";
public function handle(Request $request): Response
{
$dailySummaryRetriever = app()->make(DailySummaryRetriever::class);
$now = Carbon::today();
$attachments = new Collection([
new AbsencesAttachment($dailySummaryRetriever->getAbsences($now)),
new RemotesAttachment($dailySummaryRetriever->getRemoteDays($now)),
new BirthdaysAttachment($dailySummaryRetriever->getBirthdays($now)),
]);
return $this->respondToSlack("Podsumowanie dla dnia {$now->toDisplayString()}")
->withAttachments($attachments->all());
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Handlers;
use Illuminate\Validation\ValidationException;
use Spatie\SlashCommand\Request;
use Spatie\SlashCommand\Response;
use Toby\Domain\Notifications\KeyHasBeenGivenNotification;
use Toby\Eloquent\Models\Key;
use Toby\Infrastructure\Slack\Exceptions\UserNotFoundException;
use Toby\Infrastructure\Slack\Rules\SlackUserExistsRule;
use Toby\Infrastructure\Slack\Traits\FindsUserBySlackId;
class GiveKeysTo extends SignatureHandler
{
use FindsUserBySlackId;
protected $signature = "toby klucze:dla {user}";
protected $description = "Przekaż klucze wskazanemu użytkownikowi";
/**
* @throws UserNotFoundException
* @throws ValidationException
*/
public function handle(Request $request): Response
{
["user" => $from] = $this->validate();
$authUser = $this->findUserBySlackIdOrFail($request->userId);
$user = $this->findUserBySlackId($from);
/** @var Key $key */
$key = $authUser->keys()->first();
if (!$key) {
throw ValidationException::withMessages(["key" => "Nie masz żadnego klucza do przekazania"]);
}
if ($user->is($authUser)) {
throw ValidationException::withMessages([
"key" => "Nie możesz przekazać sobie kluczy :dzban:",
]);
}
$key->user()->associate($user);
$key->save();
$key->notify(new KeyHasBeenGivenNotification($authUser, $user));
return $this->respondToSlack(
":white_check_mark: Klucz nr {$key->id} został przekazany użytkownikowi <@{$user->profile->slack_id}>",
);
}
protected function getRules(): array
{
return [
"user" => ["required", new SlackUserExistsRule()],
];
}
protected function getMessages(): array
{
return [
"user.required" => "Musisz podać użytkownika, któremu chcesz przekazać klucze",
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Handlers;
use Spatie\SlashCommand\Request;
use Spatie\SlashCommand\Response;
use Toby\Infrastructure\Slack\Elements\Attachment;
use Toby\Infrastructure\Slack\Traits\ListsHandlers;
class Help extends SignatureHandler
{
use ListsHandlers;
protected $signature = "toby pomoc";
protected $description = "Wyświetl wszystkie dostępne polecenia";
public function handle(Request $request): Response
{
$handlers = $this->findAvailableHandlers();
$attachmentFields = $this->mapHandlersToAttachments($handlers);
return $this->respondToSlack("Dostępne polecenia:")
->withAttachment(
Attachment::create()
->setColor("good")
->useMarkdown()
->setFields($attachmentFields),
);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Handlers;
use Illuminate\Support\Carbon;
use Spatie\SlashCommand\Request;
use Spatie\SlashCommand\Response;
use Toby\Domain\Actions\VacationRequest\CreateAction;
use Toby\Domain\Enums\VacationType;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\YearPeriod;
use Toby\Infrastructure\Slack\Traits\FindsUserBySlackId;
class HomeOffice extends SignatureHandler
{
use FindsUserBySlackId;
protected $signature = "toby zdalnie";
protected $description = "Pracuj dzisiaj zdalnie";
public function handle(Request $request): Response
{
$user = $this->findUserBySlackId($request->userId);
$this->createRemoteday($user, Carbon::today());
return $this->respondToSlack(":white_check_mark: Pracujesz dzisiaj zdalnie");
}
protected function createRemoteday(User $user, Carbon $date): void
{
$yearPeriod = YearPeriod::findByYear($date->year);
app(CreateAction::class)->execute([
"user_id" => $user->id,
"type" => VacationType::HomeOffice,
"from" => $date,
"to" => $date,
"year_period_id" => $yearPeriod->id,
"flow_skipped" => false,
], $user);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Handlers;
use Spatie\SlashCommand\Request;
use Spatie\SlashCommand\Response;
use Toby\Eloquent\Models\Key;
use Toby\Infrastructure\Slack\Elements\KeysAttachment;
class KeyList extends SignatureHandler
{
protected $signature = "toby klucze";
protected $description = "Lista wszystkich kluczy";
public function handle(Request $request): Response
{
$keys = Key::query()
->orderBy("id")
->get();
return $this->respondToSlack("Lista kluczy :key:")
->withAttachment(new KeysAttachment($keys));
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Handlers;
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

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Handlers;
use Illuminate\Validation\ValidationException;
use Spatie\SlashCommand\Request;
use Spatie\SlashCommand\Response;
use Toby\Domain\Notifications\KeyHasBeenTakenNotification;
use Toby\Eloquent\Models\Key;
use Toby\Infrastructure\Slack\Exceptions\UserNotFoundException;
use Toby\Infrastructure\Slack\Rules\SlackUserExistsRule;
use Toby\Infrastructure\Slack\Traits\FindsUserBySlackId;
class TakeKeysFrom extends SignatureHandler
{
use FindsUserBySlackId;
protected $signature = "toby klucze:od {user}";
protected $description = "Zabierz klucze wskazanemu użytkownikowi";
/**
* @throws UserNotFoundException|ValidationException
*/
public function handle(Request $request): Response
{
["user" => $from] = $this->validate();
$authUser = $this->findUserBySlackIdOrFail($request->userId);
$user = $this->findUserBySlackId($from);
/** @var Key $key */
$key = $user->keys()->first();
if (!$key) {
throw ValidationException::withMessages([
"key" => "Użytkownik <@{$user->profile->slack_id}> nie ma żadnych kluczy",
]);
}
if ($key->user()->is($authUser)) {
throw ValidationException::withMessages([
"key" => "Nie możesz zabrać sobie kluczy :dzban:",
]);
}
$key->user()->associate($authUser);
$key->save();
$key->notify(new KeyHasBeenTakenNotification($authUser, $user));
return $this->respondToSlack(":white_check_mark: Klucz nr {$key->id} został zabrany użytkownikowi <@{$user->profile->slack_id}>");
}
protected function getRules(): array
{
return [
"user" => ["required", new SlackUserExistsRule()],
];
}
protected function getMessages(): array
{
return [
"user.required" => "Musisz podać użytkownika, któremu chcesz zabrać klucze",
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Rules;
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\Infrastructure\Slack\Traits;
use Illuminate\Support\Str;
use Toby\Eloquent\Models\User;
use Toby\Infrastructure\Slack\Exceptions\UserNotFoundException;
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,46 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Traits;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\SlashCommand\AttachmentField;
use Spatie\SlashCommand\Handlers\BaseHandler;
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): BaseHandler => new $handlerClassName($this->request))
->filter(fn(HandlesSlashCommand $handler): bool => $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): int => strcmp(
$handlerA->getFullCommand(),
$handlerB->getFullCommand(),
),
)
->map(
fn(SignatureHandler $handler): AttachmentField => AttachmentField::create(
$handler->getDescription(),
"`/{$handler->getSignature()}`",
),
)
->all();
}
}