初始化

This commit is contained in:
zyimm 2022-10-27 16:30:53 +08:00
commit 4bee43fc17
26 changed files with 999 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
!.gitignore
!.gitattributes
*.DS_Store
*.idea
*.svn
*.git
composer.lock
*.cache
vendor
config

85
.php-cs-fixer.php Normal file
View File

@ -0,0 +1,85 @@
<?php
$header = '';
return (new \PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@PSR2' => true,
'@Symfony' => true,
'@DoctrineAnnotation' => true,
'@PhpCsFixer' => true,
'header_comment' => [
'comment_type' => 'PHPDoc',
'header' => $header,
'separate' => 'none',
'location' => 'after_declare_strict',
],
'array_syntax' => [
'syntax' => 'short',
],
'list_syntax' => [
'syntax' => 'short',
],
'concat_space' => [
'spacing' => 'one',
],
'blank_line_before_statement' => [
'statements' => [
'declare',
],
],
'general_phpdoc_annotation_remove' => [
'annotations' => [
'author',
],
],
'ordered_imports' => [
'imports_order' => [
'class', 'function', 'const',
],
'sort_algorithm' => 'alpha',
],
'single_line_comment_style' => [
'comment_types' => [
],
],
'yoda_style' => [
'always_move_variable' => false,
'equal' => false,
'identical' => false,
],
'phpdoc_align' => [
'align' => 'left',
],
'multiline_whitespace_before_semicolons' => [
'strategy' => 'no_multi_line',
],
'constant_case' => [
'case' => 'lower',
],
'class_attributes_separation' => true,
'combine_consecutive_unsets' => true,
'declare_strict_types' => true,
'linebreak_after_opening_tag' => true,
'lowercase_static_reference' => true,
'no_useless_else' => true,
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'not_operator_with_space' => false,
'ordered_class_elements' => true,
'php_unit_strict' => false,
'phpdoc_separation' => false,
'single_quote' => true,
'standardize_not_equals' => true,
'multiline_comment_opening_closing' => true,
])
->setFinder(
\PhpCsFixer\Finder::create()
->exclude('public')
->exclude('runtime')
->exclude('vendor')
->in(__DIR__)
)
->setUsingCache(false)
;

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Vinchan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

51
README.md Normal file
View File

@ -0,0 +1,51 @@
## 三方平台消息通知组件
## 功能
* 支持钉钉群机器人、飞书群机器人、企业微信群机器人
* 支持扩展自定义通道&消息模板
## 环境要求
* hyperf >= 2.2
## 安装
```sh
composer require tm-wms/message-notify
```
## 配置文件
发布配置文件`config/message_notify.php`
```sh
php bin/hyperf.php vendor:publish tm-wms/message-notify
```
## 使用
```php
<?php
use TmWms\MessageNotify\Notify;
use TmWms\MessageNotify\Channel\DingTalkChannel;
use TmWms\MessageNotify\Template\Text\DingTalk;
// 初始通道
$channel = new DingTalkChannel();
// 初始消息模板
$template = (new DingTalk())
->setTitle('test')
->setText('test')
//@所有人
->setAt([
'all'
]);
// 发送
Notify::make($channel)->send($template);
```

46
composer.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "tm-wms/message-notify",
"description": "MIT",
"license": "MIT",
"authors": [
{
"name": "zyimm",
"email": "zyimm.qq.com"
}
],
"autoload": {
"psr-4": {
"TmWms\\MessageNotify\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"TmWms\\MessageNotifyTest\\": "test/"
}
},
"require": {
"php": ">=7.4",
"ext-json": "*",
"hyperf/guzzle": "^1.1|^2.1|^3.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
"phpunit/phpunit": "^9.4",
"hyperf/di": "^2.2|^3.0",
"hyperf/utils": "^2.2|^3.0",
"hyperf/config": "*",
"hyperf/ide-helper": "v2.2.*"
},
"scripts": {
"test": "phpunit -c phpunit.xml --colors=always",
"cs-fix": "./vendor/bin/php-cs-fixer fix"
},
"config": {
"sort-packages": true
},
"extra": {
"hyperf": {
"config": "TmWms\\MessageNotify\\ConfigProvider"
}
}
}

