Skip to content

Commit

Permalink
feat: integrate slack channel to notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
albertcht committed Nov 13, 2024
1 parent 4a3c961 commit e618dbf
Show file tree
Hide file tree
Showing 36 changed files with 4,480 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/notifications/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"hyperf/contract": "~3.1.0",
"hyperf/collection": "~3.1.0",
"hyperf/context": "~3.1.0",
"hyperf/contract": "~3.1.0",
"hyperf/database": "~3.1.0",
"hyperf/di": "~3.1.0",
"swooletw/hyperf-support": "~3.1.0",
Expand Down
11 changes: 10 additions & 1 deletion src/notifications/src/ChannelManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use InvalidArgumentException;
use Psr\EventDispatcher\EventDispatcherInterface;
use SwooleTW\Hyperf\Notifications\Channels\DatabaseChannel;
use SwooleTW\Hyperf\Notifications\Channels\SlackNotificationRouterChannel;
use SwooleTW\Hyperf\Notifications\Contracts\Dispatcher as DispatcherContract;
use SwooleTW\Hyperf\Notifications\Contracts\Factory as FactoryContract;
use SwooleTW\Hyperf\ObjectPool\Traits\HasPoolProxy;
Expand Down Expand Up @@ -38,7 +39,7 @@ class ChannelManager extends Manager implements DispatcherContract, FactoryContr
/**
* The array of drivers which will be wrapped as pool proxies.
*/
protected array $poolables = [];
protected array $poolables = ['slack'];

/**
* The array of pool config for drivers.
Expand Down Expand Up @@ -89,6 +90,14 @@ protected function createMailDriver(): MailChannel
return $this->container->get(MailChannel::class);
}

/**
* Create an instance of the slack driver.
*/
protected function createSlackDriver(): SlackNotificationRouterChannel
{
return $this->container->get(SlackNotificationRouterChannel::class);
}

/**
* Create a new driver instance.
*
Expand Down
52 changes: 52 additions & 0 deletions src/notifications/src/Channels/SlackNotificationRouterChannel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace SwooleTW\Hyperf\Notifications\Channels;

use Hyperf\Stringable\Str;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use SwooleTW\Hyperf\Notifications\Notification;

class SlackNotificationRouterChannel
{
/**
* Create a new Slack notification router channel.
*/
public function __construct(
protected ContainerInterface $container
) {
}

/**
* Send the given notification.
*/
public function send(mixed $notifiable, Notification $notification): ?ResponseInterface
{
$route = $notifiable->routeNotificationFor('slack', $notification);

if ($route === false) {
return null;
}

return $this->determineChannel($route)->send($notifiable, $notification);
}

/**
* Determine which channel the Slack notification should be routed to.
*/
protected function determineChannel(mixed $route): SlackWebApiChannel|SlackWebhookChannel
{
if ($route instanceof UriInterface) {
return $this->container->get(SlackWebhookChannel::class);
}

if (is_string($route) && Str::startsWith($route, ['http://', 'https://'])) {
return $this->container->get(SlackWebhookChannel::class);
}

return $this->container->get(SlackWebApiChannel::class);
}
}
97 changes: 97 additions & 0 deletions src/notifications/src/Channels/SlackWebApiChannel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

declare(strict_types=1);

namespace SwooleTW\Hyperf\Notifications\Channels;

use GuzzleHttp\Client as HttpClient;
use Hyperf\Contract\ConfigInterface;
use LogicException;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use SwooleTW\Hyperf\Notifications\Notification;
use SwooleTW\Hyperf\Notifications\Slack\SlackMessage;
use SwooleTW\Hyperf\Notifications\Slack\SlackRoute;

class SlackWebApiChannel
{
protected const SLACK_API_URL = 'https://slack.com/api/chat.postMessage';

/**
* Create a new Slack channel instance.
*/
public function __construct(
protected HttpClient $client,
protected ConfigInterface $config
) {
}

/**
* Send the given notification.
*/
public function send(mixed $notifiable, Notification $notification): ?ResponseInterface
{
if (! method_exists($notification, 'toSlack')) {
throw new RuntimeException('Notification is missing `toSlack` method.');
}

// @phpstan-ignore-next-line
$message = $notification->toSlack($notifiable);

$route = $this->determineRoute($notifiable, $notification);

$payload = $this->buildJsonPayload($message, $route);

if (! $payload['channel']) {
throw new LogicException('Slack notification channel is not set.');
}

if (! $route->token) {
throw new LogicException('Slack API authentication token is not set.');
}

$response = $this->client->post(static::SLACK_API_URL, [
'json' => $payload,
'headers' => [
'Authorization' => "Bearer {$route->token}",
],
]);

$result = json_decode($content = $response->getBody()->getContents(), true);
if ($response->getStatusCode() === 200 && ($result['ok'] ?? false) === false) {
throw new RuntimeException('Slack API call failed with error [' . ($result['error'] ?? $content) . '].');
}

return $response;
}

/**
* Build the JSON payload for the Slack chat.postMessage API.
*/
protected function buildJsonPayload(SlackMessage $message, SlackRoute $route): array
{
$payload = $message->toArray();

return array_merge($payload, [
'channel' => $route->channel ?? $payload['channel'] ?? $this->config->get('services.slack.notifications.channel'),
]);
}

/**
* Determine the API Token and Channel that the notification should be posted to.
*/
protected function determineRoute(mixed $notifiable, Notification $notification): SlackRoute
{
$route = $notifiable->routeNotificationFor('slack', $notification);

// When the route is a string, we will assume it is a channel name and will use the default API token for the application...
if (is_string($route)) {
return SlackRoute::make($route, $this->config->get('services.slack.notifications.bot_user_oauth_token'));
}

return SlackRoute::make(
$route->channel ?? null,
$route->token ?? $this->config->get('services.slack.notifications.bot_user_oauth_token'),
);
}
}
111 changes: 111 additions & 0 deletions src/notifications/src/Channels/SlackWebhookChannel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

