├── LICENSE ├── README.md ├── README_en.md ├── composer.json ├── config └── dingtalk_robot.php └── src ├── DingtalkRobot.php ├── DingtalkRobotChannel.php ├── DingtalkRobotNoticeServiceProvider.php ├── Exceptions ├── ErrorCodes.php ├── Exception.php ├── HttpException.php ├── InvalidArgumentException.php └── InvalidConfigurationException.php ├── Facade.php ├── Message ├── ActionCardMessage.php ├── AtTrait.php ├── FeedCardMessage.php ├── LinkMessage.php ├── MarkdownMessage.php ├── Message.php └── TextMessage.php ├── Robot.php └── helpers.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 陈恺垣 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

laravel-dingtalk-robot-notification

2 |

钉钉群机器人 Laravel/Lumen 扩展包

3 |

4 | 5 | Style CI 6 | 7 | 8 | Travis CI 9 | 10 | 11 | Coverage Status 12 | 13 | 14 | Latest Stable Version 15 | 16 | 17 | Total Downloads 18 | 19 | 20 | License 21 | 22 |

23 | 24 | > [English](https://github.com/calchen/laravel-dingtalk-robot-notification/blob/master/README_en.md) 25 | 26 | 这是一个[钉钉群机器人](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq)的 Laravel/Lumen 消息通知(Notification)扩展包 27 | 28 | ## 安装 29 | 30 | 推荐使用 composer 进行安装: 31 | 32 | ```shell 33 | $ composer require calchen/laravel-dingtalk-robot-notification:^2.0 34 | ``` 35 | 36 | ### Laravel 37 | 38 | Laravel 5.5+ 已经实现了扩展包发现机制,您不需要进行额外的加载操作,但是依然需要将配置文件发布出来: 39 | 40 | ```shell 41 | php artisan vendor:publish --provider="Calchen\LaravelDingtalkRobot\DingtalkRobotNoticeServiceProvider" 42 | ``` 43 | 44 | ### Lumen 45 | 46 | Lumen 并未移植扩展包自动发现机制,所以需要手动加载扩展包并复制配置文件。 47 | 48 | 打开配置文件 `bootstrap/app.php` 并在大约 81 行左右添加如下内容: 49 | ```php 50 | $app->register(Calchen\LaravelDingtalkRobot\DingtalkRobotNoticeServiceProvider::class); 51 | ``` 52 | 53 | 将文件系统配置文件从 `vendor/calchen/laravel-dingtalk-robot-notification/config/dingtalk_robot.php` 复制到 `config/dingtalk_robot.php` 54 | 55 | ### 非 Laravel/Lumen 框架 56 | 57 | 无需考虑加载问题,请使用全局函数 `\dingtalk_robot()` 或直接创建 \Calchen\LaravelDingtalkRobot\DingtalkRobot 实例,以发送消息。 58 | 59 | ## 配置 60 | 61 | 打开配置文件 `config/dingtalk_robot.php` 并按照如下格式添加或修改配置: 62 | 63 | ```php 64 | 'robotName' => [ 65 | 'access_token' => 'xxxx', 66 | 'timeout' => 2.0, 67 | 'security_types' => [ 68 | 'signature', 69 | ], 70 | 'security_signature' => 'SECxxxx', 71 | ], 72 | ``` 73 | 请注意,如果需要配置多个机器人,请重复以上操作,并为不同机器人给予不同的 robotName 74 | 75 | ### 配置说明 76 | | 配置项 | 必须 | 数据类型 | 说明 | 备注 | 77 | |-------------------- |------ |------------- |----------------------------------------------- |------------------------------------------------------------------------------------------------------------ | 78 | | http_client_name | 否 | string/null | Guzzle 实例的名称 | 默认值:null,注入在 Laravel 中的 Guzzle 实例的名称,以便替换 HTTP 客户端 | 79 | | robotName | 是 | string | 机器人名称 | 这个名称为了区别不同的机器人 | 80 | | access_token | 是 | string | 创建机器人后 Webhook URL 中 access_token 的值 | | 81 | | timeout | 否 | int/float | 超时时间 | 默认值:2.0秒,具体见 [Guzzle 文档](http://docs.guzzlephp.org/en/stable/request-options.html#timeout) | 82 | | security_types | 是 | array | 安全设置 | 旧机器人是不存在该项配置的传 null 或不设置该配置项;新机器人可以组合选择:自定义关键字、加签、IP地址(段) | 83 | | security_types.* | 是 | string/null | 设置项 | 枚举值:null、keywords、signature、ip | 84 | | security_signature | 否 | string | 安全模式包含加签时需要的密钥字符串 | 应当以 SEC 开头 | | 85 | 86 | ### 获取 access_token 并设置安全设置 87 | 88 | 首先在钉钉群选择添加一个群机器人(智能群助手),如果您不知道如何设置请查看 [钉钉文档](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq),请注意这里需要设置的是"自定义"类型的机器人。 89 | 90 | 根据您的需要设置机器人名字,并选择安全设置。在完成后您将获得一个 webhook 地址,该地址中 access_token 的值即配置中使用到的 access_token 的值, 91 | 请妥善保存该 access_token。选择的安全设置就是配置中 security_types 的值,如果您选择了“加签”的安全设置,您还需要妥善保存密钥,该密钥即配置中 security_signature 的值 92 | 93 | ### HTTP 客户端注入 94 | 95 | 为了方便在某些情况下需要统一管理扩展包使用的 HTTP 客户端,提供了 `http_client_name` 配置项,以实现从 Laravel 容器中获取已经注入的 Guzzle 实例。如果您不需要手动管理 HTTP 客户端,您可以不设置该配置项或将该配置项值设置成 null。 96 | 97 | ## 使用方法 98 | 99 | Tips:为了方便快速调试功能,内置了一个使用了 Notifiable Trait 的类:Calchen\LaravelDingtalkRobot\Robot ,以下均以此对象为例,实际开发中请务必根据您项目情况进行对应处理。 100 | 101 | 首先需要先创建一个 TestDingtalkNotification ,如果是 Laravel 可通过 artisan 命令创建 102 | 103 | ```shell 104 | php artisan make:notification TestDingtalkNotification 105 | ``` 106 | 107 | 如果是 Lumen 那么可能需要您手动创建 app/Notifications 文件夹并创建 TestDingtalkNotification.php 文件 108 | 109 | ```php 110 | setRobot($notifiable->getName()); 156 | 157 | return $message; 158 | } 159 | } 160 | ``` 161 | 162 | 根据 [Laravel](https://laravel.com/docs/6.x/notifications) 文档发送通知可以使用 Notifiable Trait 163 | ```php 164 | use Calchen\LaravelDingtalkRobot\Robot; 165 | 166 | (new Robot)->notify(new TestDingtalkNotification()); 167 | ``` 168 | 也可以使用 Notification Facade 169 | 170 | ```php 171 | \Notification::send(new Robot, new TestDingtalkNotification()); 172 | ``` 173 | 174 | **重点:** 175 | 176 | TestDingtalkNotification 中的 toDingTalkRobot 方法中可使用下面的五种消息类型。 177 | 178 | ### 文本类型消息 179 | 180 | ```php 181 | use Calchen\LaravelDingtalkRobot\Message\TextMessage; 182 | 183 | public function toDingTalkRobot($notifiable) 184 | { 185 | $message = new TextMessage('我就是我, @1825718XXXX 是不一样的烟火'); 186 | 187 | // 可@某人或某些人,被@的人的手机号应出现在上面的消息体中 188 | $message->at('1825718XXXX'); 189 | // $message->at(['1825718XXXX', '1825718XXXY']); 190 | 191 | // 可@全部人,被@的人的手机号不需要出现在消息体中 192 | // $message->atAll(); 193 | 194 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 195 | $message->setRobot($notifiable->getName()); 196 | 197 | return $message; 198 | } 199 | ``` 200 | 201 | ### link 类型消息 202 | 203 | ```php 204 | use Calchen\LaravelDingtalkRobot\Message\LinkMessage; 205 | 206 | public function toDingTalkRobot($notifiable) 207 | { 208 | $message = new LinkMessage( 209 | '自定义机器人协议', 210 | '群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。例如:通过聚合GitHub,GitLab等源码管理服务,实现源码更新同步;通过聚合Trello,JIRA等项目协调服务,实现项目信息同步。不仅如此,群机器人支持Webhook协议的自定义接入,支持更多可能性,例如:你可将运维报警提醒通过自定义机器人聚合到钉钉群。', 211 | 'https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.Rqyvqo&treeId=257&articleId=105735&docType=1' 212 | ); 213 | 214 | // 也可以这样写 215 | // $message = new LinkMessage(); 216 | // $message->setMessage( 217 | // '自定义机器人协议', 218 | // '群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。例如:通过聚合GitHub,GitLab等源码管理服务,实现源码更新同步;通过聚合Trello,JIRA等项目协调服务,实现项目信息同步。不仅如此,群机器人支持Webhook协议的自定义接入,支持更多可能性,例如:你可将运维报警提醒通过自定义机器人聚合到钉钉群。', 219 | // 'https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.Rqyvqo&treeId=257&articleId=105735&docType=1' 220 | // ); 221 | 222 | // 如果想让链接在 PC 端用系统默认浏览器打开可以这样 223 | // $message = new LinkMessage( 224 | // '自定义机器人协议', 225 | // '群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。例如:通过聚合GitHub,GitLab等源码管理服务,实现源码更新同步;通过聚合Trello,JIRA等项目协调服务,实现项目信息同步。不仅如此,群机器人支持Webhook协议的自定义接入,支持更多可能性,例如:你可将运维报警提醒通过自定义机器人聚合到钉钉群。', 226 | // 'https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.Rqyvqo&treeId=257&articleId=105735&docType=1', 227 | // false 228 | // ); 229 | // 230 | // $message = new LinkMessage(); 231 | // $message->setMessage( 232 | // '自定义机器人协议', 233 | // '群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。例如:通过聚合GitHub,GitLab等源码管理服务,实现源码更新同步;通过聚合Trello,JIRA等项目协调服务,实现项目信息同步。不仅如此,群机器人支持Webhook协议的自定义接入,支持更多可能性,例如:你可将运维报警提醒通过自定义机器人聚合到钉钉群。', 234 | // 'https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.Rqyvqo&treeId=257&articleId=105735&docType=1', 235 | // false 236 | // ); 237 | 238 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 239 | $message->setRobot($notifiable->getName()); 240 | 241 | return $message; 242 | } 243 | ``` 244 | 245 | ### markdown 类型消息 246 | 247 | ```php 248 | use Calchen\LaravelDingtalkRobot\Message\MarkdownMessage; 249 | 250 | public function toDingTalkRobot($notifiable) 251 | { 252 | $message = new MarkdownMessage( 253 | '杭州天气', 254 | "#### 杭州天气 \n > 9度,@1825718XXXX 西北风1级,空气良89,相对温度73%\n\n > ![screenshot](http://i01.lw.aliimg.com/media/lALPBbCc1ZhJGIvNAkzNBLA_1200_588.png)\n > ###### 10点20分发布 [天气](http://www.thinkpage.cn/) " 255 | ); 256 | 257 | // 可@某人或某些人,被@的人的手机号应出现在上面的消息体中 258 | $message->at('1825718XXXX'); 259 | // $message->at(['1825718XXXX', '1825718XXXY']); 260 | 261 | // 可@全部人,被@的人的手机号不需要出现在消息体中 262 | // $message->atAll(); 263 | 264 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 265 | $message->setRobot($notifiable->getName()); 266 | 267 | return $message; 268 | } 269 | ``` 270 | 271 | ### ActionCard 类型消息 272 | 273 | #### 整体跳转类型 274 | 275 | ```php 276 | use Calchen\LaravelDingtalkRobot\Message\ActionCardMessage; 277 | 278 | public function toDingTalkRobot($notifiable) 279 | { 280 | $message = new ActionCardMessage( 281 | '乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身', 282 | "![screenshot](@lADOpwk3K80C0M0FoA) \n #### 乔布斯 20 年前想打造的苹果咖啡厅 \n\n Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划" 283 | ); 284 | $message->setSingle('阅读全文', 'https://www.dingtalk.com/'); 285 | 286 | // 如果想让链接在 PC 端用系统默认浏览器打开可以这样 287 | // $message->setSingle('阅读全文', 'https://www.dingtalk.com/', false); 288 | 289 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 290 | $message->setRobot($notifiable->getName()); 291 | 292 | return $message; 293 | } 294 | ``` 295 | 296 | #### 独立跳转类型 297 | 298 | ```php 299 | use Calchen\LaravelDingtalkRobot\Message\ActionCardMessage; 300 | 301 | public function toDingTalkRobot($notifiable) 302 | { 303 | $message = new ActionCardMessage( 304 | '乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身', 305 | "![screenshot](@lADOpwk3K80C0M0FoA) \n #### 乔布斯 20 年前想打造的苹果咖啡厅 \n\n Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划" 306 | ); 307 | 308 | // 添加一个或多个按钮 309 | $message->addButton('内容不错', 'https://www.dingtalk.com/'); 310 | $message->addButton('不感兴趣', 'https://www.dingtalk.com/'); 311 | 312 | // 如果想让链接在 PC 端用系统默认浏览器打开可以这样 313 | // $message->addButton('不感兴趣', 'https://www.dingtalk.com/', false); 314 | 315 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 316 | $message->setRobot($notifiable->getName()); 317 | 318 | return $message; 319 | } 320 | ``` 321 | 322 | ### FeedCard 类型消息 323 | 324 | ```php 325 | use Calchen\LaravelDingtalkRobot\Message\FeedCardMessage; 326 | 327 | public function toDingTalkRobot($notifiable) 328 | { 329 | $message = new FeedCardMessage(); 330 | 331 | // 添加一个或多个链接 332 | $message->addLink( 333 | '时代的火车向前开', 334 | 'https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI', 335 | 'https://www.dingtalk.com/' 336 | ); 337 | $message->addLink( 338 | '时代的火车向前开2', 339 | 'https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI', 340 | 'https://www.dingtalk.com/' 341 | ); 342 | 343 | // 如果想让链接在 PC 端用系统默认浏览器打开可以这样 344 | // $message->addLink( 345 | // '时代的火车向前开2', 346 | // 'https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI', 347 | // 'https://www.dingtalk.com/', 348 | // false 349 | // ); 350 | 351 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 352 | $message->setRobot($notifiable->getName()); 353 | 354 | return $message; 355 | } 356 | ``` 357 | 358 | ## 使用消息通知(Notification)给多个机器人发同一条消息 359 | 360 | ```php 361 | use Calchen\LaravelDingtalkRobot\Robot; 362 | 363 | $notification = new TestDingtalkNotification(); 364 | 365 | (new Robot('robot_1'))->notify($notification); 366 | (new Robot('robot_2'))->notify($notification); 367 | 368 | // TestDingtalkNotification 文件 369 | use Calchen\LaravelDingtalkRobot\Message\TextMessage; 370 | 371 | public function toDingTalkRobot($notifiable) 372 | { 373 | $message = new TextMessage('我就是我, 是不一样的烟火'); 374 | 375 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 376 | // 这里需要 $notifiable 内保存机器人的名称,并提供获取的方法,方可实现给不同机器人发同一条消息 377 | $message->setRobot($notifiable->getName()); 378 | 379 | return $message; 380 | } 381 | ``` 382 | 383 | ## 不使用消息通知(Notification) 384 | 385 | 如果在非 Laravel/Lumen 框架中使用或者您不想使用 Notification ,而是直接调用机器人接口发送信息,那么有多种方式可以实现: 386 | 387 | ### 容器解析 388 | 389 | ```php 390 | use Calchen\LaravelDingtalkRobot\DingtalkRobot; 391 | use Calchen\LaravelDingtalkRobot\Message\TextMessage; 392 | 393 | $message = new TextMessage('我就是我, 是不一样的烟火'); 394 | $message->setRobot('机器人名字'); 395 | 396 | app(DingtalkRobot::class)->setMessage($message)->send(); 397 | ``` 398 | 399 | ### 辅助函数 dingtalk_robot 400 | 401 | ```php 402 | use Calchen\LaravelDingtalkRobot\Message\TextMessage; 403 | 404 | $message = new TextMessage('我就是我, 是不一样的烟火'); 405 | $message->setRobot('机器人名字'); 406 | 407 | dingtalk_robot()->setMessage($message)->send(); 408 | ``` 409 | 410 | ### 直接创建并调用接口 411 | 412 | ```php 413 | use Calchen\LaravelDingtalkRobot\DingtalkRobot; 414 | use Calchen\LaravelDingtalkRobot\Message\TextMessage; 415 | 416 | $message = new TextMessage('我就是我, 是不一样的烟火'); 417 | $message->setRobot('机器人名字'); 418 | 419 | (new DingtalkRobot)->setMessage($message)->send(); 420 | ``` 421 | 422 | ## 从1.x升级到2.x 423 | 424 | 首先感谢您使用 1.x 版本的扩展包,因为钉钉机器人更新了安全设置,在升级过程中为了使得代码更整洁因此出现了无法兼容的情况,故此将新版升级至2.x版本。这里将明确给出两个版本的差异,以便您以最少的成本进行升级。 425 | 426 | ### 配置项的差异 427 | 428 | #### 新增 429 | 430 | ##### http_client_name 431 | 432 | 为了方便在某些情况下需要统一管理扩展包使用的 HTTP 客户端,提供了 `http_client_name` 配置项,以实现从 Laravel 容器中获取已经注入的 Guzzle 实例。如果您不需要手动管理 HTTP 客户端,您可以不设置该配置项或将该配置项值设置成 null。 433 | 434 | ##### 机器人名称.security_types 435 | 436 | 数据结构为数组。如果您使用的是旧机器人,配置项中无“安全设置”相关内容,那么您可以不设置该字段,或将其设置成: 437 | 438 | ```php 439 | 'security_types' => [ 440 | null, 441 | ], 442 | ``` 443 | 444 | 如果您使用的是新机器人,请按照机器人的实际情况配置该项,可以组合选择:自定义关键字、加签、IP地址(段),如: 445 | 446 | ```php 447 | 'security_types' => [ 448 | 'keywords', // 自定义关键字 449 | 'signature', // 加签 450 | 'ip', // IP地址(段) 451 | ], 452 | ``` 453 | 454 | ##### 机器人名称.security_signature 455 | 456 | 密钥字符串。当安全模式包含加签时该字段是必须的。请注意,该字符串前三位应该是 SEC。 457 | 458 | ## 鸣谢 459 | 460 | 感谢 [王举](https://github.com/wowiwj),他的 [wangju/ding-notice](https://github.com/wowiwj/ding-notice) 项目给予了我很多启发。本项目中的部分代码原形来自于该项目。 461 | 462 | 感谢 [aolinver](https://github.com/aolinver),他为本项目实现了部分可以设置链接的消息的链接在 PC 端用系统默认浏览器打开的功能。 463 | 464 | 465 | ## 开源协议 466 | 467 | [MIT](http://opensource.org/licenses/MIT) 468 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 |

laravel-dingtalk-robot-notification

2 |

Dingtalk Robot for Laravel/Lumen

3 |

4 | 5 | Style CI 6 | 7 | 8 | Travis CI 9 | 10 | 11 | Coverage Status 12 | 13 | 14 | Latest Stable Version 15 | 16 | 17 | Total Downloads 18 | 19 | 20 | License 21 | 22 |

23 | 24 | > [中文](https://github.com/calchen/laravel-dingtalk-robot-notification/blob/master/README.md) 25 | 26 | This is Laravel/Lumen custom notification channel for [DingTalk group assistant](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq). 27 | 28 | ## Installing 29 | 30 | Composer is recommended for installation: 31 | 32 | ```shell 33 | $ composer require calchen/laravel-dingtalk-robot-notification:^2.0 34 | ``` 35 | 36 | ### Laravel 37 | 38 | For Laravel 5.5+ package auto discovery feature will help you loading everything you need. But you still need to publish the configuration file: 39 | 40 | ```shell 41 | php artisan vendor:publish --provider="Calchen\LaravelDingtalkRobot\DingtalkRobotNoticeServiceProvider" 42 | ``` 43 | 44 | ### Lumen 45 | 46 | Open your `bootstrap/app.php` and add this line: 47 | 48 | ```php 49 | $app->register(Calchen\LaravelDingtalkRobot\DingtalkRobotNoticeServiceProvider::class); 50 | ``` 51 | 52 | Copy configuration file from `vendor/calchen/laravel-dingtalk-robot-notification/config/dingtalk_robot.php` to `config/dingtalk_robot.php` 53 | 54 | ### Other frameworks 55 | 56 | Without considering the loading problem, please use the global function `\dingtalk_robot()` or directly create the \Calchen\LaravelDingtalkRobot\DingtalkRobot instance to send the message. 57 | 58 | ## Configuration 59 | 60 | Open your `config/dingtalk_robot.php` and add these lines: 61 | 62 | ```php 63 | 'robotName' => [ 64 | 'access_token' => 'xxxx', 65 | 'timeout' => 2.0, 66 | 'security_types' => [ 67 | 'signature', 68 | ], 69 | 'security_signature' => 'SECxxxx', 70 | ], 71 | ``` 72 | 73 | If you need to configure more than one robot, repeat the above and give different `robotName` to different robots 74 | 75 | ### Details 76 | 77 | | key | required | type | remarks | 备注 | 78 | |-------------------- |------ |------------- |----------------------------------------------- |------------------------------------------------------------------------------------------------------------ | 79 | | http_client_name | N | string/null | Guzzle 实例的名称 | 默认值:null,注入在 Laravel 中的 Guzzle 实例的名称,以便替换 HTTP 客户端 | 80 | | robotName | Y | string | 机器人名称 | 这个名称为了区别不同的机器人 | 81 | | access_token | Y | string | 创建机器人后 Webhook URL 中 access_token 的值 | | 82 | | timeout | N | int/float | 超时时间 | 默认值:2.0秒,具体见 [Guzzle 文档](http://docs.guzzlephp.org/en/stable/request-options.html#timeout) | 83 | | security_types | Y | array | 安全设置 | 旧机器人是不存在该项配置的传 null 或不设置该配置项;新机器人可以组合选择:自定义关键字、加签、IP地址(段) | 84 | | security_types.* | Y | string/null | 设置项 | 枚举值:null、keywords、signature、ip | 85 | | security_signature | N | string | 安全模式包含加签时需要的密钥字符串 | 应当以 SEC 开头 | | 86 | 87 | ### 获取 access_token 并设置安全设置 88 | 89 | 首先在钉钉群选择添加一个群机器人(智能群助手),如果您不知道如何设置请查看 [钉钉文档](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq),请注意这里需要设置的是"自定义"类型的机器人。 90 | 91 | 根据您的需要设置机器人名字,并选择安全设置。在完成后您将获得一个 webhook 地址,该地址中 access_token 的值即配置中使用到的 access_token 的值, 92 | 请妥善保存该 access_token。选择的安全设置就是配置中 security_types 的值,如果您选择了“加签”的安全设置,您还需要妥善保存密钥,该密钥即配置中 security_signature 的值 93 | 94 | ### HTTP 客户端注入 95 | 96 | 为了方便在某些情况下需要统一管理扩展包使用的 HTTP 客户端,提供了 `http_client_name` 配置项,以实现从 Laravel 容器中获取已经注入的 Guzzle 实例。如果您不需要手动管理 HTTP 客户端,您可以不设置该配置项或将该配置项值设置成 null。 97 | 98 | ## 使用方法 99 | 100 | Tips:为了方便快速调试功能,内置了一个使用了 Notifiable Trait 的类:Calchen\LaravelDingtalkRobot\Robot ,以下均以此对象为例,实际开发中请务必根据您项目情况进行对应处理。 101 | 102 | 首先需要先创建一个 TestDingtalkNotification ,如果是 Laravel 可通过 artisan 命令创建 103 | 104 | ```shell 105 | php artisan make:notification TestDingtalkNotification 106 | ``` 107 | 108 | 如果是 Lumen 那么可能需要您手动创建 app/Notifications 文件夹并创建 TestDingtalkNotification.php 文件 109 | 110 | ```php 111 | setRobot($notifiable->getName()); 157 | 158 | return $message; 159 | } 160 | } 161 | ``` 162 | 163 | 根据 [Laravel](https://laravel.com/docs/6.x/notifications) 文档发送通知可以使用 Notifiable Trait 164 | 165 | ```php 166 | use Calchen\LaravelDingtalkRobot\Robot; 167 | 168 | (new Robot)->notify(new TestDingtalkNotification()); 169 | ``` 170 | 171 | 也可以使用 Notification Facade 172 | 173 | ```php 174 | \Notification::send(new Robot, new TestDingtalkNotification()); 175 | ``` 176 | 177 | **重点:** 178 | 179 | TestDingtalkNotification 中的 toDingTalkRobot 方法中可使用下面的五种消息类型。 180 | 181 | ### 文本类型消息 182 | 183 | ```php 184 | use Calchen\LaravelDingtalkRobot\Message\TextMessage; 185 | 186 | public function toDingTalkRobot($notifiable) 187 | { 188 | $message = new TextMessage('我就是我, @1825718XXXX 是不一样的烟火'); 189 | 190 | // 可@某人或某些人,被@的人的手机号应出现在上面的消息体中 191 | $message->at('1825718XXXX'); 192 | // $message->at(['1825718XXXX', '1825718XXXY']); 193 | 194 | // 可@全部人,被@的人的手机号不需要出现在消息体中 195 | // $message->atAll(); 196 | 197 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 198 | $message->setRobot($notifiable->getName()); 199 | 200 | return $message; 201 | } 202 | ``` 203 | 204 | ### link 类型消息 205 | 206 | ```php 207 | use Calchen\LaravelDingtalkRobot\Message\LinkMessage; 208 | 209 | public function toDingTalkRobot($notifiable) 210 | { 211 | $message = new LinkMessage( 212 | '自定义机器人协议', 213 | '群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。例如:通过聚合GitHub,GitLab等源码管理服务,实现源码更新同步;通过聚合Trello,JIRA等项目协调服务,实现项目信息同步。不仅如此,群机器人支持Webhook协议的自定义接入,支持更多可能性,例如:你可将运维报警提醒通过自定义机器人聚合到钉钉群。', 214 | 'https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.Rqyvqo&treeId=257&articleId=105735&docType=1' 215 | ); 216 | 217 | // 也可以这样写 218 | // $message = new LinkMessage(); 219 | // $message->setMessage( 220 | // '自定义机器人协议', 221 | // '群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。例如:通过聚合GitHub,GitLab等源码管理服务,实现源码更新同步;通过聚合Trello,JIRA等项目协调服务,实现项目信息同步。不仅如此,群机器人支持Webhook协议的自定义接入,支持更多可能性,例如:你可将运维报警提醒通过自定义机器人聚合到钉钉群。', 222 | // 'https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.Rqyvqo&treeId=257&articleId=105735&docType=1' 223 | // ); 224 | 225 | // 如果想让链接在 PC 端用系统默认浏览器打开可以这样 226 | // $message = new LinkMessage( 227 | // '自定义机器人协议', 228 | // '群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。例如:通过聚合GitHub,GitLab等源码管理服务,实现源码更新同步;通过聚合Trello,JIRA等项目协调服务,实现项目信息同步。不仅如此,群机器人支持Webhook协议的自定义接入,支持更多可能性,例如:你可将运维报警提醒通过自定义机器人聚合到钉钉群。', 229 | // 'https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.Rqyvqo&treeId=257&articleId=105735&docType=1', 230 | // false 231 | // ); 232 | // 233 | // $message = new LinkMessage(); 234 | // $message->setMessage( 235 | // '自定义机器人协议', 236 | // '群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。例如:通过聚合GitHub,GitLab等源码管理服务,实现源码更新同步;通过聚合Trello,JIRA等项目协调服务,实现项目信息同步。不仅如此,群机器人支持Webhook协议的自定义接入,支持更多可能性,例如:你可将运维报警提醒通过自定义机器人聚合到钉钉群。', 237 | // 'https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.Rqyvqo&treeId=257&articleId=105735&docType=1', 238 | // false 239 | // ); 240 | 241 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 242 | $message->setRobot($notifiable->getName()); 243 | 244 | return $message; 245 | } 246 | ``` 247 | 248 | ### markdown 类型消息 249 | 250 | ```php 251 | use Calchen\LaravelDingtalkRobot\Message\MarkdownMessage; 252 | 253 | public function toDingTalkRobot($notifiable) 254 | { 255 | $message = new MarkdownMessage( 256 | '杭州天气', 257 | "#### 杭州天气 \n > 9度,@1825718XXXX 西北风1级,空气良89,相对温度73%\n\n > ![screenshot](http://i01.lw.aliimg.com/media/lALPBbCc1ZhJGIvNAkzNBLA_1200_588.png)\n > ###### 10点20分发布 [天气](http://www.thinkpage.cn/) " 258 | ); 259 | 260 | // 可@某人或某些人,被@的人的手机号应出现在上面的消息体中 261 | $message->at('1825718XXXX'); 262 | // $message->at(['1825718XXXX', '1825718XXXY']); 263 | 264 | // 可@全部人,被@的人的手机号不需要出现在消息体中 265 | // $message->atAll(); 266 | 267 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 268 | $message->setRobot($notifiable->getName()); 269 | 270 | return $message; 271 | } 272 | ``` 273 | 274 | ### ActionCard 类型消息 275 | 276 | #### 整体跳转类型 277 | 278 | ```php 279 | use Calchen\LaravelDingtalkRobot\Message\ActionCardMessage; 280 | 281 | public function toDingTalkRobot($notifiable) 282 | { 283 | $message = new ActionCardMessage( 284 | '乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身', 285 | "![screenshot](@lADOpwk3K80C0M0FoA) \n #### 乔布斯 20 年前想打造的苹果咖啡厅 \n\n Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划" 286 | ); 287 | $message->setSingle('阅读全文', 'https://www.dingtalk.com/'); 288 | 289 | // 如果想让链接在 PC 端用系统默认浏览器打开可以这样 290 | // $message->setSingle('阅读全文', 'https://www.dingtalk.com/', false); 291 | 292 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 293 | $message->setRobot($notifiable->getName()); 294 | 295 | return $message; 296 | } 297 | ``` 298 | 299 | #### 独立跳转类型 300 | 301 | ```php 302 | use Calchen\LaravelDingtalkRobot\Message\ActionCardMessage; 303 | 304 | public function toDingTalkRobot($notifiable) 305 | { 306 | $message = new ActionCardMessage( 307 | '乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身', 308 | "![screenshot](@lADOpwk3K80C0M0FoA) \n #### 乔布斯 20 年前想打造的苹果咖啡厅 \n\n Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划" 309 | ); 310 | 311 | // 添加一个或多个按钮 312 | $message->addButton('内容不错', 'https://www.dingtalk.com/'); 313 | $message->addButton('不感兴趣', 'https://www.dingtalk.com/'); 314 | 315 | // 如果想让链接在 PC 端用系统默认浏览器打开可以这样 316 | // $message->addButton('不感兴趣', 'https://www.dingtalk.com/', false); 317 | 318 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 319 | $message->setRobot($notifiable->getName()); 320 | 321 | return $message; 322 | } 323 | ``` 324 | 325 | ### FeedCard 类型消息 326 | 327 | ```php 328 | use Calchen\LaravelDingtalkRobot\Message\FeedCardMessage; 329 | 330 | public function toDingTalkRobot($notifiable) 331 | { 332 | $message = new FeedCardMessage(); 333 | 334 | // 添加一个或多个链接 335 | $message->addLink( 336 | '时代的火车向前开', 337 | 'https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI', 338 | 'https://www.dingtalk.com/' 339 | ); 340 | $message->addLink( 341 | '时代的火车向前开2', 342 | 'https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI', 343 | 'https://www.dingtalk.com/' 344 | ); 345 | 346 | // 如果想让链接在 PC 端用系统默认浏览器打开可以这样 347 | // $message->addLink( 348 | // '时代的火车向前开2', 349 | // 'https://mp.weixin.qq.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI', 350 | // 'https://www.dingtalk.com/', 351 | // false 352 | // ); 353 | 354 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 355 | $message->setRobot($notifiable->getName()); 356 | 357 | return $message; 358 | } 359 | ``` 360 | 361 | ## 使用消息通知(Notification)给多个机器人发同一条消息 362 | 363 | ```php 364 | use Calchen\LaravelDingtalkRobot\Robot; 365 | 366 | $notification = new TestDingtalkNotification(); 367 | 368 | (new Robot('robot_1'))->notify($notification); 369 | (new Robot('robot_2'))->notify($notification); 370 | 371 | // TestDingtalkNotification 文件 372 | use Calchen\LaravelDingtalkRobot\Message\TextMessage; 373 | 374 | public function toDingTalkRobot($notifiable) 375 | { 376 | $message = new TextMessage('我就是我, 是不一样的烟火'); 377 | 378 | // 这里可以指定机器人,如果不需要指定则默认使用名称为 default 的机器人 379 | // 这里需要 $notifiable 内保存机器人的名称,并提供获取的方法,方可实现给不同机器人发同一条消息 380 | $message->setRobot($notifiable->getName()); 381 | 382 | return $message; 383 | } 384 | ``` 385 | 386 | ## 不使用消息通知(Notification) 387 | 388 | 如果在非 Laravel/Lumen 框架中使用或者您不想使用 Notification ,而是直接调用机器人接口发送信息,那么有多种方式可以实现: 389 | 390 | ### 容器解析 391 | 392 | ```php 393 | use Calchen\LaravelDingtalkRobot\DingtalkRobot; 394 | use Calchen\LaravelDingtalkRobot\Message\TextMessage; 395 | 396 | $message = new TextMessage('我就是我, 是不一样的烟火'); 397 | $message->setRobot('机器人名字'); 398 | 399 | app(DingtalkRobot::class)->setMessage($message)->send(); 400 | ``` 401 | 402 | ### 辅助函数 dingtalk_robot 403 | 404 | ```php 405 | use Calchen\LaravelDingtalkRobot\Message\TextMessage; 406 | 407 | $message = new TextMessage('我就是我, 是不一样的烟火'); 408 | $message->setRobot('机器人名字'); 409 | 410 | dingtalk_robot()->setMessage($message)->send(); 411 | ``` 412 | 413 | ### 直接创建并调用接口 414 | 415 | ```php 416 | use Calchen\LaravelDingtalkRobot\DingtalkRobot; 417 | use Calchen\LaravelDingtalkRobot\Message\TextMessage; 418 | 419 | $message = new TextMessage('我就是我, 是不一样的烟火'); 420 | $message->setRobot('机器人名字'); 421 | 422 | (new DingtalkRobot)->setMessage($message)->send(); 423 | ``` 424 | 425 | ## 从1.x升级到2.x 426 | 427 | 首先感谢您使用 1.x 版本的扩展包,因为钉钉机器人更新了安全设置,在升级过程中为了使得代码更整洁因此出现了无法兼容的情况,故此将新版升级至2.x版本。这里将明确给出两个版本的差异,以便您以最少的成本进行升级。 428 | 429 | ### 配置项的差异 430 | 431 | #### 新增 432 | 433 | ##### http_client_name 434 | 435 | 为了方便在某些情况下需要统一管理扩展包使用的 HTTP 客户端,提供了 `http_client_name` 配置项,以实现从 Laravel 容器中获取已经注入的 Guzzle 实例。如果您不需要手动管理 HTTP 客户端,您可以不设置该配置项或将该配置项值设置成 null。 436 | 437 | ##### 机器人名称.security_types 438 | 439 | 数据结构为数组。如果您使用的是旧机器人,配置项中无“安全设置”相关内容,那么您可以不设置该字段,或将其设置成: 440 | 441 | ```php 442 | 'security_types' => [ 443 | null, 444 | ], 445 | ``` 446 | 447 | 如果您使用的是新机器人,请按照机器人的实际情况配置该项,可以组合选择:自定义关键字、加签、IP地址(段),如: 448 | 449 | ```php 450 | 'security_types' => [ 451 | 'keywords', // 自定义关键字 452 | 'signature', // 加签 453 | 'ip', // IP地址(段) 454 | ], 455 | ``` 456 | 457 | ##### 机器人名称.security_signature 458 | 459 | 密钥字符串。当安全模式包含加签时该字段是必须的。请注意,该字符串前三位应该是 SEC。 460 | 461 | ## 鸣谢 462 | 463 | 感谢 [王举](https://github.com/wowiwj),他的 [wangju/ding-notice](https://github.com/wowiwj/ding-notice) 项目给予了我很多启发。本项目中的部分代码原形来自于该项目。 464 | 465 | 感谢 [aolinver](https://github.com/aolinver),他为本项目实现了部分可以设置链接的消息的链接在 PC 端用系统默认浏览器打开的功能。 466 | 467 | 468 | ## 开源协议 469 | 470 | [MIT](http://opensource.org/licenses/MIT) 471 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calchen/laravel-dingtalk-robot-notification", 3 | "description": "钉钉智能群助手 Laravel/Lumen 消息通知扩展包(Dingtalk robot message notifications for Laravel/Lumen)", 4 | "keywords": [ 5 | "laravel", 6 | "lumen", 7 | "notification", 8 | "laravel notification", 9 | "lumen notification", 10 | "钉钉", 11 | "钉钉群机器人", 12 | "群机器人", 13 | "钉钉智能群助手", 14 | "智能群助手", 15 | "dingtalk", 16 | "group assistant", 17 | "robot", 18 | "group robot", 19 | "custom channel", 20 | "丁丁", 21 | "dingding", 22 | "ding" 23 | ], 24 | "license": "MIT", 25 | "authors": [ 26 | { 27 | "name": "陈恺垣", 28 | "email": "contact@chenky.com" 29 | } 30 | ], 31 | "require": { 32 | "php": "^7.0", 33 | "ext-json": "*", 34 | "guzzlehttp/guzzle": "^6.3.1|^7.0.1", 35 | "illuminate/notifications": "^5.5|^6.0|^7.0|^8.0", 36 | "illuminate/support": "^5.5|^6.0|^7.0|^8.0" 37 | }, 38 | "require-dev": { 39 | "php-coveralls/php-coveralls": "^2.1", 40 | "orchestra/testbench": "^3.5|^4.0|^5.0|^6.0" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Calchen\\LaravelDingtalkRobot\\": "src" 45 | }, 46 | "files": [ 47 | "src/helpers.php" 48 | ] 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "Calchen\\LaravelDingtalkRobot\\Test\\": "tests" 53 | } 54 | }, 55 | "extra": { 56 | "laravel": { 57 | "providers": [ 58 | "Calchen\\LaravelDingtalkRobot\\DingtalkRobotNoticeServiceProvider" 59 | ], 60 | "aliases": { 61 | "DingtalkRobot": "Calchen\\LaravelDingtalkRobot\\Facade" 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /config/dingtalk_robot.php: -------------------------------------------------------------------------------- 1 | null, 7 | // 默认发送的机器人 8 | 'default' => [ 9 | // 机器人的 access_token 10 | 'access_token' => env('DINGTALK_ROBOT_ACCESS_TOKEN'), 11 | 12 | // 请求的超时时间 13 | 'timeout' => env('DINGTALK_ROBOT_TIMEOUT', 2.0), 14 | 15 | // 安全设置 16 | // 多个配置项以数组形式设置(见:\Calchen\LaravelDingtalkRobot\DingtalkRobot::SECURITY_TYPES) 17 | 'security_types' => [ 18 | 'keywords', 19 | 'signature', 20 | ], 21 | 22 | // types 中包含 signature 时,该字段是必须的,为密钥字符串 23 | 'security_signature' => env('DINGTALK_ROBOT_SECURITY_SIGNATURE'), 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /src/DingtalkRobot.php: -------------------------------------------------------------------------------- 1 | $name, 86 | ]); 87 | throw new InvalidConfigurationException($message, ErrorCodes::INVALID_ROBOT_NAME); 88 | } 89 | 90 | $config = $configs[$name]; 91 | 92 | // access_token 必须有 93 | if (! isset($config['access_token'])) { 94 | throw new InvalidConfigurationException(null, ErrorCodes::ACCESS_TOKEN_IS_NECESSARY); 95 | } 96 | 97 | // security_types 只能是允许的值,默认认为是旧机器人 98 | $securityTypes = Arr::get($config, 'security_types', [null]); 99 | if (! is_array($securityTypes) || count($securityTypes) == 0 || 100 | count(array_diff($securityTypes, self::SECURITY_TYPES)) != 0 101 | ) { 102 | throw new InvalidConfigurationException(null, ErrorCodes::INVALID_SECURITY_TYPES); 103 | } 104 | // security_types 中有 null 表示旧机器人,旧机器人没有任何安全设置,故不应该有任何其他设置 105 | if (in_array(null, $securityTypes) && count($securityTypes) > 1) { 106 | throw new InvalidConfigurationException(null, ErrorCodes::OLD_ROBOT_SECURITY_TYPE_INVALID); 107 | } 108 | 109 | // 签名类型的需要有 security_signature,签名以 SEC 开头 110 | $securitySignature = Arr::get($config, 'security_signature'); 111 | if (in_array(self::SECURITY_TYPES[2], $securityTypes)) { 112 | if (is_null($securitySignature)) { 113 | throw new InvalidConfigurationException(null, ErrorCodes::SECURITY_SIGNATURE_IS_NECESSARY); 114 | } 115 | 116 | if (! is_string($securitySignature) || strpos($securitySignature, 'SEC') !== 0) { 117 | throw new InvalidConfigurationException(null, ErrorCodes::INVALID_SECURITY_SIGNATURE); 118 | } 119 | } 120 | 121 | $this->config = $config; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * 将创建好的 message 对象保存到当前对象中以便后续发送 128 | * 129 | * @param Message $message 130 | * @return $this 131 | * 132 | * @throws Exception 133 | */ 134 | public function setMessage(Message $message): self 135 | { 136 | $this->message = $message; 137 | $this->robot($message->getRobot()); 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * 获取 message 对象的内容. 144 | * 145 | * @return array 146 | */ 147 | public function getMessage(): array 148 | { 149 | return $this->message->getMessage(); 150 | } 151 | 152 | /** 153 | * 获取 http 客户端 154 | * 如果容器已经注入了可记录日志的 guzzle 优先使用. 155 | * 156 | * @return ClientInterface 157 | */ 158 | private function getHttpClient(): ClientInterface 159 | { 160 | $configs = config('dingtalk_robot'); 161 | if (isset($configs['http_client_name']) && app()->has($configs['http_client_name'])) { 162 | $client = app($configs['http_client_name']); 163 | if ($client instanceof ClientInterface) { 164 | return $client; 165 | } 166 | } 167 | 168 | if (! self::$httpClient instanceof ClientInterface) { 169 | self::$httpClient = new GuzzleClient([ 170 | 'timeout' => $this->config['timeout'] ?? 2.0, 171 | ]); 172 | } 173 | 174 | return self::$httpClient; 175 | } 176 | 177 | /** 178 | * 发起请求,返回的内容与直接调用钉钉接口返回的内容一致. 179 | * 180 | * @return array 181 | * 182 | * @throws Exception 183 | */ 184 | public function send(): array 185 | { 186 | if (is_null($this->message)) { 187 | throw new InvalidConfigurationException(null, ErrorCodes::MESSAGE_REQUIRED); 188 | } 189 | 190 | $query = [ 191 | 'access_token' => $this->config['access_token'], 192 | ]; 193 | 194 | // 在请求接口前根据安全设置进行处理 195 | $securityTypes = $this->config['security_types']; 196 | // 签名的安全设置 197 | if (in_array(self::SECURITY_TYPES[2], $securityTypes)) { 198 | $securitySignature = $this->config['security_signature']; 199 | // 这里出文档要求:当前时间戳,单位是毫秒,与请求调用时间误差不能超过1小时 200 | // 故此简单乘以1000,没有用 microtime 201 | $query['timestamp'] = time() * 1000; 202 | $strToSign = $query['timestamp']."\n".$securitySignature; 203 | $query['sign'] = base64_encode(hash_hmac('sha256', $strToSign, $securitySignature, true)); 204 | } 205 | 206 | /** @var ResponseInterface $response */ 207 | $response = $this->getHttpClient()->post( 208 | $this->robotUrl, 209 | [ 210 | 'json' => $this->message->getMessage(), 211 | 'headers' => [ 212 | 'Content-Type' => 'application/json', 213 | ], 214 | 'query' => $query, 215 | ] 216 | ); 217 | 218 | $result = $response->getBody()->getContents(); 219 | if ($response->getStatusCode() !== 200) { 220 | throw new HttpException($result, ErrorCodes::RESPONSE_FAILED); 221 | } 222 | $result = json_decode($result, true); 223 | if (is_null($result)) { 224 | throw new Exception($result, ErrorCodes::RESPONSE_BODY_ERROR); 225 | } 226 | 227 | if (isset($result['errcode']) && $result['errcode'] != 0) { 228 | // 单独处理安全设置失败 229 | if ($result['errcode'] === 310000) { 230 | $message = __(ErrorCodes::MESSAGES[ErrorCodes::SECURITY_VERIFICATION_FAILED], [ 231 | 'message' => $result['errmsg'], 232 | ]); 233 | throw new Exception($message, ErrorCodes::SECURITY_VERIFICATION_FAILED); 234 | } else { 235 | $message = __(ErrorCodes::MESSAGES[ErrorCodes::RESPONSE_RESULT_UNKNOWN_ERROR], [ 236 | 'code' => $result['errcode'], 237 | 'message' => $result['errmsg'], 238 | ]); 239 | throw new Exception($message, ErrorCodes::RESPONSE_RESULT_UNKNOWN_ERROR); 240 | } 241 | } 242 | 243 | return $result; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/DingtalkRobotChannel.php: -------------------------------------------------------------------------------- 1 | toDingTalkRobot($notifiable); 29 | if (! $message instanceof Message) { 30 | throw new Exception(null, ErrorCodes::SHOULD_BE_INSTANCEOF_MESSAGE); 31 | } 32 | 33 | $ding = new DingtalkRobot(); 34 | $ding->setMessage($message)->send(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/DingtalkRobotNoticeServiceProvider.php: -------------------------------------------------------------------------------- 1 | setupConfig(); 19 | } 20 | 21 | /** 22 | * 处理配置项. 23 | * 24 | * @return void 25 | */ 26 | protected function setupConfig() 27 | { 28 | $source = realpath($raw = __DIR__.'/../config/dingtalk_robot.php') ?: $raw; 29 | 30 | if ($this->app instanceof LaravelApplication && $this->app->runningInConsole()) { 31 | $this->publishes([ 32 | $source => config_path('dingtalk_robot.php'), 33 | ]); 34 | } elseif ($this->app instanceof LumenApplication) { 35 | $this->app->configure('dingtalk_robot'); 36 | } 37 | 38 | $this->mergeConfigFrom($source, 'dingtalk_robot'); 39 | } 40 | 41 | /** 42 | * 注册服务 43 | * 44 | * @return void 45 | */ 46 | public function register() 47 | { 48 | $this->app->singleton(DingtalkRobot::class, function ($app) { 49 | return new DingtalkRobot(); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Exceptions/ErrorCodes.php: -------------------------------------------------------------------------------- 1 | 'http request failed', 33 | self::RESPONSE_BODY_ERROR => 'response body decode failed', 34 | 35 | self::INVALID_ROBOT_NAME => 'robot name: :name not exist. Please check your config in file dingtalk_robot.php', 36 | self::ACCESS_TOKEN_IS_NECESSARY => 'access_token is necessary in config', 37 | self::INVALID_SECURITY_TYPES => 'security type in config is invalid, only value in '.DingtalkRobot::class.'::SECURITY_TYPES is acceptable', 38 | self::INVALID_SECURITY_SIGNATURE => 'security signature is invalid', 39 | self::HTTP_CLIENT_NAME_INVALID => 'http_client_name is invalid', 40 | self::MOBILES_INVALID => 'mobiles should be string or array', 41 | self::HIDE_AVATAR_INVALID => 'hideAvatar value can only be 0 or 1', 42 | self::BTN_ORIENTATION_INVALID => 'btnOrientation value can only be 0 or 1', 43 | self::SECURITY_VERIFICATION_FAILED => 'security verification failed, reason is: :message', 44 | self::RESPONSE_RESULT_UNKNOWN_ERROR => 'response result is unknown error, code is: :code, message is: :message', 45 | self::SHOULD_BE_INSTANCEOF_MESSAGE => 'function toDingTalkRobot in the $notification should return instanceof '.Message::class, 46 | self::MESSAGE_REQUIRED => 'Please set message object', 47 | self::DINGTALK_ROBOT_CONFIG_IS_EMPTY => 'Please set dingtalk_robot config', 48 | self::OLD_ROBOT_SECURITY_TYPE_INVALID => 'old robot security types could only has null', 49 | self::SECURITY_SIGNATURE_IS_NECESSARY => 'security signature is necessary when security types has signature', 50 | ]; 51 | } 52 | -------------------------------------------------------------------------------- /src/Exceptions/Exception.php: -------------------------------------------------------------------------------- 1 | setMessage($title, $text, $hideAvatar, $btnOrientation); 45 | } 46 | } 47 | 48 | /** 49 | * ActionCard 的整体跳转和独立跳转两种类型中 title text hideAvatar btnOrientation 都是共同拥有的. 50 | * 51 | * @param string $title 首屏会话透出的展示内容 52 | * @param string $text markdown 格式的消息 53 | * @param int|null $hideAvatar 0-按钮竖直排列,1-按钮横向排列 54 | * @param int|null $btnOrientation 0-正常发消息者头像,1-隐藏发消息者头像 55 | * @return ActionCardMessage 56 | * 57 | * @throws InvalidConfigurationException 58 | */ 59 | public function setMessage(string $title, string $text, $hideAvatar = null, $btnOrientation = null): self 60 | { 61 | $this->message = [ 62 | 'msgtype' => 'actionCard', 63 | 'actionCard' => [ 64 | 'title' => $title, 65 | 'text' => $text, 66 | ], 67 | ]; 68 | 69 | if (! is_null($hideAvatar)) { 70 | if (! in_array($hideAvatar, self::HIDE_AVATAR_VALUES)) { 71 | throw new InvalidConfigurationException(null, ErrorCodes::HIDE_AVATAR_INVALID); 72 | } 73 | $this->message['actionCard']['hideAvatar'] = $hideAvatar; 74 | } 75 | if (! is_null($btnOrientation)) { 76 | if (! in_array($btnOrientation, self::BTN_ORIENTATION_VALUES)) { 77 | throw new InvalidConfigurationException(null, ErrorCodes::BTN_ORIENTATION_INVALID); 78 | } 79 | $this->message['actionCard']['btnOrientation'] = $btnOrientation; 80 | } 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * 设置整体跳转类型参数,与独立跳转互斥. 87 | * 88 | * @param string $singleTitle 单个按钮的方案。(设置此项和singleURL后btns无效。) 89 | * @param string $singleUrl 点击singleTitle按钮触发的URL 90 | * @param bool $pcSlide 链接是否在 PC 端侧栏打开,true 在 PC 端侧栏打开;false 在系统默认浏览器 91 | * @return $this 92 | */ 93 | public function setSingle(string $singleTitle, string $singleUrl, bool $pcSlide = true): self 94 | { 95 | $this->message['actionCard']['singleTitle'] = $singleTitle; 96 | $this->message['actionCard']['singleURL'] = $this->getFinalUrl($singleUrl, $pcSlide); 97 | unset($this->message['actionCard']['btns']); 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * 设置独立跳转类型按钮参数,可设置多个,与整体跳转互斥. 104 | * 105 | * @param string $title 按钮方案 106 | * @param string $actionUrl 点击按钮触发的URL 107 | * @param bool $pcSlide 链接是否在 PC 端侧栏打开,true 在 PC 端侧栏打开;false 在系统默认浏览器 108 | * @return ActionCardMessage 109 | */ 110 | public function addButton(string $title, string $actionUrl, bool $pcSlide = true): self 111 | { 112 | $this->message['actionCard']['btns'][] = [ 113 | 'title' => $title, 114 | 'actionURL' => $this->getFinalUrl($actionUrl, $pcSlide), 115 | ]; 116 | unset($this->message['actionCard']['singleTitle']); 117 | unset($this->message['actionCard']['singleURL']); 118 | 119 | return $this; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Message/AtTrait.php: -------------------------------------------------------------------------------- 1 | at['atMobiles'] = $mobiles; 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * @所有人 38 | * 39 | * @return Message 40 | */ 41 | public function atAll(): self 42 | { 43 | $this->at['isAtAll'] = true; 44 | 45 | return $this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Message/FeedCardMessage.php: -------------------------------------------------------------------------------- 1 | setMessage(); 18 | } 19 | 20 | public function setMessage() 21 | { 22 | $this->message = [ 23 | 'msgtype' => 'feedCard', 24 | 'feedCard' => [ 25 | 'links' => [], 26 | ], 27 | ]; 28 | } 29 | 30 | /** 31 | * 增加链接. 32 | * 33 | * @param string $title 单条信息文本 34 | * @param string $messageUrl 点击单条信息到跳转链接 35 | * @param string $picUrl 单条信息后面图片的URL 36 | * @param bool $pcSlide 链接是否在 PC 端侧栏打开,true 在 PC 端侧栏打开;false 在系统默认浏览器 37 | * @return FeedCardMessage 38 | */ 39 | public function addLink(string $title, string $messageUrl, string $picUrl, bool $pcSlide = true): self 40 | { 41 | $this->message['feedCard']['links'][] = [ 42 | 'title' => $title, 43 | 'messageURL' => $this->getFinalUrl($messageUrl, $pcSlide), 44 | 'picURL' => $picUrl, 45 | ]; 46 | 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Message/LinkMessage.php: -------------------------------------------------------------------------------- 1 | setMessage($title, $text, $messageUrl, $picUrl, $pcSlide); 30 | } 31 | } 32 | 33 | /** 34 | * @param string $title 消息标题 35 | * @param string $text 消息内容。如果太长只会部分展示 36 | * @param string $messageUrl 点击消息跳转的 URL 37 | * @param string $picUrl 图片 URL 38 | * @param bool $pcSlide 链接是否在 PC 端侧栏打开,true 在 PC 端侧栏打开;false 在系统默认浏览器 39 | * @return LinkMessage 40 | */ 41 | public function setMessage( 42 | string $title, 43 | string $text, 44 | string $messageUrl, 45 | string $picUrl = '', 46 | bool $pcSlide = true 47 | ): self { 48 | $this->message = [ 49 | 'msgtype' => 'link', 50 | 'link' => [ 51 | 'title' => $title, 52 | 'text' => $text, 53 | 'picUrl' => $picUrl, 54 | 'messageUrl' => $this->getFinalUrl($messageUrl, $pcSlide), 55 | ], 56 | ]; 57 | 58 | return $this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Message/MarkdownMessage.php: -------------------------------------------------------------------------------- 1 | setMessage($title, $text); 24 | } 25 | } 26 | 27 | /** 28 | * @param string $title 首屏会话透出的展示内容 29 | * @param string $text markdown 格式的消息 30 | * @return MarkdownMessage 31 | */ 32 | public function setMessage(string $title, string $text): self 33 | { 34 | $this->message = [ 35 | 'msgtype' => 'markdown', 36 | 'markdown' => [ 37 | 'title' => $title, 38 | 'text' => $text, 39 | ], 40 | ]; 41 | 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Message/Message.php: -------------------------------------------------------------------------------- 1 | message + ['at' => $this->at]; 29 | } 30 | 31 | /** 32 | * 设置接受消息的机器人名称. 33 | * 34 | * @param string $robot 35 | * @return Message 36 | */ 37 | public function setRobot(string $robot): self 38 | { 39 | $this->robot = $robot; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * 获取机器人名称. 46 | * 47 | * @return string 48 | */ 49 | public function getRobot(): string 50 | { 51 | return $this->robot; 52 | } 53 | 54 | /** 55 | * 将 URL 转换成指定的 schema 形式,以便控制其 PC 端打开方式为钉钉侧边栏或系统默认浏览器. 56 | * 57 | * @link https://ding-doc.dingtalk.com/doc#/serverapi2/iat9q8/e300ae98 58 | * 59 | * @param string $url 60 | * @param bool $pcSlide 61 | * @return string 62 | */ 63 | public function getFinalUrl(string $url, bool $pcSlide = true): string 64 | { 65 | return sprintf( 66 | 'dingtalk://dingtalkclient/page/link?url=%s&pc_slide=%s', urlencode($url), $pcSlide ? 'true' : 'false' 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Message/TextMessage.php: -------------------------------------------------------------------------------- 1 | setMessage($content); 23 | } 24 | } 25 | 26 | /** 27 | * @param string $content 消息内容 28 | * @return TextMessage 29 | */ 30 | public function setMessage(string $content): self 31 | { 32 | $this->message = [ 33 | 'msgtype' => 'text', 34 | 'text' => [ 35 | 'content' => $content, 36 | ], 37 | ]; 38 | 39 | return $this; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Robot.php: -------------------------------------------------------------------------------- 1 | setName($name); 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function getName(): string 29 | { 30 | return $this->name; 31 | } 32 | 33 | /** 34 | * @param string $name 35 | */ 36 | public function setName(string $name) 37 | { 38 | $this->name = $name; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 |