-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: integrate slack channel to notifications
- Loading branch information
Showing
36 changed files
with
4,480 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
src/notifications/src/Channels/SlackNotificationRouterChannel.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
{ | ||
} |
Oops, something went wrong.