21
phpunit.xml Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="./test/bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Tests">
<directory suffix="Test.php">./test</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
</phpunit>

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
use TmWms\MessageNotify\Channel\DingTalkChannel;
use TmWms\MessageNotify\Channel\FeiShuChannel;
use TmWms\MessageNotify\Channel\WechatChannel;
use TmWms\MessageNotify\Notify;
return [
'default' => env('NOTIFY_DEFAULT_CHANNEL', 'DingTalk'),
'channels' => [
// 钉钉群机器人
'DingTalk' => [
'default' => Notify::INFO,
'channel' => DingTalkChannel::class,
'pipeline' => [
// 业务信息告警群
Notify::INFO => [
'token' => env('NOTIFY_DINGTALK_TOKEN', ''),
'secret' => env('NOTIFY_DINGTALK_SECRET', ''),
'keyword' => env('NOTIFY_DINGTALK_KEYWORD', ['test']),
],
// 错误信息告警群
Notify::ERROR => [
'token' => env('NOTIFY_DINGTALK_TOKEN', ''),
'secret' => env('NOTIFY_DINGTALK_SECRET', ''),
'keyword' => env('NOTIFY_DINGTALK_KEYWORD', []),
]
]
],
// 飞书群机器人
'Feishu' => [
'default' => Notify::INFO,
'channel' => FeiShuChannel::class,
'pipeline' => [
'info' => [
'token' => env('NOTIFY_FEISHU_TOKEN', ''),
'secret' => env('NOTIFY_FEISHU_SECRET', ''),
'keyword' => env('NOTIFY_FEISHU_KEYWORD'),
]
]
],
// 企业微信群机器人
'Wechat' => [
'default' => Notify::INFO,
'channel' => WechatChannel::class,
'pipeline' => [
'info' => [
'token' => env('NOTIFY_WECHAT_TOKEN'),
]
]
]
]
];

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace TmWms\MessageNotify\Channel;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Utils\ApplicationContext;
use TmWms\MessageNotify\Contracts\MessageChannelInterface;
use TmWms\MessageNotify\Exceptions\MessageNotificationException;
use TmWms\MessageNotify\Template\AbstractTemplate;
abstract class AbstractChannel implements MessageChannelInterface
{
protected AbstractTemplate $template;
public string $channelUrl;
protected string $type;
public function setTemplate(AbstractTemplate $template): AbstractChannel
{
$this->template = $template;
return $this;
}
/**
* getConfig
*
* @return mixed
*/
public function getConfig()
{
if (class_exists(ApplicationContext::class)) {
$configContext = make(ConfigInterface::class);
return $configContext->get('message_notify.channels.'.$this->type);
}
throw new MessageNotificationException('ApplicationContext is not exist');
}
}

