消息组件初始版本

This commit is contained in:
ykxiao
2024-01-31 09:30:04 +08:00
commit 62cfeb1772
22 changed files with 1019 additions and 0 deletions

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace MessageNotify\Channel;
use Hyperf\Contract\ConfigInterface;
use MessageNotify\Exceptions\MessageNotificationException;
use MessageNotify\Template\AbstractTemplate;
abstract class AbstractChannel
{
public function getConfig()
{
if (class_exists(\Hyperf\Utils\ApplicationContext::class)) {
$configContext = make(ConfigInterface::class);
return $configContext->get('message.channels.' . get_class($this));
}
throw new MessageNotificationException('ApplicationContext is not exist');
}
abstract public function send(AbstractTemplate $template);
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace MessageNotify\Channel;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use MessageNotify\Exceptions\MessageNotificationException;
use MessageNotify\Template\AbstractTemplate;
class DingTalkChannel extends AbstractChannel
{
/**
* @throws GuzzleException
*/
public function send(AbstractTemplate $template): bool
{
$query = $this->getQuery($template->getPipeline());
$client = $this->getClient($query);
$option = [
RequestOptions::HEADERS => [],
RequestOptions::JSON => $template->dingTalkBody(),
];
$request = $client->post('', $option);
$result = json_decode($request->getBody()->getContents(), true);
if ($result['errcode'] !== 0) {
throw new MessageNotificationException($result['errmsg']);
}
return true;
}
public function getClient(string $query)
{
$config['base_uri'] = 'https://oapi.dingtalk.com/robot/send' . $query;
if (class_exists(\Hyperf\Utils\ApplicationContext::class)) {
return make(Client::class, [$config]);
}
return new Client($config);
}
private function getQuery(string $pipeline): string
{
$timestamp = time() * 1000;
$config = $this->getConfig();
$config = $config['pipeline'][$pipeline] ?? $config['pipeline'][$config['default']];
$secret = hash_hmac('sha256', $timestamp . "\n" . $config['secret'], $config['secret'], true);
$sign = urlencode(base64_encode($secret));
return "?access_token={$config['token']}&timestamp={$timestamp}&sign={$sign}";
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace MessageNotify\Channel;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use MessageNotify\Exceptions\MessageNotificationException;
use MessageNotify\Template\AbstractTemplate;
class FeiShuChannel extends AbstractChannel
{
/**
* @throws GuzzleException
*/
public function send(AbstractTemplate $template): bool
{
$client = $this->getClient($template->getPipeline());
$timestamp = time();
$config = [
'timestamp' => $timestamp,
'sign' => $this->getSign($timestamp, $template->getPipeline()),
];
$option = [
RequestOptions::HEADERS => [],
RequestOptions::JSON => array_merge($config, $template->feiShuBody()),
];
$request = $client->post('', $option);
$result = json_decode($request->getBody()->getContents(), true);
if (! isset($result['StatusCode']) || $result['StatusCode'] !== 0) {
throw new MessageNotificationException($result['msg']);
}
return true;
}
public function getClient(string $pipeline)
{
$config = $this->config($pipeline);
$uri['base_uri'] = 'https://open.feishu.cn/open-apis/bot/v2/hook/' . $config['token'];
if (class_exists(\Hyperf\Utils\ApplicationContext::class)) {
return make(Client::class, [$uri]);
}
return new Client($config);
}
private function getSign(int $timestamp, string $pipeline): string
{
$config = $this->config($pipeline);
$secret = hash_hmac('sha256', '', $timestamp . "\n" . $config['secret'], true);
return base64_encode($secret);
}
private function config(string $pipeline)
{
$config = $this->getConfig();
return $config['pipeline'][$pipeline] ?? $config['pipeline'][$config['default']];
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace MessageNotify\Channel;
use MessageNotify\Template\AbstractTemplate;
class MailChannel extends AbstractChannel
{
public function send(AbstractTemplate $template)
{
// TODO: Implement send() method.
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace MessageNotify\Channel;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use MessageNotify\Exceptions\MessageNotificationException;
use MessageNotify\Template\AbstractTemplate;
class WechatChannel extends AbstractChannel
{
public function send(AbstractTemplate $template): bool
{
$client = $this->getClient($template->getPipeline());
$option = [
RequestOptions::HEADERS => [],
RequestOptions::JSON => $template->wechatBody(),
];
$request = $client->post('', $option);
$result = json_decode($request->getBody()->getContents(), true);
if ($result['errcode'] !== 0) {
throw new MessageNotificationException($result['errmsg']);
}
return true;
}
private function getClient(string $pipeline)
{
$config = $this->config($pipeline);
$uri['base_uri'] = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=' . $config['token'];
if (class_exists(\Hyperf\Utils\ApplicationContext::class)) {
return make(Client::class, [$uri]);
}
return new Client($config);
}
private function config(string $pipeline)
{
$config = $this->getConfig();
return $config['pipeline'][$pipeline] ?? $config['pipeline'][$config['default']];
}
}

122
src/Client.php Normal file
View File

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace MessageNotify;
use MessageNotify\Channel\AbstractChannel;
use MessageNotify\Contracts\MessageNotifyInterface;
use MessageNotify\Exceptions\MessageNotificationException;
use MessageNotify\Template\AbstractTemplate;
use MessageNotify\Template\Text;
class Client
{
protected AbstractChannel $channel;
protected AbstractTemplate $template;
protected array $at = [];
protected string $pipeline = MessageNotifyInterface::INFO;
protected string $title = '';
protected string $text = '';
private string $errorMessage;
public function getChannel(): AbstractChannel
{
return $this->channel;
}
public function getTemplate(): AbstractTemplate
{
return $this->template ?? new Text();
}
public function getAt(): array
{
return $this->at;
}
public function getTitle(): string
{
return $this->title;
}
public function getText(): string
{
return $this->text;
}
public function setChannel($channel = null): Client
{
if (! $channel instanceof AbstractChannel) {
$channel = make($channel);
}
$this->channel = $channel;
return $this;
}
public function setTemplate($template = ''): Client
{
if (! $template instanceof AbstractChannel) {
$template = make($template);
}
$this->template = $template;
return $this;
}
public function getPipeline(): string
{
return $this->pipeline;
}
public function setPipeline(string $pipeline = ''): Client
{
$this->pipeline = $pipeline ?? MessageNotifyInterface::INFO;
return $this;
}
public function setAt(array $at = []): Client
{
$this->at = $at;
return $this;
}
public function setTitle(string $title = ''): Client
{
$this->title = $title;
return $this;
}
public function setText(string $text = ''): Client
{
$this->text = $text;
return $this;
}
public function send(): bool
{
try {
$template = $this->getTemplate()->setAt($this->getAt())
->setTitle($this->getTitle())->setText($this->getText())
->setPipeline($this->getPipeline());
$this->getChannel()->send($template);
return true;
} catch (MessageNotificationException $exception) {
$this->errorMessage = $exception->getMessage();
return false;
}
}
public function getErrorMessage(): string
{
return $this->errorMessage;
}
}

34
src/ConfigProvider.php Normal file
View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace MessageNotify;
use MessageNotify\Contracts\MessageNotifyInterface;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => [
MessageNotifyInterface::class => Client::class,
],
'annotations' => [
'scan' => [
'paths' => [
__DIR__,
],
],
],
'publish' => [
[
'id' => 'config',
'description' => 'The config of message client.',
'source' => __DIR__ . '/../publish/message.php',
'destination' => BASE_PATH . '/config/autoload/message.php',
],
],
];
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace MessageNotify\Contracts;
interface MessageNotifyInterface
{
public const INFO = 'info';
public const ERROR = 'error';
public const EMERGENCY = 'emergency';
public const ALERT = 'alert';
public const CRITICAL = 'critical';
public const WARNING = 'warning';
public const NOTICE = 'notice';
public const DEBUG = 'debug';
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace MessageNotify\Exceptions;
class MessageNotificationException extends \RuntimeException
{
}

19
src/Notify.php Normal file
View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace MessageNotify;
use Hyperf\Utils\ApplicationContext;
class Notify
{
public static function make(): Client
{
if (class_exists(ApplicationContext::class)) {
return make(Client::class);
}
return new Client();
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace MessageNotify\Template;
use MessageNotify\Contracts\MessageNotifyInterface;
abstract class AbstractTemplate
{
protected array $at = [];
protected string $pipeline = MessageNotifyInterface::INFO;
protected string $text = '';
protected string $title = '';
public function getText(): string
{
return $this->text;
}
public function setText(string $text): AbstractTemplate
{
$this->text = $text;
return $this;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): AbstractTemplate
{
$this->title = $title;
return $this;
}
public function getPipeline(): string
{
return $this->pipeline;
}
public function setPipeline(string $pipeline): AbstractTemplate
{
$this->pipeline = $pipeline;
return $this;
}
public function setAt(array $at = []): AbstractTemplate
{
$this->at = $at;
return $this;
}
public function getAt(): array
{
return $this->at;
}
public function isAtAll(): bool
{
return in_array('all', $this->at) || in_array('ALL', $this->at);
}
abstract public function getBody();
}

100
src/Template/Markdown.php Normal file
View File

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace MessageNotify\Template;
class Markdown extends AbstractTemplate
{
public function getBody(): array
{
return [];
}
public function dingTalkBody(): array
{
return [
'msgtype' => 'markdown',
'markdown' => [
'title' => $this->getTitle(),
'text' => $this->getText(),
],
'at' => [
'isAtAll' => $this->isAtAll(),
'atMobiles' => $this->getAt(),
],
];
}
public function feiShuBody(): array
{
return [
'msg_type' => 'post',
'content' => [
'post' => [
'zh_cn' => [
'title' => $this->getTitle(),
'content' => [$this->getFeiShuText()],
],
],
],
];
}
public function wechatBody(): array
{
return [
'msgtype' => 'markdown',
'markdown' => [
'content' => $this->getTitle() . $this->getText(),
'mentioned_list' => in_array('all', $this->getAt()) ? [] : [$this->getAt()],
'mentioned_mobile_list' => in_array('all', $this->getAt()) ? ['@all'] : [$this->getAt()],
],
];
}
private function getFeiShuText(): array
{
$text = is_array($this->getText()) ? $this->getText() : json_decode($this->getText(), true) ?? [
[
'tag' => 'text',
'text' => $this->getText(),
],
];
$at = $this->getFeiShuAt();
return array_merge($text, $at);
}
private function getFeiShuAt(): array
{
$result = [];
if ($this->isAtAll()) {
$result[] = [
'tag' => 'at',
'user_id' => 'all',
];
return $result;
}
$at = $this->getAt();
foreach ($at as $item) {
// TODO::需要加入邮箱与收集@人
if (strchr($item, '@') === false) {
$result[] = [
'tag' => 'at',
'email' => $item,
];
} else {
$result[] = [
'tag' => 'at',
'user_id' => $item,
];
}
}
return $result;
}
}

67
src/Template/Text.php Normal file
View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace MessageNotify\Template;
class Text extends AbstractTemplate
{
public function getBody(): array
{
return [];
}
public function dingTalkBody(): array
{
return [
'msgtype' => 'text',
'text' => [
'content' => $this->getText(),
],
'at' => [
'isAtAll' => $this->isAtAll(),
'atMobiles' => $this->getAt(),
],
];
}
public function feiShuBody(): array
{
return [
'msg_type' => 'text',
'content' => [
'text' => $this->getText() . $this->getFeiShuAt(),
],
];
}
public function wechatBody(): array
{
return [
'msgtype' => 'text',
'text' => [
'content' => $this->getText(),
'mentioned_list' => in_array('all', $this->getAt()) ? [] : [$this->getAt()],
'mentioned_mobile_list' => in_array('all', $this->getAt()) ? ['@all'] : [$this->getAt()],
],
];
}
private function getFeiShuAt(): string
{
if ($this->isAtAll()) {
return '<at user_id="all">所有人</at>';
}
$at = $this->getAt();
$result = '';
foreach ($at as $item) {
if (strchr($item, '@') === false) {
$result .= '<at phone="' . $item . '">' . $item . '</at>';
} else {
$result .= '<at email="' . $item . '">' . $item . '</at>';
}
}
return $result;
}
}