declare(strict_types=1);

namespace SwooleTW\Hyperf\Notifications\Channels;

use GuzzleHttp\Client as HttpClient;
use Hyperf\Collection\Collection;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use SwooleTW\Hyperf\Notifications\Messages\SlackAttachment;
use SwooleTW\Hyperf\Notifications\Messages\SlackAttachmentField;
use SwooleTW\Hyperf\Notifications\Messages\SlackMessage;
use SwooleTW\Hyperf\Notifications\Notification;

use function Hyperf\Collection\data_get;

class SlackWebhookChannel
{
/**
* Create a new Slack channel instance.
*/
public function __construct(
protected HttpClient $client
) {
}

/**
* Send the given notification.
*/
public function send(mixed $notifiable, Notification $notification): ?ResponseInterface
{
if (! method_exists($notification, 'toSlack')) {
throw new RuntimeException('Notification is missing `toSlack` method.');
}

if (! $url = $notifiable->routeNotificationFor('slack', $notification)) {
return null;
}

return $this->client->post($url, $this->buildJsonPayload(
$notification->toSlack($notifiable) // @phpstan-ignore-line
));
}

/**
* Build up a JSON payload for the Slack webhook.
*/
public function buildJsonPayload(SlackMessage $message): array
{
$optionalFields = array_filter([
'channel' => data_get($message, 'channel'),
'icon_emoji' => data_get($message, 'icon'),
'icon_url' => data_get($message, 'image'),
'link_names' => data_get($message, 'linkNames'),
'unfurl_links' => data_get($message, 'unfurlLinks'),
'unfurl_media' => data_get($message, 'unfurlMedia'),
'username' => data_get($message, 'username'),
]);

return array_merge([
'json' => array_merge([
'text' => $message->content,
'attachments' => $this->attachments($message),
], $optionalFields),
], $message->http);
}

/**
* Format the message's attachments.
*/
protected function attachments(SlackMessage $message): array
{
return Collection::make($message->attachments)->map(function ($attachment) use ($message) {
return array_filter([
'actions' => $attachment->actions,
'author_icon' => $attachment->authorIcon,
'author_link' => $attachment->authorLink,
'author_name' => $attachment->authorName,
'callback_id' => $attachment->callbackId,
'color' => $attachment->color ?: $message->color(),
'fallback' => $attachment->fallback,
'fields' => $this->fields($attachment),
'footer' => $attachment->footer,
'footer_icon' => $attachment->footerIcon,
'image_url' => $attachment->imageUrl,
'mrkdwn_in' => $attachment->markdown,
'pretext' => $attachment->pretext,
'text' => $attachment->content,
'thumb_url' => $attachment->thumbUrl,
'title' => $attachment->title,
'title_link' => $attachment->url,
'ts' => $attachment->timestamp,
]);
})->all();
}

/**
* Format the attachment's fields.
*/
protected function fields(SlackAttachment $attachment): array
{
return Collection::make($attachment->fields)->map(function ($value, $key) {
if ($value instanceof SlackAttachmentField) {
return $value->toArray();
}

return ['title' => $key, 'value' => $value, 'short' => true];
})->values()->all();
}
}
11 changes: 11 additions & 0 deletions src/notifications/src/Contracts/Slack/BlockContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace SwooleTW\Hyperf\Notifications\Contracts\Slack;

use Hyperf\Contract\Arrayable;

interface BlockContract extends Arrayable
{
}
11 changes: 11 additions & 0 deletions src/notifications/src/Contracts/Slack/ElementContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace SwooleTW\Hyperf\Notifications\Contracts\Slack;

use Hyperf\Contract\Arrayable;

interface ElementContract extends Arrayable
{
}
11 changes: 11 additions & 0 deletions src/notifications/src/Contracts/Slack/ObjectContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace SwooleTW\Hyperf\Notifications\Contracts\Slack;

use Hyperf\Contract\Arrayable;

interface ObjectContract extends Arrayable
{
}
Loading

0 comments on commit e618dbf

Please sign in to comment.