View File

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

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace TmWms\MessageNotify\Channel;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use TmWms\MessageNotify\Exceptions\MessageNotificationException;
class FeiShuChannel extends AbstractChannel
{
public string $channelUrl = 'https://open.feishu.cn';
protected string $type = 'Feishu';
/**
* send
*
* @throws GuzzleException
*/
public function send(): bool
{
$client = $this->getClient($this->template->getPipeline());
$timestamp = time();
$config = [
'timestamp' => $timestamp,
'sign' => $this->buildSign($timestamp, $this->template->getPipeline()),
];
$option = [
RequestOptions::HEADERS => [],
RequestOptions::JSON => array_merge($config, $this->template->getBody()),
];
$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;
}
/**
* getClient
*
* @param string $pipeline
* @return Client
*/
public function getClient(string $pipeline): Client
{
$config = $this->config($pipeline);
$config['base_uri'] = $this->channelUrl.'/open-apis/bot/v2/hook/'.$config['token'];
return make(Client::class, $config);
}
/**
* buildSign
*
* @param int $timestamp
* @param string $pipeline
* @return string
*/
private function buildSign(int $timestamp, string $pipeline): string
{
$config = $this->config($pipeline);
$secret = hash_hmac('sha256', '', $timestamp."\n".$config['secret'], true);
return base64_encode($secret);
}
/**
* config
*
* @param string $pipeline
* @return mixed
*/
private function config(string $pipeline)
{
$config = $this->getConfig();
return $config['pipeline'][$pipeline] ?? $config['pipeline'][$config['default']];
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace TmWms\MessageNotify\Channel;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use TmWms\MessageNotify\Exceptions\MessageNotificationException;
/**
* 微信通知
*/
class WechatChannel extends AbstractChannel
{
public string $channelUrl = 'https://qyapi.weixin.qq.com';
protected string $type = 'Wechat';
/**
* send
*
* @return bool
* @throws GuzzleException
*/
public function send(): bool
{
$client = $this->getClient($this->template->getPipeline());
$option = [
RequestOptions::HEADERS => [],
RequestOptions::JSON => $this->template->getBody(),
];
$request = $client->post('', $option);
$result = json_decode($request->getBody()->getContents(), true);
if ($result['errcode'] !== 0) {
throw new MessageNotificationException($result['errmsg']);
}
return true;
}
/**
* getClient
*
* @param string $pipeline
* @return Client|mixed
*/
private function getClient(string $pipeline)
{
$config = $this->config($pipeline);
$config['base_uri'] = $this->channelUrl.'/cgi-bin/webhook/send?key=' . $config['token'];
return make(Client::class, $config);
}
/**
* config
*
* @param string $pipeline
* @return mixed
*/
public function config(string $pipeline)
{
$config = $this->getConfig();
return $config['pipeline'][$pipeline] ?? $config['pipeline'][$config['default']];
}
}

31
src/ConfigProvider.php Normal file
View File

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

24
src/Connection.php Normal file
View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace TmWms\MessageNotify;
use TmWms\MessageNotify\Channel\AbstractChannel;
use TmWms\MessageNotify\Template\AbstractTemplate;
class Connection
{
protected AbstractChannel $channel;
public function __construct(AbstractChannel $channel)
{
$this->channel = $channel;
}
public function send(AbstractTemplate $template): bool
{
return $this->channel->setTemplate($template)->send();
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace TmWms\MessageNotify\Contracts;
interface MessageChannelInterface
{
public function send();
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace TmWms\MessageNotify\Contracts;
interface MessageNotifyInterface
{
}

View File

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

33
src/Notify.php Normal file
View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace TmWms\MessageNotify;
use TmWms\MessageNotify\Channel\AbstractChannel;
class Notify
{
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';
public static function make(?AbstractChannel $channel = null): Connection
{
return make(Connection::class, [
$channel
]);
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace TmWms\MessageNotify\Template;
use TmWms\MessageNotify\Notify;
abstract class AbstractTemplate
{
protected array $at = [];
protected string $pipeline = Notify::INFO;
protected string $text = '';
protected string $title = '';
abstract public function getBody();
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);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace TmWms\MessageNotify\Template\MarkDown;
use TmWms\MessageNotify\Template\AbstractTemplate;
class DingTalk extends AbstractTemplate
{
public function getBody(): array
{
return [
'msgtype' => 'markdown',
'markdown' => [
'title' => $this->getTitle(),
'text' => $this->getText(),
],
'at' => [
'isAtAll' => $this->isAtAll(),
'atMobiles' => $this->getAt(),
],
];
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace TmWms\MessageNotify\Template\MarkDown;
use TmWms\MessageNotify\Template\AbstractTemplate;
class FeiShu extends AbstractTemplate
{
public function getBody(): array
{
return [
'msg_type' => 'post',
'content' => [
'post' => [
'zh_cn' => [
'title' => $this->getTitle(),
'content' => [$this->getFeiShuText()],
],
],
],
];
}
/**
* @return array
*/
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;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace TmWms\MessageNotify\Template\MarkDown;
use TmWms\MessageNotify\Template\AbstractTemplate;
class Wechat extends AbstractTemplate
{
public function getBody(): 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()],
],
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace TmWms\MessageNotify\Template\Text;
use TmWms\MessageNotify\Template\AbstractTemplate;
class DingTalk extends AbstractTemplate
{
public function getBody(): array
{
return [
'msgtype' => 'text',
'text' => [
'content' => $this->getText(),
],
'at' => [
'isAtAll' => $this->isAtAll(),
'atMobiles' => $this->getAt(),
],
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace TmWms\MessageNotify\Template\Text;
use TmWms\MessageNotify\Template\AbstractTemplate;
class FeiShu extends AbstractTemplate
{
public function getBody(): array
{
return [
'msg_type' => 'text',
'content' => [
'text' => $this->getText() . $this->getFeiShuAt(),
]
];
}
/**
* getFeiShuAt
*
* @return string
*/
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;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace TmWms\MessageNotify\Template\Text;
use TmWms\MessageNotify\Template\AbstractTemplate;
class Wechat extends AbstractTemplate
{
public function getBody(): 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()],
],
];
}
}

32
test/NotifyTest.php Normal file
View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace TmWms\MessageNotifyTest;
use PHPUnit\Framework\TestCase;
use TmWms\MessageNotify\Channel\DingTalkChannel;
use TmWms\MessageNotify\Notify;
use TmWms\MessageNotify\Template\Text\DingTalk;
class NotifyTest extends TestCase
{
public function testCase()
{
// 初始通道
$channel = new DingTalkChannel();
// 初始消息模板
$template = (new DingTalk())
->setTitle('test')
->setText('test')
//@所有人
->setAt([
'all'
]);
// 发送
Notify::make($channel)->send($template);
}
}

12
test/bootstrap.php Normal file
View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
use Hyperf\Di\Container;
use Hyperf\Di\Definition\DefinitionSourceFactory;
use Hyperf\Utils\ApplicationContext;
! defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));
$container = new Container((new DefinitionSourceFactory(true))());
ApplicationContext::setContainer($container);