commit 4bee43fc178475224499772afe4d997cc8bb8f05 Author: zyimm Date: Thu Oct 27 16:30:53 2022 +0800 初始化 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8585396 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +!.gitignore +!.gitattributes +*.DS_Store +*.idea +*.svn +*.git +composer.lock +*.cache +vendor +config \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..5d18ceb --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,85 @@ +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) + ; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e498939 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f3bd5d3 --- /dev/null +++ b/README.md @@ -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 +setTitle('test') + ->setText('test') + //@所有人 + ->setAt([ + 'all' + ]); + +// 发送 +Notify::make($channel)->send($template); + +``` + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f659d1a --- /dev/null +++ b/composer.json @@ -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" + } + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..35e65b3 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,21 @@ + + + + + ./test + + + + + ./src + + + diff --git a/publish/message_notify.php b/publish/message_notify.php new file mode 100644 index 0000000..61ca2db --- /dev/null +++ b/publish/message_notify.php @@ -0,0 +1,56 @@ + 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'), + ] + ] + ] + ] +]; diff --git a/src/Channel/AbstractChannel.php b/src/Channel/AbstractChannel.php new file mode 100644 index 0000000..8cb4550 --- /dev/null +++ b/src/Channel/AbstractChannel.php @@ -0,0 +1,40 @@ +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'); + } +} diff --git a/src/Channel/DingTalkChannel.php b/src/Channel/DingTalkChannel.php new file mode 100644 index 0000000..b8477c3 --- /dev/null +++ b/src/Channel/DingTalkChannel.php @@ -0,0 +1,81 @@ +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); + } +} diff --git a/src/Channel/FeiShuChannel.php b/src/Channel/FeiShuChannel.php new file mode 100644 index 0000000..902a4ce --- /dev/null +++ b/src/Channel/FeiShuChannel.php @@ -0,0 +1,86 @@ +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']]; + } +} diff --git a/src/Channel/WechatChannel.php b/src/Channel/WechatChannel.php new file mode 100644 index 0000000..d26c216 --- /dev/null +++ b/src/Channel/WechatChannel.php @@ -0,0 +1,72 @@ +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']]; + } + + +} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php new file mode 100644 index 0000000..2d7b78b --- /dev/null +++ b/src/ConfigProvider.php @@ -0,0 +1,31 @@ + [], + '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', + ], + ], + ]; + } +} diff --git a/src/Connection.php b/src/Connection.php new file mode 100644 index 0000000..66ddb00 --- /dev/null +++ b/src/Connection.php @@ -0,0 +1,24 @@ +channel = $channel; + } + + public function send(AbstractTemplate $template): bool + { + return $this->channel->setTemplate($template)->send(); + } + +} diff --git a/src/Contracts/MessageChannelInterface.php b/src/Contracts/MessageChannelInterface.php new file mode 100644 index 0000000..0918887 --- /dev/null +++ b/src/Contracts/MessageChannelInterface.php @@ -0,0 +1,8 @@ +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); + } +} diff --git a/src/Template/MarkDown/DingTalk.php b/src/Template/MarkDown/DingTalk.php new file mode 100644 index 0000000..17e2d21 --- /dev/null +++ b/src/Template/MarkDown/DingTalk.php @@ -0,0 +1,24 @@ + 'markdown', + 'markdown' => [ + 'title' => $this->getTitle(), + 'text' => $this->getText(), + ], + 'at' => [ + 'isAtAll' => $this->isAtAll(), + 'atMobiles' => $this->getAt(), + ], + ]; + } +} \ No newline at end of file diff --git a/src/Template/MarkDown/FeiShu.php b/src/Template/MarkDown/FeiShu.php new file mode 100644 index 0000000..5a8f51d --- /dev/null +++ b/src/Template/MarkDown/FeiShu.php @@ -0,0 +1,72 @@ + '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; + } +} \ No newline at end of file diff --git a/src/Template/MarkDown/Wechat.php b/src/Template/MarkDown/Wechat.php new file mode 100644 index 0000000..4e1b89c --- /dev/null +++ b/src/Template/MarkDown/Wechat.php @@ -0,0 +1,21 @@ + '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()], + ], + ]; + } +} \ No newline at end of file diff --git a/src/Template/Text/DingTalk.php b/src/Template/Text/DingTalk.php new file mode 100644 index 0000000..66a6140 --- /dev/null +++ b/src/Template/Text/DingTalk.php @@ -0,0 +1,22 @@ + 'text', + 'text' => [ + 'content' => $this->getText(), + ], + 'at' => [ + 'isAtAll' => $this->isAtAll(), + 'atMobiles' => $this->getAt(), + ], + ]; + } +} \ No newline at end of file diff --git a/src/Template/Text/FeiShu.php b/src/Template/Text/FeiShu.php new file mode 100644 index 0000000..e049be8 --- /dev/null +++ b/src/Template/Text/FeiShu.php @@ -0,0 +1,42 @@ + 'text', + 'content' => [ + 'text' => $this->getText() . $this->getFeiShuAt(), + ] + ]; + } + + /** + * getFeiShuAt + * + * @return string + */ + private function getFeiShuAt(): string + { + if ($this->isAtAll()) { + return '所有人'; + } + + $at = $this->getAt(); + $result = ''; + foreach ($at as $item) { + if (strchr($item, '@') === false) { + $result .= '' . $item . ''; + } else { + $result .= '' . $item . ''; + } + } + return $result; + } +} \ No newline at end of file diff --git a/src/Template/Text/Wechat.php b/src/Template/Text/Wechat.php new file mode 100644 index 0000000..9d35151 --- /dev/null +++ b/src/Template/Text/Wechat.php @@ -0,0 +1,21 @@ + '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()], + ], + ]; + } +} \ No newline at end of file diff --git a/test/NotifyTest.php b/test/NotifyTest.php new file mode 100644 index 0000000..c3da780 --- /dev/null +++ b/test/NotifyTest.php @@ -0,0 +1,32 @@ +setTitle('test') + ->setText('test') + //@所有人 + ->setAt([ + 'all' + ]); + // 发送 + Notify::make($channel)->send($template); + + } +} diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 0000000..ac5a911 --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,12 @@ +