diff --git a/.gitignore b/.gitignore index 8823c742..c44fa39b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ zm.json /zm_data/ composer.lock /resources/server.phar +/distribute/ +/bin/.phpunit.result.cache +/resources/zhamao.service +.phpunit.result.cache diff --git a/Dockerfile b/Dockerfile index 8130fa82..0225d218 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,3 @@ -FROM ubuntu:18.04 -WORKDIR /app/ -RUN echo "Asia/Shanghai" > /etc/timezone -ENV LANG C.UTF_8 -ENV LC_ALL C.UTF-8 -ENV LANGUAGE C.UTF-8 +FROM zmbot/swoole:latest -RUN apt-get update && apt-get install -y software-properties-common tzdata -RUN dpkg-reconfigure -f noninteractive tzdata -VOLUME ["/app/zhamao-framework/"] -RUN add-apt-repository ppa:ondrej/php && \ - apt-get update && \ - apt-get install php php-dev php-mbstring gcc make openssl \ - php-mbstring php-json php-curl php-mysql -y && \ - apt-get install wget composer -y && \ - wget https://github.com/swoole/swoole-src/archive/v4.5.0.tar.gz && \ - tar -zxvf v4.5.0.tar.gz && \ - cd swoole-src-4.5.0/ && \ - phpize && ./configure --enable-openssl --enable-mysqlnd && make -j2 && make install && \ - (echo "extension=swoole.so" >> $(php -i | grep "Loaded Configuration File" | awk '{print $5}')) - - -ADD . /app/zhamao-framework -ADD . /app/zhamao-framework-bak -#RUN cd /app/zhamao-framework && composer update && composer clearcache -#RUN mv zhamao-framework-master zhamao-framework -WORKDIR /app/zhamao-framework - -CMD ["/bin/bash", "-i", "/app/zhamao-framework-bak/.entry.sh"] +# TODO: auto-setup entrypoint diff --git a/README.md b/README.md index f6eb04fd..555caf4e 100644 --- a/README.md +++ b/README.md @@ -7,78 +7,65 @@ [![zhamao License](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE) [![Latest Stable Version](http://img.shields.io/packagist/v/zhamao/framework.svg)](https://packagist.org/packages/zhamao/framework) [![Banner](https://img.shields.io/badge/CQHTTP-v11-black)]() -[![dev-version](https://img.shields.io/badge/dev--version-v2.0.0--a1-green)]() [![stupid counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/stupid.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=stupid) [![TODO counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO) - + ## 开发者注意 -**v2.0 版本已经开始公测了,但是文档还在光速编写中,可以现行进行测试!** +**开发者 QQ 群:670821194** -**炸毛框架目前经过实验可以直接在 PHP8 环境上运行,但是细节部分未经充分测试,如果在 PHP8 环境下运行出现问题,请及时提出 Issue,谢谢!** +**当前 v2 版本已正式发布,此 master 分支为 2.0 版本,如需查看 v1 版本,请移步 `v1-legacy` 分支!** -**由于 CQHTTP 不再提供维护,转为 [OneBot 标准](https://github.com/howmanybots/onebot)(原 CQHTTP 插件衍生而来的机器人 HTTP 接口标准),本框架也将在未来改为兼容此标准。** - -**以上涉及的变更将在下一个大版本 (v2.0.0) 更新,请关注 2.0-dev 分支 和 Project 模块!** - -**v2.0版本即将到来,请持续关注 [新文档](https://docs-v2.zhamao.xin/) 进度和 Project 模块展示的测试进度!** +**2.0 版本如果有问题请第一时间加群反馈!** ## 简介 -zhamao-framework 是一个 PHP Swoole 的聊天机器人框架,兼容 OneBot 标准,它会对微信公众号等终端收到的消息进行解析处理,并以模块化的形式进行开发,来完成机器人的自然语言对话等功能。 +炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务,聊天机器人(OneBot 兼容的 QQ 机器人对接),包含 Websocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。 -除了起到解析消息的作用,炸毛框架 还提供了完整的 WebSocket + HTTP 服务器,你还能用此框架构建出高性能的 API 接口服务器。 +框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于 QQ 机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。 + +```php +/** + * @CQCommand("你好") + */ +public function hello() { + ctx()->reply("你好,我是炸毛!"); // 简单的命令式回复 +} +/** + * @RequestMapping("/index") + */ +public function index() { + return "

hello!

"; // 快速的 HTTP 服务开发 +} +``` ## 开始 -先安装环境,环境安装见下方文档。 -1. `composer create-project zhamao/framework-starter` 从模板新建基础文档结构进行使用 -2. 你也可以直接拉取本项目,进入文件夹后 `composer update` 加载依赖后使用 `bin/start init` 快速初始化框架文件 -3. 还可以使用 Dockerfile 构建 Docker 容器 +框架首先需要部署环境,可以参考下方文档中部署环境和框架的方法进行。 -## 文档 (v1.x) -国内服务器:[https://docs-v1.zhamao.xin/](https://docs-v1.zhamao.xin/) +## 文档(v2 版本) +查看文档:[https://docs-v2.zhamao.xin/](https://docs-v2.zhamao.xin/) -GitHub Pages:[https://docs-v1.zhamao.me/](https://docs-v1.zhamao.me/) +备用链接:[http://docs-v2.zhamao.me/](http://docs-v2.zhamao.me/) + +自行构建文档:`mkdocs build -d distribute` ## 特点 - 支持多账号 +- 使用 Swoole 多工作进程机制和协程加持,尽可能简单的情况下提升了性能 - 灵活的注解事件绑定机制 - 支持下断点调试(Psysh) - 易用的上下文,模块内随处可用 -- 采用模块化编写,功能之间高内聚低耦合 +- 采用模块化编写,可单独拆装功能 - 常驻内存,全局缓存变量随处使用 -- 自带 MySQL 查询器、数据库连接池等数据库连接方案 +- 自带 MySQL、Refis 等数据库连接池等数据库连接方案 - 自带 HTTP 服务器、WebSocket 服务器可复用,可以构建属于自己的 HTTP API 接口 - 静态文件服务器 -- 支持 phar 一键打包 -## 炸毛特色模块 +## 从 v1 升级 +炸毛框架 v2 相对 v1 版本改动了不少内容,其中包括框架底层机制、注解事件分发、调试、命名空间等变化,详情可查看上方文档。 -| 模块名称 | 说明 | 模块地址 | -| ------------------ | -------------------------------- | ------------------------------------------------------------ | -| 通用模块 | 图片上传和下载模块 | [zhamao-general-tools](https://github.com/zhamao-robot/zhamao-general-tools) | - -## 计划开发内容 -- [X] WebSocket测试脚本(客户端) -- [X] Session 和中间层管理模块 -- [X] 常驻服务脚本 -- [X] 一些常用的通用 API 例如经济(用户积分、亲密度等)的模块 -- [ ] 图灵机器人/腾讯AI 聊天模块 -- [ ] 分词模块(可能会放弃计划,因为目前好用的分词都是其他语言的) -- [ ] HTTP 过滤器、Auth 模块、完整的 MVC 兼容(可能会放弃计划,因为框架主打机器人开发) -- [ ] Redis 连接池或开箱即用的相应功能内置 -- [X] 1.3 版本使用上下文代替 -- [X] 更好的 Logger,稳定和漂亮的控制台输出 -- [ ] 日志服务 -- [X] 框架支持 Phar 打包(可能会比较靠后支持) -- [ ] 完整的单元测试(如果有需求则尽快开发) -- [X] 静态文件服务器 - -## 从 cqbot-swoole 升级 -目前新的框架采用了全新的注解机制,所以旧版的框架上写的模块到新框架需要重新编写。当然为了减少工作量,新的框架也最大限度地保留了旧版框架编写的风格,一般情况下根据新版框架的文档仅需修改少量地方即可完成重写。 - -旧版框架并入了 `old` 分支,如果想继续使用旧版框架请移步分支。升级过程中如果遇到问题可以找作者。 +如果旧版框架使用过程中无问题且对新功能暂无需求,可以继续使用 v1 版本,后续也将维护安全类更新和修复致命 bug。 ## 贡献和捐赠 如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。目前项目仅一人维护,耗费精力较大,所以非常欢迎对框架的贡献。 @@ -102,3 +89,5 @@ GitHub Pages:[https://docs-v1.zhamao.me/](https://docs-v1.zhamao.me/) 项目框架采用 Apache-2.0 协议开源,在分发或重写修改等操作时需遵守协议。项目模块部分(`Module` 文件夹) 在非借鉴框架内代码时可不遵守 Apache-2.0 协议进行分发和修改(声明版权)。 **注意**:在你使用 mirai 等 `AGPL-3.0` 协议的机器人软件与框架连接时,使用本框架需要将你编写或修改的部分使用 `AGPL-3.0` 协议重新分发。 + +![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg) \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md index ec8ef0ec..ceccc54f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,8 @@ | ------- | ------------------ | | 2.0 | :white_check_mark: | | 1.6.x | :white_check_mark: | -| 1.x | :x: | +| 1.1.x | :x: | +| 1.0.x | :x: | ## Reporting a Vulnerability diff --git a/bin/phar-build b/bin/phar-build deleted file mode 100755 index 9a5e437c..00000000 --- a/bin/phar-build +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env php -startBuffering(); - $src = realpath(__DIR__ . '/../'); - $hello = file_get_contents($src . '/src/Module/Example/Hello.php'); - $middleware = file_get_contents($src . '/src/Module/Middleware/TimerMiddleware.php'); - unlink($src . '/src/Module/Example/Hello.php'); - unlink($src . '/src/Module/Middleware/TimerMiddleware.php'); - if ($with_wechat_patch) { - global $wechat_patch; - $wechat = base64_decode($wechat_patch); - } else { - $wechat = false; - } - if ($wechat !== false) { - echo "Using wechat patch.\n"; - $modbase = file_get_contents($src . '/src/ZM/ModBase.php'); - unlink($src . '/src/ZM/ModBase.php'); - } - $phar->buildFromDirectory($src); - $phar->addFromString('tmp/Hello.php.bak', $hello); - $phar->addFromString('tmp/TimerMiddleware.php.bak', $middleware); - if ($wechat !== false) { - $phar->addFromString('src/ZM/ModBase.php', $wechat); - file_put_contents($src . '/src/ZM/ModBase.php', $modbase); - } - //$phar->compressFiles(Phar::GZ); - $phar->setStub($phar->createDefaultStub('phar-starter.php')); - $phar->stopBuffering(); - file_put_contents($src . '/src/Module/Example/Hello.php', $hello); - file_put_contents($src . '/src/Module/Middleware/TimerMiddleware.php', $middleware); - echo "Successfully built. Location: " . $src . "/resources/$filename\n"; -} - diff --git a/bin/phpunit-swoole b/bin/phpunit-swoole new file mode 100755 index 00000000..eec62ac2 --- /dev/null +++ b/bin/phpunit-swoole @@ -0,0 +1,65 @@ +#!/usr/bin/env php + + * Date: 2018/4/14 下午10:58 + */ + +Co::set([ + 'log_level' => SWOOLE_LOG_INFO, + 'trace_flags' => 0 +]); + +if (!ini_get('date.timezone')) { + ini_set('date.timezone', 'UTC'); +} + +foreach ([ + __DIR__ . '/../../../autoload.php', + __DIR__ . '/../../autoload.php', + __DIR__ . '/../vendor/autoload.php', + __DIR__ . '/vendor/autoload.php' + ] as $file +) { + if (file_exists($file)) { + define('PHPUNIT_COMPOSER_INSTALL', $file); + break; + } +} + +if (!defined('PHPUNIT_COMPOSER_INSTALL')) { + fwrite( + STDERR, + 'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL . + ' composer install' . PHP_EOL . PHP_EOL . + 'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL + ); + + die(1); +} else { + if (array_reverse(explode('/', __DIR__))[0] ?? '' === 'test') { + $vendor_dir = dirname(PHPUNIT_COMPOSER_INSTALL); + $bin_unit = "{$vendor_dir}/bin/phpunit"; + $unit_uint = "{$vendor_dir}/phpunit/phpunit/phpunit"; + if (file_exists($bin_unit)) { + @unlink($bin_unit); + @symlink(__FILE__, $bin_unit); + } + if (file_exists($unit_uint)) { + @unlink($unit_uint); + @symlink(__FILE__, $unit_uint); + } + } +} +require PHPUNIT_COMPOSER_INSTALL; +$starttime = microtime(true); +go(function (){ + try{ + PHPUnit\TextUI\Command::main(false); + } catch(Exception $e) { + echo $e->getMessage().PHP_EOL; + } +}); +Swoole\Event::wait(); +echo "Took ".round(microtime(true) - $starttime, 4). "s\n"; diff --git a/bin/start b/bin/start index 1a8d310b..5a518c8f 100755 --- a/bin/start +++ b/bin/start @@ -1,135 +1,28 @@ #!/usr/bin/env php 30000, -]); -global $vendor_mode; -$vendor_mode = false; -if (mb_strpos(__DIR__, getcwd()) !== false && substr(str_replace(getcwd(), "", __DIR__), 0, 8) == "/vendor/") { +$symbol = sha1(is_file("/flag2") ? file_get_contents("/flag2") : '1') == '6252c0ec7fcbd544c3d6f5f0a162f60407d7a896' || mb_strpos(getcwd(), "/private/tmp"); + +// 首先得判断是直接从library模式启动的框架还是从composer引入library启动的框架 +// 判断方法:判断当前目录上面有没有 /vendor 目录,如果没有 /vendor 目录说明是从 composer 引入的 +// 否则就是直接从 framework 项目启动的 +if (!is_dir(__DIR__ . '/../vendor') || $symbol) { define("LOAD_MODE", 1); //composer项目模式 define("LOAD_MODE_COMPOSER_PATH", getcwd()); + /** @noinspection PhpIncludeInspection */ + require_once LOAD_MODE_COMPOSER_PATH . "/vendor/autoload.php"; +} elseif (substr(__DIR__, 0, 7) == 'phar://') { + define("LOAD_MODE", 2); //phar模式 + // 会废弃phar启动的方式,在2.0 } else { - define("LOAD_MODE", 0); //正常模式 + define("LOAD_MODE", 0); + require_once __DIR__ . "/../vendor/autoload.php"; } - -date_default_timezone_set("Asia/Shanghai"); - -switch ($argv[1] ?? '') { - case 'scheduler': - case 'timer': - go(function () { - try { - new Scheduler(Scheduler::REMOTE); - } catch (Exception $e) { - die($e->getMessage()); - } - }); - break; - case 'phar-build': - array_shift($argv); - require_once 'phar-build'; - break; - case 'systemd': - array_shift($argv); - require_once 'systemd'; - break; - case 'init': - array_shift($argv); - if (LOAD_MODE != 1) { - echo "initialization must be started with composer-project mode!\n"; - exit(1); - } - $cwd = LOAD_MODE_COMPOSER_PATH; - echo "Copying default module file ..."; - @mkdir($cwd . "/config"); - @mkdir($cwd . "/src"); - @mkdir($cwd . "/src/Custom"); - @mkdir($cwd . "/src/Module"); - @mkdir($cwd . "/src/Module/Example"); - @mkdir($cwd . "/src/Module/Middleware"); - $ls = [ - "/config/global.php", - "/.gitignore", - "/config/file_header.json", - "/config/motd.txt", - "/src/Module/Example/Hello.php", - "/src/Module/Middleware/TimerMiddleware.php", - "/src/Custom/global_function.php" - ]; - foreach($ls as $v) { - if(!file_exists($cwd.$v)) { - echo "Copying ".$v.PHP_EOL; - copy($cwd."/vendor/zhamao/framework".$v, $cwd.$v); - } - } - $autoload = [ - "psr-4" => [ - "Module\\" => "src/Module", - "Custom\\" => "src/Custom" - ], - "files" => [ - "src/Custom/global_function.php" - ] - ]; - $scripts = [ - "server" => "vendor/bin/start server", - "server:log-debug" => "vendor/bin/start server --log-debug", - "server:log-verbose" => "vendor/bin/start server --log-verbose", - "server:log-info" => "vendor/bin/start server --log-info", - "server:log-warning" => "vendor/bin/start server --log-warning", - "server:debug-mode" => "vendor/bin/start server --debug-mode", - "systemd" => "vendor/bin/start systemd" - ]; - echo PHP_EOL; - if (file_exists($cwd . "/composer.json")) { - echo "Updating composer.json ..."; - $composer = json_decode(file_get_contents($cwd . "/composer.json"), true); - if (!isset($composer["autoload"])) { - $composer["autoload"] = $autoload; - } - if (!isset($composer["scripts"])) { - $composer["scripts"] = $scripts; - } - file_put_contents($cwd . "/composer.json", json_encode($composer, 64 | 128 | 256)); - echo PHP_EOL; - } else { - echo("Error occurred. Please check your updates.\n"); - exit(1); - } - echo "success!\n"; - break; - case '': - case 'framework': - case 'server': - if (!is_dir(__DIR__ . '/../vendor/') && LOAD_MODE == 0) { - echo "Warning: you have not update composer!\n"; - exec("composer update", $out, $var); - if ($var != 0) { - echo "You need to run \"composer update\" at root of zhamao-framework!\n"; - die; - } - } - $loader = new FrameworkLoader($argv); - break; - case '--help': - case '-h': - echo "\nUsage: " . $argv[0] . " [OPTION]\n"; - echo "\nzhamao-framework start script, provides several startup arguments."; - echo "\n\n -h, --help\t\tShow this help menu"; - echo "\n framework, server\tstart main framework, this is default option"; - echo "\n phar-build\t\tbuild a new phar archive"; - echo "\n init\t\t\tinitialize framework structure in this directory"; - echo "\n systemd\t\tgenerate a new systemd \".service\" file to use\n\n"; - break; - default: - echo "Unknown option \"{$argv[1]}\"!\n\"--help\" for more information\n"; - break; -} - +// 终端的命令行功能启动!! +$application = new ConsoleApplication("zhamao-framework"); +$application->initEnv(); +$application->run(); diff --git a/composer.json b/composer.json index 7e27f8a1..69e11482 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,8 @@ "description": "High performance QQ robot and web server development framework", "minimum-stability": "stable", "license": "Apache-2.0", - "version": "1.6.5", + "version": "2.0.0-b5", + "extra": {}, "authors": [ { "name": "whale", @@ -16,22 +17,48 @@ ], "prefer-stable": true, "bin": [ - "bin/start" + "bin/start", + "bin/phpunit-swoole" ], "require": { "php": ">=7.2", - "swoole/ide-helper": "@dev", - "swlib/saber": "^1.0", "doctrine/annotations": "~1.10", "ext-json": "*", "psy/psysh": "@stable", "symfony/polyfill-ctype": "^1.20", - "symfony/polyfill-mbstring": "^1.20" + "symfony/polyfill-mbstring": "^1.20", + "symfony/console": "^5.1", + "zhamao/connection-manager": "*@dev", + "zhamao/console": "^1.0", + "zhamao/config": "^1.0", + "zhamao/request": "*@dev", + "symfony/routing": "^5.1", + "symfony/polyfill-php80": "^1.20" + }, + "suggest": { + "ext-ctype": "*", + "ext-mbstring": "*" }, "autoload": { "psr-4": { - "Framework\\": "src/Framework", - "ZM\\": "src/ZM" - } + "ZM\\": "src/ZM", + "Module\\": "src/Module", + "Custom\\": "src/Custom" + }, + "files": [ + "src/ZM/global_functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "ZMTest\\": "test/ZMTest" + }, + "files": [ + "test/ZMTest/Mock/mock.php" + ] + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "swoole/ide-helper": "@dev" } -} \ No newline at end of file +} diff --git a/config/console_color.json b/config/console_color.json new file mode 100644 index 00000000..ffb81891 --- /dev/null +++ b/config/console_color.json @@ -0,0 +1,29 @@ +{ + "default": { + "success": "green", + "info": "lightblue", + "warning": "yellow", + "error": "red", + "verbose": "blue", + "debug": "gray", + "trace": "gray" + }, + "white-term": { + "success": "green", + "info": "", + "warning": "yellow", + "error": "red", + "verbose": "blue", + "debug": "gray", + "trace": "gray" + }, + "no-color": { + "success": "", + "info": "", + "warning": "", + "error": "", + "verbose": "", + "debug": "", + "trace": "" + } +} diff --git a/config/global.php b/config/global.php index 2a3627ea..ef75bf04 100644 --- a/config/global.php +++ b/config/global.php @@ -1,4 +1,6 @@ set参数 */ $config['swoole'] = [ 'log_file' => $config['crash_dir'] . 'swoole_error.log', - 'worker_num' => 1, - 'dispatch_mode' => 2, - //'task_worker_num' => 1, + 'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算,则可把这里改为1使用全局变量 + 'dispatch_mode' => 2, //包分配原则,见 https://wiki.swoole.com/#/server/setting?id=dispatch_mode + 'max_coroutine' => 300000, + //'task_worker_num' => 4, //'task_enable_coroutine' => true ]; +/** 轻量字符串缓存,默认开启 */ +$config['light_cache'] = [ + 'size' => 1024, //最多允许储存的条数(需要2的倍数) + 'max_strlen' => 16384, //单行字符串最大长度(需要2的倍数) + 'hash_conflict_proportion' => 0.6, //Hash冲突率(越大越好,但是需要的内存更多) + 'persistence_path' => $config['zm_data'].'_cache.json', + 'auto_save_interval' => 900 +]; + /** MySQL数据库连接信息,host留空则启动时不创建sql连接池 */ $config['sql_config'] = [ 'sql_host' => '', @@ -38,18 +50,25 @@ $config['sql_config'] = [ 'sql_username' => 'name', 'sql_database' => 'db_name', 'sql_password' => '', - 'sql_enable_cache' => true, - 'sql_reset_cache' => '0300', 'sql_options' => [ PDO::ATTR_STRINGIFY_FETCHES => false, PDO::ATTR_EMULATE_PREPARES => false ], 'sql_no_exception' => false, - 'sql_default_fetch_mode' => PDO::FETCH_BOTH // added in 1.5.6 + 'sql_default_fetch_mode' => PDO::FETCH_ASSOC // added in 1.5.6 ]; -/** CQHTTP连接约定的token */ -$config["access_token"] = ""; +/** Redis连接信息,host留空则启动时不创建Redis连接池 */ +$config['redis_config'] = [ + 'host' => '', + 'port' => 6379, + 'timeout' => 1, + 'db_index' => 0, + 'auth' => '' +]; + +/** onebot连接约定的token */ +$config["access_token"] = ''; /** HTTP服务器固定请求头的返回 */ $config['http_header'] = [ @@ -64,15 +83,11 @@ $config['http_default_code_page'] = [ /** zhamao-framework在框架启动时初始化的atomic们 */ $config['init_atomics'] = [ - 'in_count' => 0, //消息接收message的统计数量 - 'out_count' => 0, //消息发送(调用send_*_msg的统计数量) - 'reload_time' => 0, //调用reload功能统计数量 - 'wait_msg_id' => 0, //协程挂起id自增 - 'info_level' => 2, //终端显示的log等级 + //'custom_atomic_name' => 0, //自定义添加的Atomic ]; -/** 自动保存的缓存保存时间(秒) */ -$config['auto_save_interval'] = 900; +/** 终端日志显示等级(0-4) */ +$config["info_level"] = 2; /** 上下文接口类 implemented from ContextInterface */ $config['context_class'] = \ZM\Context\Context::class; @@ -88,7 +103,15 @@ $config['static_file_server'] = [ /** 注册 Swoole Server 事件注解的类列表 */ $config['server_event_handler_class'] = [ - \Framework\ServerEventHandler::class, + \ZM\Event\ServerEventHandler::class, +]; + +/** 服务器启用的外部第三方和内部插件 */ +$config['modules'] = [ + 'onebot' => [ + 'status' => true, + 'single_bot_mode' => false + ], // QQ机器人事件解析器,如果取消此项则默认为 true 开启状态,否则你手动填写 false 才会关闭 ]; return $config; diff --git a/docker_mixed/Dockerfile b/docker_mixed/Dockerfile deleted file mode 100644 index b85a7dea..00000000 --- a/docker_mixed/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM richardchien/cqhttp:latest -RUN apt-get update && apt-get install -y software-properties-common tzdata -RUN dpkg-reconfigure -f noninteractive tzdata -RUN add-apt-repository ppa:ondrej/php && \ - apt-get update && \ - apt-get install php php-dev php-mbstring gcc make openssl \ - php-mbstring php-json php-curl php-mysql -y && \ - apt-get install wget composer -y && \ - wget https://github.com/swoole/swoole-src/archive/v4.5.0.tar.gz && \ - tar -zxvf v4.5.0.tar.gz && \ - cd swoole-src-4.5.0/ && \ - phpize && ./configure --enable-openssl --enable-mysqlnd && make -j2 && make install && \ - (echo "extension=swoole.so" >> $(php -i | grep "Loaded Configuration File" | awk '{print $5}')) -ADD start.sh /home/user/start.sh -RUN chown user:user /home/user/start.sh && chmod +x /home/user/start.sh -ADD https://github.com/zhamao-robot/zhamao-framework/archive/master.zip /home/user/master.zip -RUN chown user:user /home/user/master.zip && chmod 777 /home/user/master.zip -VOLUME ["/home/user/coolq","/home/user/zhamao-framework"] diff --git a/docker_mixed/start.sh b/docker_mixed/start.sh deleted file mode 100644 index adf73f0f..00000000 --- a/docker_mixed/start.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -unzip master.zip -mv zhamao-framework-master/* zhamao-framework/ -cd zhamao-framework -php bin/start diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 00000000..4514b4c1 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1 @@ +# FAQ diff --git a/docs/advanced/index.md b/docs/advanced/index.md new file mode 100644 index 00000000..fa8e1a9d --- /dev/null +++ b/docs/advanced/index.md @@ -0,0 +1,3 @@ +# 进阶开发 +## 深入 +还没填坑,敬请期待! \ No newline at end of file diff --git a/docs/advanced/to-v2.md b/docs/advanced/to-v2.md new file mode 100644 index 00000000..5da3fc87 --- /dev/null +++ b/docs/advanced/to-v2.md @@ -0,0 +1,39 @@ +# 从炸毛框架 V1 升级 + +> 这里只写明可能在升级过程中会影响原先代码执行的部分,不包含新增的特性等。 + +### 需要改变命名空间的类 + +- `Framework\Console` -> `ZM\Console\Console` +- `Swlib\Util\SingletonTrait` -> `ZM\Utils\SingletonTrait` +- `ZM\Annotation\Http\Before` -> `ZM\Annotation\Http\HandleBefore` +- `ZM\Annotation\Http\After` -> `ZM\Annotation\Http\HandleAfter` +- `@SwooleEventAt` -> `@OnSwooleEvent` +- 删除 `@SwooleEventAfter` +- 删除 `ModBase` +- `@HandleEvent` -> `@SwooleHandler` +- `ZM\Utils\ZMRobot` -> `\ZM\API\ZMRobot` + +### 方法名称变更 + +- `ZM\Console::stackTrace()` -> `ZM\Console::trace()` + +### 注解的变化 + +`@OnSwooleEvent`(原 `@SwooleEventAt`)中,`rule` 参数不再是自定义语法的东西了(比如之前的 `connectType:qq` 之类的鸡肋语法),直接是可执行的 PHP 代码,比如 `3 == 4`,`connectIsQQ()` 之类的。 + +去除 `@CQAPISend`,因为目前没什么意义。 + +`@CQCommand` 中,`regexMatch` 变成 `pattern`,`fullMatch` 变成 `regex`,消除歧义(第一个是 * 号匹配符进行匹配的,第二个是标准的正则表达式匹配)。同时新增 `start_with`,`end_with`,`keyword` 平行选项。 + +`@OnTick` 注解新增第二个参数 `worker_id`,其中默认是 0,代表只在 `#0` 号工作进程上运行计时器。 + +### 中间件编写的改变 + +原先的 Middleware 是需要含有 `getName()` 方法才合法,现在不需要了,但是对 `@MiddlewareClass` 注解需要增加参数,也就是说原先 `getName()` 返回的名称现在需要写到 `@MiddlewareClass("xxx")` 这样的形式。 + +### ZMBuf 的变化 + +由于 2.0 框架使用了多进程模型,所以不能使用原先适用于单进程下全局变量的方式(ZMBuf)进行存取变量,所以 ZMBuf 下的所有方法都需要更改,其中 `get, set` 等对缓存操作的模型请根据 2.0 的文档变更使用 `Redis` 或内置的多进程共享内存可用的 `LightCache` 轻量缓存。 + +而获取全局配置文件,如 `global.php` 文件,也发生了变化,新框架引入了 `ZMConfig` 对象,可以快速地区分各类环境变量从而读取不同的配置文件。比如我们获取原先的 global 配置文件中的一项:`ZMBuf::globals("port")`,在 2.0 中需要使用 `ZMConfig::get("global", "port")` 方式。以此类推,`ZMBuf::config("xxx")` 也直接变为 `ZMConfig::get("xxx")` 了。 \ No newline at end of file diff --git a/docs/assets/css/extra.css b/docs/assets/css/extra.css new file mode 100644 index 00000000..82895785 --- /dev/null +++ b/docs/assets/css/extra.css @@ -0,0 +1,88 @@ + +.md-header-nav__button.md-logo { + padding: .2rem; + margin: .2rem; +} + +.md-header-nav__button.md-logo img, .md-header-nav__button.md-logo svg { + width: 1.6rem; + height: 1.6rem; +} + +.doc-chat-container { + border-radius: 6px; + width: 100%; + min-height: 30px; + /*noinspection CssUnresolvedCustomProperty*/ + background-color: var(--md-code-bg-color); + padding: 12px; + margin-right: auto; + margin-left: auto; + box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); +} + +.doc-chat-row { + margin: 0; + display: flex; + flex-wrap: wrap; + flex: 1 1 auto; + justify-content: flex-end; +} + +.doc-chat-row-robot { + justify-content: flex-start !important; +} + +.doc-chat-box { + color: #000000de; + position: relative; + width: fit-content; + max-width: 55%; + border-radius: .5rem; + padding: .4rem .6rem; + margin: .4rem .8rem; + background-color: #fff; + line-height: 1.5; + font-size: 16px; + outline: none; + overflow-wrap: break-word; + white-space: normal; + box-shadow: 0 2px 12px 0 rgba(0,0,0,.1); +} + +.doc-chat-box:after { + content: ""; + position: absolute; + right: auto; + top: 0; + width: 8px; + height: 12px; + color: #fff; + border: 0 solid transparent; + border-bottom: 7px solid; + border-radius: 0 0 8px 0; + left: calc(100% - 4px); + box-sizing: inherit; +} + +.doc-chat-box-robot:after { + content: ""; + position: absolute; + right: calc(100% - 4px); + top: 0; + width: 8px; + height: 12px; + color: #fff; + border: 0 solid transparent; + border-bottom: 7px solid; + border-radius: 0 0 0 8px; + left: auto; + box-sizing: inherit; +} + +.doc-chat-avatar { + background-color: aquamarine; + width: 36px !important; + height: 36px !important; + border-radius: 18px; +} \ No newline at end of file diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png new file mode 100644 index 00000000..fa2ccbd0 Binary files /dev/null and b/docs/assets/favicon.png differ diff --git a/docs/assets/logos.png b/docs/assets/logos.png new file mode 100644 index 00000000..c3c668dd Binary files /dev/null and b/docs/assets/logos.png differ diff --git a/docs/component/index.md b/docs/component/index.md new file mode 100644 index 00000000..ead7e943 --- /dev/null +++ b/docs/component/index.md @@ -0,0 +1,3 @@ +# 框架组件 + +还没写到这里,不着急 \ No newline at end of file diff --git a/docs/event/index.md b/docs/event/index.md new file mode 100644 index 00000000..c4955a2c --- /dev/null +++ b/docs/event/index.md @@ -0,0 +1,56 @@ +# 事件和注解 + +## 注解事件概念 + +我们知道事件,是一个底层的 event loop 收到消息后调用对应的各类方法的一个模型,比如给机器人发送消息后框架要做的就是指定到一个你定义的函数上,处理你的业务逻辑代码。比如在默认模块中,提供了 **你好** 的回复:**你好啊,我是由炸毛框架构建的机器人!**。这项简单回复的任务就是一个事件的触发到响应的全过程。 + +**注解**(Annotation)又称标注,Java 最早在 2004 年的 JDK 5 中引入的一种注释机制。目前 PHP 官方版本并未提供内置元注解和注解概念,但我们通过 `ReflectionClass` 反射类解析 PHP 代码注释从而实现了自己的一套注解机制。如果你没有写过 Java,并且不了解注解是什么,你可以理解为对 function 或 class 的一个修饰,因为传统的 PHP 代码逻辑我们都知道,不能简单给原先存在的函数贴标签,就比如,你不能在原本的 PHP 代码中给函数贴上一个可以影响它一生并且改变它行为的标签,而有了注解,就相当于有了给函数贴标签的机会。 + +在常见框架如 Spring,Swoft 等代码结构里面,注解更是其核心的存在。 + +在炸毛框架中,我们所有事件的绑定均采用这一方式进行调用模块内各个方法。包括 Swoole 自身的框架启动事件、WebSocket 连接握手事件、HTTP 请求事件等等,也包括 CQHTTP 发来的事件,如`message`,`notice`,`request` 等。 + +## 如何使用注解 + +就像我们日常开发写注释一样,只需在类、方法或成员变量上方按规则添加注释即可,这里以默认自带的 `Hello` 模块类为例子: + +```php + true]` | + +### 子表 **swoole** + +| 配置名称 | 说明 | 默认值 | +| --------------- | ------------------------------------------------------------ | ----------------------------------- | +| `log_file` | Swoole 的日志文件 | `crash_dir` 下的 `swoole_error.log` | +| `worker_num` | Worker 工作进程数 | 运行框架的主机 CPU 核心数 | +| `dispatch_mode` | 数据包分发策略,见 [文档](https://wiki.swoole.com/#/server/setting?id=dispatch_mode) | 2 | +| `max_coroutine` | 最大协程并发数 | 300000 | + +### 子表 **light_cache** + +| 配置名称 | 说明 | 默认值 | +| -------------------------- | ----------------------------------------------- | ---------------------------- | +| `size` | 最多可以缓存的 k-v 条目数(必须是 2 的 n 次方) | 1024 | +| `max_strlen` | 作为 value 字符串的最大长度 | 16384 | +| `hash_conflict_proportion` | Hash冲突率(越大越好,但是需要的内存更多) | 0.6 | +| `persistence_path` | 持久化的键值对的存储路径 | `zm_data` 下的 `_cache.json` | +| `auto_save_interval` | 持久化的键值对自动保存时间间隔(秒) | 900 | + +### 子表 **sql_config** + +| 配置名称 | 说明 | 默认值 | +| ------------------------ | ------------------------------ | ------------------------------------------------------------ | +| `sql_host` | 数据库地址(留空则不使用数据库) | 空 | +| `sql_port` | 数据库端口 | 3306 | +| `sql_username` | 连接数据库的用户名 | | +| `sql_database` | 要连接的数据库名 | | +| `sql_password` | 数据库连接密码 | | +| `sql_options` | PDO 数据库的 options 参数 | `[PDO::ATTR_STRINGIFY_FETCHES => false,PDO::ATTR_EMULATE_PREPARES => false]` | +| `sql_default_fetch_mode` | PDO 的 fetch 模式 | `PDO::FETCH_ASSOC` | + +### 子表 **redis_config** + +| 配置名称 | 说明 | 默认值 | +| ---------- | ------------------------------------------ | ------ | +| `host` | Redis 服务器地址,留空则启动时不创建连接池 | 空 | +| `port` | Redis 服务器端口 | 6379 | +| `timeout` | Redis 超时时间 | 1 | +| `db_index` | Redis 要连接的数据库 index | 0 | +| `auth` | 认证字符串 | 空 | + +### 子表 static_file_server + +| 配置名称 | 说明 | 默认值 | +| ---------------- | ---------------------- | ------------------------------ | +| `status` | 是否开启静态文件服务器 | false | +| `document_root` | 静态文件的根目录 | `{WORKING_DIR}/resources/html` | +| `document_index` | 默认索引的文件名列表 | `["index.html"]` | + +## 多环境下的配置文件 + +炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境(production / development / staging)来编写配置文件。 + +### 使用环境参数 + +在启动框架时,额外增加参数 `--env` 可以指定当前的环境,从而使用不同的配置文件。现在框架支持以下几种环境: `production`,`staging`,`development`。 + +```bash +vendor/bin/start server --env=development +``` + +### 不同环境配置文件 + +由于框架默认只带有 `global.php` 文件,所以假设你现在需要区分开发环境和生产环境的配置,将 `global.php` 文件复制或改名为 `global.development.php` 或 `global.production.php` 即可。 + +### 优先级 + +如果指定了 `--env` 环境参数:`global.{对应环境}.php` > `global.php`,如果两个配置文件都找不到则报错。 + +如果未指定 `--env` 环境参数:`global.php` > `global.development.php` > `global.staging.php` > `global.production.php`。 + +## 其他自定义配置文件 + +炸毛框架的全局配置文件为 `global.php`,为了让不同的开发者更好的二次开发或者集成更多功能,炸毛框架的配置文件模块也支持自己编写的其他 `*.php` 或 `*.json` 格式的配置文件。例如炸毛框架默认附带了 `file_header.json` 这个配置文件(用来返回各类文件扩展名对应的 `Content-Type` 头参数的表)。 + +使用也非常简单,我们先以 `.json` 格式为例,我们创建一个 `example_a.json` 文件在 `config/` 目录(和 `global.php` 一个文件夹下),并编写自己的任意配置内容: + +```json +{ + "key1": "value1" +} +``` + +在框架中,启动后就会默认加载,使用只需要用以下方式即可: + +```php +use ZM\Config\ZMConfig; # 先 use 再使用! +$r = ZMConfig::get("example_a", "key1"); # $r == "value1" +``` + +如果需要用到变量或其他动态的内容,可以使用 `.php` 格式的配置文件。这里还是以 `example_a.php` 来举例: + +```php + 这篇为炸毛框架以及环境的部署教程。 + +框架部署分为环境部署和框架部署。框架部署非常简单,只需要通用的指令,下方主要说环境部署。 + +## Docker 部署 PHP 环境 +如果你不想干扰主机的环境,可以使用 Docker 进行拉取框架适用的 PHP7 with Swoole Extension Docker Container。本框架安装教程中使用的 DockerHub 及 Dockerfile 构建文件所构建的容器均为独立的容器,和框架无关,此 Docker 也可以用作运行**其他基于 php-cli 模式的项目**。 + +方法一、直接拉取远程容器(推荐) +```bash +docker pull zmbot/swoole +``` + +方法二、从 Dockerfile 构建容器 +```bash +git clone https://github.com/zhamao-robot/zhamao-swoole-docker.git +cd zhamao-swoole-docker/ +docker build -t zm . +``` + +!!! note "从 Dockerfile 构建容器的提示" + + 使用 Dockerfile 构建后,需要将下方所有的 `zmbot/swoole` 全部更换成 `zm`,或者你上方指令中的 `-t` 参数后方的名称,具体可以详情查阅 Docker 的文档。 + +## 主机部署 PHP 环境 + +### Debian 系列(Ubuntu、Kali ) + +需要的系统内软件包为:`php php-dev php-mbstring gcc make openssl php-mbstring php-json php-curl php-mysql wget composer` + +下面是一个一键安装的命令行(最小安装,需 root 权限): + +```bash +apt-get update && apt-get install -y software-properties-common && add-apt-repository ppa:ondrej/php && apt-get update && apt-get install php php-dev php-mbstring gcc make openssl php-mbstring php-json php-curl php-mysql -y && apt-get install wget composer -y && wget https://github.com/swoole/swoole-src/archive/v4.5.7.tar.gz && tar -zxvf v4.5.7.tar.gz && cd swoole-src-4.5.7/ && phpize && ./configure --enable-openssl --enable-mysqlnd && make -j2 && make install && (echo "extension=swoole.so" >> $(php -i | grep "Loaded Configuration File" | awk '{print $5}')) +``` + +### macOS (with Homebrew) + +macOS 系统下的部署相对简单很多,只需要使用 Homebrew 安装以下包和执行安装命令即可 + +!!! note "给 macOS 开发者的提示" + + 因为苹果新的 Apple Sillicon 对 Homebrew 的支持目前仅限于 Rosetta2 转译版, + 所以在使用 M1-based Mac 时出现问题暂时无解。 + 使用以下指令可能会遇到报错等问题,如有疑问可直接使用 Docker 或咨询我(炸毛框架开发者)。 + +```bash +brew install php composer +pecl install swoole +``` + +### 其他 Linux 发行版 + +其他 Linux 发行版,如 CentOS,Fedora,Arch 等暂时还没有经过严格的测试需要哪些依赖,大体和 Ubuntu、Debian 系需要的依赖包差不多,可根据安装过程中报错提示依次安装,或者直接使用 Docker 环境。 + +## 安装框架 + +恭喜你,前方通过 Docker 或主机安装环境后可以开始构建框架的开发脚手架了! + +如果你是通过**主机安装 PHP 部署的环境**,下方是通过脚手架来构建项目的命令行。 + +```bash +git clone https://github.com/zhamao-robot/zhamao-framework-starter.git +cd zhamao-framework-starter/ +composer update +``` + +如果是通过 **Docker 部署的环境**,则需要在先克隆脚手架后在文件夹内使用 Docker 命令下的 `composer update`。 + +```bash +git clone https://github.com/zhamao-robot/zhamao-framework-starter.git +cd zhamao-framework-starter/ +docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole composer update +``` + +或者在 Docker 环境下,你可以直接使用如下方法拉取和快速启动一个最标准的框架。 + +```bash +git clone https://github.com/zhamao-robot/zhamao-framework-starter.git +cd zhamao-framework-starter +./run-docker.sh # 在正式版炸毛框架 v2 发布后可用,测试版暂不放出 +``` + + +## 启动框架 +本地环境启动方式: +```bash +cd zhamao-framework-starter +vendor/bin/start server +``` + +使用 Docker 启动: +```bash +cd zhamao-framework-starter +docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole vendor/bin/start server +``` + +启动后你会看到和下方类似的初始化内容,表明启动成功了 + +```verilog +$ vendor/bin/start server +host: 0.0.0.0 | port: 20001 +log_level: 2 | version: 2.0.0 +config: global.php | worker_num: 4 +working_dir: /Users/jerry/project/git-project/zhamao-framework + ______ +|__ / |__ __ _ _ __ ___ __ _ ___ + / /| '_ \ / _` | '_ ` _ \ / _` |/ _ \ + / /_| | | | (_| | | | | | | (_| | (_) | +/____|_| |_|\__,_|_| |_| |_|\__,_|\___/ + +[14:27:31] [I] [#0] Worker #0 启动中 +[14:27:31] [I] [#2] Worker #2 启动中 +[14:27:31] [I] [#1] Worker #1 启动中 +[14:27:31] [I] [#3] Worker #3 启动中 +[14:27:31] [S] [#3] Worker #3 已启动 +[14:27:31] [S] [#0] Worker #0 已启动 +[14:27:31] [S] [#2] Worker #2 已启动 +[14:27:31] [S] [#1] Worker #1 已启动 +``` + +单纯运行 炸毛框架 后,如果不部署或安装启动任何机器人客户端的话,仅仅相当于启动了一个 监听 20001 端口的WebSoket + HTTP 服务器。你可以通过浏览器访问:http://127.0.0.1:20001 ,或者你部署到了服务器后需要输入服务器地址。 + +!!! note "安装和部署总结" + + 根据上方描述,此文档中剩余提到的所有 Bash 命令,如果使用 Docker 部署环境,则需要加上 Docker 环境的指令:`docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole`,如执行其他 Linux 指令(以查看 PHP 版本为例):`docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole php -v`。 + +## 使用 IDE 等工具开发代码 + +我们使用文本编辑器进行炸毛框架开发,在使用集成开发环境 **IDEA** 或 **PhpStorm** 时,推荐通过插件市场搜索并安装 **PHP Annotations** 插件以提供注解命名空间自动补全、注解属性代码提醒、注解类跳转等,非常有助于提升开发效率的功能。 + +## 进阶环境部署和开发 +炸毛框架还支持更多种启动方式,如源码模式、守护进程模式,具体后续有关环境和部署的进阶教程,请查看 [进阶开发](/advanced/) 部分! \ No newline at end of file diff --git a/docs/guide/快速上手-http.md b/docs/guide/快速上手-http.md new file mode 100644 index 00000000..87bd4162 --- /dev/null +++ b/docs/guide/快速上手-http.md @@ -0,0 +1,4 @@ +# 快速上手 - HTTP 服务器篇 + +HTTP 服务器篇暂时先放一放,大家应该主要都是奔着机器人开发来的吧~ + diff --git a/docs/guide/快速上手-机器人.md b/docs/guide/快速上手-机器人.md new file mode 100644 index 00000000..f94df246 --- /dev/null +++ b/docs/guide/快速上手-机器人.md @@ -0,0 +1,228 @@ +# 快速上手 - 机器人篇 + + + +## 简介 + +看到这里,你已经完成了前面的环境部署,到了最关键的第一步了! + +一切都安装成功后,你就已经做好了进行简单配置以运行一个最小的 **机器人问答模块** 的准备。 + +炸毛框架和机器人客户端是什么关系呢?炸毛框架就好比我们传统的一系列例如 Spring 框架、ThinkPHP 框架等,是服务端,而机器人客户端是一个 HTTP / WebSocket 客户端,时刻准备着连接到炸毛框架的。 + +## 机器人客户端 + +开发之前请注意,**机器人客户端和框架相互独立!**故有关**机器人客户端**出现的问题请到对应机器人客户端开发者或 GitHub 项目中咨询和讨论,炸毛框架为对接机器人客户端的一个快速开发的框架。 + +机器人客户端是炸毛框架以外的程序或软件,目前炸毛框架支持的机器人客户端通信标准为 OneBot 标准(原 CQHTTP),只要你的机器人客户端是 OneBot 标准的,就可以和炸毛框架进行无缝对接。 + +OneBot 机器人部分的选择详情见 [OneBot 实例](/guide/OneBot实例/)。 + +这里以炸毛框架开发过程中使用的 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 来举例进行第一个机器人的配置工作。 + +简要步骤描述为: + +1. 下载 go-cqhttp 对应平台的 [release 文件](https://github.com/Mrs4s/go-cqhttp/releases) +2. 双击 exe 文件或者使用 `./go-cqhttp` 启动 +3. 生成默认配置文件并修改默认配置 + +!!! warning "注意" + + 由于 go-cqhttp 项目还处于开发期,而且配置文件格式也发生了多次变化,但大体内容没有变(比如编写此文档时发布的版本中配置文件格式变成了 `hjson` 取代了原来的 `json`。 + +=== "config.json(旧格式)" + + ``` json hl_lines="2 3 30 31" + { + "uin": 你的QQ号, + "password": "你的密码", + "encrypt_password": false, + "password_encrypted": "", + "enable_db": true, + "access_token": "", + "relogin": { + "enabled": true, + "relogin_delay": 3, + "max_relogin_times": 0 + }, + "ignore_invalid_cqcode": false, + "force_fragmented": true, + "heartbeat_interval": 0, + "http_config": { + "enabled": false, + "host": "0.0.0.0", + "port": 5700, + "timeout": 0, + "post_urls": {} + }, + "ws_config": { + "enabled": false, + "host": "0.0.0.0", + "port": 6700 + }, + "ws_reverse_servers": [ + { + "enabled": true, + "reverse_url": "ws://127.0.0.1:20001/", + "reverse_api_url": "", + "reverse_event_url": "", + "reverse_reconnect_interval": 3000 + } + ], + "post_message_format": "string", + "debug": false, + "log_level": "" + } + ``` + +=== "config.hjson(新格式)" + + ``` json hl_lines="3 5 81 84" + { + // QQ号 + uin: 你的机器人QQ + // QQ密码 + password: "你的QQ密码" + // 是否启用密码加密 + encrypt_password: false + // 加密后的密码, 如未启用密码加密将为空, 请勿随意修改. + password_encrypted: "" + // 是否启用内置数据库 + // 启用将会增加10-20MB的内存占用和一定的磁盘空间 + // 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能 + enable_db: true + // 访问密钥, 强烈推荐在公网的服务器设置 + access_token: "" + // 重连设置 + relogin: { + // 是否启用自动重连 + // 如不启用掉线后将不会自动重连 + enabled: true + // 重连延迟, 单位秒 + relogin_delay: 3 + // 最大重连次数, 0为无限制 + max_relogin_times: 0 + } + // API限速设置 + // 该设置为全局生效 + // 原 cqhttp 虽然启用了 rate_limit 后缀, 但是基本没插件适配 + // 目前该限速设置为令牌桶算法, 请参考: + //https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin + _rate_limit: { + // 是否启用限速 + enabled: false + // 令牌回复频率, 单位秒 + frequency: 1 + // 令牌桶大小 + bucket_size: 1 + } + // 是否忽略无效的CQ码 + // 如果为假将原样发送 + ignore_invalid_cqcode: false + // 是否强制分片发送消息 + // 分片发送将会带来更快的速度 + // 但是兼容性会有些问题 + force_fragmented: false + // 心跳频率, 单位秒 + // -1 为关闭心跳 + heartbeat_interval: 0 + // HTTP设置 + http_config: { + // 是否启用正向HTTP服务器 + enabled: true + // 服务端监听地址 + host: 0.0.0.0 + // 服务端监听端口 + port: 5700 + // 反向HTTP超时时间, 单位秒 + // 最小值为5,小于5将会忽略本项设置 + timeout: 0 + // 反向HTTP POST地址列表 + // 格式: + // { + // 地址: secret + // } + post_urls: {} + } + // 正向WS设置 + ws_config: { + // 是否启用正向WS服务器 + enabled: true + // 正向WS服务器监听地址 + host: 0.0.0.0 + // 正向WS服务器监听端口 + port: 6700 + } + // 反向WS设置 + ws_reverse_servers: [ + // 可以添加多个反向WS推送 + { + // 是否启用该推送 + enabled: true + // 反向WS Universal 地址 + // 注意 设置了此项地址后下面两项将会被忽略 + reverse_url: ws://127.0.0.1:20001/ + // 反向WS API 地址 + reverse_api_url: "" + // 反向WS Event 地址 + reverse_event_url: "" + // 重连间隔 单位毫秒 + reverse_reconnect_interval: 3000 + } + ] + // 上报数据类型 + // 可选: string array + post_message_format: string + // 是否使用服务器下发的新地址进行重连 + // 注意, 此设置可能导致在海外服务器上连接情况更差 + use_sso_address: false + // 是否启用 DEBUG + debug: false + // 日志等级 + // WebUi 设置 + web_ui: { + // 是否启用 WebUi + enabled: true + // 监听地址 + host: 127.0.0.1 + // 监听端口 + web_ui_port: 9999 + // 是否接收来自web的输入 + web_input: false + } + } + ``` + +其中 ws://127.0.0.1:20001/ 中的 127.0.0.1 和 20001 应分别对应炸毛框架配置的 HOST 和 PORT + +## 第一次对话 + +一旦新的配置文件正确生效之后,所在的控制台(如果正在运行的话)应该会输出类似下面的内容: + +```verilog +[15:26:34] [I] [#2] 机器人 你的QQ号 已连接! +``` + +表明机器人已成功连接到炸毛框架了! + +这时,如果你是根据安装教程走下来并且未编写任何模块,炸毛自带一个示例模块,里面含有命令:`你好`,`随机数`。如果你对机器人回复:`你好`,它会回复你 `你好啊,我是由炸毛框架构建的机器人!`。这一历史性的对话标志着你已经成功地运行了炸毛框架,开始了编写更强大的 QQ 机器人的创意之旅! + +## 编写一个命令 + +让我们转到框架的模块源代码部分,目录是 `src/Module/Example`,文件是 `Hello.php`。我们插入一段这样的代码: + +```php +/** + * @CQCommand("echo") + */ +public function repeat() { + $repeat = ctx()->getFullArg("请输入你要回复的内容"); + ctx()->reply($repeat); + //return $repeat; // 这样的效果等同于 ctx()->reply() +} +``` + +这样,一个简易的复读机就做好了!回到 QQ 机器人聊天,向机器人发送 `echo 你好啊`,它会回复你 `你好啊`。 + +> 如果你只回复 `echo` 的话,它会先和你进入一个会话状态,并问你 `请输入你要回复的内容`,这时你再次说一些内容例如 `哦豁`,会回复你 `哦豁`。效果和直接输入 `echo 哦豁` 是一致的,这是炸毛框架内的一个封装好的命令参数对话询问功能。有关参数询问功能,请看后面的进阶模块。 + diff --git a/docs/guide/注册事件响应.md b/docs/guide/注册事件响应.md new file mode 100644 index 00000000..80cc0374 --- /dev/null +++ b/docs/guide/注册事件响应.md @@ -0,0 +1,66 @@ +# 注册事件响应(机器人篇) + +现在模块已经创建完毕,我们可以开始编写实际代码了。本段以机器人会话为例子来讲述事件注册和响应,有关 HTTP 服务器等注册事件响应请看后面事件和注解章节。 + +## 机器人聊天事件处理 + +首先知道,QQ 等聊天机器人的消息我们的处理逻辑为如下简单的模式: + +- QQ 用户消息 -> 机器人客户端 -> 连接客户端的框架(炸毛框架) +- 框架处理逻辑后返回给用户的消息 -> 机器人客户端 -> QQ用户 + +第一步,我们以框架这边的角度考虑,我们称之为“事件”,我们编写代码所做的就是要响应这一事件。 + +首先我们以一句简单的功能——查天气,我们要从零实现一个查天气的功能进行示范如何快捷有效地开发一个功能。 + +### 确定问法 + +我们首先要确定的用户问法是一般由我们自己定义,但最好贴合用户的自然语言来进行定义。比如我们这里提到的天气功能,用户一般就会询问“北京天气”,“北京天气怎么样”,“天气 北京”。 + +### 注册消息事件 + +我们以最简单的命令方式“天气 北京”进行处理。问法为参数化的,通过空格来分开,这也是炸毛框架默认支持最基本的聊天事件之一。我们通过上一部分的方式新建一个单文件模块 `Weather.php` 在 `src/Module` 目录下,并编写: + +```php +getNextArg("请告诉我你要查询的城市"); // 发送 “天气 北京”时,变量为“北京” + // 这里假设是天气API接口的对接,返回了天气的数据 + $weather = "2020年12月22日,晴,-2~9℃ blablabla"; + return "$city 天气情况:".$weather; + } +} +``` + +!!! note "提示" + 为了简单起见,我们在这里的例子中没有接入真实的天气数据,但要接入也非常简单,你可以使用中国天气网、和风天气等网站提供的 API,本教程的进阶一栏后期会详细编写如何对接一个天气 API 接口。 + +在上方代码编写完毕后,运行框架(或运行过程中终端输入 `reload`),然后使用机器人客户端连接到炸毛框架,即可实现我们的第一个功能。我们在代码中编写了一个**注解事件**:`@CQCommand`,此注解事件是用于接收用户的普通消息并切分成各类命令规则的一个注解事件绑定。代码中的注解事件省去了注解中的键名,也可以写作 `@CQCommand(match="天气")`。 + +这里注解事件的概念请看 [事件和注解](/event/) 一栏描述的概念即可。到这里,我们就完成一个可以处理命令 `天气 xxx` 的方法了! + +### 处理消息事件 + +第一行 `ctx()` 是炸毛框架内的上下文获取方式,每条用户聊天信息发过来,被炸毛框架收到,都会创建一次上下文,同时这次聊天的全部信息,比如用户的 ID(QQ 号码),发消息的时间,如果是群消息的话所在的群号等等,都被存到了上下文中。`ctx()` 获取的是一个上下文对象,内部有许多可操作上下文的方法,其中代码的 `getNextArg()`,作用是根据空格分隔获取命令中的下一个参数。 + +在之前我们知道:`天气 北京` 是我们发送的消息,我们要获取到用户发送的参数 `北京`。`getNextArg()` 是框架封装好的一个快速获取下一个参数的方法,我们这里直接使用它来获取。 + +对于 `getNextArg()` 中的文本,可为空,不为空的时候如果用户只发送天气两个字,机器人还是会响应,但是它会询问你这句话,然后你回复机器人“北京”,这里 `$city` 变量就接受到并赋值为“北京”了,代码会继续执行,和直接一次性发送机器人“天气 北京”是一个效果,此为框架封装的消息会话机制,以贴近自然会话的方式来编写代码逻辑。 + +最后,函数直接返回了一个字符串,作为事件的响应,炸毛框架会自动处理并调用机器人客户端的接口,最后返回给用户消息。这里也可以使用上下文的 `ctx()->reply("xxx")` 方法替代,不返回字符串。注意两者只能选择一种方式,取决于开发者的开发习惯。 + + +) 天气 北京 +( 北京 天气情况:2020年12月22日,晴,-2~9℃ blablabla +) 天气 +( 请告诉我你要查询的城市 +) 北京 +( 北京 天气情况:2020年12月22日,晴,-2~9℃ blablabla + diff --git a/docs/guide/编写模块.md b/docs/guide/编写模块.md new file mode 100644 index 00000000..d681d7d8 --- /dev/null +++ b/docs/guide/编写模块.md @@ -0,0 +1,68 @@ +# 编写模块 + +到现在为止,我们还在使用框架的默认模块 `Example/Hello.php`,在开始编写自己的模块应用之前,我们先说明一些编写代码的约定。 + +## 加载模块 + +框架默认使用脚手架构建好后,目录结构大致为下面这样: + +```bash +zhamao-framework-starter/ +├── config/ # 项目的配置文件文件夹,如 global.php +├── src/ # 项目的主要源码目录 +│ ├── Module/ # 用户编写的模块目录 +│ │ └── Example/ # 模块文件夹名称 +│ │ └── Hello.php # 模块内的类 +│ └── Custom/ # 用户自定义的全局方法、全局注解类等存放的目录 +├── vendor/ # Composer 依赖加载目录 +└── composer.json # Composer 配置文件 +``` + +其中我们脚手架包含的默认模块 `Example` 下的 `Hello` 类,就是用户写模块的位置。你也可以根据实际情况,自行添加更多的模块文件夹甚至单文件模块。 + +需要注意的是,所有文件夹名称和 `.php` 文件必须遵循 [psr-4 规范](https://learnku.com/docs/psr/psr-4-autoloader/1608),简单来说,`src/` 目录下的文件夹,子文件夹要写成命名空间,比如默认框架中 `Example/` 下的 `.php` 文件的命名空间为 `namespace Module\Example;`,且一个 `.php` 文件推荐只包含一个 `class`、`trait` 或 `interface`。 + +```php +; +class ModuleA {} +``` + +!!! fail "警告" + 如果没有遵守上方的类和文件命名规则的话(文件名、文件夹名和命名空间的统一性),在加载框架时就会报错,无法找到对应的类。因为框架的注解解析依赖于 Composer 中 psr-4 规则的自动加载。 + +## 创建模块 +### 标准形式 +我们这里以 `Entertain` 娱乐模块的创建为例,新建一个内有 `Dice.php` 掷骰子功能的模块,目录结构如下,在 `Module/` 下新建文件夹 `Entertain/`,再在此子目录下新建 `Dice.php` 文件。 +```bash +zhamao-framework-starter/ +└── src/ + └── Module/ + └── Entertain/ + └── Dice.php +``` +新建的 PHP 文件按照如下方式编写: +```php + 本文档为炸毛框架 v2 版本,如需查看 v1 版本,[点我](https://docs-v1.zhamao.xin/)。 + +> 如果是从 v1.x 版本升级到 v2.x,[点我看升级指南](/advanced/to-v2/)。 + +炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务,聊天机器人(CQHTTP 对接),包含 websocket、http 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。 + +框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于 QQ 机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。 + +在 HTTP 和 WebSocket 服务器上,PHP 的扩展 Swoole 提供了高性能的支持,使其效率可媲美 nginx 静态网页处理的效率。 + +此外,QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快,未来也会兼容微信公众号开发者模式。 + +```php +/** + * @CQCommand("你好") + */ +public function hello() { + ctx()->reply("你好,我是炸毛!"); +} +/** + * @RequestMapping("/index") + */ +public function index() { + return "

hello!

"; +} +``` + + + +## 开始前 + +首先,你需要了解你需要知道哪些事情才能开始着手使用框架: + +1. Linux 命令行基础 +2. php 7.2+ 开发环境 +3. HTTP 协议(可选) +4. OneBot 机器人聊天接口标准(可选) + +需要值得注意的是,本教程中所涉及的内容均为尽可能翻译为白话的方式进行描述,但对于框架的组件或事件等需要单独拆分说明文档的部分则需要足够详细,所以本教程提供一个快速上手的教程,并且会将最典型的安装方式写到快速教程篇。 + +!!! bug "文档提示" + + 此文档采用 MkDocs 驱动,但因为本文档的搜索组件原生不支持中文搜索,所以搜索体验会大打折扣,敬请谅解!搜不到不是没这个东西哦! + + +## 框架特色 +- 支持MySQL数据库(连接池),自带查询缓存提高多查询时的效率 +- Websocket 服务器、HTTP 服务器兼容运行,一个框架多个用处 +- 支持命令、自然语言处理等多种插件形式 +- 支持多个机器人账号负载均衡 +- 协程 + TaskWorker 进程重度任务处理机制,保证高效,单个请求响应时间为 0.1 ms 左右 +- 模块分离和自由组合,可根据自身需求自己建立模块内的目录结构和代码结构 +- 灵活的注释注解注册事件方式,弥补 PHP 语言缺少注解的遗憾 + +## 文档主题 + +### 主题 +
+ + +
+ + + +### 主色调 +
+ + + + + + + + + + + + + + + + + + + + + +
+ +### 辅色调 +
+ + + + diff --git a/docs/javascripts/config.js b/docs/javascripts/config.js new file mode 100644 index 00000000..b6abc718 --- /dev/null +++ b/docs/javascripts/config.js @@ -0,0 +1,85 @@ +hljs.initHighlighting() + +var _hmt = _hmt || []; +(function () { + var hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?f0f276cefa10aa31a20ae3815a50b795"; + var s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); +})(); + +function appendChatModule(id, chatDialogs) { + let insertDiv = document.getElementById(id); + let ss = ''; + ss += '
'; + for(let i of chatDialogs) { + if (i.role === 0) { + ss += '
\n' + + ' \n' + + '
' + i.msg + '
\n' + + '
'; + } else { + ss += '
\n' + + '
' + i.msg + '
\n' + + ' \n' + + '
'; + } + } + insertDiv.innerHTML = ss + '
'; +} + +function getCookie(name) { + var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)"); + + if (arr = document.cookie.match(reg)) + + return unescape(arr[2]); + else + return null; +} + +function setCookie(name, value) { + var Days = 30; + var exp = new Date(); + exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000); + document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString(); +} + +s_theme=getCookie("_theme"); +if(s_theme === undefined) s_theme = "default"; +document.body.setAttribute("data-md-color-scheme", s_theme) +var name = document.querySelector("#__code_0 code span:nth-child(7)") +name.textContent = s_theme + +s_primary=getCookie("_primary_color"); +document.body.setAttribute("data-md-color-primary", s_primary); +var name2 = document.querySelector("#__code_2 code span:nth-child(7)"); +if(s_primary !== null && name2 !== null) name2.textContent = s_primary.replace("-", " "); + +s_accent=getCookie("_accent_color"); +document.body.setAttribute("data-md-color-accent", s_accent); +var name3 = document.querySelector("#__code_3 code span:nth-child(7)"); +if(s_accent !== null && name3 !== null) name3.textContent = s_accent.replace("-", " "); + +setTimeout(() => { + let ls = document.querySelectorAll("chat-box"); + for(let i of ls) { + let final = '
'; + let dialogs = i.innerHTML.split("\n"); + for(let j of dialogs) { + if(j === '') continue; + if(j.substr(0, 2) === ') ') { + final += '
\n' + + '
' + j.substr(2) + '
\n' + + ' \n' + + '
'; + } else if (j.substr(0, 2) === '( ') { + final += '
\n' + + ' \n' + + '
' + j.substr(2) + '
\n' + + '
'; + } + } + i.innerHTML = final; + } +}, 500); diff --git a/docs/update/v1.md b/docs/update/v1.md new file mode 100644 index 00000000..89d53221 --- /dev/null +++ b/docs/update/v1.md @@ -0,0 +1,220 @@ +# 更新日志(v1 版本) + +## v1.6.5 + +> 更新时间:2020.12.9 + +- 修复:版本号显示 +- 优化:依赖问题,减少对 PHP 扩展的依赖,转变为可选 + +## v1.6.4 + +> 更新时间:2020.12.9 + +- 修复:composer require模式下自动加载的问题 +- 优化:减少不是必需的依赖问题 + +## v1.6.3 + +> 更新时间:2020.11.15 + +- 修复:Response 对象使用 redirect 造成的递归报错 +- 修复:`document_index` 配置项在 `/` 路径下无法使用的 bug + +## v1.6.2 + +> 更新时间:2020.7.27 + +- 修复:不写配置 `server_event_handler_class` 项无法启动的 bug + +## v1.6.1 + +> 更新时间:2020.7.26 + +- 新增:`ZMRequest::downloadFile($url, $dst)` 方法,可直接将文件下载到本地 + +## v1.6 + +> 更新时间:2020.7.14 + +- 新增:现在可以对类修饰自定义的注解了 +- 修复:数据库操作 where 对象时产生的歧义 +- 新增:支持自定义任何 Swoole Server 事件的注解绑定,详见文档 +- 修复:多个中间件注解对类只生效最后一个的 bug + +❗ 下面是框架升级需要手动进行的变更: + +- 新版本由于引进了自定义 Swoole Server 事件的机制,对 global.php 全局配置文件有了变动,需要添加以下内容才能正常启动(旧版本升级新版本用户,新用户无需操作): + +```php +/** 注册 Swoole Server 事件注解的类列表 */ +$config['server_event_handler_class'] = [ + \Framework\ServerEventHandler::class, //默认不可删除,否则会不能使用框架 +]; +``` + +## v1.5.8 + +> 更新时间:2020.6.26 + +- 新增:`@CQCommand` 注解的 fullMatch 参数(全量正则表达式匹配) + +## v1.5.7 + +> 更新时间:2020.6.20 + +- 新增:ZM_BREAKPOINT 的短名称:BP +- 优化:终端连接器自动重连 +- 修复:语法错误时防止循环报错 + +## v1.5.6 + +> 更新时间:2020.6.15 + +- 新增:`@CQCommand` 注解支持 `message_type`,`user_id`,`group_id`,`discuss_id` 限定条件 +- 新增:PDO 数据库支持自定义 fetch_mode,可在 `global.php` 中的 `sql_config["sql_default_fetch_mode"]` 字段设置,也可以调用时 `DB::rawQuery("语句", [], PDO::FETCH_ASSOC);` 第三个参数可选 +- 🔴 废弃:`ModBase` 基类,基类继承机制将在 1.6 版本起完全删除 + +## v1.5.5 + +> 更新时间:2020.6.13 + +- 修复:`@SwooleEventAt("close")` 下不能使用 `ctx()->getConnection()` 获取链接对象的 bug +- 新增:init 命令,可在 `composer require zhamao/framework` 后使用 `vendor/bin/start init` 初始化项目目录结构和配置文件 +- 更新:默认模块新增机器人断开连接的回调事件 + +## v1.5.4 + +> 更新时间:2020.6.13 + +- 新增:`@CQCommand` 下支持 alias 参数 +- 更新:将 autoload 变为 composer autoload(需要重新 composer update) + +## v1.5.3 + +> 更新时间:2020.6.10 + +- 修复:在 Linux 系统下 Terminal 无法正常使用的 bug + +## v1.5.2 + +> 更新时间:2020.6.8 + +- 新增:`ZM_VERSION` 常量,对应为当前框架版本 +- 修复:部分链接不带 `/` 会导致 ZMRequest 模块报错的 bug + +## v1.5.1 + +> 更新时间:2020.6.5 + +- 新增:ZMRequest::request() 自定义构建 HTTP 请求方法 +- 修复:一个不会导致崩溃的 warning 提示 + +## v1.5 + +> 更新时间:2020.6.5 + +- 重要变更:支持从 composer 使用框架 +- 新增:数据库 Select 选择器支持 `count()` 方法 +- 修复:ZMRequest 中 https 和端口的指定顺序问题 +- 新增:ZMWebSocket 创建 WS 链接的轻量级客户端 +- 修复:数据库异常的捕获更改为 PDOException + +## v1.4 + +> 更新时间:2020.5.23 + +- 新增:自定义 motd +- 新增:debug_mode 下断点调试功能 +- 新增:`@OnSave` 注解,储存自动保存的变量时事件激活 +- 新增:Swoole 版本检测 +- 新增:全局函数,以 `zm_` 开头的,详情见文档 +- 新增:`@LoadBuffer` 注解,只加载内存不自动保存的变量 +- 新增:局部静态文件服务 +- 新增:mysqlnd 扩展状态检测 +- 更新:将终端输入更换为多进程 +- 更新:将数据库连接池变更为 Swoole 官方的连接池,需要 Swoole 版本 >= 4.4.13 +- 更新:提升注解绑定的事件函数的执行效率 +- 修复:上下文 `getConnection()` 的 fd 无法获取的 bug +- 修复:MySQL 长链接 gone away 自动重连的问题 +- 修复:MySQL 查询构造器无 WHERE 语句时会造成的 bug +- 修复:调整各项资源初始化前后顺序 + +不可逆修改:你需要重新执行一次 `composer update` 或重新拉取一次 Docker Image,因为 composer 依赖发生了变化。 + +## v1.3.1 + +> 更新时间:2020.5.10 + +- 修复:DataProvider 下 setJsonData 新建文件夹的问题 +- 优化:默认 / 页面显示 `Hello Zhamao!` 文字 +- 优化:Exception 和 Fatal error 报错机制的改进 +- 修复:计时器没有上下文环境,发不了 API 的 bug + +❗ 下面是框架升级需要手动进行的变更: + +- 更改 MySQL 客户端为原生 PDO mysqlnd,如果之前使用 Docker 启动,则需使用新的 Dockerfile 构建。如果安装在本机,需安装 php-mysql 扩展。本次更新不影响框架内的 API,不需要更改任何代码。 + +## v1.3.0 + +> 更新时间:2020.5.8 + +- 新增:上下文,具体更新都写到了文档里了! +- 修复:ZMRobot 的 `setPrefix()` 的严重错误 +- 优化:优化部分代码 +- 改动:现在你可以和任意事件的注解使用任意中间件啦,而且还支持多中间件 +- 新增:CQHTTP + 酷Q + 炸毛框架 的 Dockerfile +- 新增注解:`@CQAPISend`,`@CQAPIResponse`,是 API 调用后触发的事件,具体见文档说明 + +## v1.2.1 + +> 更新时间:2020.5.2 + +- 新增:phar 启动模式构建脚本,你可以直接拉取 phar 运行框架了! +- 优化:优化部分代码 + +## v1.2 + +> 更新时间:2020.4.29 + +- 新增:systemd 生成脚本、一键 daemonize 守护进程方式常驻后台 +- 新增:示例模块的注释 +- 重构:Console 模块,现在有准确的控制台输出分级功能了 +- 新增:`@OnTick` 注解,用于绑定定时器(毫秒级) +- 新增:`ZMRobot` 类,比调用 `CQAPI` 类发送 API 更方便,同时兼容最新版本的 `CQHTTP` 插件 +- 优化:使用键盘中断 `Ctrl+C`,不会丢失未保存的缓存数据了 +- 优化:完善上下文对象的方法 +- 新增:终端命令:`logtest`,测试输出的 log 类型 + +:exclamation:下面是框架模块开发中需要注意的或有不兼容的修改内容: + +- 修改:`global.php` 中原来的 `info_level` 默认数值需要改为 `2`,保证终端输出和原来一致 + +## v1.1.2 + +> 更新时间:2020.4.26 + +- 新增:静态文件服务器 +- 修复:`/` 路径的 Mapping 无法正常绑定的 bug + +## v1.1.1 + +> 更新时间:2020.4.26 + +- 新增:中间件对类的修饰 +- 新增:上下文对象对 IDE 的支持 +- 修复:数据库插入查询的愚蠢错误 +- 修复:数据库查询的 `value()` 不支持指定参数的 bug + +## v1.1.0 + +> 更新时间:2020.3.29 + +- 新增:中间件 `@Middleware` 功能 +- 修复:Websocket 链接关闭后未自动删除连接对象的bug + +## v1.0.0 + +> 更新时间:2020.3.19 + +正式版发布。 \ No newline at end of file diff --git a/docs/update/v2.md b/docs/update/v2.md new file mode 100644 index 00000000..dfe073bc --- /dev/null +++ b/docs/update/v2.md @@ -0,0 +1,3 @@ +# 更新日志(v2 版本) + +> 暂未发布正式版。 \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..d367b3ee --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,74 @@ +site_name: 炸毛框架 v2 + +repo_name: '炸毛框架' +repo_url: 'https://github.com/zhamao-robot/zhamao-framework' +edit_uri: 'blob/2.0-dev/docs/' + +theme: + name: material + logo: assets/logos.png + favicon: assets/favicon.png + language: zh + features: + - navigation.tabs +extra_javascript: + - https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js + - javascripts/config.js +extra_css: + - assets/css/extra.css + - https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/default.min.css +markdown_extensions: + - admonition + - pymdownx.tabbed + - pymdownx.superfences + - pymdownx.inlinehilite + - pymdownx.snippets + - abbr + - pymdownx.highlight: + linenums: true + linenums_style: pymdownx.inline +extra: + version: + method: mike + +copyright: 'Copyright © 2019 - 2020 CrazyBot Team      + + + + +
蒙ICP备18000198号-1' + +nav: + - 指南: + - 介绍: index.md + - 安装框架: guide/安装.md + - 快速上手(机器人篇): guide/快速上手-机器人.md + - 快速上手(HTTP篇): guide/快速上手-http.md + - 选择聊天机器人实例: guide/OneBot实例.md + - 基本配置: guide/基本配置.md + - 编写模块: guide/编写模块.md + - 注册事件响应: guide/注册事件响应.md + - 事件和注解: + - 事件和注解: event/index.md + - 框架组件: + - 框架组件: component/index.md + - 进阶开发: + - 进阶开发: advanced/index.md + - 从 v1 升级: advanced/to-v2.md + - FAQ: + - FAQ: FAQ.md + - 更新日志: + - 更新日志(v2): update/v2.md + - 更新日志(v1): update/v1.md + - 炸毛框架 v1: https://docs-v1.zhamao.xin/ diff --git a/phar-starter.php b/phar-starter.php deleted file mode 100644 index ba2fc9a0..00000000 --- a/phar-starter.php +++ /dev/null @@ -1,85 +0,0 @@ - 30000, -]); - -date_default_timezone_set("Asia/Shanghai"); - -define('WORKING_DIR', __DIR__); -define('FRAMEWORK_DIR', __DIR__); -define('LOAD_MODE', 2); - -$s = new FrameworkLoader($argv); - -function loadPhp($dir) { - $dirs = scandir($dir); - foreach ($dirs as $v) { - $path = $dir . '/' . $v; - if (is_dir($path)) { - loadPhp($path); - } else { - if (pathinfo($dir . '/' . $v)['extension'] == 'php') { - if(pathinfo($dir . '/' . $v)['basename'] == 'terminal_listener.php') continue; - //echo 'loading '.$path.PHP_EOL; - require_once $path; - } - } - } -} - -function testEnvironment() { - $current_dir = realpath('.'); - @mkdir($current_dir . '/config/'); - if (!is_file($current_dir . '/config/global.php')) { - echo "Exporting default global config...\n"; - $global = file_get_contents(__DIR__ . '/config/global.php'); - $global = str_replace("WORKING_DIR", 'realpath(__DIR__ . "/../")', $global); - file_put_contents($current_dir . '/config/global.php', $global); - } - if (!is_file($current_dir . '/config/file_header.json')) { - echo "Exporting default file_header config...\n"; - $global = file_get_contents(__DIR__ . '/config/file_header.json'); - file_put_contents($current_dir . '/config/file_header.json', $global); - } - if (!is_dir($current_dir . '/resources')) mkdir($current_dir . '/resources'); - if (!is_dir($current_dir . '/src')) mkdir($current_dir . '/src'); - if (!is_dir($current_dir . '/src')) mkdir($current_dir . '/src'); - if (!is_dir($current_dir . '/src/Module')) { - mkdir($current_dir . '/src/Module'); - mkdir($current_dir . '/src/Module/Example'); - file_put_contents($current_dir . '/src/Module/Example/Hello.php', file_get_contents(__DIR__ . '/tmp/Hello.php.bak')); - mkdir($current_dir . '/src/Module/Middleware'); - file_put_contents($current_dir . '/src/Module/Middleware/TimerMiddleware.php', file_get_contents(__DIR__ . '/tmp/TimerMiddleware.php.bak')); - } - if (!is_dir($current_dir . '/src/Custom')) { - mkdir($current_dir . '/src/Custom'); - mkdir($current_dir . '/src/Custom/Annotation'); - mkdir($current_dir . '/src/Custom/Connection'); - file_put_contents($current_dir . '/src/Custom/global_function.php', " + + + + Example page + + +
+ + diff --git a/src/Custom/Annotation/Example.php b/src/Custom/Annotation/Example.php index 8d41fd51..4c255730 100644 --- a/src/Custom/Annotation/Example.php +++ b/src/Custom/Annotation/Example.php @@ -4,6 +4,7 @@ namespace Custom\Annotation; use Doctrine\Common\Annotations\Annotation\Target; +use ZM\Annotation\AnnotationBase; use ZM\Annotation\Interfaces\CustomAnnotation; /** @@ -12,8 +13,8 @@ use ZM\Annotation\Interfaces\CustomAnnotation; * @Target("ALL") * @package Custom\Annotation */ -class Example implements CustomAnnotation +class Example extends AnnotationBase implements CustomAnnotation { /** @var string */ - public $str; -} \ No newline at end of file + public $str = ''; +} diff --git a/src/Custom/Command/CustomCommand.php b/src/Custom/Command/CustomCommand.php new file mode 100644 index 00000000..9fe702f4 --- /dev/null +++ b/src/Custom/Command/CustomCommand.php @@ -0,0 +1,31 @@ +setDescription("custom description | 自定义命令的描述字段"); + $this->addOption("failure", null, null, "以错误码为1返回结果"); + // ... + } + + protected function execute(InputInterface $input, OutputInterface $output) { + if ($input->getOption("failure")) { + $output->writeln("Hello error! I am wrong message."); + return Command::FAILURE; + } else { + $output->writeln("Hello world! I am successful message."); + return Command::SUCCESS; + } + } +} diff --git a/src/Custom/Connection/CustomConnection.php b/src/Custom/Connection/CustomConnection.php deleted file mode 100644 index 87b806cd..00000000 --- a/src/Custom/Connection/CustomConnection.php +++ /dev/null @@ -1,14 +0,0 @@ -get(), [1, 2])) { - $trace = debug_backtrace()[1] ?? ['file' => '', 'function' => '']; - $trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] "; - } - if (!is_string($obj)) { - if (isset($trace)) { - var_dump($obj); - return; - } else $obj = "{Object}"; - } - echo(self::setColor($head . ($trace ?? "") . $obj, "red") . "\n"); - } - - static function warning($obj, $head = null) { - if ($head === null) $head = date("[H:i:s]") . " [W] "; - if (ZMBuf::$info_level !== null && in_array(ZMBuf::$info_level->get(), [1, 2])) { - $trace = debug_backtrace()[1] ?? ['file' => '', 'function' => '']; - $trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] "; - } - if (ZMBuf::$atomics["info_level"]->get() >= 1) { - if (!is_string($obj)) { - if (isset($trace)) { - var_dump($obj); - return; - } else $obj = "{Object}"; - } - echo(self::setColor($head . ($trace ?? "") . $obj, in_array("--white-term", FrameworkLoader::$argv) ? "blue" : "yellow") . "\n"); - } - } - - static function info($obj, $head = null) { - if ($head === null) $head = date("[H:i:s] ") . "[I] "; - if (ZMBuf::$info_level !== null && in_array(ZMBuf::$info_level->get(), [1, 2])) { - $trace = debug_backtrace()[1] ?? ['file' => '', 'function' => '']; - $trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] "; - } - if (ZMBuf::$atomics["info_level"]->get() >= 2) { - if (!is_string($obj)) { - if (isset($trace)) { - var_dump($obj); - return; - } else $obj = "{Object}"; - } - echo(self::setColor($head . ($trace ?? "") . $obj, in_array("--white-term", FrameworkLoader::$argv) ? "black" : "lightblue") . "\n"); - } - } - - static function success($obj, $head = null) { - if ($head === null) $head = date("[H:i:s] ") . "[S] "; - if (ZMBuf::$info_level !== null && in_array(ZMBuf::$info_level->get(), [1, 2])) { - $trace = debug_backtrace()[1] ?? ['file' => '', 'function' => '']; - $trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] "; - } - if (ZMBuf::$atomics["info_level"]->get() >= 2) { - if (!is_string($obj)) { - if (isset($trace)) { - var_dump($obj); - return; - } else $obj = "{Object}"; - } - echo(self::setColor($head . ($trace ?? "") . $obj, "green") . "\n"); - } - } - - static function verbose($obj, $head = null) { - if ($head === null) $head = date("[H:i:s] ") . "[V] "; - if (ZMBuf::$atomics["info_level"]->get() >= 3) { - if (!is_string($obj)) { - if (isset($trace)) { - var_dump($obj); - return; - } else $obj = "{Object}"; - } - echo(self::setColor($head . ($trace ?? "") . $obj, "blue") . "\n"); - } - } - - static function debug($msg) { - if (ZMBuf::$atomics["info_level"]->get() >= 4) Console::log(date("[H:i:s] ") . "[D] " . $msg, 'gray'); - } - - static function log($obj, $color = "") { - if (!is_string($obj)) var_dump($obj); - else echo(self::setColor($obj, $color) . "\n"); - } - - static function stackTrace() { - $log = "Stack trace:\n"; - $trace = debug_backtrace(); - //array_shift($trace); - foreach ($trace as $i => $t) { - if (!isset($t['file'])) { - $t['file'] = 'unknown'; - } - if (!isset($t['line'])) { - $t['line'] = 0; - } - if (!isset($t['function'])) { - $t['function'] = 'unknown'; - } - $log .= "#$i {$t['file']}({$t['line']}): "; - if (isset($t['object']) and is_object($t['object'])) { - $log .= get_class($t['object']) . '->'; - } - $log .= "{$t['function']}()\n"; - } - $log = Console::setColor($log, "gray"); - echo $log; - } - - static function listenConsole() { - if (in_array('--disable-console-input', FrameworkLoader::$argv) || in_array('--debug-mode', FrameworkLoader::$argv)) { - self::info("ConsoleCommand disabled."); - return; - } - global $terminal_id; - global $port; - $port = ZMBuf::globals("port"); - $vss = new SwooleEventAt(); - $vss->type = "open"; - $vss->level = 256; - $vss->rule = "connectType:terminal"; - $terminal_id = call_user_func(function () { - try { - $data = random_bytes(16); - } catch (Exception $e) { - return ""; - } - $data[6] = chr(ord($data[6]) & 0x0f | 0x40); - $data[8] = chr(ord($data[8]) & 0x3f | 0x80); - return strtoupper(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4))); - }); - $vss->callback = function(?WSConnection $conn) use ($terminal_id){ - $req = ctx()->getRequest(); - if($conn->getType() != "terminal") return false; - Console::debug("Terminal fd: ".$conn->fd); - if(($req->header["x-terminal-id"] ?? "") != $terminal_id) { - $conn->close(); - return false; - } - return false; - }; - ZMBuf::$events[SwooleEventAt::class][] = $vss; - $vss2 = new SwooleEventAt(); - $vss2->type = "message"; - $vss2->rule = "connectType:terminal"; - $vss2->callback = function(?WSConnection $conn){ - if ($conn === null) return false; - if($conn->getType() != "terminal") return false; - $cmd = ctx()->getFrame()->data; - self::executeCommand($cmd); - return false; - }; - ZMBuf::$events[SwooleEventAt::class][] = $vss2; - go(function () { - global $terminal_id, $port; - $descriptorspec = array( - 0 => STDIN, - 1 => STDOUT, - 2 => STDERR - ); - self::$console_proc = proc_open('php -r \'$terminal_id = "'.$terminal_id.'";$port = '.$port.';require "'.__DIR__.'/terminal_listener.php";\'', $descriptorspec, $pipes); - }); - } - - /** - * @param string $cmd - * @return bool - */ - private static function executeCommand(string $cmd) { - $it = explodeMsg($cmd); - switch ($it[0] ?? '') { - case 'logtest': - Console::log(date("[H:i:s]") . " [L] This is normal msg. (0)"); - Console::error("This is error msg. (0)"); - Console::warning("This is warning msg. (1)"); - Console::info("This is info msg. (2)"); - Console::success("This is success msg. (2)"); - Console::verbose("This is verbose msg. (3)"); - Console::debug("This is debug msg. (4)"); - return true; - case 'call': - $class_name = $it[1]; - $function_name = $it[2]; - $class = new $class_name([]); - call_user_func_array([$class, $function_name], []); - return true; - case 'bc': - $code = base64_decode($it[1] ?? '', true); - try { - eval($code); - } catch (Exception $e) { - } - return true; - case 'echo': - Console::info($it[1]); - return true; - case 'color': - Console::log($it[2], $it[1]); - return true; - case 'stop': - ZMUtil::stop(); - return false; - case 'reload': - case 'r': - ZMUtil::reload(); - return false; - case 'save': - $origin = ZMBuf::$atomics["info_level"]->get(); - //ZMBuf::$atomics["info_level"]->set(3); - DataProvider::saveBuffer(); - //ZMBuf::$atomics["info_level"]->set($origin); - return true; - case '': - return true; - default: - Console::info("Command not found: " . $cmd); - return true; - } - } - - public static function withSleep(string $string, int $int) { - self::info($string); - sleep($int); - } -} diff --git a/src/Framework/DataProvider.php b/src/Framework/DataProvider.php deleted file mode 100644 index 13de875e..00000000 --- a/src/Framework/DataProvider.php +++ /dev/null @@ -1,78 +0,0 @@ -get() >= 3) - echo $head; - foreach (self::$buffer_list as $k => $v) { - Console::debug("Saving " . $k . " to " . $v); - self::setJsonData($v, ZMBuf::get($k)); - } - foreach (ZMBuf::$events[OnSave::class] ?? [] as $v) { - $c = $v->class; - $method = $v->method; - $class = new $c(); - Console::debug("Calling @OnSave: $c -> $method"); - $class->$method(); - } - if (ZMBuf::$atomics["info_level"]->get() >= 3) - echo Console::setColor("saved", "blue") . PHP_EOL; - } - - public static function getFrameworkLink() { - return ZMBuf::globals("http_reverse_link"); - } - - public static function getJsonData(string $string) { - if (!file_exists(self::getDataConfig() . $string)) return []; - return json_decode(file_get_contents(self::getDataConfig() . $string), true); - } - - public static function setJsonData($filename, array $args) { - $pathinfo = pathinfo($filename); - if (!is_dir(self::getDataConfig() . $pathinfo["dirname"])) { - Console::debug("Making Directory: " . self::getDataConfig() . $pathinfo["dirname"]); - mkdir(self::getDataConfig() . $pathinfo["dirname"]); - } - $r = file_put_contents(self::getDataConfig() . $filename, json_encode($args, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_BIGINT_AS_STRING)); - if ($r === false) { - Console::warning("无法保存文件: " . $filename); - } - } - - public static function getDataFolder() { - return ZM_DATA; - } -} diff --git a/src/Framework/FrameworkLoader.php b/src/Framework/FrameworkLoader.php deleted file mode 100644 index fec6a6f6..00000000 --- a/src/Framework/FrameworkLoader.php +++ /dev/null @@ -1,213 +0,0 @@ -requireGlobalFunctions(); - if (LOAD_MODE == 0) define("WORKING_DIR", getcwd()); - elseif (LOAD_MODE == 1) define("WORKING_DIR", realpath(__DIR__ . "/../../")); - elseif (LOAD_MODE == 2) echo "Phar mode: " . WORKING_DIR . PHP_EOL; - //$this->registerAutoloader('classLoader'); - require_once "DataProvider.php"; - if (file_exists(DataProvider::getWorkingDir() . "/vendor/autoload.php")) { - /** @noinspection PhpIncludeInspection */ - require_once DataProvider::getWorkingDir() . "/vendor/autoload.php"; - } - if (LOAD_MODE == 0) { - echo "* This is repository mode.\n"; - $composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true); - if (!isset($composer["autoload"]["psr-4"]["Module\\"])) { - echo "框架源码模式需要在autoload文件中添加Module目录为自动加载,是否添加?[Y/n] "; - $r = strtolower(trim(fgets(STDIN))); - if ($r === "" || $r === "y") { - $composer["autoload"]["psr-4"]["Module\\"] = "src/Module"; - $composer["autoload"]["psr-4"]["Custom\\"] = "src/Custom"; - $r = file_put_contents(DataProvider::getWorkingDir() . "/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); - if ($r !== false) { - echo "成功添加!请重新进行 composer update !\n"; - exit(1); - } else { - echo "添加失败!请按任意键继续!"; - fgets(STDIN); - exit(1); - } - } else { - exit(1); - } - } - } - if (LOAD_MODE == 2) { - require_once FRAMEWORK_DIR . "/vendor/autoload.php"; - spl_autoload_register('phar_classloader'); - } - - - self::$settings = new GlobalConfig(); - if (self::$settings->get("debug_mode") === true) { - $args[] = "--debug-mode"; - $args[] = "--disable-console-input"; - } - self::$argv = $args; - if (!in_array("--debug-mode", self::$argv)) { - Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); - } - self::$settings = new GlobalConfig(); - ZMBuf::$globals = self::$settings; - if (!self::$settings->success) die("Failed to load global config. Please check config/global.php file"); - $this->defineProperties(); - - //start swoole Framework - $this->selfCheck(); - try { - $this->server = new Server(self::$settings->get("host"), self::$settings->get("port")); - $settings = self::$settings->get("swoole"); - if (in_array("--daemon", $args)) { - $settings["daemonize"] = 1; - Console::log("已启用守护进程,输出重定向到 " . $settings["log_file"]); - self::$argv[] = "--disable-console-input"; - } - $this->server->set($settings); - $all_event_class = self::$settings->get("server_event_handler_class") ?? []; - if (!in_array(ServerEventHandler::class, $all_event_class)) { - $all_event_class[] = ServerEventHandler::class; - } - $event_list = []; - foreach ($all_event_class as $v) { - $reader = new AnnotationReader(); - $reflection_class = new ReflectionClass($v); - $methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC); - foreach ($methods as $vs) { - $method_annotations = $reader->getMethodAnnotations($vs); - if ($method_annotations != []) { - $annotation = $method_annotations[0]; - if ($annotation instanceof OnEvent) { - $annotation->class = $v; - $annotation->method = $vs->getName(); - $event_list[strtolower($annotation->event)] = $annotation; - } - } - } - } - foreach ($event_list as $k => $v) { - $this->server->on($k, function (...$param) use ($v) { - $c = $v->class; - //echo $c.PHP_EOL; - $c = new $c(); - call_user_func_array([$c, $v->method], $param); - }); - } - - ZMBuf::initAtomic(); - if (in_array("--remote-shell", $args)) RemoteShell::listen($this->server, "127.0.0.1"); - if (in_array("--log-error", $args)) ZMBuf::$atomics["info_level"]->set(0); - if (in_array("--log-warning", $args)) ZMBuf::$atomics["info_level"]->set(1); - if (in_array("--log-info", $args)) ZMBuf::$atomics["info_level"]->set(2); - if (in_array("--log-verbose", $args)) ZMBuf::$atomics["info_level"]->set(3); - if (in_array("--log-debug", $args)) ZMBuf::$atomics["info_level"]->set(4); - Console::log( - "host: " . self::$settings->get("host") . - ", port: " . self::$settings->get("port") . - ", log_level: " . ZMBuf::$atomics["info_level"]->get() . - ", version: " . ZM_VERSION . - "\nworking_dir: " . DataProvider::getWorkingDir() - ); - global $motd; - if (!file_exists(DataProvider::getWorkingDir() . "/config/motd.txt")) { - echo $motd; - } else { - echo file_get_contents(DataProvider::getWorkingDir() . "/config/motd.txt"); - } - if (in_array("--debug-mode", self::$argv)) - Console::warning("You are in debug mode, do not use in production!"); - $this->server->start(); - } catch (Exception $e) { - Console::error("Framework初始化出现错误,请检查!"); - Console::error($e->getMessage()); - die; - } - } - - private function requireGlobalFunctions() - { - require_once __DIR__ . '/global_functions.php'; - } - - private function defineProperties() - { - define("ZM_START_TIME", microtime(true)); - define("ZM_DATA", self::$settings->get("zm_data")); - define("ZM_VERSION", json_decode(file_get_contents(__DIR__ . "/../../composer.json"), true)["version"] ?? "unknown"); - define("CONFIG_DIR", self::$settings->get("config_dir")); - define("CRASH_DIR", self::$settings->get("crash_dir")); - @mkdir(ZM_DATA); - @mkdir(CONFIG_DIR); - @mkdir(CRASH_DIR); - define("ZM_MATCH_ALL", 0); - define("ZM_MATCH_FIRST", 1); - define("ZM_MATCH_NUMBER", 2); - define("ZM_MATCH_SECOND", 3); - define("ZM_BREAKPOINT", 'if(in_array("--debug-mode", \Framework\FrameworkLoader::$argv)) extract(\Psy\debug(get_defined_vars(), isset($this) ? $this : @get_called_class()));'); - define("BP", ZM_BREAKPOINT); - define("ZM_DEFAULT_FETCH_MODE", self::$settings->get("sql_config")["sql_default_fetch_mode"] ?? 4); - } - - private function selfCheck() - { - if (!extension_loaded("swoole")) die("Can not find swoole extension.\n"); - if (version_compare(SWOOLE_VERSION, "4.4.13") == -1) die("You must install swoole version >= 4.4.13 !"); - //if (!extension_loaded("gd")) die("Can not find gd extension.\n"); - //if (!extension_loaded("sockets")) die("Can not find sockets extension.\n"); - if (!function_exists("ctype_alpha")) die("Can not find ctype extension.\n"); - if (!function_exists("mb_substr")) die("Can not find mbstring extension.\n"); - if (substr(PHP_VERSION, 0, 1) < "7") die("PHP >=7 required.\n"); - //if (!function_exists("curl_exec")) die("Can not find curl extension.\n"); - //if (!class_exists("ZipArchive")) die("Can not find Zip extension.\n"); - //if (!file_exists(CRASH_DIR . "last_error.log")) die("Can not find log file.\n"); - return true; - } -} - -global $motd; -$motd = <<success = true; - $this->config = $config; - } - - public function get($key) { - $r = $this->config[$key] ?? null; - if ($r === null) return null; - return $r; - } - - public function getAll() { - return $this->config; - } -} diff --git a/src/Framework/RemoteShell.php b/src/Framework/RemoteShell.php deleted file mode 100644 index 0e718824..00000000 --- a/src/Framework/RemoteShell.php +++ /dev/null @@ -1,264 +0,0 @@ -listen($host, $port, SWOOLE_SOCK_TCP); - if (!$port) { - throw new Exception("listen fail."); - } - $port->set(array( - "open_eof_split" => true, - 'package_eof' => "\r\n", - )); - $port->on("Connect", array(__CLASS__, 'onConnect')); - $port->on("Close", array(__CLASS__, 'onClose')); - $port->on("Receive", array(__CLASS__, 'onReceive')); - if (method_exists($serv, 'getCallback')) { - self::$oriPipeMessageCallback = $serv->getCallback('PipeMessage'); - } - $serv->on("PipeMessage", array(__CLASS__, 'onPipeMessage')); - self::$serv = $serv; - } - - static function onConnect($serv, $fd, $reactor_id) { - self::$contexts[$fd]['worker_id'] = $serv->worker_id; - self::output($fd, implode("\r\n", self::$menu)); - } - - static function output($fd, $msg) { - if (!isset(self::$contexts[$fd]['worker_id'])) { - $msg .= "\r\nworker#" . self::$serv->worker_id . "$ "; - } else { - $msg .= "\r\nworker#" . self::$contexts[$fd]['worker_id'] . "$ "; - } - self::$serv->send($fd, $msg); - } - - static function onClose($serv, $fd, $reactor_id) { - unset(self::$contexts[$fd]); - } - - static function onPipeMessage($serv, $src_worker_id, $message) { - //不是 debug 消息 - if (!is_string($message) or substr($message, 0, strlen(self::STX)) != self::STX) { - if (self::$oriPipeMessageCallback == null) { - trigger_error("require swoole-4.3.0 or later.", E_USER_WARNING); - return true; - } - return call_user_func(self::$oriPipeMessageCallback, $serv, $src_worker_id, $message); - } else { - $request = unserialize(substr($message, strlen(self::STX))); - self::call($request['fd'], $request['func'], $request['args']); - } - return true ; - } - - static protected function call($fd, $func, $args) { - ob_start(); - call_user_func_array($func, $args); - self::output($fd, ob_get_clean()); - } - - static protected function exec($fd, $func, $args) { - //不在当前Worker进程 - if (self::$contexts[$fd]['worker_id'] != self::$serv->worker_id) { - self::$serv->sendMessage(self::STX . serialize(['fd' => $fd, 'func' => $func, 'args' => $args]), self::$contexts[$fd]['worker_id']); - } else { - self::call($fd, $func, $args); - } - } - - static function getCoros() { - var_export(iterator_to_array(Coroutine::listCoroutines())); - } - - static function getBackTrace($_cid) { - $info = Co::getBackTrace($_cid); - if (!$info) { - echo "coroutine $_cid not found."; - } else { - echo get_debug_print_backtrace($info); - } - } - - static function printVariant($var) { - $var = ltrim($var, '$ '); - var_dump($var); - var_dump($$var); - } - - static function evalCode($code) { - eval($code . ';'); - } - - /** - * @param $serv server - * @param $fd - * @param $reactor_id - * @param $data - */ - static function onReceive($serv, $fd, $reactor_id, $data) { - $args = explode(" ", $data, 2); - $cmd = trim($args[0]); - unset($args[0]); - switch ($cmd) { - case 'w': - case 'worker': - if (!isset($args[1])) { - self::output($fd, "invalid command."); - break; - } - $dstWorkerId = intval($args[1]); - self::$contexts[$fd]['worker_id'] = $dstWorkerId; - self::output($fd, "[switching to worker " . self::$contexts[$fd]['worker_id'] . "]"); - break; - case 'e': - case 'exec': - if (!isset($args[1])) { - self::output($fd, "invalid command."); - break; - } - $var = trim($args[1]); - self::exec($fd, 'self::evalCode', [$var]); - break; - case 'p': - case 'print': - $var = trim($args[1]); - self::exec($fd, 'self::printVariant', [$var]); - break; - case 'h': - case 'help': - self::output($fd, implode("\r\n", self::$menu)); - break; - case 's': - case 'stats': - $stats = $serv->stats(); - self::output($fd, var_export($stats, true)); - break; - case 'c': - case 'coros': - self::exec($fd, 'self::getCoros', []); - break; - /** - * 查看协程堆栈 - */ - case 'bt': - case 'b': - case 'backtrace': - if (empty($args[1])) { - self::output($fd, "invalid command [" . trim($args[1]) . "]."); - break; - } - $_cid = intval($args[1]); - self::exec($fd, 'self::getBackTrace', [$_cid]); - break; - case 'i': - case 'info': - if (empty($args[1])) { - self::output($fd, "invalid command [" . trim($args[1]) . "]."); - break; - } - $_fd = intval($args[1]); - $info = $serv->getClientInfo($_fd); - if (!$info) { - self::output($fd, "connection $_fd not found."); - } else { - self::output($fd, var_export($info, true)); - } - break; - case 'l': - case 'list': - $tmp = array(); - foreach ($serv->connections as $fd) { - $tmp[] = $fd; - if (count($tmp) > self::PAGESIZE) { - self::output($fd, json_encode($tmp)); - $tmp = array(); - } - } - if (count($tmp) > 0) { - self::output($fd, json_encode($tmp)); - } - break; - case 'q': - case 'quit': - $serv->close($fd); - break; - default: - self::output($fd, "unknow command[$cmd]"); - break; - } - } -} - -function get_debug_print_backtrace($traces) { - $ret = array(); - foreach ($traces as $i => $call) { - $object = ''; - if (isset($call['class'])) { - $object = $call['class'] . $call['type']; - if (is_array($call['args'])) { - foreach ($call['args'] as &$arg) { - get_arg($arg); - } - } - } - $ret[] = '#' . str_pad($i, 3, ' ') - . $object . $call['function'] . '(' . implode(', ', $call['args']) - . ') called at [' . $call['file'] . ':' . $call['line'] . ']'; - } - return implode("\n", $ret); -} - -function get_arg(&$arg) { - if (is_object($arg)) { - $arr = (array)$arg; - $args = array(); - foreach ($arr as $key => $value) { - if (strpos($key, chr(0)) !== false) { - $key = ''; // Private variable found - } - $args[] = '[' . $key . '] => ' . get_arg($value); - } - $arg = get_class($arg) . ' Object (' . implode(',', $args) . ')'; - } -} \ No newline at end of file diff --git a/src/Framework/ServerEventHandler.php b/src/Framework/ServerEventHandler.php deleted file mode 100644 index b4cd762b..00000000 --- a/src/Framework/ServerEventHandler.php +++ /dev/null @@ -1,84 +0,0 @@ -taskworker === false) { - FrameworkLoader::$run_time = microtime(true); - EventHandler::callSwooleEvent("WorkerStart", $server, $worker_id); - } else { - ob_start(); - AnnotationParser::registerMods(); - //加载Custom目录下的自定义的内部类 - ConnectionManager::registerCustomClass(); - ob_get_clean(); - } - } - - /** - * @OnEvent("message") - * @param $server - * @param Frame $frame - * @throws AnnotationException - */ - public function onMessage($server, Frame $frame) { - Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd); - EventHandler::callSwooleEvent("message", $server, $frame); - } - - /** - * @OnEvent("request") - * @param $request - * @param $response - * @throws AnnotationException - */ - public function onRequest($request, $response) { - $response = new Response($response); - Console::debug("Receiving Http request event, cid=" . Co::getCid()); - EventHandler::callSwooleEvent("request", $request, $response); - } - - /** - * @OnEvent("open") - * @param $server - * @param Request $request - * @throws AnnotationException - */ - public function onOpen($server, Request $request) { - Console::debug("Calling Swoole \"open\" event from fd=" . $request->fd); - EventHandler::callSwooleEvent("open", $server, $request); - } - - /** - * @OnEvent("close") - * @param $server - * @param $fd - * @throws AnnotationException - */ - public function onClose($server, $fd) { - Console::debug("Calling Swoole \"close\" event from fd=" . $fd); - EventHandler::callSwooleEvent("close", $server, $fd); - } -} diff --git a/src/Framework/ZMBuf.php b/src/Framework/ZMBuf.php deleted file mode 100755 index 238c0d3a..00000000 --- a/src/Framework/ZMBuf.php +++ /dev/null @@ -1,121 +0,0 @@ -get($key); - } - - static function config($config_name) { - return self::$config[$config_name] ?? null; - } - - public static function resetCache() { - self::$cache = []; - self::$connect = []; - self::$time_nlp = null; - self::$instance = []; - } - - /** - * 初始化atomic计数器 - */ - public static function initAtomic() { - foreach (ZMBuf::globals("init_atomics") as $k => $v) { - self::$atomics[$k] = new Atomic($v); - } - } -} diff --git a/src/Framework/terminal_listener.php b/src/Framework/terminal_listener.php deleted file mode 100644 index 24f72ac8..00000000 --- a/src/Framework/terminal_listener.php +++ /dev/null @@ -1,33 +0,0 @@ -set(['websocket_mask' => true]); - $client->setHeaders(["x-terminal-id" => $terminal_id, 'x-pid' => posix_getppid()]); - $ret = $client->upgrade("/?type=terminal"); - if ($ret) { - while (true) { - $line = fgets(STDIN); - if ($line !== false) { - $r = $client->push(trim($line)); - if (trim($line) == "reload" || trim($line) == "r" || trim($line) == "stop") { - break; - } - if($r === false) { - echo "Unable to connect framework terminal, connection closed. Trying to reconnect after 5s.\n"; - sleep(5); - goto hello; - } - } else { - break; - } - } - } else { - echo "Unable to connect framework terminal. port: $port\n"; - } -}); - diff --git a/src/Module/Example/Hello.php b/src/Module/Example/Hello.php index b87b350c..858ffe88 100644 --- a/src/Module/Example/Hello.php +++ b/src/Module/Example/Hello.php @@ -1,44 +1,66 @@ getQQ() . " 已连接!"); + public function testCase() { + $a = new ZMRedis(); + $redis = $a->get(); + $r1 = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想设置的操作[r/w]"); + switch ($r1) { + case "r": + $k = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想读取的键名"); + $result = $redis->get($k); + ctx()->reply("结果:" . $result); + break; + case "w": + $k = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想写入的键名"); + $v = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想写入的字符串"); + $result = $redis->set($k, $v); + ctx()->reply("结果:" . ($result ? "成功" : "失败")); + break; + } } /** - * 在机器人连接后向终端输出信息 - * @SwooleEventAt("close",rule="connectType:qq") + * 使用命令 .reload 发给机器人远程重载,注意将 user_id 换成你自己的 QQ + * @CQCommand(".reload",user_id=627577391) */ - public function onDisconnect() { - $conn = ctx()->getConnection(); - Console::info("机器人 " . $conn->getQQ() . " 已断开连接!"); + public function reload() { + ctx()->reply("重启中..."); + ZMUtil::reload(); } /** - * 向机器人发送"你好",即可回复这句话 + * @CQCommand("我是谁") + */ + public function whoami() { + $user = ctx()->getRobot()->getLoginInfo(); + return "你是" . $user["data"]["nickname"] . ",QQ号是" . $user["data"]["user_id"]; + } + + /** + * 向机器人发送"你好啊",也可回复这句话 * @CQCommand(match="你好",alias={"你好啊","你是谁"}) */ public function hello() { @@ -46,27 +68,22 @@ class Hello } /** - * @CQCommand(".reload") - */ - public function reload() { - context()->reply("reloading..."); - ZMUtil::reload(); - } - - /** + * 一个简单随机数的功能demo + * 问法1:随机数 1 20 + * 问法2:从1到20的随机数 * @CQCommand("随机数") - * @CQCommand(regexMatch="*从*到*的随机数") - * @param $arg + * @CQCommand(pattern="*从*到*的随机数") + * @return string */ - public function randNum($arg) { + public function randNum() { // 获取第一个数字类型的参数 - $num1 = context()->getArgs($arg, ZM_MATCH_NUMBER, "请输入第一个数字"); + $num1 = ctx()->getArgs(ZM_MATCH_NUMBER, "请输入第一个数字"); // 获取第二个数字类型的参数 - $num2 = context()->getArgs($arg, ZM_MATCH_NUMBER, "请输入第二个数字"); + $num2 = ctx()->getArgs(ZM_MATCH_NUMBER, "请输入第二个数字"); $a = min(intval($num1), intval($num2)); $b = max(intval($num1), intval($num2)); // 回复用户结果 - context()->reply("随机数是:".mt_rand($a, $b)); + return "随机数是:" . mt_rand($a, $b); } /** @@ -87,13 +104,48 @@ class Hello return "Hello Zhamao!"; } + /** + * 使用自定义参数的路由参数 + * @RequestMapping("/whoami/{name}") + * @param $param + * @return string + */ + public function paramGet($param) { + return "Your name: {$param["name"]}"; + } + + /** + * 在机器人连接后向终端输出信息 + * @OnSwooleEvent("open",rule="connectIsQQ()") + * @param $conn + */ + public function onConnect(ConnectionObject $conn) { + Console::info("机器人 " . $conn->getOption("connect_id") . " 已连接!"); + } + + /** + * 在机器人断开连接后向终端输出信息 + * @OnSwooleEvent("close",rule="connectIsQQ()") + * @param ConnectionObject $conn + */ + public function onDisconnect(ConnectionObject $conn) { + Console::info("机器人 " . $conn->getOption("connect_id") . " 已断开连接!"); + } + + /** + * 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰 + * @OnSwooleEvent("request",rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200) + */ + public function onRequest() { + EventDispatcher::interrupt(); + } /** * 框架会默认关闭未知的WebSocket链接,因为这个绑定的事件,你可以根据你自己的需求进行修改 - * @SwooleEventAt(type="open",rule="connectType:unknown") + * @OnSwooleEvent(type="open",rule="connectIsDefault()") */ public function closeUnknownConn() { Console::info("Unknown connection , I will close it."); - context()->getConnection()->close(); + server()->close(ctx()->getConnection()->getFd()); } } diff --git a/src/Module/Middleware/TimerMiddleware.php b/src/Module/Middleware/TimerMiddleware.php index e78e8b67..d90d2570 100644 --- a/src/Module/Middleware/TimerMiddleware.php +++ b/src/Module/Middleware/TimerMiddleware.php @@ -2,24 +2,25 @@ namespace Module\Middleware; -use Framework\Console; -use ZM\Annotation\Http\After; -use ZM\Annotation\Http\Before; +use ZM\Annotation\Http\HandleAfter; +use ZM\Annotation\Http\HandleBefore; +use ZM\Annotation\Http\HandleException; use ZM\Annotation\Http\MiddlewareClass; +use ZM\Console\Console; use ZM\Http\MiddlewareInterface; /** - * Class AuthMiddleware + * Class TimerMiddleware * 示例中间件:用于统计路由函数运行时间用的 * @package Module\Middleware - * @MiddlewareClass() + * @MiddlewareClass("timer") */ class TimerMiddleware implements MiddlewareInterface { private $starttime; /** - * @Before() + * @HandleBefore() * @return bool */ public function onBefore() { @@ -28,11 +29,16 @@ class TimerMiddleware implements MiddlewareInterface } /** - * @After() + * @HandleAfter() */ public function onAfter() { Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms."); } - public function getName() { return "timer"; } + /** + * @HandleException(\Exception::class) + */ + public function onException() { + Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms but an Exception occurred."); + } } diff --git a/src/Scheduler/MessageEvent.php b/src/Scheduler/MessageEvent.php deleted file mode 100644 index b3e67f6e..00000000 --- a/src/Scheduler/MessageEvent.php +++ /dev/null @@ -1,29 +0,0 @@ -client = $client; - $this->frame = $frame; - } - - public function onActivate() { - //TODO: 写Scheduler计时器内的处理逻辑 - } -} \ No newline at end of file diff --git a/src/Scheduler/Scheduler.php b/src/Scheduler/Scheduler.php deleted file mode 100644 index 3d8a1deb..00000000 --- a/src/Scheduler/Scheduler.php +++ /dev/null @@ -1,140 +0,0 @@ -initProcess(); - elseif ($method == self::REMOTE) $this->initRemote(); - } - - private function initProcess() { //TODO: 完成Process模式的代码 - $m_pid = posix_getpid(); - $this->process = new Process(function (Process $worker) use ($m_pid) { self::onWork($worker, $m_pid); }, false, 2, true); - $this->pid = $this->process->start(); - while (1) { - $ret = Process::wait(); - if ($ret) { - $this->process = new Process(function (Process $worker) use ($m_pid) { self::onWork($worker, $m_pid); }, false, 2, true); - $this->pid = $this->process->start(); - echo "Reboot done.\n"; - } - } - } - - private function initRemote() { - define('WORKING_DIR', __DIR__ . '../..'); - $this->requireGlobalFunctions(); - $this->registerAutoloader('classLoader'); - $this->settings = new GlobalConfig(); - if (!$this->settings->success) die("Failed to load global config. Please check config/global.php file"); - $this->defineProperties(); - - //start swoole Framework - $this->selfCheck(); - try { - $host = $this->settings->get("scheduler")["host"]; - $port = $this->settings->get("scheduler")["port"]; - $token = $this->settings->get("scheduler")["token"]; - $this->client = new Client($host, $port); - $path = "/" . ($token != "" ? ("?token=" . urlencode($token)) : ""); - while (true) { - if ($this->client->upgrade($path)) { - while (true) { - $recv = $this->client->recv(); - if ($recv instanceof Frame) { - (new MessageEvent($this->client, $recv))->onActivate(); - } else { - break; - } - } - } else { - Console::warning("无法连接Framework,将在5秒后重连..."); - Coroutine::sleep(5); - } - } - } catch (Exception $e) { - Console::error($e); - } - } - - private function requireGlobalFunctions() { - /** @noinspection PhpIncludeInspection */ - require WORKING_DIR . '/src/Framework/global_functions.php'; - } - - private function registerAutoloader(string $string) { - if (!spl_autoload_register($string)) die("Failed to register autoloader named \"$string\" !"); - } - - private function defineProperties() { - define("ZM_START_TIME", microtime(true)); - define("ZM_DATA", $this->settings->get("zm_data")); - //define("CONFIG_DIR", $this->settings->get("config_dir")); - define("CRASH_DIR", $this->settings->get("crash_dir")); - } - - private function selfCheck() { - if (!extension_loaded("swoole")) die("Can not find swoole extension.\n"); - if (!extension_loaded("sockets")) die("Can not find sockets extension.\n"); - if (!function_exists("mb_substr")) die("Can not find mbstring extension.\n"); - if (substr(PHP_VERSION, 0, 1) != "7") die("PHP >=7 required.\n"); - //if (!class_exists("ZipArchive")) die("Can not find Zip extension.\n"); - if (!file_exists(CRASH_DIR . "last_error.log")) die("Can not find log file.\n"); - return true; - } - - private static function onWork(Process $worker, $m_pid) { - swoole_set_process_name('php-scheduler'); - for ($j = 0; $j < 16000; $j++) { - self::checkMpid($worker, $m_pid); - echo "msg: {$j}\n"; - sleep(1); - } - } - - private static function checkMpid(Process $worker, $m_pid) { - if (!Process::kill($m_pid, 0)) { - $worker->exit(); //主进程死了我也死 - // 这句提示,实际是看不到的.需要写到日志中 - echo "Master process exited, I [{$worker['pid']}] also quit\n"; - } - } -} \ No newline at end of file diff --git a/src/ZM/API/CQ.php b/src/ZM/API/CQ.php index d8e172d4..61191446 100644 --- a/src/ZM/API/CQ.php +++ b/src/ZM/API/CQ.php @@ -4,8 +4,7 @@ namespace ZM\API; -use Framework\Console; -use ZM\Utils\ZMUtil; +use ZM\Console\Console; class CQ { diff --git a/src/ZM/API/CQAPI.php b/src/ZM/API/CQAPI.php index b715a595..b4b35baa 100644 --- a/src/ZM/API/CQAPI.php +++ b/src/ZM/API/CQAPI.php @@ -3,239 +3,52 @@ namespace ZM\API; - use Co; -use Framework\Console; -use Framework\ZMBuf; -use ZM\Connection\ConnectionManager; -use ZM\Connection\CQConnection; -use ZM\Event\EventHandler; -use ZM\Utils\ZMRobot; +use ZM\ConnectionManager\ConnectionObject; +use ZM\Console\Console; +use ZM\Store\LightCacheInside; +use ZM\Store\Lock\SpinLock; +use ZM\Store\ZMAtomic; -/** - * @method static send_private_msg($self_id, $params, $function = null) - * @method static send_group_msg($self_id, $params, $function = null) - * @method static send_discuss_msg($self_id, $params, $function = null) - * @method static send_msg($self_id, $params, $function = null) - * @method static delete_msg($self_id, $params, $function = null) - * @method static send_like($self_id, $params, $function = null) - * @method static set_group_kick($self_id, $params, $function = null) - * @method static set_group_ban($self_id, $params, $function = null) - * @method static set_group_anonymous_ban($self_id, $params, $function = null) - * @method static set_group_whole_ban($self_id, $params, $function = null) - * @method static set_group_admin($self_id, $params, $function = null) - * @method static set_group_anonymous($self_id, $params, $function = null) - * @method static set_group_card($self_id, $params, $function = null) - * @method static set_group_leave($self_id, $params, $function = null) - * @method static set_group_special_title($self_id, $params, $function = null) - * @method static set_discuss_leave($self_id, $params, $function = null) - * @method static set_friend_add_request($self_id, $params, $function = null) - * @method static set_group_add_request($self_id, $params, $function = null) - * @method static get_login_info($self_id, $params, $function = null) - * @method static get_stranger_info($self_id, $params, $function = null) - * @method static get_group_list($self_id, $params, $function = null) - * @method static get_group_member_info($self_id, $params, $function = null) - * @method static get_group_member_list($self_id, $params, $function = null) - * @method static get_cookies($self_id, $params, $function = null) - * @method static get_csrf_token($self_id, $params, $function = null) - * @method static get_credentials($self_id, $params, $function = null) - * @method static get_record($self_id, $params, $function = null) - * @method static get_status($self_id, $params, $function = null) - * @method static get_version_info($self_id, $params, $function = null) - * @method static set_restart($self_id, $params, $function = null) - * @method static set_restart_plugin($self_id, $params, $function = null) - * @method static clean_data_dir($self_id, $params, $function = null) - * @method static clean_plugin_log($self_id, $params, $function = null) - * @method static _get_friend_list($self_id, $params, $function = null) - * @method static _get_group_info($self_id, $params, $function = null) - * @method static _get_vip_info($self_id, $params, $function = null) - * @method static send_private_msg_async($self_id, $params, $function = null) - * @method static send_group_msg_async($self_id, $params, $function = null) - * @method static send_discuss_msg_async($self_id, $params, $function = null) - * @method static send_msg_async($self_id, $params, $function = null) - * @method static delete_msg_async($self_id, $params, $function = null) - * @method static set_group_kick_async($self_id, $params, $function = null) - * @method static set_group_ban_async($self_id, $params, $function = null) - * @method static set_group_anonymous_ban_async($self_id, $params, $function = null) - * @method static set_group_whole_ban_async($self_id, $params, $function = null) - * @method static set_group_admin_async($self_id, $params, $function = null) - * @method static set_group_anonymous_async($self_id, $params, $function = null) - * @method static set_group_card_async($self_id, $params, $function = null) - * @method static set_group_leave_async($self_id, $params, $function = null) - * @method static set_group_special_title_async($self_id, $params, $function = null) - * @method static set_discuss_leave_async($self_id, $params, $function = null) - * @method static set_friend_add_request_async($self_id, $params, $function = null) - * @method static set_group_add_request_async($self_id, $params, $function = null) - */ -class CQAPI +trait CQAPI { - public static function quick_reply(CQConnection $conn, $data, $msg, $yield = null) { - switch ($data["message_type"]) { - case "group": - return (new ZMRobot($conn))->setCallback($yield)->sendGroupMsg($data["group_id"], $msg); - case "private": - return (new ZMRobot($conn))->setCallback($yield)->sendPrivateMsg($data["user_id"], $msg); - case "discuss": - return (new ZMRobot($conn))->setCallback($yield)->sendDiscussMsg($data["discuss_id"], $msg); - } - return null; - } - /** - * @param $name - * @param $arg - * @return bool - * @deprecated - */ - public static function __callStatic($name, $arg) { - trigger_error("This dynamic CQAPI calling method will be removed after 2.0 version.", E_USER_DEPRECATED); - $all = self::getSupportedAPIs(); - $find = null; - if (in_array($name, $all)) $find = $name; - else { - foreach ($all as $v) { - if (strtolower($name) == strtolower(str_replace("_", "", $v))) { - $find = $v; - break; - } - } - } - if ($find === null) { - Console::warning("Unknown API " . $name); - return false; - } - $reply = ["action" => $find]; - if (!is_array($arg[1])) { - Console::warning("Error when parsing params. Please make sure your params is an array."); - return false; - } - if ($arg[1] != []) { - $reply["params"] = $arg[1]; - } - if (!($arg[0] instanceof CQConnection)) { - $robot = ConnectionManager::getByType("qq", ["self_id" => $arg[0]]); - if ($robot == []) { - Console::warning("发送错误,机器人连接不存在!"); - return false; - } - $arg[0] = $robot[0]; - } - return self::processAPI($arg[0], $reply, $arg[2] ?? null); - } - - /********************** non-API Part **********************/ - - private static function getSupportedAPIs() { - return [ - "send_private_msg", - "send_group_msg", - "send_discuss_msg", - "send_msg", - "delete_msg", - "send_like", - "set_group_kick", - "set_group_ban", - "set_group_anonymous_ban", - "set_group_whole_ban", - "set_group_admin", - "set_group_anonymous", - "set_group_card", - "set_group_leave", - "set_group_special_title", - "set_discuss_leave", - "set_friend_add_request", - "set_group_add_request", - "get_login_info", - "get_stranger_info", - "get_group_list", - "get_group_member_info", - "get_group_member_list", - "get_cookies", - "get_csrf_token", - "get_credentials", - "get_record", - "get_status", - "get_version_info", - "set_restart", - "set_restart_plugin", - "clean_data_dir", - "clean_plugin_log", - "_get_friend_list", - "_get_group_info", - "_get_vip_info", - //异步API - "send_private_msg_async", - "send_group_msg_async", - "send_discuss_msg_async", - "send_msg_async", - "delete_msg_async", - "set_group_kick_async", - "set_group_ban_async", - "set_group_anonymous_ban_async", - "set_group_whole_ban_async", - "set_group_admin_async", - "set_group_anonymous_async", - "set_group_card_async", - "set_group_leave_async", - "set_group_special_title_async", - "set_discuss_leave_async", - "set_friend_add_request_async", - "set_group_add_request_async" - ]; - } - - public static function getLoggedAPIs() { - return [ - "send_private_msg", - "send_group_msg", - "send_discuss_msg", - "send_msg", - "send_private_msg_async", - "send_group_msg_async", - "send_discuss_msg_async", - "send_msg_async" - ]; - } - - /** - * @param CQConnection $connection + * @param ConnectionObject $connection * @param $reply * @param |null $function * @return bool|array */ - public static function processAPI($connection, $reply, $function = null) { - $api_id = ZMBuf::$atomics["wait_msg_id"]->get(); + private function processAPI($connection, $reply, $function = null) { + if ($connection->getOption("type") === CONN_WEBSOCKET) + return $this->processWebsocketAPI($connection, $reply, $function); + else + return $this->processHttpAPI($connection, $reply, $function); + + + } + + public function processWebsocketAPI($connection, $reply, $function = false) { + $api_id = ZMAtomic::get("wait_msg_id")->add(1); $reply["echo"] = $api_id; - ZMBuf::$atomics["wait_msg_id"]->add(1); - EventHandler::callCQAPISend($reply, $connection); - if (is_callable($function)) { - ZMBuf::appendKey("sent_api", $api_id, [ - "data" => $reply, - "time" => microtime(true), - "func" => $function, - "self_id" => $connection->getQQ() - ]); - } elseif ($function === true) { - ZMBuf::appendKey("sent_api", $api_id, [ - "data" => $reply, - "time" => microtime(true), - "coroutine" => Co::getuid(), - "self_id" => $connection->getQQ() - ]); - } else { - ZMBuf::appendKey("sent_api", $api_id, [ - "data" => $reply, - "time" => microtime(true), - "self_id" => $connection->getQQ() - ]); - } - if ($connection->push(json_encode($reply))) { - //Console::msg($reply, $connection->getQQ()); - ZMBuf::$atomics["out_count"]->add(1); + SpinLock::lock("wait_api"); + $r = LightCacheInside::get("wait_api", "wait_api"); + $r[$api_id] = [ + "data" => $reply, + "time" => microtime(true), + "self_id" => $connection->getOption("connect_id") + ]; + if ($function === true) $r[$api_id]["coroutine"] = Co::getuid(); + LightCacheInside::set("wait_api", "wait_api", $r); + SpinLock::unlock("wait_api"); + if (server()->push($connection->getFd(), json_encode($reply))) { if ($function === true) { Co::suspend(); - $data = ZMBuf::get("sent_api")[$api_id]; - ZMBuf::unsetByValue("sent_api", $reply["echo"]); + SpinLock::lock("wait_api"); + $r = LightCacheInside::get("wait_api", "wait_api"); + $data = $r[$api_id]; + unset($r[$api_id]); + LightCacheInside::set("wait_api", "wait_api", $r); + SpinLock::unlock("wait_api"); return isset($data['result']) ? $data['result'] : null; } return true; @@ -245,14 +58,30 @@ class CQAPI "status" => "failed", "retcode" => -1000, "data" => null, - "self_id" => $connection->getQQ() + "self_id" => $connection->getOption("connect_id") ]; - $s = ZMBuf::get("sent_api")[$reply["echo"]]; - if (($s["func"] ?? null) !== null) - call_user_func($s["func"], $response, $reply); - ZMBuf::unsetByValue("sent_api", $reply["echo"]); + SpinLock::lock("wait_api"); + $r = LightCacheInside::get("wait_api", "wait_api"); + unset($r[$reply["echo"]]); + LightCacheInside::set("wait_api", "wait_api", $r); + SpinLock::unlock("wait_api"); if ($function === true) return $response; return false; } } + + /** + * @param $connection + * @param $reply + * @param null $function + * @return bool + * @noinspection PhpUnusedParameterInspection + */ + public function processHttpAPI($connection, $reply, $function = null) { + return false; + } + + public function __call($name, $arguments) { + return false; + } } diff --git a/src/ZM/API/ZMRobot.php b/src/ZM/API/ZMRobot.php new file mode 100644 index 00000000..45230948 --- /dev/null +++ b/src/ZM/API/ZMRobot.php @@ -0,0 +1,689 @@ +getOption('connect_id') == $robot_id) return new ZMRobot($v); + } + throw new RobotNotFoundException("机器人 " . $robot_id . " 未连接到框架!"); + } + + /** + * @return ZMRobot + * @throws RobotNotFoundException + */ + public static function getRandom() { + $r = ManagerGM::getAllByName('qq'); + if ($r == []) throw new RobotNotFoundException("没有任何机器人连接到框架!"); + return new ZMRobot($r[array_rand($r)]); + } + + public static function getFirst() { + } + + /** + * @return ZMRobot[] + */ + public static function getAllRobot() { + $r = ManagerGM::getAllByName('qq'); + $obj = []; + foreach($r as $v) { + $obj[] = new ZMRobot($v); + } + return $obj; + } + + public function __construct(ConnectionObject $connection) { + $this->connection = $connection; + } + + public function setCallback($callback = true) { + $this->callback = $callback; + return $this; + } + + public function setPrefix($prefix = self::API_NORMAL) { + $this->prefix = $prefix; + return $this; + } + + public function getSelfId() { + return $this->connection->getOption('connect_id'); + } + + /* 下面是 OneBot 标准的 V11 公开 API */ + + /** + * 发送私聊消息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF + * @param $user_id + * @param $message + * @param bool $auto_escape + * @return array|bool|null + */ + public function sendPrivateMsg($user_id, $message, $auto_escape = false) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'user_id' => $user_id, + 'message' => $message, + 'auto_escape' => $auto_escape + ] + ], $this->callback); + } + + /** + * 发送群消息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF + * @param $group_id + * @param $message + * @param bool $auto_escape + * @return array|bool|null + */ + public function sendGroupMsg($group_id, $message, $auto_escape = false) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'message' => $message, + 'auto_escape' => $auto_escape + ] + ], $this->callback); + } + + /** + * 发送消息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_msg-%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF + * @param $message_type + * @param $target_id + * @param $message + * @param bool $auto_escape + * @return array|bool|null + */ + public function sendMsg($message_type, $target_id, $message, $auto_escape = false) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'message_type' => $message_type, + ($message_type == 'private' ? 'user' : $message_type) . '_id' => $target_id, + 'message' => $message, + 'auto_escape' => $auto_escape + ] + ], $this->callback); + } + + /** + * 撤回消息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#delete_msg-%E6%92%A4%E5%9B%9E%E6%B6%88%E6%81%AF + * @param $message_id + * @return array|bool|null + */ + public function deleteMsg($message_id) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'message_id' => $message_id + ] + ], $this->callback); + } + + /** + * 获取消息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_msg-%E8%8E%B7%E5%8F%96%E6%B6%88%E6%81%AF + * @param $message_id + * @return array|bool|null + */ + public function getMsg($message_id) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'message_id' => $message_id + ] + ], $this->callback); + } + + /** + * 获取合并转发消息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_forward_msg-%E8%8E%B7%E5%8F%96%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E6%B6%88%E6%81%AF + * @param $id + * @return array|bool|null + */ + public function getForwardMsg($id) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'id' => $id + ] + ], $this->callback); + } + + /** + * 发送好友赞 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_like-%E5%8F%91%E9%80%81%E5%A5%BD%E5%8F%8B%E8%B5%9E + * @param $user_id + * @param int $times + * @return array|bool|null + */ + public function sendLike($user_id, $times = 1) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'user_id' => $user_id, + 'times' => $times + ] + ], $this->callback); + } + + /** + * 群组踢人 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_kick-%E7%BE%A4%E7%BB%84%E8%B8%A2%E4%BA%BA + * @param $group_id + * @param $user_id + * @param bool $reject_add_request + * @return array|bool|null + */ + public function setGroupKick($group_id, $user_id, $reject_add_request = false) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'user_id' => $user_id, + 'reject_add_request' => $reject_add_request + ] + ], $this->callback); + } + + /** + * 群组单人禁言 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80 + * @param int $group_id + * @param int $user_id + * @param int $duration + * @return array|bool|null + */ + public function setGroupBan($group_id, $user_id, $duration = 1800) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'user_id' => $user_id, + 'duration' => $duration + ] + ], $this->callback); + } + + /** + * 群组匿名用户禁言 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_anonymous_ban-%E7%BE%A4%E7%BB%84%E5%8C%BF%E5%90%8D%E7%94%A8%E6%88%B7%E7%A6%81%E8%A8%80 + * @param $group_id + * @param $anonymous_or_flag + * @param int $duration + * @return array|bool|null + */ + public function setGroupAnonymousBan($group_id, $anonymous_or_flag, $duration = 1800) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + (is_string($anonymous_or_flag) ? 'flag' : 'anonymous') => $anonymous_or_flag, + 'duration' => $duration + ] + ], $this->callback); + } + + /** + * 群组全员禁言 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_whole_ban-%E7%BE%A4%E7%BB%84%E5%85%A8%E5%91%98%E7%A6%81%E8%A8%80 + * @param $group_id + * @param bool $enable + * @return array|bool|null + */ + public function setGroupWholeBan($group_id, $enable = true) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'enable' => $enable + ] + ], $this->callback); + } + + /** + * 群组设置管理员 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_admin-%E7%BE%A4%E7%BB%84%E8%AE%BE%E7%BD%AE%E7%AE%A1%E7%90%86%E5%91%98 + * @param $group_id + * @param $user_id + * @param bool $enable + * @return array|bool|null + */ + public function setGroupAdmin($group_id, $user_id, $enable = true) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'user_id' => $user_id, + 'enable' => $enable + ] + ], $this->callback); + } + + /** + * 群组匿名 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_anonymous-%E7%BE%A4%E7%BB%84%E5%8C%BF%E5%90%8D + * @param $group_id + * @param bool $enable + * @return array|bool|null + */ + public function setGroupAnonymous($group_id, $enable = true) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'enable' => $enable + ] + ], $this->callback); + } + + /** + * 设置群名片(群备注) + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_card-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D%E7%89%87%E7%BE%A4%E5%A4%87%E6%B3%A8 + * @param $group_id + * @param $user_id + * @param string $card + * @return array|bool|null + */ + public function setGroupCard($group_id, $user_id, $card = "") { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'user_id' => $user_id, + 'card' => $card + ] + ], $this->callback); + } + + /** + * 设置群名 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_name-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D + * @param $group_id + * @param $group_name + * @return array|bool|null + */ + public function setGroupName($group_id, $group_name) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'group_name' => $group_name + ] + ], $this->callback); + } + + /** + * 退出群组 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_leave-%E9%80%80%E5%87%BA%E7%BE%A4%E7%BB%84 + * @param $group_id + * @param bool $is_dismiss + * @return array|bool|null + */ + public function setGroupLeave($group_id, $is_dismiss = false) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'is_dismiss' => $is_dismiss + ] + ], $this->callback); + } + + /** + * 设置群组专属头衔 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_special_title-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E7%BB%84%E4%B8%93%E5%B1%9E%E5%A4%B4%E8%A1%94 + * @param $group_id + * @param $user_id + * @param string $special_title + * @param int $duration + * @return array|bool|null + */ + public function setGroupSpecialTitle($group_id, $user_id, $special_title = "", $duration = -1) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'user_id' => $user_id, + 'special_title' => $special_title, + 'duration' => $duration + ] + ], $this->callback); + } + + /** + * 处理加好友请求 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_friend_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82 + * @param $flag + * @param bool $approve + * @param string $remark + * @return array|bool|null + */ + public function setFriendAddRequest($flag, $approve = true, $remark = "") { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'flag' => $flag, + 'approve' => $approve, + 'remark' => $remark + ] + ], $this->callback); + } + + /** + * 处理加群请求/邀请 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7 + * @param $flag + * @param $sub_type + * @param bool $approve + * @param string $reason + * @return array|bool|null + */ + public function setGroupAddRequest($flag, $sub_type, $approve = true, $reason = "") { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'flag' => $flag, + 'sub_type' => $sub_type, + 'approve' => $approve, + 'reason' => $reason + ] + ], $this->callback); + } + + /** + * 获取登录号信息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_login_info-%E8%8E%B7%E5%8F%96%E7%99%BB%E5%BD%95%E5%8F%B7%E4%BF%A1%E6%81%AF + * @return array|bool|null + */ + public function getLoginInfo() { + return $this->processAPI($this->connection, ['action' => $this->getActionName(__FUNCTION__)], $this->callback); + } + + /** + * 获取陌生人信息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_stranger_info-%E8%8E%B7%E5%8F%96%E9%99%8C%E7%94%9F%E4%BA%BA%E4%BF%A1%E6%81%AF + * @param $user_id + * @param bool $no_cache + * @return array|bool|null + */ + public function getStrangerInfo($user_id, $no_cache = false) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'user_id' => $user_id, + 'no_cache' => $no_cache + ] + ], $this->callback); + } + + /** + * 获取好友列表 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_friend_list-%E8%8E%B7%E5%8F%96%E5%A5%BD%E5%8F%8B%E5%88%97%E8%A1%A8 + * @return array|bool|null + */ + public function getFriendList() { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__) + ], $this->callback); + } + + /** + * 获取群信息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E4%BF%A1%E6%81%AF + * @param $group_id + * @param bool $no_cache + * @return array|bool|null + */ + public function getGroupInfo($group_id, $no_cache = false) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'no_cache' => $no_cache + ] + ], $this->callback); + } + + /** + * 获取群列表 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%88%97%E8%A1%A8 + * @return array|bool|null + */ + public function getGroupList() { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__) + ], $this->callback); + } + + /** + * 获取群成员信息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF + * @param $group_id + * @param $user_id + * @param bool $no_cache + * @return array|bool|null + */ + public function getGroupMemberInfo($group_id, $user_id, $no_cache = false) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'user_id' => $user_id, + 'no_cache' => $no_cache + ] + ], $this->callback); + } + + /** + * 获取群成员列表 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E5%88%97%E8%A1%A8 + * @param $group_id + * @return array|bool|null + */ + public function getGroupMemberList($group_id) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id + ] + ], $this->callback); + } + + /** + * 获取群荣誉信息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_honor_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E8%8D%A3%E8%AA%89%E4%BF%A1%E6%81%AF + * @param $group_id + * @param $type + * @return array|bool|null + */ + public function getGroupHonorInfo($group_id, $type) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'group_id' => $group_id, + 'type' => $type + ] + ], $this->callback); + } + + /** + * 获取 CSRF Token + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_csrf_token-%E8%8E%B7%E5%8F%96-csrf-token + * @return array|bool|null + */ + public function getCsrfToken() { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__) + ], $this->callback); + } + + /** + * 获取 QQ 相关接口凭证 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_credentials-%E8%8E%B7%E5%8F%96-qq-%E7%9B%B8%E5%85%B3%E6%8E%A5%E5%8F%A3%E5%87%AD%E8%AF%81 + * @param string $domain + * @return array|bool|null + */ + public function getCredentials($domain = "") { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'domain' => $domain + ] + ], $this->callback); + } + + /** + * 获取语音 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_record-%E8%8E%B7%E5%8F%96%E8%AF%AD%E9%9F%B3 + * @param $file + * @param $out_format + * @return array|bool|null + */ + public function getRecord($file, $out_format) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'file' => $file, + 'out_format' => $out_format + ] + ], $this->callback); + } + + /** + * 获取图片 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_image-%E8%8E%B7%E5%8F%96%E5%9B%BE%E7%89%87 + * @param $file + * @return array|bool|null + */ + public function getImage($file) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'file' => $file + ] + ], $this->callback); + } + + /** + * 检查是否可以发送图片 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_image-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E5%9B%BE%E7%89%87 + * @return array|bool|null + */ + public function canSendImage() { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__) + ], $this->callback); + } + + /** + * 检查是否可以发送语音 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_record-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E8%AF%AD%E9%9F%B3 + * @return array|bool|null + */ + public function canSendRecord() { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__) + ], $this->callback); + } + + /** + * 获取运行状态 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_status-%E8%8E%B7%E5%8F%96%E8%BF%90%E8%A1%8C%E7%8A%B6%E6%80%81 + * @return array|bool|null + */ + public function getStatus() { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__) + ], $this->callback); + } + + /** + * 获取版本信息 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_version_info-%E8%8E%B7%E5%8F%96%E7%89%88%E6%9C%AC%E4%BF%A1%E6%81%AF + * @return array|bool|null + */ + public function getVersionInfo() { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__) + ], $this->callback); + } + + /** + * 重启 OneBot 实现 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_restart-%E9%87%8D%E5%90%AF-onebot-%E5%AE%9E%E7%8E%B0 + * @param int $delay + * @return array|bool|null + */ + public function setRestart($delay = 0) { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__), + 'params' => [ + 'delay' => $delay + ] + ], $this->callback); + } + + /** + * 清理缓存 + * @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#clean_cache-%E6%B8%85%E7%90%86%E7%BC%93%E5%AD%98 + * @return array|bool|null + */ + public function cleanCache() { + return $this->processAPI($this->connection, [ + 'action' => $this->getActionName(__FUNCTION__) + ], $this->callback); + } + + public function callExtendedAPI($action, $params = []) { + return $this->processAPI($this->connection, [ + 'action' => $action, + 'params' => $params + ], $this->callback); + } + + private function getActionName(string $method) { + $prefix = ($this->prefix == self::API_ASYNC ? '_async' : ($this->prefix == self::API_RATE_LIMITED ? '_rate_limited' : '')); + $func_name = strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $method)); + return $func_name . $prefix; + } +} diff --git a/src/ZM/Annotation/AnnotationParser.php b/src/ZM/Annotation/AnnotationParser.php index e4b6dbce..0024768a 100644 --- a/src/ZM/Annotation/AnnotationParser.php +++ b/src/ZM/Annotation/AnnotationParser.php @@ -3,260 +3,187 @@ namespace ZM\Annotation; -use Doctrine\Common\Annotations\{AnnotationException, AnnotationReader}; -use Co; -use Framework\{Console, ZMBuf}; -use Error; -use Exception; +use Doctrine\Common\Annotations\AnnotationReader; +use ZM\Annotation\Interfaces\ErgodicAnnotation; +use ZM\Console\Console; use ReflectionClass; use ReflectionException; use ReflectionMethod; -use ZM\Annotation\CQ\{CQAfter, - CQAPIResponse, - CQAPISend, - CQBefore, - CQCommand, - CQMessage, - CQMetaEvent, - CQNotice, - CQRequest -}; -use ZM\Annotation\Http\{After, Before, Controller, HandleException, Middleware, MiddlewareClass, RequestMapping}; -use Swoole\Timer; -use ZM\Annotation\Interfaces\CustomAnnotation; +use ZM\Annotation\Http\{HandleAfter, HandleBefore, Controller, HandleException, Middleware, MiddlewareClass, RequestMapping}; use ZM\Annotation\Interfaces\Level; -use ZM\Annotation\Module\{Closed, InitBuffer, LoadBuffer, SaveBuffer}; -use ZM\Annotation\Swoole\{OnSave, OnStart, OnTick, SwooleEventAfter, SwooleEventAt}; -use ZM\Annotation\Interfaces\Rule; -use ZM\Connection\WSConnection; -use ZM\Event\EventHandler; -use ZM\Http\MiddlewareInterface; -use Framework\DataProvider; -use ZM\Utils\ZMUtil; +use ZM\Annotation\Module\Closed; +use ZM\Utils\DataProvider; class AnnotationParser { + private $path_list = []; + + private $start_time; + + private $annotation_map = []; + private $middleware_map = []; + private $middlewares = []; + + /** @var null|AnnotationReader */ + private $reader = null; + private $req_mapping = []; + /** - * 注册各个模块类的注解和模块level的排序 - * @throws ReflectionException - * @throws AnnotationException + * AnnotationParser constructor. */ - public static function registerMods() { - self::loadAnnotationClasses(); - $all_class = getAllClasses(DataProvider::getWorkingDir() . "/src/Module/", "Module"); - ZMBuf::$req_mapping[0] = [ + public function __construct() { + $this->start_time = microtime(true); + $this->loadAnnotationClasses(); + $this->req_mapping[0] = [ 'id' => 0, 'pid' => -1, 'name' => '/' ]; - $reader = new AnnotationReader(); - foreach ($all_class as $v) { - Console::debug("正在检索 " . $v); - $reflection_class = new ReflectionClass($v); - $class_prefix = ''; - $methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC); - $class_annotations = $reader->getClassAnnotations($reflection_class); - $middleware_addon = []; - foreach ($class_annotations as $vs) { - if ($vs instanceof Closed) { - continue 2; - } elseif ($vs instanceof Controller) { - Console::debug("找到 Controller 中间件: " . $vs->class); - $class_prefix = $vs->prefix; - } elseif ($vs instanceof SaveBuffer) { - Console::debug("注册自动保存的缓存变量: " . $vs->buf_name . " (Dir:" . $vs->sub_folder . ")"); - DataProvider::addSaveBuffer($vs->buf_name, $vs->sub_folder); - } elseif ($vs instanceof LoadBuffer) { - Console::debug("注册到内存的缓存变量: " . $vs->buf_name . " (Dir:" . $vs->sub_folder . ")"); - ZMBuf::set($vs->buf_name, DataProvider::getJsonData(($vs->sub_folder ?? "") . "/" . $vs->buf_name . ".json")); - } elseif ($vs instanceof InitBuffer) { - ZMBuf::set($vs->buf_name, []); - } elseif ($vs instanceof MiddlewareClass) { - Console::verbose("正在注册中间件 " . $reflection_class->getName()); - $result = [ - "class" => "\\" . $reflection_class->getName() - ]; - foreach ($methods as $vss) { - if ($vss->getName() == "getName") { - /** @var MiddlewareInterface $tmp */ - $tmp = new $v(); - $result["name"] = $tmp->getName(); - continue; - } - $method_annotations = $reader->getMethodAnnotations($vss); - foreach ($method_annotations as $vsss) { - if ($vss instanceof Rule) $vss = self::registerRuleEvent($vsss, $vss, $reflection_class); - else $vss = self::registerMethod($vsss, $vss, $reflection_class); - //echo get_class($vsss) . PHP_EOL; - if ($vsss instanceof Before) $result["before"] = $vsss->method; - if ($vsss instanceof After) $result["after"] = $vsss->method; - if ($vsss instanceof HandleException) { - $result["exceptions"][$vsss->class_name] = $vsss->method; - } - } - } - ZMBuf::$events[MiddlewareClass::class][$result["name"]] = $result; - continue 2; - } elseif ($vs instanceof Middleware) { - $middleware_addon[] = $vs; - } elseif ($vs instanceof CustomAnnotation) { - $vs->class = $reflection_class->getName(); - ZMBuf::$events[get_class($vs)][] = $vs; - } - } - foreach ($methods as $vs) { - if ($middleware_addon !== []) { - foreach($middleware_addon as $value){ - Console::debug("Added middleware " . $value->middleware . " to $v -> " . $vs->getName()); - ZMBuf::$events[MiddlewareInterface::class][$v][$vs->getName()][] = $value->middleware; - } - } - $method_annotations = $reader->getMethodAnnotations($vs); - foreach ($method_annotations as $vss) { - if ($vss instanceof Rule) $vss = self::registerRuleEvent($vss, $vs, $reflection_class); - else $vss = self::registerMethod($vss, $vs, $reflection_class); - Console::debug("寻找 " . $vs->getName() . " -> " . get_class($vss)); + } - if ($vss instanceof SwooleEventAt) ZMBuf::$events[SwooleEventAt::class][] = $vss; - elseif ($vss instanceof SwooleEventAfter) ZMBuf::$events[SwooleEventAfter::class][] = $vss; - elseif ($vss instanceof CQMessage) ZMBuf::$events[CQMessage::class][] = $vss; - elseif ($vss instanceof CQNotice) ZMBuf::$events[CQNotice::class][] = $vss; - elseif ($vss instanceof CQRequest) ZMBuf::$events[CQRequest::class][] = $vss; - elseif ($vss instanceof CQMetaEvent) ZMBuf::$events[CQMetaEvent::class][] = $vss; - elseif ($vss instanceof CQCommand) ZMBuf::$events[CQCommand::class][] = $vss; - elseif ($vss instanceof RequestMapping) { - self::registerRequestMapping($vss, $vs, $reflection_class, $class_prefix); - } elseif ($vss instanceof CustomAnnotation) ZMBuf::$events[get_class($vss)][] = $vss; - elseif ($vss instanceof CQBefore) ZMBuf::$events[CQBefore::class][$vss->cq_event][] = $vss; - elseif ($vss instanceof CQAfter) ZMBuf::$events[CQAfter::class][$vss->cq_event][] = $vss; - elseif ($vss instanceof OnStart) ZMBuf::$events[OnStart::class][] = $vss; - elseif ($vss instanceof OnSave) ZMBuf::$events[OnSave::class][] = $vss; - elseif ($vss instanceof Middleware) ZMBuf::$events[MiddlewareInterface::class][$vss->class][$vss->method][] = $vss->middleware; - elseif ($vss instanceof OnTick) self::addTimerTick($vss); - elseif ($vss instanceof CQAPISend) ZMBuf::$events[CQAPISend::class][] = $vss; - elseif ($vss instanceof CQAPIResponse) ZMBuf::$events[CQAPIResponse::class][$vss->retcode] = [$vss->class, $vss->method]; + /** + * 注册各个模块类的注解和模块level的排序 + * @throws ReflectionException + */ + public function registerMods() { + foreach ($this->path_list as $path) { + Console::debug("parsing annotation in ".$path[0]); + $all_class = getAllClasses($path[0], $path[1]); + $this->reader = new AnnotationReader(); + foreach ($all_class as $v) { + Console::debug("正在检索 " . $v); + $reflection_class = new ReflectionClass($v); + $methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC); + $class_annotations = $this->reader->getClassAnnotations($reflection_class); + + // 这段为新加的:start + //这里将每个类里面所有的类注解、方法注解通通加到一颗大树上,后期解析 + /* + $annotation_map: { + Module\Example\Hello: { + class_annotations: [ + 注解对象1, 注解对象2, ... + ], + methods: [ + ReflectionMethod, ReflectionMethod, ... + ], + methods_annotations: { + foo: [ 注解对象1, 注解对象2, ... ], + bar: [ 注解对象1, 注解对象2, ... ], + } + } } - } - } - $tree = self::genTree(ZMBuf::$req_mapping); - ZMBuf::$req_mapping = $tree[0]; - //给支持level的排个序 - foreach (ZMBuf::$events as $class_name => $v) { - if (is_a($class_name, Level::class, true)) { - for ($i = 0; $i < count(ZMBuf::$events[$class_name]) - 1; ++$i) { - for ($j = 0; $j < count(ZMBuf::$events[$class_name]) - $i - 1; ++$j) { - $l1 = ZMBuf::$events[$class_name][$j]->level; - $l2 = ZMBuf::$events[$class_name][$j + 1]->level; - if ($l1 < $l2) { - $t = ZMBuf::$events[$class_name][$j + 1]; - ZMBuf::$events[$class_name][$j + 1] = ZMBuf::$events[$class_name][$j]; - ZMBuf::$events[$class_name][$j] = $t; + */ + + // 生成主树 + $this->annotation_map[$v]["class_annotations"] = $class_annotations; + $this->annotation_map[$v]["methods"] = $methods; + foreach ($methods as $method) { + $this->annotation_map[$v]["methods_annotations"][$method->getName()] = $this->reader->getMethodAnnotations($method); + } + + + foreach ($this->annotation_map[$v]["class_annotations"] as $ks => $vs) { + $vs->class = $v; + + //预处理1:将适用于每一个函数的注解到类注解重新注解到每个函数下面 + if ($vs instanceof ErgodicAnnotation) { + foreach ($this->annotation_map[$v]["methods"] as $method) { + $copy = clone $vs; + $copy->method = $method->getName(); + $this->annotation_map[$v]["methods_annotations"][$method->getName()][] = $copy; + } + } + + //预处理2:处理 class 下面的注解 + if ($vs instanceof Closed) { + unset($this->annotation_map[$v]); + continue 2; + } elseif ($vs instanceof MiddlewareClass) { + Console::verbose("正在注册中间件 " . $reflection_class->getName()); + $rs = $this->registerMiddleware($vs, $reflection_class); + $this->middlewares[$rs["name"]] = $rs; + } + } + + //预处理3:处理每个函数上面的特殊注解,就是需要操作一些东西的 + foreach ($this->annotation_map[$v]["methods_annotations"] as $method_name => $methods_annotations) { + foreach ($methods_annotations as $method_anno) { + /** @var AnnotationBase $method_anno */ + $method_anno->class = $v; + $method_anno->method = $method_name; + if ($method_anno instanceof RequestMapping) { + $this->registerRequestMapping($method_anno, $method_name, $v, $methods_annotations); //TODO: 用symfony的routing重写 + } elseif ($method_anno instanceof Middleware) { + $this->middleware_map[$method_anno->class][$method_anno->method][] = $method_anno->middleware; } } } } } + + //预处理4:生成路由树(换成symfony后就不需要了) + $tree = $this->genTree($this->req_mapping); + $this->req_mapping = $tree[0]; + Console::debug("解析注解完毕!"); - if (ZMBuf::isset("timer_count")) { - Console::info("Added " . ZMBuf::get("timer_count") . " timer(s)!"); - ZMBuf::unsetCache("timer_count"); - } } - public static function getRuleCallback($rule_str) { - $func = null; - $rule = $rule_str; - if ($rule != "") { - $asp = explode(":", $rule); - $asp_name = array_shift($asp); - $rest = implode(":", $asp); - //Swoole 事件时走此switch - switch ($asp_name) { - case "connectType": //websocket连接类型 - $func = function (?WSConnection $connection) use ($rest) { - if ($connection === null) return false; - return $connection->getType() == $rest ? true : false; - }; - break; - case "containsGet": //handle http request事件时才能用 - case "containsPost": - $get_list = explode(",", $rest); - if ($asp_name == "containsGet") - $func = function ($request) use ($get_list) { - foreach ($get_list as $v) if (!isset($request->get[$v])) return false; - return true; - }; - else - $func = function ($request) use ($get_list) { - foreach ($get_list as $v) if (!isset($request->post[$v])) return false; - return true; - }; - /* - if ($controller_prefix != '') { - $p = ZMBuf::$req_mapping_node; - $prefix_exp = explode("/", $controller_prefix); - foreach ($prefix_exp as $k => $v) { - if ($v == "" || $v == ".." || $v == ".") { - unset($prefix_exp[$k]); - } - } - while (($shift = array_shift($prefix_exp)) !== null) { - $p->addRoute($shift, new MappingNode($shift)); - $p = $p->getRoute($shift); - } - if ($p->getNodeName() != "/") { - $p->setMethod($method->getName()); - $p->setClass($class->getName()); - $p->setRule($func); - return "mapped"; - } - }*/ - break; - case "containsJson": //handle http request事件时才能用 - $json_list = explode(",", $rest); - $func = function ($json) use ($json_list) { - foreach ($json_list as $v) if (!isset($json[$v])) return false; - return true; - }; - break; - case "dataEqual": //handle websocket message事件时才能用 - $func = function ($data) use ($rest) { - return $data == $rest; - }; - break; + /** + * @return array + */ + public function generateAnnotationEvents() { + $o = []; + foreach ($this->annotation_map as $module => $obj) { + foreach ($obj["class_annotations"] as $class_annotation) { + if ($class_annotation instanceof ErgodicAnnotation) continue; + else $o[get_class($class_annotation)][] = $class_annotation; + } + foreach ($obj["methods_annotations"] as $method_name => $methods_annotations) { + foreach ($methods_annotations as $annotation) { + $o[get_class($annotation)][] = $annotation; + } } - switch ($asp_name) { - case "msgMatch": //handle cq message事件时才能用 - $func = function ($msg) use ($rest) { - return matchPattern($rest, $msg); - }; - break; - case "msgEqual": //handle cq message事件时才能用 - $func = function ($msg) use ($rest) { - return trim($msg) == $rest; - }; - break; + } + foreach ($o as $k => $v) { + $this->sortByLevel($o, $k); + } + return $o; + } + + /** + * @return array + */ + public function getMiddlewares() { return $this->middlewares; } + + /** + * @return array + */ + public function getMiddlewareMap() { return $this->middleware_map; } + + /** + * @return array + */ + public function getReqMapping() { return $this->req_mapping; } + + /** + * @param $path + * @param $indoor_name + */ + public function addRegisterPath($path, $indoor_name) { $this->path_list[] = [$path, $indoor_name]; } + + //private function below + + private function registerRequestMapping(RequestMapping $vss, $method, $class, $methods_annotations) { + $prefix = ''; + foreach ($methods_annotations as $annotation) { + if ($annotation instanceof Controller) { + $prefix = $annotation->prefix; + break; } } - return $func; - } - - public static function registerRuleEvent(?AnnotationBase $vss, ReflectionMethod $method, ReflectionClass $class) { - $vss->callback = self::getRuleCallback($vss->getRule()); - $vss->method = $method->getName(); - $vss->class = $class->getName(); - return $vss; - } - - public static function registerMethod(?AnnotationBase $vss, ReflectionMethod $method, ReflectionClass $class) { - $vss->method = $method->getName(); - $vss->class = $class->getName(); - return $vss; - } - - private static function registerRequestMapping(RequestMapping $vss, ReflectionMethod $method, ReflectionClass $class, string $prefix) { - $array = ZMBuf::$req_mapping; + $array = $this->req_mapping; $uid = count($array); $prefix_exp = explode("/", $prefix); $route_exp = explode("/", $vss->route); @@ -271,10 +198,11 @@ class AnnotationParser } } if ($prefix_exp == [] && $route_exp == []) { - $array[0]['method'] = $method->getName(); - $array[0]['class'] = $class->getName(); + $array[0]['method'] = $method; + $array[0]['class'] = $class; $array[0]['request_method'] = $vss->request_method; - ZMBuf::$req_mapping = $array; + $array[0]['route'] = $vss->route; + $this->req_mapping = $array; return; } $pid = 0; @@ -317,16 +245,19 @@ class AnnotationParser ]; $pid = $uid - 1; } - $array[$uid - 1]['method'] = $method->getName(); - $array[$uid - 1]['class'] = $class->getName(); + $array[$uid - 1]['method'] = $method; + $array[$uid - 1]['class'] = $class; $array[$uid - 1]['request_method'] = $vss->request_method; - ZMBuf::$req_mapping = $array; + $array[$uid - 1]['route'] = $vss->route; + $this->req_mapping = $array; } - private static function loadAnnotationClasses() { + /** @noinspection PhpIncludeInspection */ + private function loadAnnotationClasses() { $class = getAllClasses(WORKING_DIR . "/src/ZM/Annotation/", "ZM\\Annotation"); foreach ($class as $v) { $s = WORKING_DIR . '/src/' . str_replace("\\", "/", $v) . ".php"; + //Console::debug("Requiring annotation " . $s); require_once $s; } $class = getAllClasses(DataProvider::getWorkingDir() . "/src/Custom/Annotation/", "Custom\\Annotation"); @@ -337,7 +268,7 @@ class AnnotationParser } } - public static function genTree($items) { + private function genTree($items) { $tree = array(); foreach ($items as $item) if (isset($items[$item['pid']])) @@ -347,44 +278,33 @@ class AnnotationParser return $tree; } - private static function addTimerTick(?OnTick $vss) { - ZMBuf::set("timer_count", ZMBuf::get("timer_count", 0) + 1); - $class = ZMUtil::getModInstance($vss->class); - $method = $vss->method; - $ms = $vss->tick_ms; - $cid = go(function () use ($class, $method, $ms) { - Co::suspend(); - $plain_class = get_class($class); - if (!isset(ZMBuf::$events[MiddlewareInterface::class][$plain_class][$method])) { - Console::debug("Added timer: " . $plain_class . " -> " . $method); - Timer::tick($ms, function () use ($class, $method) { - set_coroutine_params([]); - try { - $class->$method(); - } catch (Exception $e) { - Console::error("Uncaught error from TimerTick: " . $e->getMessage() . " at " . $e->getFile() . "({$e->getLine()})"); - } catch (Error $e) { - Console::error("Uncaught fatal error from TimerTick: " . $e->getMessage()); - echo Console::setColor($e->getTraceAsString(), "gray"); - Console::error("Please check your code!"); - } - }); - } else { - Console::debug("Added Middleware-based timer: " . $plain_class . " -> " . $method); - Timer::tick($ms, function () use ($class, $method) { - set_coroutine_params([]); - try { - EventHandler::callWithMiddleware($class, $method, [], []); - } catch (Exception $e) { - Console::error("Uncaught error from TimerTick: " . $e->getMessage() . " at " . $e->getFile() . "({$e->getLine()})"); - } catch (Error $e) { - Console::error("Uncaught fatal error from TimerTick: " . $e->getMessage()); - echo Console::setColor($e->getTraceAsString(), "gray"); - Console::error("Please check your code!"); - } - }); + private function registerMiddleware(MiddlewareClass $vs, ReflectionClass $reflection_class) { + $result = [ + "class" => "\\" . $reflection_class->getName(), + "name" => $vs->name + ]; + + foreach ($reflection_class->getMethods() as $vss) { + $method_annotations = $this->reader->getMethodAnnotations($vss); + foreach ($method_annotations as $vsss) { + if ($vsss instanceof HandleBefore) $result["before"] = $vss->getName(); + if ($vsss instanceof HandleAfter) $result["after"] = $vss->getName(); + if ($vsss instanceof HandleException) { + $result["exceptions"][$vsss->class_name] = $vss->getName(); + } } - }); - ZMBuf::append("paused_tick", $cid); + } + return $result; + } + + private function sortByLevel(&$events, string $class_name, $prefix = "") { + if (is_a($class_name, Level::class, true)) { + $class_name .= $prefix; + usort($events[$class_name], function ($a, $b) { + $left = $a->level; + $right = $b->level; + return $left > $right ? -1 : ($left == $right ? 0 : 1); + }); + } } } diff --git a/src/ZM/Annotation/CQ/CQAPISend.php b/src/ZM/Annotation/CQ/CQAPISend.php deleted file mode 100644 index bf4429c0..00000000 --- a/src/ZM/Annotation/CQ/CQAPISend.php +++ /dev/null @@ -1,43 +0,0 @@ -level; - } - - /** - * @param mixed $level - */ - public function setLevel($level) { - $this->level = $level; - } -} diff --git a/src/ZM/Annotation/CQ/CQCommand.php b/src/ZM/Annotation/CQ/CQCommand.php index 6d468b6b..a7ec162b 100644 --- a/src/ZM/Annotation/CQ/CQCommand.php +++ b/src/ZM/Annotation/CQ/CQCommand.php @@ -18,9 +18,15 @@ class CQCommand extends AnnotationBase implements Level /** @var string */ public $match = ""; /** @var string */ - public $regexMatch = ""; + public $pattern = ""; /** @var string */ - public $fullMatch = ""; + public $regex = ""; + /** @var string */ + public $start_with = ""; + /** @var string */ + public $end_with = ""; + /** @var string */ + public $keyword = ""; /** @var string[] */ public $alias = []; /** @var string */ diff --git a/src/ZM/Annotation/Http/Controller.php b/src/ZM/Annotation/Http/Controller.php index 9556a6bd..347c1caf 100644 --- a/src/ZM/Annotation/Http/Controller.php +++ b/src/ZM/Annotation/Http/Controller.php @@ -6,6 +6,7 @@ namespace ZM\Annotation\Http; use Doctrine\Common\Annotations\Annotation\Required; use Doctrine\Common\Annotations\Annotation\Target; use ZM\Annotation\AnnotationBase; +use ZM\Annotation\Interfaces\ErgodicAnnotation; /** * Class Controller @@ -13,11 +14,11 @@ use ZM\Annotation\AnnotationBase; * @Target("CLASS") * @package ZM\Annotation\Http */ -class Controller extends AnnotationBase +class Controller extends AnnotationBase implements ErgodicAnnotation { /** * @var string * @Required() */ public $prefix = ''; -} \ No newline at end of file +} diff --git a/src/ZM/Annotation/Http/After.php b/src/ZM/Annotation/Http/HandleAfter.php similarity index 80% rename from src/ZM/Annotation/Http/After.php rename to src/ZM/Annotation/Http/HandleAfter.php index ef24db54..9d2e43a4 100644 --- a/src/ZM/Annotation/Http/After.php +++ b/src/ZM/Annotation/Http/HandleAfter.php @@ -9,11 +9,11 @@ use Doctrine\Common\Annotations\Annotation\Target; use ZM\Annotation\AnnotationBase; /** - * Class After + * Class HandleAfter * @package ZM\Annotation\Http * @Annotation * @Target("METHOD") */ -class After extends AnnotationBase +class HandleAfter extends AnnotationBase { -} \ No newline at end of file +} diff --git a/src/ZM/Annotation/Http/Before.php b/src/ZM/Annotation/Http/HandleBefore.php similarity index 65% rename from src/ZM/Annotation/Http/Before.php rename to src/ZM/Annotation/Http/HandleBefore.php index f09cfc40..90a092a5 100644 --- a/src/ZM/Annotation/Http/Before.php +++ b/src/ZM/Annotation/Http/HandleBefore.php @@ -4,16 +4,15 @@ namespace ZM\Annotation\Http; -use Doctrine\Common\Annotations\Annotation\Required; use Doctrine\Common\Annotations\Annotation\Target; use ZM\Annotation\AnnotationBase; /** - * Class Before + * Class HandleBefore * @package ZM\Annotation\Http * @Annotation * @Target("METHOD") */ -class Before extends AnnotationBase +class HandleBefore extends AnnotationBase { -} \ No newline at end of file +} diff --git a/src/ZM/Annotation/Http/Middleware.php b/src/ZM/Annotation/Http/Middleware.php index 60a928c3..cb83ba63 100644 --- a/src/ZM/Annotation/Http/Middleware.php +++ b/src/ZM/Annotation/Http/Middleware.php @@ -7,6 +7,7 @@ namespace ZM\Annotation\Http; use Doctrine\Common\Annotations\Annotation\Required; use Doctrine\Common\Annotations\Annotation\Target; use ZM\Annotation\AnnotationBase; +use ZM\Annotation\Interfaces\ErgodicAnnotation; /** * Class Middleware @@ -14,7 +15,7 @@ use ZM\Annotation\AnnotationBase; * @Annotation * @Target("ALL") */ -class Middleware extends AnnotationBase +class Middleware extends AnnotationBase implements ErgodicAnnotation { /** * @var string diff --git a/src/ZM/Annotation/Http/MiddlewareClass.php b/src/ZM/Annotation/Http/MiddlewareClass.php index 4d182aae..58f98d1d 100644 --- a/src/ZM/Annotation/Http/MiddlewareClass.php +++ b/src/ZM/Annotation/Http/MiddlewareClass.php @@ -4,6 +4,7 @@ namespace ZM\Annotation\Http; +use Doctrine\Common\Annotations\Annotation\Required; use Doctrine\Common\Annotations\Annotation\Target; use ZM\Annotation\AnnotationBase; @@ -15,5 +16,9 @@ use ZM\Annotation\AnnotationBase; */ class MiddlewareClass extends AnnotationBase { - -} \ No newline at end of file + /** + * @var string + * @Required() + */ + public $name = ''; +} diff --git a/src/ZM/Annotation/Http/RequestMapping.php b/src/ZM/Annotation/Http/RequestMapping.php index 966ad84d..7a8538dd 100644 --- a/src/ZM/Annotation/Http/RequestMapping.php +++ b/src/ZM/Annotation/Http/RequestMapping.php @@ -10,7 +10,7 @@ use ZM\Annotation\AnnotationBase; /** * Class RequestMapping * @Annotation - * @Target("ALL") + * @Target("METHOD") * @package ZM\Annotation\Http */ class RequestMapping extends AnnotationBase @@ -36,4 +36,4 @@ class RequestMapping extends AnnotationBase * @var array */ public $params = []; -} \ No newline at end of file +} diff --git a/src/ZM/Annotation/Http/RequestMethod.php b/src/ZM/Annotation/Http/RequestMethod.php index 2c54129e..6332b331 100644 --- a/src/ZM/Annotation/Http/RequestMethod.php +++ b/src/ZM/Annotation/Http/RequestMethod.php @@ -9,7 +9,6 @@ use ZM\Annotation\AnnotationBase; /** * Class RequestMethod * @Annotation - * * @package ZM\Annotation\Http */ class RequestMethod extends AnnotationBase @@ -27,4 +26,4 @@ class RequestMethod extends AnnotationBase public const DELETE = 'DELETE'; public const OPTIONS = 'OPTIONS'; public const HEAD = 'HEAD'; -} \ No newline at end of file +} diff --git a/src/ZM/Annotation/Interfaces/ErgodicAnnotation.php b/src/ZM/Annotation/Interfaces/ErgodicAnnotation.php new file mode 100644 index 00000000..b6b3be2f --- /dev/null +++ b/src/ZM/Annotation/Interfaces/ErgodicAnnotation.php @@ -0,0 +1,10 @@ +level = $level; } -} \ No newline at end of file +} diff --git a/src/ZM/Annotation/Swoole/OnTaskWorkerStart.php b/src/ZM/Annotation/Swoole/OnTaskWorkerStart.php deleted file mode 100644 index 462563c6..00000000 --- a/src/ZM/Annotation/Swoole/OnTaskWorkerStart.php +++ /dev/null @@ -1,15 +0,0 @@ -type; - } - - /** - * @param string $type - */ - public function setType(string $type) { - $this->type = $type; - } - - /** - * @return string - */ - public function getRule(): string { - return $this->rule; - } - - /** - * @param string $rule - */ - public function setRule(string $rule) { - $this->rule = $rule; - } - - /** - * @return int - */ - public function getLevel(): int { - return $this->level; - } - - /** - * @param int $level - */ - public function setLevel(int $level) { - $this->level = $level; - } - - -} \ No newline at end of file diff --git a/src/ZM/Annotation/Swoole/OnEvent.php b/src/ZM/Annotation/Swoole/SwooleHandler.php similarity index 83% rename from src/ZM/Annotation/Swoole/OnEvent.php rename to src/ZM/Annotation/Swoole/SwooleHandler.php index 86d26032..d70fb7d0 100644 --- a/src/ZM/Annotation/Swoole/OnEvent.php +++ b/src/ZM/Annotation/Swoole/SwooleHandler.php @@ -9,12 +9,12 @@ use Doctrine\Common\Annotations\Annotation\Target; use ZM\Annotation\AnnotationBase; /** - * Class OnEvent + * Class SwooleHandler * @package ZM\Annotation\Swoole * @Annotation * @Target("METHOD") */ -class OnEvent extends AnnotationBase +class SwooleHandler extends AnnotationBase { /** * @var string diff --git a/src/ZM/Command/BuildCommand.php b/src/ZM/Command/BuildCommand.php new file mode 100644 index 00000000..956a3c53 --- /dev/null +++ b/src/ZM/Command/BuildCommand.php @@ -0,0 +1,73 @@ +setDescription("Build an \".phar\" file | 将项目构建一个phar包"); + $this->setHelp("此功能将会把炸毛框架的模块打包为\".phar\",供发布和执行。"); + $this->addOption("target", "D", InputOption::VALUE_REQUIRED, "Output Directory | 指定输出目录"); + // ... + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $this->output = $output; + $target_dir = $input->getOption("target") ?? (__DIR__ . '/../../../resources/'); + if (mb_strpos($target_dir, "../")) $target_dir = realpath($target_dir); + if ($target_dir === false) { + $output->writeln(TermColor::color8(31) . "Error: No such file or directory (".__DIR__ . '/../../../resources/'.")" . TermColor::RESET); + return Command::FAILURE; + } + $output->writeln("Target: " . $target_dir . " , Version: " . ($version = json_decode(file_get_contents(__DIR__ . "/../../../composer.json"), true)["version"])); + if (mb_substr($target_dir, -1, 1) !== '/') $target_dir .= "/"; + if (ini_get('phar.readonly') == 1) { + $output->writeln(TermColor::color8(31) . "You need to set \"phar.readonly\" to \"Off\"!"); + $output->writeln(TermColor::color8(31) . "See: https://stackoverflow.com/questions/34667606/cant-enable-phar-writing"); + return Command::FAILURE; + } + if (!is_dir($target_dir)) { + $output->writeln(TermColor::color8(31) . "Error: No such file or directory ($target_dir)" . TermColor::RESET); + return Command::FAILURE; + } + $filename = "server.phar"; + $this->build($target_dir, $filename); + + return Command::SUCCESS; + } + + private function build ($target_dir, $filename) { + @unlink($target_dir . $filename); + $phar = new Phar($target_dir . $filename); + $phar->startBuffering(); + $src = realpath(__DIR__ . '/../../zhamao-framework/'); + $hello = file_get_contents($src . '/src/Module/Example/Hello.php'); + $middleware = file_get_contents($src . '/src/Module/Middleware/TimerMiddleware.php'); + unlink($src . '/src/Module/Example/Hello.php'); + unlink($src . '/src/Module/Middleware/TimerMiddleware.php'); + $phar->buildFromDirectory($src); + $phar->addFromString('tmp/Hello.php.bak', $hello); + $phar->addFromString('tmp/TimerMiddleware.php.bak', $middleware); + //$phar->compressFiles(Phar::GZ); + $phar->setStub($phar->createDefaultStub('phar-starter.php')); + $phar->stopBuffering(); + file_put_contents($src . '/src/Module/Example/Hello.php', $hello); + file_put_contents($src . '/src/Module/Middleware/TimerMiddleware.php', $middleware); + $this->output->writeln("Successfully built. Location: " . $target_dir . "$filename"); + } +} diff --git a/src/ZM/Command/InitCommand.php b/src/ZM/Command/InitCommand.php new file mode 100644 index 00000000..19090668 --- /dev/null +++ b/src/ZM/Command/InitCommand.php @@ -0,0 +1,101 @@ +setDescription("Initialize framework starter | 初始化框架运行的基础文件"); + $this->setHelp("此命令将会解压以下文件到项目的根目录:\n" . implode("\n", $this->getExtractFiles())); + // ... + } + + protected function execute(InputInterface $input, OutputInterface $output) { + if (LOAD_MODE === 1) { // 从composer依赖而来的项目模式,最基本的需要初始化的模式 + $output->writeln("Initializing files"); + $base_path = LOAD_MODE_COMPOSER_PATH; + foreach ($this->extract_files as $file) { + if (!file_exists($base_path . $file)) { + $info = pathinfo($file); + @mkdir($base_path . $info["dirname"], 0777, true); + echo "Copying " . $file . PHP_EOL; + $package_name = ($version = json_decode(file_get_contents(__DIR__ . "/../../../composer.json"), true)["name"]); + copy($base_path . "/vendor/" . $package_name . $file, $base_path . $file); + } else { + echo "Skipping " . $file . " , file exists." . PHP_EOL; + } + } + $autoload = [ + "psr-4" => [ + "Module\\" => "src/Module", + "Custom\\" => "src/Custom" + ], + "files" => [ + "src/Custom/global_function.php" + ] + ]; + if (file_exists($base_path . "/composer.json")) { + $composer = json_decode(file_get_contents($base_path . "/composer.json"), true); + if (!isset($composer["autoload"])) { + $composer["autoload"] = $autoload; + } else { + foreach ($autoload["psr-4"] as $k => $v) { + if (!isset($composer["autoload"]["psr-4"][$k])) $composer["autoload"]["psr-4"][$k] = $v; + } + foreach ($autoload["files"] as $k => $v) { + if (!in_array($v, $composer["autoload"]["files"])) $composer["autoload"]["files"][] = $v; + } + } + file_put_contents($base_path . "/composer.json", json_encode($composer, 64 | 128 | 256)); + $output->writeln("Executing composer update command"); + exec("composer update"); + echo PHP_EOL; + } else { + echo("Error occurred. Please check your updates.\n"); + return Command::FAILURE; + } + return Command::SUCCESS; + } elseif (LOAD_MODE === 2) { //从phar启动的框架包,初始化的模式 + $phar_link = new Phar(__DIR__); + $current_dir = pathinfo($phar_link->getPath())["dirname"]; + chdir($current_dir); + $phar_link = "phar://" . $phar_link->getPath(); + foreach ($this->extract_files as $file) { + if (!file_exists($current_dir . $file)) { + $info = pathinfo($file); + @mkdir($current_dir . $info["dirname"], 0777, true); + echo "Copying " . $file . PHP_EOL; + file_put_contents($current_dir . $file, file_get_contents($phar_link . $file)); + } else { + echo "Skipping " . $file . " , file exists." . PHP_EOL; + } + } + } + $output->writeln("initialization must be started with composer-project mode!"); + return Command::FAILURE; + } + + private function getExtractFiles() { + return $this->extract_files; + } +} diff --git a/src/ZM/Command/PureHttpCommand.php b/src/ZM/Command/PureHttpCommand.php new file mode 100644 index 00000000..99c9114c --- /dev/null +++ b/src/ZM/Command/PureHttpCommand.php @@ -0,0 +1,94 @@ +setDescription("Run a simple http server | 启动一个简单的文件 HTTP 服务器"); + $this->setHelp("直接运行可以启动"); + $this->addArgument('dir', InputArgument::REQUIRED, 'Your directory'); + $this->addOption("host", 'H', InputOption::VALUE_REQUIRED, "启动监听地址"); + $this->addOption("port", 'P', InputOption::VALUE_REQUIRED, "启动监听地址的端口"); + // ... + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $tty_width = explode(" ", trim(exec("stty size")))[1]; + if(realpath($input->getArgument('dir') ?? '.') === false) { + $output->writeln("Directory error(".($input->getArgument('dir') ?? '.')."): no such file or directory."); + return self::FAILURE; + } + $global = ZMConfig::get("global"); + $host = $input->getOption("host") ?? $global["host"]; + $port = $input->getOption("port") ?? $global["port"]; + $server = new Server($host, $port); + $server->set(ZMConfig::get("global", "swoole")); + Console::init(0, $server); + ZMAtomic::$atomics["request"] = []; + for ($i = 0; $i < 32; ++$i) { + ZMAtomic::$atomics["request"][$i] = new Atomic(0); + } + $index = ["index.html", "index.htm"]; + $server->on("request", function (Request $request, Response $response) use ($input, $index, $server) { + ZMAtomic::$atomics["request"][$server->worker_id]->add(1); + HttpUtil::handleStaticPage( + $request->server["request_uri"], + $response, + [ + "document_root" => realpath($input->getArgument('dir') ?? '.'), + "document_index" => $index + ]); + echo "\r".Coroutine::stats()["coroutine_peak_num"]; + }); + $server->on("start", function ($server) { + Process::signal(SIGINT, function () use ($server) { + Console::warning("Server interrupted by keyboard."); + for ($i = 0; $i < 32; ++$i) { + $num = ZMAtomic::$atomics["request"][$i]->get(); + if($num != 0) + echo "[$i]: ".$num."\n"; + } + $server->shutdown(); + $server->stop(); + }); + Console::success("Server started. Use Ctrl+C to stop."); + }); + $out = [ + "host" => $host, + "port" => $port, + "document_root" => realpath($input->getArgument('dir') ?? '.'), + "document_index" => implode(", ", $index) + ]; + Console::printProps($out, $tty_width); + $server->start(); + // return this if there was no problem running the command + // (it's equivalent to returning int(0)) + return Command::SUCCESS; + + // or return this if some error happened during the execution + // (it's equivalent to returning int(1)) + // return Command::FAILURE; + } +} diff --git a/src/ZM/Command/RunServerCommand.php b/src/ZM/Command/RunServerCommand.php new file mode 100644 index 00000000..639b6091 --- /dev/null +++ b/src/ZM/Command/RunServerCommand.php @@ -0,0 +1,57 @@ +setDefinition([ + new InputOption("debug-mode", "D", null, "开启调试模式 (这将关闭协程化)"), + new InputOption("log-debug", null, null, "调整消息等级到debug (log-level=4)"), + new InputOption("log-verbose", null, null, "调整消息等级到verbose (log-level=3)"), + new InputOption("log-info", null, null, "调整消息等级到info (log-level=2)"), + new InputOption("log-warning", null, null, "调整消息等级到warning (log-level=1)"), + new InputOption("log-error", null, null, "调整消息等级到error (log-level=0)"), + new InputOption("log-theme", null, InputOption::VALUE_REQUIRED, "改变终端的主题配色"), + new InputOption("disable-console-input", null, null, "禁止终端输入内容 (后台服务时需要)"), + new InputOption("disable-coroutine", null, null, "关闭协程Hook"), + new InputOption("daemon", null, null, "以守护进程的方式运行框架"), + new InputOption("watch", null, null, "监听 src/ 目录的文件变化并热更新"), + new InputOption("env", null, InputOption::VALUE_REQUIRED, "设置环境类型 (production, development, staging)"), + ]); + $this->setDescription("Run zhamao-framework | 启动框架"); + $this->setHelp("直接运行可以启动"); + + // ... + } + + protected function execute(InputInterface $input, OutputInterface $output) { + if(($opt = $input->getOption("env")) !== null) { + if(!in_array($opt, ["production", "staging", "development", ""])) { + $output->writeln(" \"--env\" option only accept production, development, staging and [empty] ! "); + return Command::FAILURE; + } + } + // ... put here the code to run in your command + // this method must return an integer number with the "exit status code" + // of the command. You can also use these constants to make code more readable + (new Framework($input->getOptions()))->start(); + // return this if there was no problem running the command + // (it's equivalent to returning int(0)) + return Command::SUCCESS; + + // or return this if some error happened during the execution + // (it's equivalent to returning int(1)) + // return Command::FAILURE; + } +} diff --git a/src/ZM/Command/SystemdCommand.php b/src/ZM/Command/SystemdCommand.php new file mode 100644 index 00000000..8cea8607 --- /dev/null +++ b/src/ZM/Command/SystemdCommand.php @@ -0,0 +1,20 @@ +self_id = $self_id; - } - - public function getQQ(){ - return $this->self_id; - } - - public function getType() { - return "qq"; - } -} diff --git a/src/ZM/Connection/ConnectionManager.php b/src/ZM/Connection/ConnectionManager.php deleted file mode 100644 index 16e01fb4..00000000 --- a/src/ZM/Connection/ConnectionManager.php +++ /dev/null @@ -1,76 +0,0 @@ -fd == $fd) return $v; - } - return null; - } - - /** - * @param string $type - * @param array $option - * @return WSConnection[]|CQConnection[] - */ - public static function getByType(string $type, $option = []) { - $conn = []; - foreach (ZMBuf::$connect as $v) { - foreach ($option as $ks => $vs) { - if (($v->$ks ?? "") == $vs) continue; - else continue 2; - } - if ($v->getType() == $type) $conn[] = $v; - } - return $conn; - } - - public static function getTypeClassName(string $type) { - switch (strtolower($type)) { - case "qq": - case "universal": - return CQConnection::class; - case "webconsole": - return WCConnection::class; - case "proxy": - return ProxyConnection::class; - case "terminal": - return TerminalConnection::class; - default: - foreach (ZMBuf::$custom_connection_class as $v) { - /** @var WSConnection $r */ - $r = new $v(ZMBuf::$server, -1); - if ($r->getType() == strtolower($type)) return $v; - } - return UnknownConnection::class; - } - } - - public static function close($fd) { - foreach (ZMBuf::$connect as $k => $v) { - if ($v->fd == $fd) { - ZMBuf::$server->close($fd); - unset(ZMBuf::$connect[$k]); - break; - } - } - } - - public static function registerCustomClass() { - $classes = getAllClasses(DataProvider::getWorkingDir(). "/src/Custom/Connection/", "Custom\\Connection"); - ZMBuf::$custom_connection_class = $classes; - } -} diff --git a/src/ZM/Connection/ProxyConnection.php b/src/ZM/Connection/ProxyConnection.php deleted file mode 100644 index f43083f0..00000000 --- a/src/ZM/Connection/ProxyConnection.php +++ /dev/null @@ -1,13 +0,0 @@ -server = $server; - $this->fd = $fd; - } - - public abstract function getType(); - - public function exists() { - return $this->available = $this->server->exist($this->fd); - } - - public function close() { - ConnectionManager::close($this->fd); - } - - public function push($data, $push_error_record = true) { - if ($data === null || $data == "") { - Console::warning("推送了空消息"); - return false; - } - if (!$this->server->exist($this->fd)) { - Console::warning("Swoole 原生 websocket连接池中无此连接"); - return false; - } - if ($this->server->push($this->fd, $data) === false) { - $data = unicode_decode($data); - if ($push_error_record) Console::warning("API push failed. Data: " . $data); - Console::warning("websocket数据未成功推送,长度:" . strlen($data)); - return false; - } - return true; - } -} diff --git a/src/ZM/ConsoleApplication.php b/src/ZM/ConsoleApplication.php new file mode 100644 index 00000000..8e776c0a --- /dev/null +++ b/src/ZM/ConsoleApplication.php @@ -0,0 +1,104 @@ +selfCheck(); + + //if (LOAD_MODE === 0) $this->add(new BuildCommand()); //只有在git源码模式才能使用打包指令 + if (LOAD_MODE === 0) define("WORKING_DIR", getcwd()); + elseif (LOAD_MODE == 1) define("WORKING_DIR", realpath(__DIR__ . "/../../")); + elseif (LOAD_MODE == 2) echo "Phar mode: " . WORKING_DIR . PHP_EOL; + if (file_exists(DataProvider::getWorkingDir() . "/vendor/autoload.php")) { + /** @noinspection PhpIncludeInspection */ + require_once DataProvider::getWorkingDir() . "/vendor/autoload.php"; + } + if (LOAD_MODE == 2) { + // Phar 模式,2.0 不提供哦 + //require_once FRAMEWORK_DIR . "/vendor/autoload.php"; + spl_autoload_register('phar_classloader'); + } elseif (LOAD_MODE == 0) { + /** @noinspection PhpIncludeInspection + * @noinspection RedundantSuppression + */ + require_once WORKING_DIR . "/vendor/autoload.php"; + echo "* This is repository mode.\n"; + $composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true); + if (!isset($composer["autoload"]["psr-4"]["Module\\"])) { + echo "框架源码模式需要在autoload文件中添加Module目录为自动加载,是否添加?[Y/n] "; + $r = strtolower(trim(fgets(STDIN))); + if ($r === "" || $r === "y") { + $composer["autoload"]["psr-4"]["Module\\"] = "src/Module"; + $composer["autoload"]["psr-4"]["Custom\\"] = "src/Custom"; + $r = file_put_contents(DataProvider::getWorkingDir() . "/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + if ($r !== false) { + echo "成功添加!请重新进行 composer update !\n"; + exit(1); + } else { + echo "添加失败!请按任意键继续!"; + fgets(STDIN); + exit(1); + } + } else { + exit(1); + } + } + } + + $this->addCommands([ + new RunServerCommand(), //运行主服务的指令控制器 + new InitCommand(), //初始化用的,用于项目初始化和phar初始化 + new PureHttpCommand() //纯HTTP服务器指令 + ]); + /* + $command_register = ZMConfig::get("global", "command_register_class") ?? []; + foreach ($command_register as $v) { + $obj = new $v(); + if (!($obj instanceof Command)) throw new TypeError("Command register class must be extended by Symfony\\Component\\Console\\Command\\Command"); + $this->add($obj); + }*/ + } + + /** + * @param InputInterface|null $input + * @param OutputInterface|null $output + * @return int + */ + public function run(InputInterface $input = null, OutputInterface $output = null) { + try { + return parent::run($input, $output); + } catch (Exception $e) { + die("{$e->getMessage()} at {$e->getFile()}({$e->getLine()})"); + } + } + + private function selfCheck() { + if (!extension_loaded("swoole")) die("Can not find swoole extension.\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/19"); + if (version_compare(SWOOLE_VERSION, "4.4.13") == -1) die("You must install swoole version >= 4.4.13 !"); + //if (!extension_loaded("gd")) die("Can not find gd extension.\n"); + //if (!extension_loaded("sockets")) die("Can not find sockets extension.\n"); + if (substr(PHP_VERSION, 0, 1) < "7") die("PHP >=7 required.\n"); + //if (!function_exists("curl_exec")) die("Can not find curl extension.\n"); + //if (!class_exists("ZipArchive")) die("Can not find Zip extension.\n"); + //if (!file_exists(CRASH_DIR . "last_error.log")) die("Can not find log file.\n"); + return true; + } +} diff --git a/src/ZM/Context/Context.php b/src/ZM/Context/Context.php index 83c5b600..e1de0e1e 100644 --- a/src/ZM/Context/Context.php +++ b/src/ZM/Context/Context.php @@ -5,21 +5,22 @@ namespace ZM\Context; use Co; -use Framework\ZMBuf; +use Exception; use Swoole\Http\Request; use Swoole\WebSocket\Frame; use swoole_server; -use ZM\API\CQAPI; -use ZM\Connection\ConnectionManager; -use ZM\Connection\CQConnection; -use ZM\Connection\WSConnection; +use ZM\ConnectionManager\ConnectionObject; +use ZM\ConnectionManager\ManagerGM; +use ZM\Console\Console; use ZM\Exception\InvalidArgumentException; use ZM\Exception\WaitTimeoutException; use ZM\Http\Response; -use ZM\Utils\ZMRobot; +use ZM\API\ZMRobot; +use ZM\Utils\CoMessage; class Context implements ContextInterface { + public static $context = []; private $cid; public function __construct($cid) { $this->cid = $cid; } @@ -27,34 +28,34 @@ class Context implements ContextInterface /** * @return swoole_server|null */ - public function getServer() { return ZMBuf::$context[$this->cid]["server"] ?? null; } + public function getServer() { return self::$context[$this->cid]["server"] ?? server(); } /** * @return Frame|null */ - public function getFrame() { return ZMBuf::$context[$this->cid]["frame"] ?? null; } + public function getFrame() { return self::$context[$this->cid]["frame"] ?? null; } - public function getFd() { return ZMBuf::$context[$this->cid]["fd"] ?? $this->getFrame()->fd ?? null; } + public function getFd() { return self::$context[$this->cid]["fd"] ?? $this->getFrame()->fd ?? null; } /** * @return array|null */ - public function getData() { return ZMBuf::$context[$this->cid]["data"] ?? null; } + public function getData() { return self::$context[$this->cid]["data"] ?? null; } - public function setData($data) { ZMBuf::$context[$this->cid]["data"] = $data; } + public function setData($data) { self::$context[$this->cid]["data"] = $data; } /** * @return Request|null */ - public function getRequest() { return ZMBuf::$context[$this->cid]["request"] ?? null; } + public function getRequest() { return self::$context[$this->cid]["request"] ?? null; } /** * @return Response|null */ - public function getResponse() { return ZMBuf::$context[$this->cid]["response"] ?? null; } + public function getResponse() { return self::$context[$this->cid]["response"] ?? null; } - /** @return WSConnection */ - public function getConnection() { return ConnectionManager::get($this->getFd()); } + /** @return ConnectionObject|null */ + public function getConnection() { return ManagerGM::get($this->getFd()); } /** * @return int|null @@ -65,37 +66,37 @@ class Context implements ContextInterface * @return ZMRobot|null */ public function getRobot() { - $conn = ConnectionManager::get($this->getFrame()->fd); - return $conn instanceof CQConnection ? new ZMRobot($conn) : null; + $conn = ManagerGM::get($this->getFrame()->fd); + return $conn instanceof ConnectionObject ? new ZMRobot($conn) : null; } - public function getMessage() { return ZMBuf::$context[$this->cid]["data"]["message"] ?? null; } + public function getMessage() { return self::$context[$this->cid]["data"]["message"] ?? null; } - public function setMessage($msg) { ZMBuf::$context[$this->cid]["data"]["message"] = $msg; } + public function setMessage($msg) { self::$context[$this->cid]["data"]["message"] = $msg; } public function getUserId() { return $this->getData()["user_id"] ?? null; } - public function setUserId($id) { ZMBuf::$context[$this->cid]["data"]["user_id"] = $id; } + public function setUserId($id) { self::$context[$this->cid]["data"]["user_id"] = $id; } public function getGroupId() { return $this->getData()["group_id"] ?? null; } - public function setGroupId($id) { ZMBuf::$context[$this->cid]["data"]["group_id"] = $id; } + public function setGroupId($id) { self::$context[$this->cid]["data"]["group_id"] = $id; } public function getDiscussId() { return $this->getData()["discuss_id"] ?? null; } - public function setDiscussId($id) { ZMBuf::$context[$this->cid]["data"]["discuss_id"] = $id; } + public function setDiscussId($id) { self::$context[$this->cid]["data"]["discuss_id"] = $id; } public function getMessageType() { return $this->getData()["message_type"] ?? null; } - public function setMessageType($type) { ZMBuf::$context[$this->cid]["data"]["message_type"] = $type; } + public function setMessageType($type) { self::$context[$this->cid]["data"]["message_type"] = $type; } public function getRobotId() { return $this->getData()["self_id"] ?? null; } - public function getCache($key) { return ZMBuf::$context[$this->cid]["cache"][$key] ?? null; } + public function getCache($key) { return self::$context[$this->cid]["cache"][$key] ?? null; } - public function setCache($key, $value) { ZMBuf::$context[$this->cid]["cache"][$key] = $value; } + public function setCache($key, $value) { self::$context[$this->cid]["cache"][$key] = $value; } - public function getCQResponse() { return ZMBuf::$context[$this->cid]["cq_response"] ?? null; } + public function getCQResponse() { return self::$context[$this->cid]["cq_response"] ?? null; } /** * only can used by cq->message event function @@ -109,13 +110,21 @@ class Context implements ContextInterface case "private": case "discuss": $this->setCache("has_reply", true); - return CQAPI::quick_reply(ConnectionManager::get($this->getFrame()->fd), $this->getData(), $msg, $yield); + $data = $this->getData(); + $conn = $this->getConnection(); + switch ($data["message_type"]) { + case "group": + return (new ZMRobot($conn))->setCallback($yield)->sendGroupMsg($data["group_id"], $msg); + case "private": + return (new ZMRobot($conn))->setCallback($yield)->sendPrivateMsg($data["user_id"], $msg); + } + return null; } return false; } public function finalReply($msg, $yield = false) { - ZMBuf::$context[$this->cid]["cache"]["block_continue"] = true; + self::$context[$this->cid]["cache"]["block_continue"] = true; if ($msg == "") return true; return $this->reply($msg, $yield); } @@ -129,12 +138,24 @@ class Context implements ContextInterface * @throws WaitTimeoutException */ public function waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "") { - if ($prompt != "") $this->reply($prompt); if (!isset($this->getData()["user_id"], $this->getData()["message"], $this->getData()["self_id"])) throw new InvalidArgumentException("协程等待参数缺失"); + + Console::debug("==== 开始等待输入 ===="); + if ($prompt != "") $this->reply($prompt); + + try { + $r = CoMessage::yieldByWS($this->getData(), ["user_id", "self_id", "message_type", onebot_target_id_name($this->getMessageType())]); + } catch (Exception $e) { + $r = false; + } + if ($r === false) { + throw new WaitTimeoutException($this, $timeout_prompt); + } + return $r["message"]; + /* $cid = Co::getuid(); - $api_id = ZMBuf::$atomics["wait_msg_id"]->get(); - ZMBuf::$atomics["wait_msg_id"]->add(1); + $api_id = ZMAtomic::get("wait_msg_id")->add(1); $hang = [ "coroutine" => $cid, "user_id" => $this->getData()["user_id"], @@ -146,49 +167,57 @@ class Context implements ContextInterface if ($hang["message_type"] == "group" || $hang["message_type"] == "discuss") { $hang[$hang["message_type"] . "_id"] = $this->getData()[$this->getData()["message_type"] . "_id"]; } - ZMBuf::appendKey("wait_api", $api_id, $hang); + SpinLock::lock("wait_api"); + $hw = LightCacheInside::get("wait_api", "wait_api") ?? []; + $hw[$api_id] = $hang; + LightCacheInside::set("wait_api", "wait_api", $hw); + SpinLock::unlock("wait_api"); $id = swoole_timer_after($timeout * 1000, function () use ($api_id, $timeout_prompt) { - $r = ZMBuf::get("wait_api")[$api_id] ?? null; - if ($r !== null) { + $r = LightCacheInside::get("wait_api", "wait_api")[$api_id] ?? null; + if (is_array($r)) { Co::resume($r["coroutine"]); } }); Co::suspend(); - $sess = ZMBuf::get("wait_api")[$api_id]; - ZMBuf::unsetByValue("wait_api", $api_id); + SpinLock::lock("wait_api"); + $hw = LightCacheInside::get("wait_api", "wait_api") ?? []; + $sess = $hw[$api_id]; + unset($hw[$api_id]); + LightCacheInside::set("wait_api", "wait_api", $hw); $result = $sess["result"]; if (isset($id)) swoole_timer_clear($id); if ($result === null) throw new WaitTimeoutException($this, $timeout_prompt); - return $result; + return $result;*/ } /** - * @param $arg * @param $mode * @param $prompt_msg * @return mixed|string * @throws InvalidArgumentException * @throws WaitTimeoutException */ - public function getArgs(&$arg, $mode, $prompt_msg) { + public function getArgs($mode, $prompt_msg) { + $arg = ctx()->getCache("match"); switch ($mode) { case ZM_MATCH_ALL: $p = $arg; - array_shift($p); return trim(implode(" ", $p)) == "" ? $this->waitMessage($prompt_msg) : trim(implode(" ", $p)); case ZM_MATCH_NUMBER: foreach ($arg as $k => $v) { if (is_numeric($v)) { array_splice($arg, $k, 1); + ctx()->setCache("match", $arg); return $v; } } return $this->waitMessage($prompt_msg); case ZM_MATCH_FIRST: - if (isset($arg[1])) { - $a = $arg[1]; - array_splice($arg, 1, 1); + if (isset($arg[0])) { + $a = $arg[0]; + array_splice($arg, 0, 1); + ctx()->setCache("match", $arg); return $a; } else { return $this->waitMessage($prompt_msg); @@ -197,10 +226,28 @@ class Context implements ContextInterface throw new InvalidArgumentException(); } + /** + * @param string $prompt_msg + * @return int|mixed|string + * @throws InvalidArgumentException + * @throws WaitTimeoutException + */ + public function getNextArg($prompt_msg = "") { return $this->getArgs(ZM_MATCH_FIRST, $prompt_msg); } + + /** + * @param string $prompt_msg + * @return int|mixed|string + * @throws InvalidArgumentException + * @throws WaitTimeoutException + */ + public function getFullArg($prompt_msg = "") { return $this->getArgs(ZM_MATCH_ALL, $prompt_msg); } + public function cloneFromParent() { - set_coroutine_params(ZMBuf::$context[Co::getPcid()] ?? ZMBuf::$context[$this->cid]); + set_coroutine_params(self::$context[Co::getPcid()] ?? self::$context[$this->cid]); return context(); } - public function copy() { return ZMBuf::$context[$this->cid]; } + public function copy() { return self::$context[$this->cid]; } + + public function getOption() { return self::getCache("match"); } } diff --git a/src/ZM/Context/ContextInterface.php b/src/ZM/Context/ContextInterface.php index d736331d..00cbda60 100644 --- a/src/ZM/Context/ContextInterface.php +++ b/src/ZM/Context/ContextInterface.php @@ -7,9 +7,9 @@ namespace ZM\Context; use Swoole\Http\Request; use Swoole\WebSocket\Frame; use Swoole\WebSocket\Server; -use ZM\Connection\WSConnection; +use ZM\ConnectionManager\ConnectionObject; use ZM\Http\Response; -use ZM\Utils\ZMRobot; +use ZM\API\ZMRobot; interface ContextInterface { @@ -26,7 +26,7 @@ interface ContextInterface public function setData($data); - /** @return WSConnection */ + /** @return ConnectionObject */ public function getConnection(); /** @return int|null */ @@ -97,12 +97,15 @@ interface ContextInterface public function waitMessage($prompt = "", $timeout = 600, $timeout_prompt = ""); /** - * @param $arg * @param $mode * @param $prompt_msg * @return mixed */ - public function getArgs(&$arg, $mode, $prompt_msg); + public function getArgs($mode, $prompt_msg); + + public function getNextArg($prompt_msg = ""); + + public function getFullArg($prompt_msg = ""); public function setCache($key, $value); @@ -115,4 +118,6 @@ interface ContextInterface public function cloneFromParent(); public function copy(); + + public function getOption(); } diff --git a/src/ZM/DB/DB.php b/src/ZM/DB/DB.php index a2da49a0..0dd1c35d 100644 --- a/src/ZM/DB/DB.php +++ b/src/ZM/DB/DB.php @@ -1,15 +1,15 @@ -get(); + $conn = SqlPoolStorage::$sql_pool->get(); if ($conn === false) { - ZMBuf::$sql_pool->put(null); + SqlPoolStorage::$sql_pool->put(null); throw new DbException("无法连接SQL!" . $line); } $result = $conn->query($line) === false ? false : true; - ZMBuf::$sql_pool->put($conn); + SqlPoolStorage::$sql_pool->put($conn); return $result; } catch (DBException $e) { - if (ZMBuf::get("sql_log") === true) { - $log = - "[" . date("Y-m-d H:i:s") . - " " . round(microtime(true) - $starttime, 5) . - "] " . $line . " (Error:" . $e->getMessage() . ")\n"; - Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND); - } Console::warning($e->getMessage()); throw $e; } @@ -95,24 +85,21 @@ class DB * @throws DbException */ public static function rawQuery(string $line, $params = [], $fetch_mode = ZM_DEFAULT_FETCH_MODE) { - if (ZMBuf::get("sql_log") === true) { - $starttime = microtime(true); - } - Console::debug("MySQL: ".$line); + Console::debug("MySQL: ".$line." | ". implode(", ", $params)); try { - $conn = ZMBuf::$sql_pool->get(); + $conn = SqlPoolStorage::$sql_pool->get(); if ($conn === false) { - ZMBuf::$sql_pool->put(null); + SqlPoolStorage::$sql_pool->put(null); throw new DbException("无法连接SQL!" . $line); } $ps = $conn->prepare($line); if ($ps === false) { - ZMBuf::$sql_pool->put(null); + SqlPoolStorage::$sql_pool->put(null); throw new DbException("SQL语句查询错误," . $line . ",错误信息:" . $conn->error); } else { if (!($ps instanceof PDOStatement) && !($ps instanceof PDOStatementProxy)) { var_dump($ps); - ZMBuf::$sql_pool->put(null); + SqlPoolStorage::$sql_pool->put(null); throw new DbException("语句查询错误!返回的不是 PDOStatement" . $line); } if ($params == []) $result = $ps->execute(); @@ -120,28 +107,14 @@ class DB $result = $ps->execute([$params]); } else $result = $ps->execute($params); if ($result !== true) { - ZMBuf::$sql_pool->put(null); + SqlPoolStorage::$sql_pool->put(null); throw new DBException("语句[$line]错误!" . $ps->errorInfo()[2]); //echo json_encode(debug_backtrace(), 128 | 256); } - ZMBuf::$sql_pool->put($conn); - if (ZMBuf::get("sql_log") === true) { - $log = - "[" . date("Y-m-d H:i:s") . - " " . round(microtime(true) - $starttime, 4) . - "] " . $line . " " . json_encode($params, JSON_UNESCAPED_UNICODE) . "\n"; - Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND); - } + SqlPoolStorage::$sql_pool->put($conn); return $ps->fetchAll($fetch_mode); } } catch (DbException $e) { - if (ZMBuf::get("sql_log") === true) { - $log = - "[" . date("Y-m-d H:i:s") . - " " . round(microtime(true) - $starttime, 4) . - "] " . $line . " " . json_encode($params, JSON_UNESCAPED_UNICODE) . " (Error:" . $e->getMessage() . ")\n"; - Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND); - } if(mb_strpos($e->getMessage(), "has gone away") !== false) { zm_sleep(0.2); Console::warning("Gone away of MySQL! retrying!"); @@ -150,13 +123,6 @@ class DB Console::warning($e->getMessage()); throw $e; } catch (PDOException $e) { - if (ZMBuf::get("sql_log") === true) { - $log = - "[" . date("Y-m-d H:i:s") . - " " . round(microtime(true) - $starttime, 4) . - "] " . $line . " " . json_encode($params, JSON_UNESCAPED_UNICODE) . " (Error:" . $e->getMessage() . ")\n"; - Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND); - } if(mb_strpos($e->getMessage(), "has gone away") !== false) { zm_sleep(0.2); Console::warning("Gone away of MySQL! retrying!"); diff --git a/src/ZM/DB/SelectBody.php b/src/ZM/DB/SelectBody.php index 7d0f8995..903916db 100644 --- a/src/ZM/DB/SelectBody.php +++ b/src/ZM/DB/SelectBody.php @@ -4,7 +4,6 @@ namespace ZM\DB; -use Framework\Console; use ZM\Exception\DbException; class SelectBody @@ -46,17 +45,7 @@ class SelectBody * @throws DbException */ public function fetchAll($fetch_mode = ZM_DEFAULT_FETCH_MODE) { - if ($this->table->isCacheEnabled()) { - $rr = md5(implode(",", $this->select_thing) . serialize($this->where_thing)); - if (array_key_exists($rr, $this->table->cache)) { - Console::debug('SQL query cached: ' . $rr); - return $this->table->cache[$rr]->getResult(); - } - } $this->execute($fetch_mode); - if ($this->table->isCacheEnabled() && !in_array($rr, $this->table->cache)) { - $this->table->cache[$rr] = $this; - } return $this->getResult(); } diff --git a/src/ZM/DB/Table.php b/src/ZM/DB/Table.php index cf85d4cd..939bc8ad 100644 --- a/src/ZM/DB/Table.php +++ b/src/ZM/DB/Table.php @@ -14,10 +14,7 @@ class Table private static $table_instance = []; - private $enable_cache; - - public function __construct($table_name, $enable_cache) { - $this->enable_cache = $enable_cache; + public function __construct($table_name) { $this->table_name = $table_name; self::$table_instance[$table_name] = $this; } @@ -75,5 +72,4 @@ class Table */ public function getTableName() { return $this->table_name; } - public function isCacheEnabled() { return $this->enable_cache; } -} \ No newline at end of file +} diff --git a/src/ZM/Event/CQ/MessageEvent.php b/src/ZM/Event/CQ/MessageEvent.php deleted file mode 100644 index 11f9492a..00000000 --- a/src/ZM/Event/CQ/MessageEvent.php +++ /dev/null @@ -1,201 +0,0 @@ -data = $data; - $this->connection = $conn_or_response; - $this->circle = $circle; - } - - /** - * @return bool - * @throws AnnotationException - */ - public function onBefore() { - $obj_list = ZMBuf::$events[CQBefore::class]["message"] ?? []; - foreach ($obj_list as $v) { - if ($v->level < 200) break; - EventHandler::callWithMiddleware( - $v->class, - $v->method, - ["data" => context()->getData(), "connection" => $this->connection], - [], - function ($r) { - if (!$r) context()->setCache("block_continue", true); - } - ); - if (context()->getCache("block_continue") === true) return false; - } - foreach (ZMBuf::get("wait_api", []) as $k => $v) { - if (context()->getData()["user_id"] == $v["user_id"] && - context()->getData()["self_id"] == $v["self_id"] && - context()->getData()["message_type"] == $v["message_type"] && - (context()->getData()[context()->getData()["message_type"] . "_id"] ?? context()->getData()["user_id"]) == - ($v[$v["message_type"] . "_id"] ?? $v["user_id"])) { - $v["result"] = context()->getData()["message"]; - ZMBuf::appendKey("wait_api", $k, $v); - Co::resume($v["coroutine"]); - return false; - } - } - foreach (ZMBuf::$events[CQBefore::class]["message"] ?? [] as $v) { - if ($v->level >= 200) continue; - $c = $v->class; - if (ctx()->getCache("level") != 0) continue; - EventHandler::callWithMiddleware( - $c, - $v->method, - ["data" => context()->getData(), "connection" => $this->connection], - [], - function ($r) { - if (!$r) context()->setCache("block_continue", true); - } - ); - if (context()->getCache("block_continue") === true) return false; - } - return true; - } - - /** - * @throws AnnotationException - */ - public function onActivate() { - try { - $word = split_explode(" ", str_replace("\r", "", context()->getMessage())); - if (count(explode("\n", $word[0])) >= 2) { - $enter = explode("\n", context()->getMessage()); - $first = split_explode(" ", array_shift($enter)); - $word = array_merge($first, $enter); - foreach ($word as $k => $v) { - $word[$k] = trim($word[$k]); - } - } - /** @var ModBase[] $obj */ - $obj = []; - foreach (ZMBuf::$events[CQCommand::class] ?? [] as $v) { - /** @var CQCommand $v */ - if ($v->match == "" && $v->regexMatch == "" && $v->fullMatch == "") continue; - elseif (($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == context()->getData()["user_id"])) && - ($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (context()->getData()["group_id"] ?? 0))) && - ($v->discuss_id == 0 || ($v->discuss_id != 0 && $v->discuss_id == (context()->getData()["discuss_id"] ?? 0))) && - ($v->message_type == '' || ($v->message_type != '' && $v->message_type == context()->getData()["message_type"])) - ) { - $c = $v->class; - $class_construct = [ - "data" => context()->getData(), - "connection" => context()->getConnection() - ]; - if (!isset($obj[$c])) { - $obj[$c] = new $c($class_construct); - } - if ($word[0] != "" && $v->match == $word[0]) { - Console::debug("Calling $c -> {$v->method}"); - $this->function_call = EventHandler::callWithMiddleware($obj[$c], $v->method, $class_construct, [$word], function ($r) { - if (is_string($r)) context()->reply($r); - return true; - }); - return; - } elseif (in_array($word[0], $v->alias)) { - Console::debug("Calling $c -> {$v->method}"); - $this->function_call = EventHandler::callWithMiddleware($obj[$c], $v->method, $class_construct, [$word], function ($r) { - if (is_string($r)) context()->reply($r); - return true; - }); - return; - } elseif ($v->regexMatch != "" && ($args = matchArgs($v->regexMatch, context()->getMessage())) !== false) { - Console::debug("Calling $c -> {$v->method}"); - $this->function_call = EventHandler::callWithMiddleware($obj[$c], $v->method, $class_construct, [$args], function ($r) { - if (is_string($r)) context()->reply($r); - return true; - }); - return; - } elseif ($v->fullMatch != "" && (preg_match("/".$v->fullMatch."/u", ctx()->getMessage(), $args)) != 0) { - Console::debug("Calling $c -> {$v->method}"); - array_shift($args); - $this->function_call = EventHandler::callWithMiddleware($obj[$c], $v->method, $class_construct, [$args], function ($r) { - if (is_string($r)) context()->reply($r); - return true; - }); - return; - } - } - } - foreach (ZMBuf::$events[CQMessage::class] ?? [] as $v) { - /** @var CQMessage $v */ - if ( - ($v->message == '' || ($v->message != '' && $v->message == context()->getData()["message"])) && - ($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == context()->getData()["user_id"])) && - ($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (context()->getData()["group_id"] ?? 0))) && - ($v->discuss_id == 0 || ($v->discuss_id != 0 && $v->discuss_id == (context()->getData()["discuss_id"] ?? 0))) && - ($v->message_type == '' || ($v->message_type != '' && $v->message_type == context()->getData()["message_type"])) && - ($v->raw_message == '' || ($v->raw_message != '' && $v->raw_message == context()->getData()["raw_message"]))) { - $c = $v->class; - Console::debug("Calling CQMessage: $c -> {$v->method}"); - if (!isset($obj[$c])) - $obj[$c] = new $c([ - "data" => context()->getData(), - "connection" => $this->connection - ], ModHandleType::CQ_MESSAGE); - EventHandler::callWithMiddleware($obj[$c], $v->method, [], [context()->getData()["message"]], function ($r) { - if (is_string($r)) context()->reply($r); - }); - if (context()->getCache("block_continue") === true) return; - } - } - } catch (WaitTimeoutException $e) { - $e->module->finalReply($e->getMessage()); - } - } - - /** - * 在调用完事件后执行的 - * @throws AnnotationException - */ - public function onAfter() { - context()->setCache("block_continue", null); - foreach (ZMBuf::$events[CQAfter::class]["message"] ?? [] as $v) { - $c = $v->class; - EventHandler::callWithMiddleware( - $c, - $v->method, - ["data" => context()->getData(), "connection" => $this->connection], - [], - function ($r) { - if (!$r) context()->setCache("block_continue", true); - } - ); - if (context()->getCache("block_continue") === true) return false; - } - return true; - } - - public function hasReply() { - return $this->function_call; - } -} diff --git a/src/ZM/Event/CQ/MetaEvent.php b/src/ZM/Event/CQ/MetaEvent.php deleted file mode 100644 index bd418917..00000000 --- a/src/ZM/Event/CQ/MetaEvent.php +++ /dev/null @@ -1,79 +0,0 @@ -data = $data; - $this->connection = $connection; - $this->circle = $circle; - } - - /** - * @return bool - * @throws AnnotationException - */ - public function onBefore() { - foreach (ZMBuf::$events[CQBefore::class]["meta_event"] ?? [] as $v) { - $c = $v->class; - EventHandler::callWithMiddleware( - $c, - $v->method, - ["data" => context()->getData(), "connection" => $this->connection], - [], - function ($r) { - if(!$r) context()->setCache("block_continue", true); - } - ); - if(context()->getCache("block_continue") === true) return false; - } - return true; - } - - /** - * @throws AnnotationException - */ - public function onActivate() { - try { - /** @var ModBase[] $obj */ - $obj = []; - foreach (ZMBuf::$events[CQMetaEvent::class] ?? [] as $v) { - /** @var CQMetaEvent $v */ - if ( - ($v->meta_event_type == '' || ($v->meta_event_type != '' && $v->meta_event_type == $this->data["meta_event_type"])) && - ($v->sub_type == 0 || ($v->sub_type != 0 && $v->sub_type == $this->data["sub_type"]))) { - $c = $v->class; - if (!isset($obj[$c])) - $obj[$c] = new $c([ - "data" => $this->data, - "connection" => $this->connection - ], ModHandleType::CQ_META_EVENT); - EventHandler::callWithMiddleware($obj[$c],$v->method, [], [], function($r) { - if (is_string($r)) context()->reply($r); - }); - if (context()->getCache("block_continue") === true) return; - } - } - } catch (WaitTimeoutException $e) { - $e->module->finalReply($e->getMessage()); - } - } -} diff --git a/src/ZM/Event/CQ/NoticeEvent.php b/src/ZM/Event/CQ/NoticeEvent.php deleted file mode 100644 index 0973f826..00000000 --- a/src/ZM/Event/CQ/NoticeEvent.php +++ /dev/null @@ -1,103 +0,0 @@ -data = $data; - $this->connection = $connection; - $this->circle = $circle; - } - - /** - * @return bool - * @throws AnnotationException - */ - public function onBefore() { - foreach (ZMBuf::$events[CQBefore::class]["notice"] ?? [] as $v) { - $c = $v->class; - EventHandler::callWithMiddleware( - $c, - $v->method, - ["data" => context()->getData(), "connection" => $this->connection], - [], - function ($r) { - if(!$r) context()->setCache("block_continue", true); - } - ); - if(context()->getCache("block_continue") === true) return false; - } - return true; - } - - /** - * @throws AnnotationException - */ - public function onActivate() { - try { - /** @var ModBase[] $obj */ - $obj = []; - foreach (ZMBuf::$events[CQNotice::class] ?? [] as $v) { - /** @var CQNotice $v */ - if ( - ($v->notice_type == '' || ($v->notice_type != '' && $v->notice_type == $this->data["notice_type"])) && - ($v->sub_type == 0 || ($v->sub_type != 0 && $v->sub_type == $this->data["sub_type"])) && - ($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == ($this->data["group_id"] ?? 0))) && - ($v->operator_id == 0 || ($v->operator_id != 0 && $v->operator_id == ($this->data["operator_id"] ?? 0)))) { - $c = $v->class; - if (!isset($obj[$c])) - $obj[$c] = new $c([ - "data" => $this->data, - "connection" => $this->connection - ], ModHandleType::CQ_NOTICE); - EventHandler::callWithMiddleware($obj[$c],$v->method, [], [], function($r) { - if (is_string($r)) context()->reply($r); - }); - if (context()->getCache("block_continue") === true) return; - } - } - } /** @noinspection PhpRedundantCatchClauseInspection */ catch (WaitTimeoutException $e) { - $e->module->finalReply($e->getMessage()); - } - } - - /** - * @return bool - * @throws AnnotationException - */ - public function onAfter() { - foreach (ZMBuf::$events[CQAfter::class]["notice"] ?? [] as $v) { - $c = $v->class; - EventHandler::callWithMiddleware( - $c, - $v->method, - ["data" => context()->getData(), "connection" => $this->connection], - [], - function ($r) { - if(!$r) context()->setCache("block_continue", true); - } - ); - if(context()->getCache("block_continue") === true) return false; - } - return true; - } -} diff --git a/src/ZM/Event/CQ/RequestEvent.php b/src/ZM/Event/CQ/RequestEvent.php deleted file mode 100644 index 84f9cf7f..00000000 --- a/src/ZM/Event/CQ/RequestEvent.php +++ /dev/null @@ -1,103 +0,0 @@ -data = $data; - $this->connection = $connection; - $this->circle = $circle; - } - - /** - * @return bool - * @throws AnnotationException - */ - public function onBefore() { - foreach (ZMBuf::$events[CQBefore::class]["request"] ?? [] as $v) { - $c = $v->class; - EventHandler::callWithMiddleware( - $c, - $v->method, - ["data" => context()->getData(), "connection" => $this->connection], - [], - function ($r) { - if(!$r) context()->setCache("block_continue", true); - } - ); - if(context()->getCache("block_continue") === true) return false; - } - return true; - } - - /** - * @throws AnnotationException - */ - public function onActivate() { - try { - /** @var ModBase[] $obj */ - $obj = []; - foreach (ZMBuf::$events[CQRequest::class] ?? [] as $v) { - /** @var CQRequest $v */ - if ( - ($v->request_type == '' || ($v->request_type != '' && $v->request_type == $this->data["request_type"])) && - ($v->sub_type == 0 || ($v->sub_type != 0 && $v->sub_type == $this->data["sub_type"])) && - ($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == ($this->data["user_id"] ?? 0))) && - ($v->comment == 0 || ($v->comment != 0 && $v->comment == ($this->data["comment"] ?? 0)))) { - $c = $v->class; - if (!isset($obj[$c])) - $obj[$c] = new $c([ - "data" => $this->data, - "connection" => $this->connection - ], ModHandleType::CQ_REQUEST); - EventHandler::callWithMiddleware($obj[$c],$v->method, [], [], function($r) { - if (is_string($r)) context()->reply($r); - }); - if (context()->getCache("block_continue") === true) return; - } - } - } catch (WaitTimeoutException $e) { - $e->module->finalReply($e->getMessage()); - } - } - - /** - * @return bool - * @throws AnnotationException - */ - public function onAfter() { - foreach (ZMBuf::$events[CQAfter::class]["request"] ?? [] as $v) { - $c = $v->class; - EventHandler::callWithMiddleware( - $c, - $v->method, - ["data" => context()->getData(), "connection" => $this->connection], - [], - function ($r) { - if(!$r) context()->setCache("block_continue", true); - } - ); - if(context()->getCache("block_continue") === true) return false; - } - return true; - } -} diff --git a/src/ZM/Event/Event.php b/src/ZM/Event/Event.php deleted file mode 100644 index f78aff33..00000000 --- a/src/ZM/Event/Event.php +++ /dev/null @@ -1,11 +0,0 @@ -class = $class; + try { + $this->eid = ZMAtomic::get("_event_id")->add(1); + $list = LightCacheInside::get("wait_api", "event_trace"); + } catch (ZMException $e) { + $list = []; + } + if (isset($list[$class])) $this->log = true; + if ($this->log) Console::verbose("[事件分发{$this->eid}] 开始分发事件: " . $class); + } + + public function setRuleFunction(callable $rule = null) { + $this->rule = $rule; + return $this; + } + + public function setReturnFunction(callable $return_func) { + $this->return_func = $return_func; + return $this; + } + + public function dispatchEvents(...$params) { + try { + foreach ((EventManager::$events[$this->class] ?? []) as $v) { + $result = $this->dispatchEvent($v, $this->rule, ...$params); + if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 分发结束。"); + if ($result !== false && is_callable($this->return_func)) { + if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 正在执行返回值处理函数 ..."); + ($this->return_func)($result); + } + } + return true; + } catch (InterruptException $e) { + return $e->return_var; + } catch (AnnotationException $e) { + return false; + } + } + + /** + * @param AnnotationBase|null $v + * @param null $rule_func + * @param mixed ...$params + * @throws AnnotationException + * @throws InterruptException + * @return mixed + */ + public function dispatchEvent(?AnnotationBase $v, $rule_func = null, ...$params) { + $q_c = $v->class; + $q_f = $v->method; + if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在判断 " . $q_c . "::" . $q_f . " 方法下的 rule ..."); + if ($rule_func !== null && !$rule_func($v)) { + if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 rule 判断为 false, 拒绝执行此方法。"); + return false; + } + if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 rule 为真,继续执行方法本身 ..."); + if (isset(EventManager::$middleware_map[$q_c][$q_f])) { + $middlewares = EventManager::$middleware_map[$q_c][$q_f]; + if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法还绑定了 Middleware:" . implode(", ", $middlewares)); + $before_result = true; + $r = []; + foreach ($middlewares as $k => $middleware) { + if (!isset(EventManager::$middlewares[$middleware])) throw new AnnotationException("Annotation parse error: Unknown MiddlewareClass named \"{$middleware}\"!"); + $middleware_obj = EventManager::$middlewares[$middleware]; + $before = $middleware_obj["class"]; + //var_dump($middleware_obj); + $r[$k] = new $before(); + $r[$k]->class = $q_c; + $r[$k]->method = $q_f; + if (isset($middleware_obj["before"])) { + if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 存在前置事件,执行中 ..."); + $rs = $middleware_obj["before"]; + $before_result = $r[$k]->$rs(...$params); + if ($before_result === false) { + if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 前置事件为 false,停止执行原事件,开始执行下一事件。"); + break; + } else { + if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 前置事件为 true,继续执行原事件。"); + } + } + } + if ($before_result) { + try { + $q_o = ZMUtil::getModInstance($q_c); + if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ..."); + $result = $q_o->$q_f(...$params); + } catch (Exception $e) { + if ($e instanceof InterruptException) { + if ($this->log) Console::verbose("[事件分发{$this->eid}] 检测到事件阻断调用,正在跳出事件分发器 ..."); + throw $e; + } + if ($this->log) Console::verbose("[事件分发{$this->eid}] 方法 " . $q_c . "::" . $q_f . " 执行过程中抛出了异常,正在倒序查找 Middleware 中的捕获方法 ..."); + for ($i = count($middlewares) - 1; $i >= 0; --$i) { + $middleware_obj = EventManager::$middlewares[$middlewares[$i]]; + if (!isset($middleware_obj["exceptions"])) continue; + foreach ($middleware_obj["exceptions"] as $name => $method) { + if ($e instanceof $name) { + if ($this->log) Console::verbose("[事件分发{$this->eid}] 方法 " . $q_c . "::" . $q_f . " 的异常 " . get_class($e) . " 被 Middleware:" . $middlewares[$i] . " 下的 " . get_class($r[$i]) . "::" . $method . " 捕获。"); + $r[$i]->$method($e); + self::interrupt(); + } + } + } + throw $e; + } + for ($i = count($middlewares) - 1; $i >= 0; --$i) { + $middleware_obj = EventManager::$middlewares[$middlewares[$i]]; + if (isset($middleware_obj["after"], $r[$i])) { + if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 存在后置事件,执行中 ..."); + $r[$i]->{$middleware_obj["after"]}(...$params); + if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 后置事件执行完毕!"); + } + } + return $result; + } + return false; + } else { + $q_o = ZMUtil::getModInstance($q_c); + if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ..."); + return $q_o->$q_f(...$params); + } + } +} diff --git a/src/ZM/Event/EventHandler.php b/src/ZM/Event/EventHandler.php deleted file mode 100644 index 7d7dce31..00000000 --- a/src/ZM/Event/EventHandler.php +++ /dev/null @@ -1,320 +0,0 @@ -shutdown(); - else ZMBuf::$server->shutdown(); - }); - ZMBuf::$server = $param0; - $r = (new WorkerStartEvent($param0, $param1))->onActivate(); - Console::log("\n=== Worker #" . $param0->worker_id . " 已启动 ===\n", "gold"); - $r->onAfter(); - self::startTick(); - } catch (Exception $e) { - Console::error("Worker加载出错!停止服务!"); - Console::error($e->getMessage() . "\n" . $e->getTraceAsString()); - ZMUtil::stop(); - return; - } catch (Error $e) { - Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine()); - Console::error("Maybe it caused by your own code if in your own Module directory."); - Console::log($e->getTraceAsString(), 'gray'); - ZMUtil::stop(); - } - break; - case "message": - /** @var Frame $param1 */ - /** @var Server $param0 */ - $conn = ConnectionManager::get($param1->fd); - set_coroutine_params(["server" => $param0, "frame" => $param1, "connection" => $conn]); - try { - (new MessageEvent($param0, $param1))->onActivate()->onAfter(); - } catch (Error $e) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - Console::error("Fatal error when calling $event_name: " . $error_msg); - Console::stackTrace(); - } - break; - case "request": - try { - set_coroutine_params(["request" => $param0, "response" => $param1]); - (new RequestEvent($param0, $param1))->onActivate()->onAfter(); - } catch (Exception $e) { - /** @var Response $param1 */ - $param1->status(500); - Console::info($param0->server["remote_addr"] . ":" . $param0->server["remote_port"] . - " [" . $param1->getStatusCode() . "] " . $param0->server["request_uri"] - ); - if (!$param1->isEnd()) $param1->end("Internal server error: " . $e->getMessage()); - Console::error("Internal server exception (500), caused by " . get_class($e)); - Console::log($e->getTraceAsString(), "gray"); - } catch (Error $e) { - /** @var Response $param1 */ - $param1->status(500); - Console::info($param0->server["remote_addr"] . ":" . $param0->server["remote_port"] . - " [" . $param1->getStatusCode() . "] " . $param0->server["request_uri"] - ); - $doc = "Internal server error
"; - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - if (ZMBuf::$atomics["info_level"]->get() >= 4) $doc .= $error_msg; - if (!$param1->isEnd()) $param1->end($doc); - Console::error("Internal server error (500): " . $error_msg); - Console::log($e->getTraceAsString(), "gray"); - } - break; - case "open": - /** @var Request $param1 */ - set_coroutine_params(["server" => $param0, "request" => $param1, "fd" => $param1->fd]); - try { - (new WSOpenEvent($param0, $param1))->onActivate()->onAfter(); - } catch (Error $e) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - Console::error("Fatal error when calling $event_name: " . $error_msg); - Console::stackTrace(); - } - break; - case "close": - set_coroutine_params(["server" => $param0, "fd" => $param1]); - try { - (new WSCloseEvent($param0, $param1))->onActivate()->onAfter(); - } catch (Error $e) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - Console::error("Fatal error when calling $event_name: " . $error_msg); - Console::stackTrace(); - } - break; - } - //Console::info(Console::setColor("Event: " . $event_name . " 运行了 " . round(microtime(true) - $starttime, 5) . " 秒", "gold")); - } - - /** - * @param $event_data - * @param $conn_or_response - * @param int $level - * @return bool - * @throws AnnotationException - */ - public static function callCQEvent($event_data, $conn_or_response, int $level = 0) { - ctx()->setCache("level", $level); - if ($level >= 5) { - Console::warning("Recursive call reached " . $level . " times"); - Console::stackTrace(); - return false; - } - $starttime = microtime(true); - switch ($event_data["post_type"]) { - case "message": - $event = new CQ\MessageEvent($event_data, $conn_or_response, $level); - if ($event->onBefore()) $event->onActivate(); - $event->onAfter(); - return $event->hasReply(); - break; - case "notice": - $event = new CQ\NoticeEvent($event_data, $conn_or_response, $level); - if ($event->onBefore()) $event->onActivate(); - $event->onAfter(); - return true; - case "request": - $event = new CQ\RequestEvent($event_data, $conn_or_response, $level); - if ($event->onBefore()) $event->onActivate(); - $event->onAfter(); - return true; - case "meta_event": - $event = new CQ\MetaEvent($event_data, $conn_or_response, $level); - if ($event->onBefore()) $event->onActivate(); - return true; - } - unset($starttime); - return false; - } - - /** - * @param $req - * @throws AnnotationException - */ - public static function callCQResponse($req) { - Console::debug("收到来自API连接的回复:" . json_encode($req, 128 | 256)); - $status = $req["status"]; - $retcode = $req["retcode"]; - $data = $req["data"]; - if (isset($req["echo"]) && ZMBuf::array_key_exists("sent_api", $req["echo"])) { - $origin = ZMBuf::get("sent_api")[$req["echo"]]; - $self_id = $origin["self_id"]; - $response = [ - "status" => $status, - "retcode" => $retcode, - "data" => $data, - "self_id" => $self_id, - "echo" => $req["echo"] - ]; - set_coroutine_params(["cq_response" => $response]); - if (isset(ZMBuf::$events[CQAPIResponse::class][$req["retcode"]])) { - list($c, $method) = ZMBuf::$events[CQAPIResponse::class][$req["retcode"]]; - $class = new $c(["data" => $origin["data"]]); - call_user_func_array([$class, $method], [$origin["data"], $req]); - } - $origin_ctx = ctx()->copy(); - ctx()->setCache("action", $origin["data"]["action"] ?? "unknown"); - ctx()->setData($origin["data"]); - foreach (ZMBuf::$events[CQAPISend::class] ?? [] as $k => $v) { - if (($v->action == "" || $v->action == ctx()->getCache("action")) && $v->with_result) { - $c = $v->class; - self::callWithMiddleware($c, $v->method, context()->copy(), [ctx()->getCache("action"), $origin["data"]["params"] ?? [], ctx()->getRobotId()]); - if (context()->getCache("block_continue") === true) break; - } - } - set_coroutine_params($origin_ctx); - if (($origin["func"] ?? null) !== null) { - call_user_func($origin["func"], $response, $origin["data"]); - } elseif (($origin["coroutine"] ?? false) !== false) { - $p = ZMBuf::get("sent_api"); - $p[$req["echo"]]["result"] = $response; - ZMBuf::set("sent_api", $p); - Co::resume($origin['coroutine']); - } - ZMBuf::unsetByValue("sent_api", $req["echo"]); - } - } - - public static function callCQAPISend($reply, ?CQConnection $connection) { - $action = $reply["action"] ?? null; - if ($action === null) { - Console::warning("API 激活事件异常!"); - return; - } - if (ctx() === null) $content = []; - else $content = ctx()->copy(); - go(function () use ($action, $reply, $connection, $content) { - set_coroutine_params($content); - context()->setCache("action", $action); - context()->setCache("reply", $reply); - foreach (ZMBuf::$events[CQAPISend::class] ?? [] as $k => $v) { - if (($v->action == "" || $v->action == $action) && !$v->with_result) { - $c = $v->class; - self::callWithMiddleware($c, $v->method, context()->copy(), [$reply["action"], $reply["params"] ?? [], $connection->getQQ()]); - if (context()->getCache("block_continue") === true) break; - } - } - }); - } - - /** - * @param $c - * @param $method - * @param array $class_construct - * @param array $func_args - * @param null $after_call - * @return mixed|null - * @throws AnnotationException - * @throws Exception - */ - public static function callWithMiddleware($c, $method, array $class_construct, array $func_args, $after_call = null) { - $return_value = null; - $plain_class = is_object($c) ? get_class($c) : $c; - if (isset(ZMBuf::$events[MiddlewareInterface::class][$plain_class][$method])) { - $middlewares = ZMBuf::$events[MiddlewareInterface::class][$plain_class][$method]; - $before_result = true; - $r = []; - foreach ($middlewares as $k => $middleware) { - if (!isset(ZMBuf::$events[MiddlewareClass::class][$middleware])) throw new AnnotationException("Annotation parse error: Unknown MiddlewareClass named \"{$middleware}\"!"); - $middleware_obj = ZMBuf::$events[MiddlewareClass::class][$middleware]; - $before = $middleware_obj["class"]; - $r[$k] = new $before(); - $r[$k]->class = is_object($c) ? get_class($c) : $c; - $r[$k]->method = $method; - if (isset($middleware_obj["before"])) { - $before_result = call_user_func_array([$r[$k], $middleware_obj["before"]], $func_args); - if ($before_result === false) break; - } - } - if ($before_result) { - try { - if (is_object($c)) $class = $c; - elseif ($class_construct == []) $class = ZMUtil::getModInstance($c); - else $class = new $c($class_construct); - $result = call_user_func_array([$class, $method], $func_args); - if (is_callable($after_call)) - $return_value = call_user_func_array($after_call, [$result]); - } catch (Exception $e) { - for ($i = count($middlewares) - 1; $i >= 0; --$i) { - $middleware_obj = ZMBuf::$events[MiddlewareClass::class][$middlewares[$i]]; - if (!isset($middleware_obj["exceptions"])) continue; - foreach ($middleware_obj["exceptions"] as $name => $method) { - if ($e instanceof $name) { - $r[$i]->$method($e); - context()->setCache("block_continue", true); - } - } - if (context()->getCache("block_continue") === true) return $return_value; - } - throw $e; - } - } - for ($i = count($middlewares) - 1; $i >= 0; --$i) { - $middleware_obj = ZMBuf::$events[MiddlewareClass::class][$middlewares[$i]]; - if (isset($middleware_obj["after"], $r[$i])) - call_user_func_array([$r[$i], $middleware_obj["after"]], $func_args); - } - } else { - if (is_object($c)) $class = $c; - elseif ($class_construct == []) $class = ZMUtil::getModInstance($c); - else $class = new $c($class_construct); - $result = call_user_func_array([$class, $method], $func_args); - if (is_callable($after_call)) - $return_value = call_user_func_array($after_call, [$result]); - } - return $return_value; - } - - private static function startTick() { - Console::debug("Starting " . count(ZMBuf::get("paused_tick", [])) . " custom tick function"); - foreach (ZMBuf::get("paused_tick", []) as $cid) { - Co::resume($cid); - } - - } -} diff --git a/src/ZM/Event/EventManager.php b/src/ZM/Event/EventManager.php new file mode 100644 index 00000000..3fea9f3e --- /dev/null +++ b/src/ZM/Event/EventManager.php @@ -0,0 +1,62 @@ +generateAnnotationEvents(); + self::$middlewares = $parser->getMiddlewares(); + self::$middleware_map = $parser->getMiddlewareMap(); + self::$req_mapping = $parser->getReqMapping(); + } + + /** + * 注册所有计时器给每个进程 + */ + public static function registerTimerTick() { + $dispatcher = new EventDispatcher(OnTick::class); + foreach (self::$events[OnTick::class] ?? [] as $vss) { + if (server()->worker_id !== $vss->worker_id) return; + //echo server()->worker_id.PHP_EOL; + $plain_class = $vss->class; + Console::debug("Added Middleware-based timer: " . $plain_class . " -> " . $vss->method); + Timer::tick($vss->tick_ms, function () use ($vss, $dispatcher) { + set_coroutine_params([]); + if (ZMAtomic::get("stop_signal")->get() != 0) { + Timer::clearAll(); + return; + } + try { + $dispatcher->dispatchEvent($vss, null); + } catch (Exception $e) { + Console::error("Uncaught error from TimerTick: " . $e->getMessage() . " at " . $e->getFile() . "({$e->getLine()})"); + } catch (Error $e) { + Console::error("Uncaught fatal error from TimerTick: " . $e->getMessage()); + echo Console::setColor($e->getTraceAsString(), "gray"); + Console::error("Please check your code!"); + } + }); + } + } +} diff --git a/src/ZM/Event/ServerEventHandler.php b/src/ZM/Event/ServerEventHandler.php new file mode 100644 index 00000000..e1b75265 --- /dev/null +++ b/src/ZM/Event/ServerEventHandler.php @@ -0,0 +1,560 @@ +getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"); + } catch (Error $e) { + Console::error("Uncaught error " . get_class($e) . ": " . $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"); + } + }); + } + Process::signal(SIGINT, function () use ($r) { + echo "\r"; + Console::warning("Server interrupted by keyboard on Master."); + if ((Framework::$server->inotify ?? null) !== null) + /** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify); + ZMUtil::stop(); + }); + if (Framework::$argv["watch"]) { + if (extension_loaded('inotify')) { + Console::warning("Enabled File watcher, do not use in production."); + /** @noinspection PhpUndefinedFieldInspection */ + Framework::$server->inotify = $fd = inotify_init(); + $this->addWatcher(DataProvider::getWorkingDir() . "/src", $fd); + Event::add($fd, function () use ($fd) { + $r = inotify_read($fd); + var_dump($r); + ZMUtil::reload(); + }); + } else { + Console::warning("You have not loaded inotify extension."); + } + } + } + + /** + * @SwooleHandler("shutdown") + */ + public function onShutdown() { + Console::debug("正在关闭 Master 进程,pid=" . posix_getpid()); + } + + /** + * @SwooleHandler("WorkerStop") + * @param $server + * @param $worker_id + */ + public function onWorkerStop(Server $server, $worker_id) { + Console::debug(($server->taskworker ? "Task" : "") . "Worker #$worker_id 已停止"); + } + + /** + * @SwooleHandler("WorkerStart") + * @param Server $server + * @param $worker_id + */ + public function onWorkerStart(Server $server, $worker_id) { + //if (ZMBuf::atomic("stop_signal")->get() != 0) return; + Process::signal(SIGINT, function () use ($worker_id, $server) { + Console::debug("正在关闭 " . ($server->taskworker ? "Task" : "") . "Worker 进程 " . Console::setColor("#" . \server()->worker_id, "gold") . TermColor::frontColor256(59) . ", pid=" . posix_getpid()); + server()->stop($worker_id); + }); + unset(Context::$context[Co::getCid()]); + if ($server->taskworker === false) { + try { + register_shutdown_function(function () use ($server) { + $error = error_get_last(); + if ($error["type"] != 0) { + Console::error("Internal fatal error: " . $error["message"] . " at " . $error["file"] . "({$error["line"]})"); + } + //DataProvider::saveBuffer(); + /** @var Server $server */ + if (server() === null) $server->shutdown(); + else server()->shutdown(); + }); + + Console::info("Worker #{$server->worker_id} 启动中"); + Framework::$server = $server; + //ZMBuf::resetCache(); //清空变量缓存 + //ZMBuf::set("wait_start", []); //添加队列,在workerStart运行完成前先让其他协程等待执行 + foreach ($server->connections as $v) { + $server->close($v); + } + + + // 这里执行的是只需要执行一遍的代码,比如终端监听器和键盘监听器 + /*if ($server->worker_id === 0) { + global $terminal_id; + if ($terminal_id !== null) + go(function () { + while (true) { + $r = server()->process->exportSocket(); + $result = $r->recv(); + try { + if (!Terminal::executeCommand($result)) { + //if ($result == "stop" || $result == "reload" || $result == "r") { + //echo "Stopped.\n"; + break; + } + } catch (Exception $e) { + Console::error($e->getMessage()); + } catch (Error $e) { + Console::error($e->getMessage()); + } + } + }); + }*/ + //TODO: 单独抽出来MySQL和Redis连接池 + if (ZMConfig::get("global", "sql_config")["sql_host"] != "") { + if (SqlPoolStorage::$sql_pool !== null) { + SqlPoolStorage::$sql_pool->close(); + SqlPoolStorage::$sql_pool = null; + } + Console::info("新建SQL连接池中"); + ob_start(); + phpinfo(); + $str = ob_get_clean(); + $str = explode("\n", $str); + foreach ($str as $k => $v) { + $v = trim($v); + if ($v == "") continue; + if (mb_strpos($v, "API Extensions") === false) continue; + if (mb_strpos($v, "pdo_mysql") === false) { + throw new DbException("未安装 mysqlnd php-mysql扩展。"); + } + } + $sql = ZMConfig::get("global", "sql_config"); + SqlPoolStorage::$sql_pool = new PDOPool((new PDOConfig()) + ->withHost($sql["sql_host"]) + ->withPort($sql["sql_port"]) + // ->withUnixSocket('/tmp/mysql.sock') + ->withDbName($sql["sql_database"]) + ->withCharset('utf8mb4') + ->withUsername($sql["sql_username"]) + ->withPassword($sql["sql_password"]) + ->withOptions($sql["sql_options"] ?? [PDO::ATTR_STRINGIFY_FETCHES => false]) + ); + DB::initTableList(); + } + + // 开箱即用的Redis + $redis = ZMConfig::get("global", "redis_config"); + if($redis !== null && $redis["host"] != "") { + if (!extension_loaded("redis")) Console::error("Can not find redis extension.\n"); + else ZMRedisPool::init($redis); + } + + $this->loadAnnotations(); //加载composer资源、phar外置包、注解解析注册等 + + //echo json_encode(debug_backtrace(), 128|256); + Console::success("Worker #" . $worker_id . " 已启动"); + EventManager::registerTimerTick(); //启动计时器 + //ZMBuf::unsetCache("wait_start"); + set_coroutine_params(["server" => $server, "worker_id" => $worker_id]); + $dispatcher = new EventDispatcher(OnStart::class); + $dispatcher->setRuleFunction(function ($v) { + return server()->worker_id === $v->worker_id || $v->worker_id === -1; + }); + $dispatcher->dispatchEvents($server, $worker_id); + Console::debug("@OnStart 执行完毕"); + } catch (Exception $e) { + Console::error("Worker加载出错!停止服务!"); + Console::error($e->getMessage() . "\n" . $e->getTraceAsString()); + ZMUtil::stop(); + return; + } catch (Error $e) { + Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine()); + Console::error("Maybe it caused by your own code if in your own Module directory."); + Console::log($e->getTraceAsString(), 'gray'); + ZMUtil::stop(); + } + } else { + // 这里是TaskWorker初始化的内容部分 + try { + Framework::$server = $server; + $this->loadAnnotations(); + Console::debug("TaskWorker #" . $server->worker_id . " 已启动"); + } catch (Exception $e) { + Console::error("Worker加载出错!停止服务!"); + Console::error($e->getMessage() . "\n" . $e->getTraceAsString()); + ZMUtil::stop(); + return; + } catch (Error $e) { + Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine()); + Console::error("Maybe it caused by your own code if in your own Module directory."); + Console::log($e->getTraceAsString(), 'gray'); + ZMUtil::stop(); + } + } + } + + /** + * @SwooleHandler("message") + * @param $server + * @param Frame $frame + */ + public function onMessage($server, Frame $frame) { + + Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd.": ".TermColor::ITALIC.$frame->data.TermColor::RESET); + unset(Context::$context[Co::getCid()]); + $conn = ManagerGM::get($frame->fd); + set_coroutine_params(["server" => $server, "frame" => $frame, "connection" => $conn]); + $dispatcher = new EventDispatcher(OnSwooleEvent::class); + $dispatcher->setRuleFunction(function ($v) { + if ($v->getRule() == '') { + return strtolower($v->type) == 'message'; + } else { + /** @noinspection PhpUnreachableStatementInspection + * @noinspection RedundantSuppression + */ + if (strtolower($v->type) == 'message' && eval("return " . $v->getRule() . ";")) return true; + else return false; + } + }); + try { + //$starttime = microtime(true); + $dispatcher->dispatchEvents($conn); + //Console::success("Used ".round((microtime(true) - $starttime) * 1000, 3)." ms!"); + } catch (Exception $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught exception " . get_class($e) . " when calling \"message\": " . $error_msg); + Console::trace(); + } catch (Error $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught Error " . get_class($e) . " when calling \"message\": " . $error_msg); + Console::trace(); + } + + } + + /** + * @SwooleHandler("request") + * @param $request + * @param $response + */ + public function onRequest($request, $response) { + $response = new Response($response); + unset(Context::$context[Co::getCid()]); + Console::debug("Calling Swoole \"request\" event from fd=" . $request->fd); + set_coroutine_params(["request" => $request, "response" => $response]); + + $dis = new EventDispatcher(OnSwooleEvent::class); + $dis->setRuleFunction(function ($v) { + if ($v->getRule() == '') { + return strtolower($v->type) == 'request'; + } else { + /** @noinspection PhpUnreachableStatementInspection */ + if (strtolower($v->type) == 'request' && eval("return " . $v->getRule() . ";")) return true; + else return false; + } + }); + + try { + $no_interrupt = $dis->dispatchEvents($request, $response); + if ($no_interrupt !== null) { + $result = HttpUtil::parseUri($request, $response, $request->server["request_uri"], $node, $params); + if ($result === true) { + ctx()->setCache("params", $params); + $dispatcher = new EventDispatcher(RequestMapping::class); + $div = new RequestMapping(); + $div->route = $node["route"]; + $div->params = $params; + $div->method = $node["method"]; + $div->request_method = $node["request_method"]; + $div->class = $node["class"]; + //Console::success("正在执行路由:".$node["method"]); + $r = $dispatcher->dispatchEvent($div, null, $params, $request, $response); + if (is_string($r) && !$response->isEnd()) $response->end($r); + } + } + if (!$response->isEnd()) { + //Console::warning('返回了404'); + HttpUtil::responseCodePage($response, 404); + } + } catch (Exception $e) { + $response->status(500); + Console::info($request->server["remote_addr"] . ":" . $request->server["remote_port"] . + " [" . $response->getStatusCode() . "] " . $request->server["request_uri"] + ); + if (!$response->isEnd()) { + if (ZMConfig::get("global", "debug_mode")) + $response->end("Internal server exception: " . $e->getMessage()); + else + $response->end("Internal server error."); + } + Console::error("Internal server exception (500), caused by " . get_class($e).": ".$e->getMessage()); + Console::log($e->getTraceAsString(), "gray"); + } catch (Error $e) { + $response->status(500); + Console::info($request->server["remote_addr"] . ":" . $request->server["remote_port"] . + " [" . $response->getStatusCode() . "] " . $request->server["request_uri"] + ); + if (!$response->isEnd()) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + if (ZMConfig::get("global", "debug_mode")) + $response->end("Internal server error: " . $error_msg); + else + $response->end("Internal server error."); + } + Console::error("Internal server error (500), caused by " . get_class($e).": ".$e->getMessage()); + Console::log($e->getTraceAsString(), "gray"); + } + } + + /** + * @SwooleHandler("open") + * @param $server + * @param Request $request + */ + public function onOpen($server, Request $request) { + Console::debug("Calling Swoole \"open\" event from fd=" . $request->fd); + unset(Context::$context[Co::getCid()]); + $type = strtolower($request->get["type"] ?? $request->header["x-client-role"] ?? ""); + $type_conn = ManagerGM::getTypeClassName($type); + ManagerGM::pushConnect($request->fd, $type_conn); + $conn = ManagerGM::get($request->fd); + set_coroutine_params(["server" => $server, "request" => $request, "connection" => $conn, "fd" => $request->fd]); + $conn->setOption("connect_id", strval($request->header["x-self-id"]) ?? ""); + + $dispatcher = new EventDispatcher(OnSwooleEvent::class); + $dispatcher->setRuleFunction(function ($v) { + if ($v->getRule() == '') { + return strtolower($v->type) == 'open'; + } else { + /** @noinspection PhpUnreachableStatementInspection */ + if (strtolower($v->type) == 'open' && eval("return " . $v->getRule() . ";")) return true; + else return false; + } + }); + try { + if ($conn->getName() === 'qq' && ZMConfig::get("global", "modules")["onebot"]["status"] === true) { + if (ZMConfig::get("global", "modules")["onebot"]["single_bot_mode"]) { + LightCacheInside::set("connect", "conn_fd", $request->fd); + } + } + $dispatcher->dispatchEvents($conn); + } catch (Exception $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught exception " . get_class($e) . " when calling \"open\": " . $error_msg); + Console::trace(); + } catch (Error $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught Error " . get_class($e) . " when calling \"open\": " . $error_msg); + Console::trace(); + } + //EventHandler::callSwooleEvent("open", $server, $request); + } + + /** + * @SwooleHandler("close") + * @param $server + * @param $fd + */ + public function onClose($server, $fd) { + unset(Context::$context[Co::getCid()]); + $conn = ManagerGM::get($fd); + if ($conn === null) return; + Console::debug("Calling Swoole \"close\" event from fd=" . $fd); + set_coroutine_params(["server" => $server, "connection" => $conn, "fd" => $fd]); + + + $dispatcher = new EventDispatcher(OnSwooleEvent::class); + $dispatcher->setRuleFunction(function ($v) { + if ($v->getRule() == '') { + return strtolower($v->type) == 'close'; + } else { + /** @noinspection PhpUnreachableStatementInspection */ + if (strtolower($v->type) == 'close' && eval("return " . $v->getRule() . ";")) return true; + else return false; + } + }); + try { + if ($conn->getName() === 'qq' && ZMConfig::get("global", "modules")["onebot"]["status"] === true) { + if (ZMConfig::get("global", "modules")["onebot"]["single_bot_mode"]) { + LightCacheInside::set("connect", "conn_fd", -1); + } + } + $dispatcher->dispatchEvents($conn); + } catch (Exception $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught exception " . get_class($e) . " when calling \"close\": " . $error_msg); + Console::trace(); + } catch (Error $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught Error " . get_class($e) . " when calling \"close\": " . $error_msg); + Console::trace(); + } + ManagerGM::popConnect($fd); + } + + /** + * @SwooleHandler("pipeMessage") + * @param $server + * @param $src_worker_id + * @param $data + */ + public function onPipeMessage(Server $server, $src_worker_id, $data) { + //var_dump($data, $server->worker_id); + //unset(Context::$context[Co::getCid()]); + $data = json_decode($data, true); + switch ($data["action"] ?? '') { + case "resume_ws_message": + $obj = $data["data"]; + Co::resume($obj["coroutine"]); + break; + case "stop": + Console::verbose('正在清理 #' . $server->worker_id . ' 的计时器'); + Timer::clearAll(); + break; + case "terminate": + $server->stop(); + break; + case 'echo': + Console::success('接收到来自 #' . $src_worker_id . ' 的消息'); + break; + case 'send': + $server->sendMessage(json_encode(["action" => "echo"]), $data["target"]); + break; + default: + echo $data . PHP_EOL; + } + } + + /** + * @SwooleHandler("task") + */ + public function onTask() { + } + + /** + * @throws ReflectionException + * @throws Exception + */ + private function loadAnnotations() { + //加载phar包 + /*Console::debug("加载外部phar包中"); + $dir = DataProvider::getWorkingDir() . "/resources/package/"; + if (version_compare(SWOOLE_VERSION, "4.4.0", ">=")) Timer::clearAll(); + if (is_dir($dir)) { + $list = scandir($dir); + unset($list[0], $list[1]); + foreach ($list as $v) { + if (is_dir($dir . $v)) continue; + if (pathinfo($dir . $v, 4) == "phar") { + Console::debug("加载Phar: " . $dir . $v . " 中"); + require_once($dir . $v); + } + } + }*/ + + //加载各个模块的注解类,以及反射 + Console::debug("检索Module中"); + $parser = new AnnotationParser(); + $parser->addRegisterPath(DataProvider::getWorkingDir() . "/src/Module/", "Module"); + $parser->registerMods(); + EventManager::loadEventByParser($parser); //加载事件 + + //加载自定义的全局函数 + Console::debug("加载自定义上下文中..."); + $context_class = ZMConfig::get("global", "context_class"); + if (!is_a($context_class, ContextInterface::class, true)) { + throw new Exception("Context class must implemented from ContextInterface!"); + } + + //加载插件 + $plugins = ZMConfig::get("global", "modules") ?? []; + if (!isset($plugins["onebot"])) $plugins["onebot"] = ["status" => true, "single_bot_mode" => false]; + + if ($plugins["onebot"]) { + $obj = new OnSwooleEvent(); + $obj->class = QQBot::class; + $obj->method = 'handle'; + $obj->type = 'message'; + $obj->level = 99999; + $obj->rule = 'connectIsQQ()'; + EventManager::addEvent(OnSwooleEvent::class, $obj); + if ($plugins["onebot"]["single_bot_mode"]) { + LightCacheInside::set("connect", "conn_fd", -1); + } else { + LightCacheInside::set("connect", "conn_fd", -2); + } + } + + //TODO: 编写加载外部插件的方式 + $this->loadExternalModules($plugins); + } + + private function addWatcher($maindir, $fd) { + $dir = scandir($maindir); + unset($dir[0], $dir[1]); + foreach ($dir as $subdir) { + if (is_dir($maindir . "/" . $subdir)) { + Console::debug("添加监听目录:" . $maindir . "/" . $subdir); + inotify_add_watch($fd, $maindir . "/" . $subdir, IN_ATTRIB | IN_ISDIR); + $this->addWatcher($maindir . "/" . $subdir, $fd); + } + } + } + + private function loadExternalModules($plugins) { + foreach ($plugins as $k => $v) { + if ($k == "onebot") continue; + + } + } +} diff --git a/src/ZM/Event/Swoole/MessageEvent.php b/src/ZM/Event/Swoole/MessageEvent.php deleted file mode 100644 index 0c6f9298..00000000 --- a/src/ZM/Event/Swoole/MessageEvent.php +++ /dev/null @@ -1,103 +0,0 @@ -server = $server; - $this->frame = $frame; - } - - /** - * @inheritDoc - */ - public function onActivate() { - ZMUtil::checkWait(); - $conn = ConnectionManager::get(context()->getFrame()->fd); - try { - if ($conn->getType() == "qq") { - $data = json_decode(context()->getFrame()->data, true); - if (isset($data["post_type"])) { - set_coroutine_params(["data" => $data, "connection" => $conn]); - ctx()->setCache("level", 0); - Console::debug("Calling CQ Event from fd=" . $conn->fd); - EventHandler::callCQEvent($data, ConnectionManager::get(context()->getFrame()->fd), 0); - } else{ - set_coroutine_params(["connection" => $conn]); - EventHandler::callCQResponse($data); - } - } - foreach (ZMBuf::$events[SwooleEventAt::class] ?? [] as $v) { - if (strtolower($v->type) == "message" && $this->parseSwooleRule($v)) { - $c = $v->class; - EventHandler::callWithMiddleware( - $c, - $v->method, - ["server" => $this->server, "frame" => $this->frame, "connection" => $conn], - [$conn] - ); - if (context()->getCache("block_continue") === true) break; - } - } - } catch (Exception $e) { - Console::warning("Websocket message event exception: " . (($cs = $e->getMessage()) == "" ? get_class($e) : $cs)); - Console::warning("In ". $e->getFile() . " at line ".$e->getLine()); - } - return $this; - } - - /** - * @inheritDoc - */ - public function onAfter() { - foreach (ZMBuf::$events[SwooleEventAfter::class] ?? [] as $v) { - if (strtolower($v->type) == "message" && $this->parseSwooleRule($v) === true) { - $conn = ConnectionManager::get($this->frame->fd); - $c = $v->class; - /** @var ModBase $class */ - $class = new $c(["server" => $this->server, "frame" => $this->frame, "connection" => $conn], ModHandleType::SWOOLE_MESSAGE); - call_user_func_array([$class, $v->method], []); - if (context()->getCache("block_continue") === true) break; - } - } - return $this; - } - - private function parseSwooleRule($v) { - switch (explode(":", $v->rule)[0]) { - case "connectType": //websocket连接类型 - if ($v->callback instanceof Closure) return call_user_func($v->callback, ConnectionManager::get($this->frame->fd)); - break; - case "dataEqual": //handle websocket message事件时才能用 - if ($v->callback instanceof Closure) return call_user_func($v->callback, $this->frame->data); - break; - } - return true; - } -} diff --git a/src/ZM/Event/Swoole/RequestEvent.php b/src/ZM/Event/Swoole/RequestEvent.php deleted file mode 100644 index 45e59bf4..00000000 --- a/src/ZM/Event/Swoole/RequestEvent.php +++ /dev/null @@ -1,198 +0,0 @@ -request = $request; - $this->response = $response; - } - - /** - * @return $this|SwooleEvent - * @throws Exception - */ - public function onActivate() - { - ZMUtil::checkWait(); - foreach (ZMBuf::globals("http_header") as $k => $v) { - $this->response->setHeader($k, $v); - } - $uri = $this->request->server["request_uri"]; - Console::verbose($this->request->server["remote_addr"] . " request " . $uri); - $uri = explode("/", $uri); - $uri = array_diff($uri, ["..", "", "."]); - $node = ZMBuf::$req_mapping; - $params = []; - while (true) { - $r = array_shift($uri); - if ($r === null) { - if ($node == ZMBuf::$req_mapping) goto statics; - else break; - } - if (($cnt = count($node["son"] ?? [])) == 1) { - if (isset($node["param_route"])) { - foreach ($node["son"] as $k => $v) { - if ($v["id"] == $node["param_route"]) { - $node = $v; - $params[mb_substr($v["name"], 1, -1)] = $r; - continue 2; - } - } - } elseif ($node["son"][0]["name"] == $r) { - $node = $node["son"][0]; - continue; - } - } elseif ($cnt >= 1) { - if (isset($node["param_route"])) { - foreach ($node["son"] as $k => $v) { - if ($v["id"] == $node["param_route"]) { - $node = $v; - $params[mb_substr($v["name"], 1, -1)] = $r; - continue 2; - } - } - } - foreach ($node["son"] as $k => $v) { - if ($v["name"] == $r) { - $node = $v; - continue 2; - } - } - } - statics: - if (ZMBuf::globals("static_file_server")["status"]) { - $base_dir = ZMBuf::globals("static_file_server")["document_root"]; - $base_index = ZMBuf::globals("static_file_server")["document_index"]; - $uri = $this->request->server["request_uri"]; - $path = realpath($base_dir . urldecode($uri)); - if ($path !== false) { - if (is_dir($path) && mb_substr($uri, -1, 1) != "/") { - $this->response->redirect($uri . "/", 301); - $this->response->end(); - return $this; - } - if (is_dir($path)) $path = $path . '/'; - $work = realpath(DataProvider::getWorkingDir()) . '/'; - if (strpos($path, $work) !== 0) { - $this->responseStatus(403); - return $this; - } - if (is_dir($path)) { - foreach ($base_index as $vp) { - if (is_file($path . $vp)) { - Console::info("[200] " . $uri . " (static)"); - $exp = strtolower(pathinfo($path . $vp)['extension'] ?? "unknown"); - $this->response->setHeader("Content-Type", ZMBuf::config("file_header")[$exp] ?? "application/octet-stream"); - $this->response->end(file_get_contents($path . $vp)); - return $this; - } - } - } elseif (is_file($path)) { - Console::info("[200] " . $uri . " (static)"); - $exp = strtolower(pathinfo($path)['extension'] ?? "unknown"); - $this->response->setHeader("Content-Type", ZMBuf::config("file_header")[$exp] ?? "application/octet-stream"); - $this->response->end(file_get_contents($path)); - return $this; - } - } - } - $this->response->status(404); - $this->response->end(ZMUtil::getHttpCodePage(404)); - return $this; - } - - context()->setCache("params", $params); - - if (in_array(strtoupper($this->request->server["request_method"]), $node["request_method"] ?? [])) { //判断目标方法在不在里面 - $c_name = $node["class"]; - EventHandler::callWithMiddleware( - $c_name, - $node["method"], - ["request" => $this->request, "response" => &$this->response, "params" => $params], - [$params], - function ($result) { - if (is_string($result) && !$this->response->isEnd()) $this->response->end($result); - if ($this->response->isEnd()) context()->setCache("block_continue", true); - } - ); - } - foreach (ZMBuf::$events[SwooleEventAt::class] ?? [] as $v) { - if (strtolower($v->type) == "request" && $this->parseSwooleRule($v)) { - $c = $v->class; - EventHandler::callWithMiddleware($c, $v->method, ["request" => $this->request, "response" => $this->response], []); - if (context()->getCache("block_continue") === true) break; - } - } - - if (!$this->response->isEnd()) { - $this->response->status(404); - $this->response->end(ZMUtil::getHttpCodePage(404)); - } - return $this; - } - - /** - * @inheritDoc - */ - public function onAfter() - { - foreach (ZMBuf::$events[SwooleEventAfter::class] ?? [] as $v) { - if (strtolower($v->type) == "request" && $this->parseSwooleRule($v)) { - $c = $v->class; - $class = new $c(["request" => $this->request, "response" => $this->response]); - call_user_func_array([$class, $v->method], []); - if ($class->block_continue) break; - } - } - return $this; - } - - private function responseStatus(int $int) - { - $this->response->status($int); - $this->response->end(); - } - - private function parseSwooleRule($v) - { - switch (explode(":", $v->rule)[0]) { - case "containsGet": - case "containsPost": - if ($v->callback instanceof Closure) return call_user_func($v->callback, $this->request); - break; - case "containsJson": - $content = $this->request->rawContent(); - $content = json_decode($content, true); - if ($content === null) return false; - if ($v->callback instanceof Closure) return call_user_func($v->callback, $content); - break; - } - return true; - } -} diff --git a/src/ZM/Event/Swoole/SwooleEvent.php b/src/ZM/Event/Swoole/SwooleEvent.php deleted file mode 100644 index 73b1d54e..00000000 --- a/src/ZM/Event/Swoole/SwooleEvent.php +++ /dev/null @@ -1,20 +0,0 @@ -server = $server; - $this->fd = $fd; - } - - /** - * @inheritDoc - * @throws AnnotationException - */ - public function onActivate() { - ZMUtil::checkWait(); - set_coroutine_params(["server" => $this->server, "fd" => $this->fd, "connection" => ConnectionManager::get($this->fd)]); - foreach(ZMBuf::$events[SwooleEventAt::class] ?? [] as $v) { - if(strtolower($v->type) == "close" && $this->parseSwooleRule($v)) { - $c = $v->class; - EventHandler::callWithMiddleware($c, $v->method, ["server" => $this->server, "fd" => $this->fd], []); - if(context()->getCache("block_continue") === true) break; - } - } - ConnectionManager::close($this->fd); - return $this; - } - - /** - * @inheritDoc - * @throws AnnotationException - */ - public function onAfter() { - foreach (ZMBuf::$events[SwooleEventAfter::class] ?? [] as $v) { - if (strtolower($v->type) == "close" && $this->parseSwooleRule($v) === true) { - $c = $v->class; - EventHandler::callWithMiddleware($c, $v->method, ["server" => $this->server, "fd" => $this->fd], []); - if(context()->getCache("block_continue") === true) break; - } - } - return $this; - } - - private function parseSwooleRule($v) { - switch (explode(":", $v->rule)[0]) { - case "connectType": //websocket连接类型 - if ($v->callback instanceof Closure) return call_user_func($v->callback, ConnectionManager::get($this->fd)); - break; - } - return true; - } -} diff --git a/src/ZM/Event/Swoole/WSOpenEvent.php b/src/ZM/Event/Swoole/WSOpenEvent.php deleted file mode 100644 index 833749e1..00000000 --- a/src/ZM/Event/Swoole/WSOpenEvent.php +++ /dev/null @@ -1,109 +0,0 @@ -server = $server; - $this->request = $request; - } - - /** - * @inheritDoc - * @throws AnnotationException - */ - public function onActivate() { - ZMUtil::checkWait(); - $type = strtolower($this->request->get["type"] ?? $this->request->header["x-client-role"] ?? ""); - $type_conn = ConnectionManager::getTypeClassName($type); - if ($type_conn == CQConnection::class) { - $qq = $this->request->get["qq"] ?? $this->request->header["x-self-id"] ?? ""; - $self_token = ZMBuf::globals("access_token") ?? ""; - if(isset($this->request->header["authorization"])) { - Console::debug($this->request->header["authorization"]); - } - $remote_token = $this->request->get["token"] ?? (isset($this->request->header["authorization"]) ? explode(" ", $this->request->header["authorization"])[1] : ""); - if ($qq != "" && ($self_token == $remote_token)) $this->conn = new CQConnection($this->server, $this->request->fd, $qq); - else { - $this->conn = new UnknownConnection($this->server, $this->request->fd); - Console::warning("connection of CQ has invalid QQ or token!"); - Console::debug("Remote token: ".$remote_token); - } - } else { - $this->conn = new $type_conn($this->server, $this->request->fd); - } - ZMBuf::$connect[$this->request->fd] = $this->conn; - set_coroutine_params(["server" => $this->server, "request" => $this->request, "connection" => $this->conn]); - foreach (ZMBuf::$events[SwooleEventAt::class] ?? [] as $v) { - if (strtolower($v->type) == "open" && $this->parseSwooleRule($v) === true) { - $c = $v->class; - EventHandler::callWithMiddleware( - $c, - $v->method, - ["server" => $this->server, "request" => $this->request, "connection" => $this->conn], - [$this->conn] - ); - if (context()->getCache("block_continue") === true) break; - } - } - return $this; - } - - /** - * @inheritDoc - */ - public function onAfter() { - if (!$this->conn->exists()) return $this; - foreach (ZMBuf::$events[SwooleEventAfter::class] ?? [] as $v) { - if (strtolower($v->type) == "open" && $this->parseSwooleRule($v) === true) { - /** @var ModBase $class */ - $class = new $v["class"](["server" => $this->server, "request" => $this->request, "connection" => $this->conn], ModHandleType::SWOOLE_OPEN); - call_user_func_array([$class, $v["method"]], [$this->conn]); - if (context()->getCache("block_continue") === true) break; - } - } - return $this; - } - - private function parseSwooleRule($v) { - switch (explode(":", $v->rule)[0]) { - case "connectType": //websocket连接类型 - if ($v->callback instanceof Closure) return call_user_func($v->callback, $this->conn); - break; - } - return true; - } -} diff --git a/src/ZM/Event/Swoole/WorkerStartEvent.php b/src/ZM/Event/Swoole/WorkerStartEvent.php deleted file mode 100644 index 5eb11223..00000000 --- a/src/ZM/Event/Swoole/WorkerStartEvent.php +++ /dev/null @@ -1,212 +0,0 @@ -server = $server; - $this->worker_id = $worker_id; - } - - /** - * @return WorkerStartEvent - * @throws AnnotationException - * @throws ReflectionException - * @throws DbException - */ - public function onActivate(): WorkerStartEvent { - Console::info("Worker启动中"); - ZMBuf::$server = $this->server; - Console::listenConsole(); //这个方法只能在这里调用,且如果worker_num不为1的话,此功能不可用 - - Process::signal(SIGINT, function () { - Console::warning("Server interrupted by keyboard."); - ZMUtil::stop(true); - }); - ZMBuf::resetCache(); //清空变量缓存 - ZMBuf::set("wait_start", []); //添加队列,在workerStart运行完成前先让其他协程等待执行 - $this->resetConnections();//释放所有与framework的连接 - - //设置炸毛buf中储存的对象 - ZMBuf::$globals = new GlobalConfig(); - ZMBuf::$config = []; - $file = scandir(DataProvider::getWorkingDir() . '/config/'); - unset($file[0], $file[1]); - foreach ($file as $k => $v) { - if ($v == "global.php") continue; - $name = explode(".", $v); - if (($prefix = end($name)) == "json") { - ZMBuf::$config[$name[0]] = json_decode(Co::readFile(DataProvider::getWorkingDir() . '/config/' . $v), true); - Console::info("已读取配置文件:" . $v); - } elseif ($prefix == "php") { - ZMBuf::$config[$name[0]] = include_once DataProvider::getWorkingDir() . '/config/' . $v; - if (is_array(ZMBuf::$config[$name[0]])) - Console::info("已读取配置文件:" . $v); - } - } - if (ZMBuf::globals("sql_config")["sql_host"] != "") { - Console::info("新建SQL连接池中"); - ob_start(); - phpinfo(); - $str = ob_get_clean(); - $str = explode("\n", $str); - foreach($str as $k => $v) { - $v = trim($v); - if($v == "") continue; - if(mb_strpos($v, "API Extensions") === false) continue; - if(mb_strpos($v, "pdo_mysql") === false) { - throw new DbException("未安装 mysqlnd php-mysql扩展。"); - } - } - $sql = ZMBuf::globals("sql_config"); - ZMBuf::$sql_pool = new PDOPool((new PDOConfig()) - ->withHost($sql["sql_host"]) - ->withPort($sql["sql_port"]) - // ->withUnixSocket('/tmp/mysql.sock') - ->withDbName($sql["sql_database"]) - ->withCharset('utf8mb4') - ->withUsername($sql["sql_username"]) - ->withPassword($sql["sql_password"]) - ->withOptions($sql["sql_options"] ?? [PDO::ATTR_STRINGIFY_FETCHES => false]) - ); - DB::initTableList(); - } - - ZMBuf::$atomics['reload_time']->add(1); - - Console::info("监听console输入"); - - $this->setAutosaveTimer(ZMBuf::globals("auto_save_interval")); - $this->loadAllClass(); //加载composer资源、phar外置包、注解解析注册等 - return $this; - } - - /** - * @return WorkerStartEvent - * @throws AnnotationException - */ - public function onAfter(): WorkerStartEvent { - foreach (ZMBuf::get("wait_start") as $v) { - Coroutine::resume($v); - } - ZMBuf::unsetCache("wait_start"); - set_coroutine_params(["server" => $this->server, "worker_id" => $this->worker_id]); - - foreach (ZMBuf::$events[OnStart::class] ?? [] as $v) { - $class_name = $v->class; - Console::debug("正在调用启动时函数: " . $class_name . " -> " . $v->method); - EventHandler::callWithMiddleware($class_name, $v->method, ["server" => $this->server, "worker_id" => $this->worker_id], []); - } - foreach (ZMBuf::$events[SwooleEventAfter::class] ?? [] as $v) { - /** @var AnnotationBase $v */ - if (strtolower($v->type) == "workerstart") { - $class_name = $v->class; - Console::debug("正在调用启动时函数after: " . $class_name . " -> " . $v->method); - EventHandler::callWithMiddleware($class_name, $v->method, ["server" => $this->server, "worker_id" => $this->worker_id], []); - if (context()->getCache("block_continue") === true) break; - } - } - Console::debug("调用完毕!"); - return $this; - } - - private function resetConnections() { - foreach ($this->server->connections as $v) { - $this->server->close($v); - } - if (ZMBuf::$sql_pool !== null) { - ZMBuf::$sql_pool->close(); - ZMBuf::$sql_pool = null; - } - } - - /** - * @throws AnnotationException - * @throws ReflectionException - * @throws Exception - */ - private function loadAllClass() { - //加载phar包 - Console::info("加载外部phar包中"); - $dir = DataProvider::getWorkingDir() . "/resources/package/"; - if (version_compare(SWOOLE_VERSION, "4.4.0", ">=")) Timer::clearAll(); - if (is_dir($dir)) { - $list = scandir($dir); - unset($list[0], $list[1]); - foreach ($list as $v) { - if (is_dir($dir . $v)) continue; - if (pathinfo($dir . $v, 4) == "phar") { - Console::verbose("加载Phar: " . $dir . $v . " 中"); - require_once($dir . $v); - } - } - } - //加载composer类 - //remove stupid duplicate code - - //加载各个模块的注解类,以及反射 - Console::info("检索Module中"); - AnnotationParser::registerMods(); - - //加载Custom目录下的自定义的内部类 - ConnectionManager::registerCustomClass(); - - //加载自定义的全局函数 - Console::debug("加载自定义的全局函数中"); - if (file_exists(DataProvider::getWorkingDir() . "/src/Custom/global_function.php")) - require_once DataProvider::getWorkingDir() . "/src/Custom/global_function.php"; - $this->afterCheck(); - } - - private function setAutosaveTimer($globals) { - DataProvider::$buffer_list = []; - zm_timer_tick($globals * 1000, function () { - DataProvider::saveBuffer(); - }); - } - - /** - * @throws Exception - */ - private function afterCheck() { - $context_class = ZMBuf::globals("context_class"); - if (!is_a($context_class, ContextInterface::class, true)) { - throw new Exception("Context class must implemented from ContextInterface!"); - } - } -} diff --git a/src/ZM/Exception/DbException.php b/src/ZM/Exception/DbException.php index f5575ce2..007d9ffe 100644 --- a/src/ZM/Exception/DbException.php +++ b/src/ZM/Exception/DbException.php @@ -4,9 +4,7 @@ namespace ZM\Exception; -use Exception; - -class DbException extends Exception +class DbException extends ZMException { -} \ No newline at end of file +} diff --git a/src/ZM/Exception/InterruptException.php b/src/ZM/Exception/InterruptException.php new file mode 100644 index 00000000..cb97d954 --- /dev/null +++ b/src/ZM/Exception/InterruptException.php @@ -0,0 +1,17 @@ +return_var = $return_var; + } +} diff --git a/src/ZM/Exception/InvalidArgumentException.php b/src/ZM/Exception/InvalidArgumentException.php index eb4b47a5..0052a29e 100644 --- a/src/ZM/Exception/InvalidArgumentException.php +++ b/src/ZM/Exception/InvalidArgumentException.php @@ -4,9 +4,7 @@ namespace ZM\Exception; -use Exception; - -class InvalidArgumentException extends Exception +class InvalidArgumentException extends ZMException { -} \ No newline at end of file +} diff --git a/src/ZM/Exception/NotInitializedException.php b/src/ZM/Exception/NotInitializedException.php new file mode 100644 index 00000000..6f56e830 --- /dev/null +++ b/src/ZM/Exception/NotInitializedException.php @@ -0,0 +1,10 @@ +module = $module; } -} \ No newline at end of file +} diff --git a/src/ZM/Exception/ZMException.php b/src/ZM/Exception/ZMException.php new file mode 100644 index 00000000..01d8bf96 --- /dev/null +++ b/src/ZM/Exception/ZMException.php @@ -0,0 +1,12 @@ +getTtyWidth(); + + self::$argv = $args; + + //定义常量 + include_once "global_defines.php"; + + ZMConfig::setDirectory(DataProvider::getWorkingDir() . '/config'); + ZMConfig::setEnv($args["env"] ?? ""); + if (ZMConfig::get("global") === false) { + die ("Global config load failed: " . ZMConfig::$last_error . "\nPlease init first!\n"); + } + ZMAtomic::init(); + try { + ManagerGM::init(ZMConfig::get("global", "swoole")["max_connection"] ?? 2048, 0.5, [ + [ + "key" => "connect_id", + "type" => "string", + "size" => 30 + ], + [ + "key" => "type", + "type" => "int" + ] + ]); + } catch (ConnectionManager\TableException $e) { + die($e->getMessage()); + } + try { + self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port")); + $this->server_set = ZMConfig::get("global", "swoole"); + Console::init( + ZMConfig::get("global", "info_level") ?? 2, + self::$server, + $args["log-theme"] ?? "default", + ($o = ZMConfig::get("console_color")) === false ? [] : $o + ); + + $timezone = ZMConfig::get("global", "timezone") ?? "Asia/Shanghai"; + date_default_timezone_set($timezone); + + $this->parseCliArgs(self::$argv); + + $out = [ + "host" => ZMConfig::get("global", "host"), + "port" => ZMConfig::get("global", "port"), + "log_level" => Console::getLevel(), + "version" => ZM_VERSION, + "config" => $args["env"] === null ? 'global.php' : $args["env"] + ]; + if (isset(ZMConfig::get("global", "swoole")["task_worker_num"])) { + $out["task_worker_num"] = ZMConfig::get("global", "swoole")["task_worker_num"]; + } + if (($num = ZMConfig::get("global", "swoole")["worker_num"] ?? swoole_cpu_num()) != 1) { + $out["worker_num"] = $num; + } + $out["working_dir"] = DataProvider::getWorkingDir(); + Console::printProps($out, $tty_width); + + self::$server->set($this->server_set); + if (file_exists(DataProvider::getWorkingDir() . "/config/motd.txt")) { + $motd = file_get_contents(DataProvider::getWorkingDir() . "/config/motd.txt"); + } else { + $motd = file_get_contents(__DIR__ . "/../../config/motd.txt"); + } + $motd = explode("\n", $motd); + foreach ($motd as $k => $v) { + $motd[$k] = substr($v, 0, $tty_width); + } + $motd = implode("\n", $motd); + echo $motd; + global $asd; + $asd = get_included_files(); + // 注册 Swoole Server 的事件 + $this->registerServerEvents(); + $r = ZMConfig::get("global", "light_cache") ?? [ + "size" => 1024, + "max_strlen" => 8192, + "hash_conflict_proportion" => 0.6, + "persistence_path" => realpath(DataProvider::getDataFolder() . "_cache.json"), + "auto_save_interval" => 900 + ]; + LightCache::init($r); + LightCacheInside::init(); + SpinLock::init($r["size"]); + } catch (Exception $e) { + Console::error("Framework初始化出现错误,请检查!"); + Console::error($e->getMessage()); + die; + } + } + + public function start() { + self::$server->start(); + } + + /** + * 从全局配置文件里读取注入系统事件的类 + * @throws ReflectionException + * @throws ReflectionException + */ + private function registerServerEvents() { + $all_event_class = ZMConfig::get("global", "server_event_handler_class") ?? []; + if (!in_array(ServerEventHandler::class, $all_event_class)) { + $all_event_class[] = ServerEventHandler::class; + } + $event_list = []; + foreach ($all_event_class as $v) { + $reader = new AnnotationReader(); + $reflection_class = new ReflectionClass($v); + $methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC); + foreach ($methods as $vs) { + $method_annotations = $reader->getMethodAnnotations($vs); + if ($method_annotations != []) { + $annotation = $method_annotations[0]; + if ($annotation instanceof SwooleHandler) { + $annotation->class = $v; + $annotation->method = $vs->getName(); + $event_list[strtolower($annotation->event)] = $annotation; + } elseif ($annotation instanceof OnSetup) { + $annotation->class = $v; + $annotation->method = $vs->getName(); + $c = new $v(); + $m = $annotation->method; + $c->$m(); + } + } + } + } + foreach ($event_list as $k => $v) { + self::$server->on($k, function (...$param) use ($v) { + $c = ZMUtil::getModInstance($v->class); + $m = $v->method; + $c->$m(...$param); + }); + } + } + + /** + * 解析命令行的 $argv 参数们 + * @param $args + * @throws Exception + */ + private function parseCliArgs($args) { + $coroutine_mode = true; + global $terminal_id; + $terminal_id = call_user_func(function () { + try { + $data = random_bytes(16); + } catch (Exception $e) { + return ""; + } + $data[6] = chr(ord($data[6]) & 0x0f | 0x40); + $data[8] = chr(ord($data[8]) & 0x3f | 0x80); + return strtoupper(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4))); + }); + foreach ($args as $x => $y) { + switch ($x) { + case 'disable-coroutine': + if ($y) { + $coroutine_mode = false; + } + break; + case 'debug-mode': + if ($y || ZMConfig::get("global", "debug_mode")) { + $coroutine_mode = false; + $terminal_id = null; + Console::warning("You are in debug mode, do not use in production!"); + } + break; + case 'daemon': + if ($y) { + $this->server_set["daemonize"] = 1; + Console::log("已启用守护进程,输出重定向到 " . $this->server_set["log_file"]); + $terminal_id = null; + } + break; + case 'disable-console-input': + if ($y) $terminal_id = null; + break; + case 'log-error': + if ($y) Console::setLevel(0); + break; + case 'log-warning': + if ($y) Console::setLevel(1); + break; + case 'log-info': + if ($y) Console::setLevel(2); + break; + case 'log-verbose': + if ($y) Console::setLevel(3); + break; + case 'log-debug': + if ($y) Console::setLevel(4); + break; + case 'log-theme': + if ($y !== null) { + Console::$theme = $y; + } + break; + } + } + if ($coroutine_mode) Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); + } + + public static function getTtyWidth() { + return explode(" ", trim(exec("stty size")))[1]; + } +} diff --git a/src/ZM/Http/MiddlewareInterface.php b/src/ZM/Http/MiddlewareInterface.php index 79c00865..a0ed6969 100644 --- a/src/ZM/Http/MiddlewareInterface.php +++ b/src/ZM/Http/MiddlewareInterface.php @@ -6,5 +6,5 @@ namespace ZM\Http; interface MiddlewareInterface { - public function getName(); + } diff --git a/src/ZM/Http/Response.php b/src/ZM/Http/Response.php index 061a597b..a304e88d 100644 --- a/src/ZM/Http/Response.php +++ b/src/ZM/Http/Response.php @@ -4,6 +4,8 @@ namespace ZM\Http; +use ZM\Console\Console; + class Response { @@ -184,6 +186,7 @@ class Response * @return mixed */ public function redirect($location, $http_code = null) { + $this->is_end = true; return $this->response->redirect($location, $http_code); } diff --git a/src/ZM/Http/StaticFileHandler.php b/src/ZM/Http/StaticFileHandler.php index fec943bc..e31407d0 100644 --- a/src/ZM/Http/StaticFileHandler.php +++ b/src/ZM/Http/StaticFileHandler.php @@ -4,9 +4,9 @@ namespace ZM\Http; -use Framework\Console; -use Framework\ZMBuf; -use ZM\Utils\ZMUtil; +use ZM\Config\ZMConfig; +use ZM\Console\Console; +use ZM\Utils\HttpUtil; class StaticFileHandler { @@ -22,14 +22,14 @@ class StaticFileHandler } else { if(is_file($full_path)) { $exp = strtolower(pathinfo($full_path)['extension'] ?? "unknown"); - $response->setHeader("Content-Type", ZMBuf::config("file_header")[$exp] ?? "application/octet-stream"); + $response->setHeader("Content-Type", ZMConfig::get("file_header")[$exp] ?? "application/octet-stream"); $response->end(file_get_contents($full_path)); return true; } } } $response->status(404); - $response->end(ZMUtil::getHttpCodePage(404)); + $response->end(HttpUtil::getHttpCodePage(404)); return true; } } diff --git a/src/ZM/ModBase.php b/src/ZM/ModBase.php deleted file mode 100644 index a6097a89..00000000 --- a/src/ZM/ModBase.php +++ /dev/null @@ -1,170 +0,0 @@ -server = $param0["server"]; - if (isset($param0["frame"])) $this->frame = $param0["frame"]; - if (isset($param0["data"])) $this->data = $param0["data"]; - if (isset($param0["request"])) $this->request = $param0["request"]; - if (isset($param0["response"])) $this->response = $param0["response"]; - if (isset($param0["fd"])) $this->fd = $param0["fd"]; - if (isset($param0["worker_id"])) $this->worker_id = $param0["worker_id"]; - if (isset($param0["connection"])) $this->connection = $param0["connection"]; - $this->handle_type = $handle_type; - } - - /** - * only can used by cq->message event function - * @param $msg - * @param bool $yield - * @return mixed - */ - public function reply($msg, $yield = false) { - switch ($this->data["message_type"]) { - case "group": - case "private": - case "discuss": - return CQAPI::quick_reply($this->connection, $this->data, $msg, $yield); - } - return false; - } - - public function finalReply($msg, $yield = false) { - $this->setBlock(); - if ($msg == "") return true; - return $this->reply($msg, $yield); - } - - /** - * @param string $prompt - * @param int $timeout - * @param string $timeout_prompt - * @return string - * @throws InvalidArgumentException - * @throws WaitTimeoutException - */ - public function waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "") { - if ($prompt != "") $this->reply($prompt); - if (!isset($this->data["user_id"], $this->data["message"], $this->data["self_id"])) - throw new InvalidArgumentException("协程等待参数缺失"); - $cid = Co::getuid(); - $api_id = ZMBuf::$atomics["wait_msg_id"]->get(); - ZMBuf::$atomics["wait_msg_id"]->add(1); - $hang = [ - "coroutine" => $cid, - "user_id" => $this->data["user_id"], - "message" => $this->data["message"], - "self_id" => $this->data["self_id"], - "message_type" => $this->data["message_type"], - "result" => null - ]; - if ($hang["message_type"] == "group" || $hang["message_type"] == "discuss") { - $hang[$hang["message_type"] . "_id"] = $this->data[$this->data["message_type"] . "_id"]; - } - ZMBuf::appendKey("wait_api", $api_id, $hang); - $id = swoole_timer_after($timeout * 1000, function () use ($api_id, $timeout_prompt) { - $r = ZMBuf::get("wait_api")[$api_id] ?? null; - if ($r !== null) { - Co::resume($r["coroutine"]); - } - }); - - Co::suspend(); - $sess = ZMBuf::get("wait_api")[$api_id]; - ZMBuf::unsetByValue("wait_api", $api_id); - $result = $sess["result"]; - if (isset($id)) swoole_timer_clear($id); - if ($result === null) throw new WaitTimeoutException($this, $timeout_prompt); - return $result; - } - - /** - * @param $arg - * @param $mode - * @param $prompt_msg - * @return mixed|string - * @throws InvalidArgumentException - * @throws WaitTimeoutException - */ - public function getArgs(&$arg, $mode, $prompt_msg) { - switch ($mode) { - case ZM_MATCH_ALL: - $p = $arg; - array_shift($p); - return trim(implode(" ", $p)) == "" ? $this->waitMessage($prompt_msg) : trim(implode(" ", $p)); - case ZM_MATCH_NUMBER: - foreach ($arg as $k => $v) { - if (is_numeric($v)) { - array_splice($arg, $k, 1); - return $v; - } - } - return $this->waitMessage($prompt_msg); - case ZM_MATCH_FIRST: - if (isset($arg[1])) { - $a = $arg[1]; - array_splice($arg, 1, 1); - return $a; - } else { - return $this->waitMessage($prompt_msg); - } - } - throw new InvalidArgumentException(); - } - - public function getMessage() { return $this->data["message"] ?? null; } - - public function getUserId() { return $this->data["user_id"] ?? null; } - - public function getGroupId() { return $this->data["group_id"] ?? null; } - - public function getMessageType() { return $this->data["message_type"] ?? null; } - - public function getRobotId() { return $this->data["self_id"]; } - - public function getConnection() { return $this->connection; } - - public function setBlock($result = true) { context()->setCache("block_continue", $result); } -} diff --git a/src/ZM/ModHandleType.php b/src/ZM/ModHandleType.php deleted file mode 100644 index 1082dfc7..00000000 --- a/src/ZM/ModHandleType.php +++ /dev/null @@ -1,20 +0,0 @@ -getFrame()->data, true); + if (isset($data["post_type"])) { + //echo TermColor::ITALIC.json_encode($data, 128|256).TermColor::RESET.PHP_EOL; + set_coroutine_params(["data" => $data]); + ctx()->setCache("level", 0); + //Console::debug("Calling CQ Event from fd=" . ctx()->getConnection()->getFd()); + $this->dispatchBeforeEvents($data); // >= 200 的level before在这里执行 + if (CoMessage::resumeByWS()) { + EventDispatcher::interrupt(); + } + //Console::warning("最上数据包:".json_encode($data)); + $this->dispatchEvents($data); + } else { + $this->dispatchAPIResponse($data); + } + } /** @noinspection PhpRedundantCatchClauseInspection */ catch (WaitTimeoutException $e) { + $e->module->finalReply($e->getMessage()); + } + } + + public function dispatchBeforeEvents($data) { + $before = new EventDispatcher(CQBefore::class); + $before->setRuleFunction(function ($v) use ($data) { + if ($v->level < 200) EventDispatcher::interrupt(); + elseif ($v->cq_event != $data["post_type"]) return false; + return true; + }); + $before->setReturnFunction(function ($result) { + if (!$result) EventDispatcher::interrupt(); + }); + $before->dispatchEvents($data); + } + + private function dispatchEvents($data) { + //Console::warning("最xia数据包:".json_encode($data)); + switch ($data["post_type"]) { + case "message": + $word = explodeMsg(str_replace("\r", "", context()->getMessage())); + if (count(explode("\n", $word[0])) >= 2) { + $enter = explode("\n", context()->getMessage()); + $first = split_explode(" ", array_shift($enter)); + $word = array_merge($first, $enter); + foreach ($word as $k => $v) { + $word[$k] = trim($word[$k]); + } + } + + //分发CQCommand事件 + $dispatcher = new EventDispatcher(CQCommand::class); + $dispatcher->setRuleFunction(function (CQCommand $v) use ($word) { + if(array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], [""]) == []) return false; + elseif (($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == ctx()->getUserId())) && + ($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (ctx()->getGroupId() ?? 0))) && + ($v->message_type == '' || ($v->message_type != '' && $v->message_type == ctx()->getMessageType())) + ) { + if(($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) { + array_shift($word); + ctx()->setCache("match", $word); + return true; + } elseif ($v->start_with != "" && mb_strpos(ctx()->getMessage(), $v->start_with) === 0) { + ctx()->setCache("match", [mb_substr(ctx()->getMessage(), mb_strlen($v->start_with))]); + return true; + } elseif ($v->end_with != "" && strlen(ctx()->getMessage()) == (strripos(ctx()->getMessage(), $v->end_with) + strlen($v->end_with))) { + ctx()->setCache("match", [substr(ctx()->getMessage(), 0, strripos(ctx()->getMessage(), $v->end_with))]); + return true; + } elseif ($v->keyword != "" && mb_strpos(ctx()->getMessage(), $v->keyword) !== false) { + ctx()->setCache("match", explode($v->keyword, ctx()->getMessage())); + return true; + }elseif ($v->pattern != "") { + $match = matchArgs($v->pattern, ctx()->getMessage()); + if($match !== false) { + ctx()->setCache("match", $match); + return true; + } + } elseif ($v->regex != "") { + if(preg_match("/" . $v->regex . "/u", ctx()->getMessage(), $word2) != 0) { + ctx()->setCache("match", $word2); + return true; + } + } + } + return false; + }); + $dispatcher->setReturnFunction(function ($result) { + if (is_string($result)) ctx()->reply($result); + EventDispatcher::interrupt(); + }); + $r = $dispatcher->dispatchEvents(); + if ($r === null) EventDispatcher::interrupt(); + + //分发CQMessage事件 + $msg_dispatcher = new EventDispatcher(CQMessage::class); + $msg_dispatcher->setRuleFunction(function ($v) { + return ($v->message == '' || ($v->message != '' && $v->message == context()->getData()["message"])) && + ($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == context()->getData()["user_id"])) && + ($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (context()->getData()["group_id"] ?? 0))) && + ($v->message_type == '' || ($v->message_type != '' && $v->message_type == context()->getData()["message_type"])) && + ($v->raw_message == '' || ($v->raw_message != '' && $v->raw_message == context()->getData()["raw_message"])); + }); + $msg_dispatcher->setReturnFunction(function ($result) { + if (is_string($result)) ctx()->reply($result); + }); + $msg_dispatcher->dispatchEvents(ctx()->getMessage()); + return; + case "meta_event": + //Console::success("当前数据包:".json_encode(ctx()->getData())); + $dispatcher = new EventDispatcher(CQMetaEvent::class); + $dispatcher->setRuleFunction(function (CQMetaEvent $v) { + return ($v->meta_event_type == '' || ($v->meta_event_type != '' && $v->meta_event_type == ctx()->getData()["meta_event_type"])) && + ($v->sub_type == '' || ($v->sub_type != '' && $v->sub_type == (ctx()->getData()["sub_type"] ?? ''))); + }); + //eval(BP); + $dispatcher->dispatchEvents(ctx()->getData()); + return; + case "notice": + $dispatcher = new EventDispatcher(CQNotice::class); + $dispatcher->setRuleFunction(function (CQNotice $v) { + return + ($v->notice_type == '' || ($v->notice_type != '' && $v->notice_type == ctx()->getData()["notice_type"])) && + ($v->sub_type == '' || ($v->sub_type != '' && $v->sub_type == ctx()->getData()["sub_type"])) && + ($v->group_id == '' || ($v->group_id != '' && $v->group_id == ctx()->getData()["group_id"])) && + ($v->operator_id == '' || ($v->operator_id != '' && $v->operator_id == ctx()->getData()["operator_id"])); + }); + $dispatcher->dispatchEvents(ctx()->getData()); + return; + case "request": + $dispatcher = new EventDispatcher(CQRequest::class); + $dispatcher->setRuleFunction(function (CQRequest $v) { + return ($v->request_type == '' || ($v->request_type != '' && $v->request_type == ctx()->getData()['request_type'])) && + ($v->sub_type == '' || ($v->sub_type != '' && $v->sub_type == ctx()->getData()['sub_type'])) && + ($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == ctx()->getData()["user_id"])) && + ($v->comment == '' || ($v->comment != '' && $v->comment == ctx()->getData()['comment'])); + }); + $dispatcher->dispatchEvents(ctx()->getData()); + return; + } + } + + private function dispatchAPIResponse($req) { + $status = $req["status"]; + $retcode = $req["retcode"]; + $data = $req["data"]; + if (isset($req["echo"]) && is_numeric($req["echo"])) { + $r = LightCacheInside::get("wait_api", "wait_api"); + if (isset($r[$req["echo"]])) { + $origin = $r[$req["echo"]]; + $self_id = $origin["self_id"]; + $response = [ + "status" => $status, + "retcode" => $retcode, + "data" => $data, + "self_id" => $self_id, + "echo" => $req["echo"] + ]; + set_coroutine_params(["cq_response" => $response]); + $dispatcher = new EventDispatcher(CQAPIResponse::class); + $dispatcher->setRuleFunction(function (CQAPIResponse $response) { + return $response->retcode == ctx()->getCQResponse()["retcode"]; + }); + $dispatcher->dispatchEvents($response); + + $origin_ctx = ctx()->copy(); + set_coroutine_params($origin_ctx); + if (($origin["coroutine"] ?? false) !== false) { + SpinLock::lock("wait_api"); + $r = LightCacheInside::get("wait_api", "wait_api"); + $r[$req["echo"]]["result"] = $response; + LightCacheInside::set("wait_api", "wait_api", $r); + SpinLock::unlock("wait_api"); + Coroutine::resume($origin['coroutine']); + } + SpinLock::lock("wait_api"); + $r = LightCacheInside::get("wait_api", "wait_api"); + unset($r[$req["echo"]]); + LightCacheInside::set("wait_api", "wait_api", $r); + SpinLock::unlock("wait_api"); + } + } + } +} diff --git a/src/ZM/Store/LightCache.php b/src/ZM/Store/LightCache.php new file mode 100644 index 00000000..c28c61e5 --- /dev/null +++ b/src/ZM/Store/LightCache.php @@ -0,0 +1,207 @@ +column("value", Table::TYPE_STRING, $config["max_strlen"]); + self::$kv_table->column("expire", Table::TYPE_INT); + self::$kv_table->column("data_type", Table::TYPE_STRING, 8); + $result = self::$kv_table->create(); + self::$kv_lock = new Table($config["size"], $config["hash_conflict_proportion"]); + $result = $result && self::$kv_lock->create(); + if ($result === true && isset($config["persistence_path"])) { + if (file_exists($config["persistence_path"])) { + $r = json_decode(file_get_contents($config["persistence_path"]), true); + if ($r === null) $r = []; + foreach ($r as $k => $v) { + $write = self::set($k, $v, -2); + Console::verbose("Writing LightCache: " . $k); + if ($write === false) { + self::$last_error = '可能是由于 Hash 冲突过多导致动态空间无法分配内存'; + return false; + } + } + } + } + if ($result === false) { + self::$last_error = '系统内存不足,申请失败'; + } + return $result; + } + + /** + * @param string $key + * @return null|string + * @throws Exception + */ + public static function get(string $key) { + if (self::$kv_table === null) throw new Exception("not initialized LightCache"); + self::checkExpire($key); + $r = self::$kv_table->get($key); + return $r === false ? null : self::parseGet($r); + } + + /** + * @param string $key + * @return mixed|null + * @throws Exception + */ + public static function getExpire(string $key) { + if (self::$kv_table === null) throw new Exception("not initialized LightCache"); + self::checkExpire($key); + $r = self::$kv_table->get($key, "expire"); + return $r === false ? null : $r - time(); + } + + /** + * @param string $key + * @param string|array|int $value + * @param int $expire + * @return mixed + * @throws Exception + */ + public static function set(string $key, $value, int $expire = -1) { + if (self::$kv_table === null) throw new Exception("not initialized LightCache"); + if (is_array($value)) { + $value = json_encode($value, JSON_UNESCAPED_UNICODE); + if (strlen($value) >= self::$config["max_strlen"]) return false; + $data_type = "json"; + } elseif (is_string($value)) { + $data_type = ""; + } elseif (is_int($value)) { + $data_type = "int"; + } elseif (is_bool($value)) { + $data_type = "bool"; + $value = json_encode($value); + } else { + throw new Exception("Only can set string, array and int"); + } + try { + return self::$kv_table->set($key, [ + "value" => $value, + "expire" => $expire >= 0 ? $expire + time() : $expire, + "data_type" => $data_type + ]); + } catch (Exception $e) { + return false; + } + } + + /** + * @param string $key + * @param $value + * @return bool|mixed + * @throws Exception + */ + public static function update(string $key, $value) { + if (self::$kv_table === null) throw new Exception("not initialized LightCache."); + if (is_array($value)) { + $value = json_encode($value, JSON_UNESCAPED_UNICODE); + if (strlen($value) >= self::$config["max_strlen"]) return false; + $data_type = "json"; + } elseif (is_string($value)) { + $data_type = ""; + } elseif (is_int($value)) { + $data_type = "int"; + } else { + throw new Exception("Only can set string, array and int"); + } + try { + if (self::$kv_table->get($key) === false) return false; + return self::$kv_table->set($key, [ + "value" => $value, + "data_type" => $data_type + ]); + } catch (Exception $e) { + return false; + } + } + + public static function getMemoryUsage() { + return self::$kv_table->getMemorySize(); + } + + /** + * @param string $key + * @return bool + * @throws Exception + */ + public static function isset(string $key) { + return self::get($key) !== null; + } + + public static function unset(string $key) { + return self::$kv_table->del($key); + } + + public static function getAll() { + $r = []; + $del = []; + foreach (self::$kv_table as $k => $v) { + if ($v["expire"] <= time() && $v["expire"] >= 0) { + $del[] = $k; + continue; + } + $r[$k] = self::parseGet($v); + } + foreach ($del as $v) { + self::unset($v); + } + return $r; + } + + public static function savePersistence() { + if (self::$kv_table === null) return; + $r = []; + foreach (self::$kv_table as $k => $v) { + if ($v["expire"] === -2) { + Console::verbose("Saving " . $k); + $r[$k] = self::parseGet($v); + } + } + if(self::$config["persistence_path"] == "") return; + if (file_exists(self::$config["persistence_path"])) { + $r = file_put_contents(self::$config["persistence_path"], json_encode($r, 128 | 256)); + if ($r === false) Console::error("Not saved, please check your \"persistence_path\"!"); + } + } + + private static function checkExpire($key) { + if (($expire = self::$kv_table->get($key, "expire")) >= 0) { + if ($expire <= time()) { + self::$kv_table->del($key); + } + } + } + + private static function parseGet($r) { + switch ($r["data_type"]) { + case "json": + case "int": + case "bool": + return json_decode($r["value"], true); + case "": + default: + return $r["value"]; + } + } +} diff --git a/src/ZM/Store/LightCacheInside.php b/src/ZM/Store/LightCacheInside.php new file mode 100644 index 00000000..c161eca0 --- /dev/null +++ b/src/ZM/Store/LightCacheInside.php @@ -0,0 +1,64 @@ +column("value", Table::TYPE_STRING, 65536); + self::$kv_table["connect"] = new Table(8, 0); + self::$kv_table["connect"]->column("value", Table::TYPE_STRING, 256); + $result = self::$kv_table["wait_api"]->create() && self::$kv_table["connect"]->create(); + if ($result === false) { + self::$last_error = '系统内存不足,申请失败'; + return $result; + } + return $result; + } + + /** + * @param string $table + * @param string $key + * @return mixed|null + * @throws ZMException + */ + public static function get(string $table, string $key) { + if (!isset(self::$kv_table[$table])) throw new ZMException("not initialized LightCache"); + $r = self::$kv_table[$table]->get($key); + return $r === false ? null : json_decode($r["value"], true); + } + + /** + * @param string $table + * @param string $key + * @param string|array|int $value + * @return mixed + * @throws ZMException + */ + public static function set(string $table, string $key, $value) { + if (self::$kv_table === null) throw new ZMException("not initialized LightCache"); + try { + return self::$kv_table[$table]->set($key, [ + "value" => json_encode($value, 256) + ]); + } catch (Exception $e) { + return false; + } + } + + public static function unset(string $table, string $key) { + return self::$kv_table[$table]->del($key); + } +} diff --git a/src/ZM/Store/Lock/SpinLock.php b/src/ZM/Store/Lock/SpinLock.php new file mode 100644 index 00000000..25b85fa4 --- /dev/null +++ b/src/ZM/Store/Lock/SpinLock.php @@ -0,0 +1,44 @@ +column('lock_num', Table::TYPE_INT, 8); + return self::$kv_lock->create(); + } + + public static function lock(string $key) + { + while (($r = self::$kv_lock->incr($key, 'lock_num')) > 1) { //此资源已经被锁上了 + if(Coroutine::getCid() != -1) System::sleep(self::$delay / 1000); + else usleep(self::$delay * 1000); + } + } + + public static function tryLock(string $key) { + if (($r = self::$kv_lock->incr($key, 'lock_num')) > 1) { + return false; + } + return true; + } + + public static function unlock(string $key) { + return self::$kv_lock->set($key, ['lock_num' => 0]); + } +} diff --git a/src/ZM/Store/MySQL/SqlPoolStorage.php b/src/ZM/Store/MySQL/SqlPoolStorage.php new file mode 100644 index 00000000..1df61eda --- /dev/null +++ b/src/ZM/Store/MySQL/SqlPoolStorage.php @@ -0,0 +1,13 @@ +get(); + $result = $callable($r); + if (isset($r->wasted)) ZMRedisPool::$pool->put(null); + else ZMRedisPool::$pool->put($r); + return $result; + } + + /** + * ZMRedis constructor. + * @throws NotInitializedException + */ + public function __construct() { + if(ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized."); + $this->conn = ZMRedisPool::$pool->get(); + } + + /** + * @return Redis + */ + public function get() { + return $this->conn; + } + + public function __destruct() { + if (isset($this->conn->wasted)) ZMRedisPool::$pool->put(null); + else ZMRedisPool::$pool->put($this->conn); + } +} diff --git a/src/ZM/Store/Redis/ZMRedisPool.php b/src/ZM/Store/Redis/ZMRedisPool.php new file mode 100644 index 00000000..2ca1473e --- /dev/null +++ b/src/ZM/Store/Redis/ZMRedisPool.php @@ -0,0 +1,37 @@ +withHost($config['host']) + ->withPort($config['port']) + ->withAuth($config['auth']) + ->withDbIndex($config['db_index']) + ->withTimeout($config['timeout'] ?? 1) + ); + try { + $r = self::$pool->get()->ping('123'); + if(strpos(strtolower($r), "123") !== false) { + Console::debug("成功连接redis连接池!"); + } else { + var_dump($r); + } + } catch (RedisException $e) { + Console::error("Redis init failed! ".$e->getMessage()); + self::$pool = null; + } + } +} diff --git a/src/ZM/Store/ZMAtomic.php b/src/ZM/Store/ZMAtomic.php new file mode 100644 index 00000000..b4e88ff8 --- /dev/null +++ b/src/ZM/Store/ZMAtomic.php @@ -0,0 +1,39 @@ + $v) { + self::$atomics[$k] = new Atomic($v); + } + self::$atomics["stop_signal"] = new Atomic(0); + self::$atomics["wait_msg_id"] = new Atomic(0); + self::$atomics["_event_id"] = new Atomic(0); + for ($i = 0; $i < 10; ++$i) { + self::$atomics["_tmp_" . $i] = new Atomic(0); + } + } + + +} diff --git a/src/ZM/Store/ZMBuf.php b/src/ZM/Store/ZMBuf.php new file mode 100755 index 00000000..32c0621a --- /dev/null +++ b/src/ZM/Store/ZMBuf.php @@ -0,0 +1,17 @@ +add(1); + $hang["compare"] = $compare; + $hang["coroutine"] = $cid; + $hang["worker_id"] = server()->worker_id; + $hang["result"] = null; + SpinLock::lock("wait_api"); + $wait = LightCacheInside::get("wait_api", "wait_api"); + $wait[$api_id] = $hang; + LightCacheInside::set("wait_api", "wait_api", $wait); + SpinLock::unlock("wait_api"); + $id = swoole_timer_after($timeout * 1000, function () use ($api_id) { + $r = LightCacheInside::get("wait_api", "wait_api")[$api_id] ?? null; + if (is_array($r)) { + Co::resume($r["coroutine"]); + } + }); + Co::suspend(); + SpinLock::lock("wait_api"); + $sess = LightCacheInside::get("wait_api", "wait_api"); + $result = $sess[$api_id]["result"]; + unset($sess[$api_id]); + LightCacheInside::set("wait_api", "wait_api", $sess); + SpinLock::unlock("wait_api"); + if (isset($id)) swoole_timer_clear($id); + if ($result === null) return false; + return $result; + } + + public static function resumeByWS() { + $dat = ctx()->getData(); + $last = null; + SpinLock::lock("wait_api"); + $all = LightCacheInside::get("wait_api", "wait_api") ?? []; + foreach ($all as $k => $v) { + if(!isset($v["compare"])) continue; + foreach ($v["compare"] as $vs) { + if ($v[$vs] != ($dat[$vs] ?? null)) { + continue 2; + } + } + $last = $k; + } + if($last !== null) { + $all[$last]["result"] = $dat; + LightCacheInside::set("wait_api", "wait_api", $all); + SpinLock::unlock("wait_api"); + if ($all[$last]["worker_id"] != server()->worker_id) { + ZMUtil::sendActionToWorker($all[$k]["worker_id"], "resume_ws_message", $all[$last]); + } else { + Co::resume($all[$last]["coroutine"]); + } + return true; + } else { + SpinLock::unlock("wait_api"); + return false; + } + } +} diff --git a/src/ZM/Utils/CoroutinePool.php b/src/ZM/Utils/CoroutinePool.php new file mode 100644 index 00000000..042500b8 --- /dev/null +++ b/src/ZM/Utils/CoroutinePool.php @@ -0,0 +1,51 @@ += (self::$sizes[$name] ?? self::$default_size)) { + self::$yields[] = Coroutine::getCid(); + Coroutine::suspend(); + } + go(function () use ($func, $name) { + self::$cids[$name][] = Coroutine::getCid(); + //Console::debug("正在执行协程,当前协程池中有 " . count(self::$cids[$name]) . " 个正在运行的协程: ".implode(", ", self::$cids[$name])); + $func(); + self::checkCids($name); + }); + } + + public static function defaultSize(int $size) { + self::$default_size = $size; + } + + public static function setSize($name, int $size) { + self::$sizes[$name] = $size; + } + + private static function checkCids($name) { + if (in_array(Coroutine::getCid(), self::$cids[$name])) { + $a = array_search(Coroutine::getCid(), self::$cids[$name]); + array_splice(self::$cids[$name], $a, 1); + $r = array_shift(self::$yields); + if ($r !== null) { + Coroutine::resume($r); + } + } + } +} diff --git a/src/ZM/Utils/DataProvider.php b/src/ZM/Utils/DataProvider.php new file mode 100644 index 00000000..04b02738 --- /dev/null +++ b/src/ZM/Utils/DataProvider.php @@ -0,0 +1,31 @@ + $v) { + if ($v["id"] == $node["param_route"]) { + $node = $v; + $params[mb_substr($v["name"], 1, -1)] = $r; + continue 2; + } + } + } elseif ($node["son"][0]["name"] == $r) { + $node = $node["son"][0]; + continue; + } + } elseif ($cnt >= 1) { + if (isset($node["param_route"])) { + foreach ($node["son"] as $k => $v) { + if ($v["id"] == $node["param_route"]) { + $node = $v; + $params[mb_substr($v["name"], 1, -1)] = $r; + continue 2; + } + } + } + foreach ($node["son"] as $k => $v) { + if ($v["name"] == $r) { + $node = $v; + continue 2; + } + } + } + if (ZMConfig::get("global", "static_file_server")["status"]) { + HttpUtil::handleStaticPage($request->server["request_uri"], $response); + return null; + } + } + return !isset($node["route"]) ? false : true; + } + + public static function getHttpCodePage(int $http_code) { + if (isset(ZMConfig::get("global", "http_default_code_page")[$http_code])) { + return Co::readFile(DataProvider::getResourceFolder() . "html/" . ZMConfig::get("global", "http_default_code_page")[$http_code]); + } else return null; + } + + /** + * @param $uri + * @param Response|\Swoole\Http\Response $response + * @param array $settings + * @return bool + */ + public static function handleStaticPage($uri, $response, $settings = []) { + $base_dir = $settings["document_root"] ?? ZMConfig::get("global", "static_file_server")["document_root"]; + $base_index = $settings["document_index"] ?? ZMConfig::get("global", "static_file_server")["document_index"]; + $path = realpath($base_dir . urldecode($uri)); + if ($path !== false) { + if (is_dir($path)) $path = $path . '/'; + $work = realpath($base_dir) . '/'; + if (strpos($path, $work) !== 0) { + Console::info("[403] " . $uri); + self::responseCodePage($response, 403); + return true; + } + if (is_dir($path)) { + if(mb_substr($uri, -1, 1) != "/") { + Console::info("[302] " . $uri); + $response->redirect($uri."/", 302); + return true; + } + foreach ($base_index as $vp) { + if (is_file($path . "/" . $vp)) { + Console::info("[200] " . $uri); + $exp = strtolower(pathinfo($path . $vp)['extension'] ?? "unknown"); + $response->setHeader("Content-Type", ZMConfig::get("file_header")[$exp] ?? "application/octet-stream"); + $response->end(file_get_contents($path . $vp)); + return true; + } + } + } elseif (is_file($path)) { + Console::info("[200] " . $uri); + $exp = strtolower(pathinfo($path)['extension'] ?? "unknown"); + $response->setHeader("Content-Type", ZMConfig::get("file_header")[$exp] ?? "application/octet-stream"); + $response->end(file_get_contents($path)); + return true; + } + } + Console::info("[404] " . $uri); + self::responseCodePage($response, 404); + return true; + } + + public static function responseCodePage($response, $code) { + $response->status($code); + $response->end(self::getHttpCodePage($code)); + } +} diff --git a/src/ZM/Utils/SQLPool.php b/src/ZM/Utils/SQLPool.php deleted file mode 100644 index 9fb66280..00000000 --- a/src/ZM/Utils/SQLPool.php +++ /dev/null @@ -1,122 +0,0 @@ -pool = new SplQueue; - $this->info = [ - "host" => ZMBuf::globals("sql_config")["sql_host"], - "port" => ZMBuf::globals("sql_config")["sql_port"], - "user" => ZMBuf::globals("sql_config")["sql_username"], - "password" => ZMBuf::globals("sql_config")["sql_password"], - "database" => ZMBuf::globals("sql_config")["sql_database"] - ]; - Console::debug("新建检测 MySQL 连接的计时器"); - zm_timer_tick(10000, function () { - //Console::debug("正在检测是否有坏死的MySQL连接,当前连接池有 ".count($this->pool) . " 个连接"); - if (count($this->pool) > 0) { - /** @var PDO $cnn */ - $cnn = $this->pool->pop(); - $this->connect_cnt -= 1; - try { - $cnn->getAttribute(PDO::ATTR_SERVER_INFO); - } catch (PDOException $e) { - if (strpos($e->getMessage(), 'MySQL server has gone away') !== false) { - Console::info("MySQL 长连接丢失,取消连接"); - unset($cnn); - return; - } - } - $this->pool->push($cnn); - $this->connect_cnt += 1; - } - }); - } - - /** - * 将利用过的连接入队 - * @param $mysql - */ - public function put($mysql) { - $this->pool->push($mysql); - if (($a = array_shift($this->co_list)) !== null) { - Coroutine::resume($a); - } - } - - /** - * 获取队中的连接,如果不存在则创建新的 - * @param bool $no_new_conn - * @return bool|mixed|PDO - */ - public function get($no_new_conn = false) { - if (count($this->pool) == 0 && $this->connect_cnt <= 70) { - if ($no_new_conn) return false; - $this->connect_cnt += 1; - $r = $this->newConnect(); - if ($r !== false) { - return $r; - } else { - $this->connect_cnt -= 1; - return false; - } - } elseif (count($this->pool) > 0) { - /** @var PDO $con */ - $con = $this->pool->pop(); - return $con; - } elseif ($this->connect_cnt > 70) { - $this->co_list[] = Coroutine::getuid(); - Console::warning("数据库连接过多,协程等待重复利用中...当前协程数 " . Coroutine::stats()["coroutine_num"]); - Coroutine::suspend(); - return $this->get($no_new_conn); - } - return false; - } - - public function getCount() { - return $this->pool->count(); - } - - public function destruct() { - // 连接池销毁, 置不可用状态, 防止新的客户端进入常驻连接池, 导致服务器无法平滑退出 - $this->available = false; - while (!$this->pool->isEmpty()) { - $this->pool->pop(); - } - } - - private function newConnect() { - //无空闲连接,创建新连接 - $dsn = "mysql:host=" . $this->info["host"] . ";dbname=" . $this->info["database"] . ";charset=utf8"; - try { - $mysql = new PDO($dsn, $this->info["user"], $this->info["password"], array(PDO::ATTR_PERSISTENT => true)); - } catch (PDOException $e) { - Console::error("PDO Error: " . $e->getMessage()); - return false; - } - Console::info("创建SQL连接中,当前有" . $this->connect_cnt . "个连接"); - return $mysql; - } -} diff --git a/src/ZM/Utils/ScheduleManager.php b/src/ZM/Utils/ScheduleManager.php deleted file mode 100644 index e87e15ce..00000000 --- a/src/ZM/Utils/ScheduleManager.php +++ /dev/null @@ -1,10 +0,0 @@ -$function_name(); + return true; + case 'psysh': + if (Framework::$argv["disable-coroutine"]) { + (new Shell())->run(); + } else + Console::error("Only \"--disable-coroutine\" mode can use psysh!!!"); + return true; + case 'bc': + $code = base64_decode($it[1] ?? '', true); + try { + eval($code); + } catch (Exception $e) { + } + return true; + case 'echo': + Console::info($it[1]); + return true; + case 'color': + Console::log($it[2], $it[1]); + return true; + case 'stop': + Event::del($resource); + ZMUtil::stop(); + return false; + case 'reload': + case 'r': + ZMUtil::reload(); + return false; + case '': + return true; + default: + Console::info("Command not found: " . $cmd); + return true; + } + } +} diff --git a/src/ZM/Utils/ZMRequest.php b/src/ZM/Utils/ZMRequest.php deleted file mode 100644 index fe1c5da5..00000000 --- a/src/ZM/Utils/ZMRequest.php +++ /dev/null @@ -1,153 +0,0 @@ -set($set == [] ? ['timeout' => 15.0] : $set); - $cli->setHeaders($headers); - $cli->get($parse["path"] . (isset($parse["query"]) ? "?" . $parse["query"] : "")); - if ($return_body) { - if ($cli->errCode != 0 || $cli->statusCode != 200) return false; - $a = $cli->body; - $cli->close(); - return $a; - } else { - $cli->close(); - return $cli; - } - } - - /** - * 使用Swoole协程客户端发起HTTP POST请求 - * 返回请求后的body - * 如果请求失败或返回状态不是200,则返回 false - * @param $url - * @param array $header - * @param $data - * @param array $set - * @param bool $return_body - * @return bool|string|Client - */ - public static function post($url, array $header, $data, $set = [], $return_body = true) { - /** @var Client $cli */ - list($cli, $parse) = self::getNewClient($url); - if($cli === null) return false; - $cli->set($set == [] ? ['timeout' => 15.0] : $set); - $cli->setHeaders($header); - $cli->post($parse["path"] . (isset($parse["query"]) ? ("?" . $parse["query"]) : ""), $data); - if ($return_body) { - if ($cli->errCode != 0 || $cli->statusCode != 200) return false; - $a = $cli->body; - $cli->close(); - return $a; - } else { - $cli->close(); - return $cli; - } - } - - /** - * @param $url - * @param array $set - * @param array $header - * @return ZMWebSocket - * @since 1.5 - */ - public static function websocket($url, $set = ['websocket_mask' => true], $header = []) { - return new ZMWebSocket($url, $set, $header); - } - - /** - * @param $option - * @return Saber - */ - public static function session($option) { - return Saber::session($option); - } - - /** - * @param $url - * @param array $attribute - * @param bool $return_body - * @return bool|string|Client - */ - public static function request($url, $attribute = [], $return_body = true) { - /** @var Client $cli */ - list($cli, $parse) = self::getNewClient($url); - if($cli === null) return false; - $cli->set($attribute["set"] ?? ["timeout" => 15.0]); - $cli->setMethod($attribute["method"] ?? "GET"); - $cli->setHeaders($attribute["headers"] ?? []); - if(isset($attribute["data"])) $cli->setData($attribute["data"]); - if(isset($attribute["file"])) { - foreach($attribute["file"] as $k => $v) { - $cli->addFile($v["path"], $v["name"], $v["mime_type"] ?? null, $v["filename"] ?? null, $v["offset"] ?? 0, $v["length"] ?? 0); - } - } - $cli->execute($parse["path"] . (isset($parse["query"]) ? "?" . $parse["query"] : "")); - if ($return_body) { - if ($cli->errCode != 0 || $cli->statusCode != 200) return false; - $a = $cli->body; - $cli->close(); - return $a; - } else { - $cli->close(); - return $cli; - } - } - - /** - * @param $url - * @param null|bool $dst - * @return bool - */ - public static function downloadFile($url, $dst = null) { - /** @var Client $cli */ - list($cli, $parse) = self::getNewClient($url); - if($cli === null) return false; - $cli->set(["timeout" => 60.0]); - $save_path = $dst === null ? "/tmp/_zm_".mt_rand(1000000, 9999999) : $dst; - $result = $cli->download($parse["path"] . (isset($parse["query"]) ? "?" . $parse["query"] : ""), $save_path); - if($result === false) return false; - elseif ($dst === null) return $save_path; - else return true; - } - - /** - * @param $url - * @return bool|array - */ - private static function getNewClient($url) { - $parse = parse_url($url); - if (!isset($parse["host"])) { - Console::warning("ZMRequest: url must contains scheme such as \"http(s)://\""); - return false; - } - if(!isset($parse["path"])) $parse["path"] = "/"; - $port = $parse["port"] ?? (($parse["scheme"] ?? "http") == "https" ? 443 : 80); - $cli = new Client($parse["host"], $port, ($parse["scheme"] ?? "http") == "https"); - return [$cli, $parse]; - } -} diff --git a/src/ZM/Utils/ZMRobot.php b/src/ZM/Utils/ZMRobot.php deleted file mode 100644 index 13184072..00000000 --- a/src/ZM/Utils/ZMRobot.php +++ /dev/null @@ -1,412 +0,0 @@ - $robot_id]); - if ($r == []) throw new RobotNotFoundException("机器人 " . $robot_id . " 未连接到框架!"); - return new ZMRobot($r[0]); - } - - /** - * @throws RobotNotFoundException - * @return ZMRobot - */ - public static function getRandom() { - $r = ConnectionManager::getByType("qq"); - if($r == []) throw new RobotNotFoundException("没有任何机器人连接到框架!"); - return new ZMRobot($r[array_rand($r)]); - } - - public function __construct(CQConnection $connection) { - $this->connection = $connection; - } - - public function setCallback($callback = true) { - $this->callback = $callback; - return $this; - } - - public function setPrefix($prefix = self::API_NORMAL) { - $this->prefix = $prefix; - return $this; - } - - public function sendPrivateMsg($user_id, $message, $auto_escape = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'user_id' => $user_id, - 'message' => $message, - 'auto_escape' => $auto_escape - ] - ], $this->callback); - } - - public function sendGroupMsg($group_id, $message, $auto_escape = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'message' => $message, - 'auto_escape' => $auto_escape - ] - ], $this->callback); - } - - public function sendDiscussMsg($discuss_id, $message, $auto_escape = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'discuss_id' => $discuss_id, - 'message' => $message, - 'auto_escape' => $auto_escape - ] - ], $this->callback); - } - - public function sendMsg($message_type, $target_id, $message, $auto_escape = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'message_type' => $message_type, - ($message_type == 'private' ? 'user' : $message_type) . '_id' => $target_id, - 'message' => $message, - 'auto_escape' => $auto_escape - ] - ], $this->callback); - } - - public function deleteMsg($message_id) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'message_id' => $message_id - ] - ], $this->callback); - } - - public function sendLike($user_id, $times = 1) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'user_id' => $user_id, - 'times' => $times - ] - ], $this->callback); - } - - public function setGroupKick($group_id, $user_id, $reject_add_request = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'user_id' => $user_id, - 'reject_add_request' => $reject_add_request - ] - ], $this->callback); - } - - public function setGroupBan($group_id, $user_id, $duration = 1800) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'user_id' => $user_id, - 'duration' => $duration - ] - ], $this->callback); - } - - public function setGroupAnonymousBan($group_id, $anonymous_or_flag, $duration = 1800) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - (is_string($anonymous_or_flag) ? 'flag' : 'anonymous') => $anonymous_or_flag, - 'duration' => $duration - ] - ], $this->callback); - } - - public function setGroupWholeBan($group_id, $enable = true) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'enable' => $enable - ] - ], $this->callback); - } - - public function setGroupAdmin($group_id, $user_id, $enable = true) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'user_id' => $user_id, - 'enable' => $enable - ] - ], $this->callback); - } - - public function setGroupAnonymous($group_id, $enable = true) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'enable' => $enable - ] - ], $this->callback); - } - - public function setGroupCard($group_id, $user_id, $card = "") { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'user_id' => $user_id, - 'card' => $card - ] - ], $this->callback); - } - - public function setGroupLeave($group_id, $is_dismiss = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'is_dismiss' => $is_dismiss - ] - ], $this->callback); - } - - public function setGroupSpecialTitle($group_id, $user_id, $special_title = "", $duration = -1) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'user_id' => $user_id, - 'special_title' => $special_title, - 'duration' => $duration - ] - ], $this->callback); - } - - public function setDiscussLeave($discuss_id) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'discuss_id' => $discuss_id - ] - ], $this->callback); - } - - public function setFriendAddRequest($flag, $approve = true, $remark = "") { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'flag' => $flag, - 'approve' => $approve, - 'remark' => $remark - ] - ], $this->callback); - } - - public function setGroupAddRequest($flag, $sub_type, $approve = true, $reason = "") { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'flag' => $flag, - 'sub_type' => $sub_type, - 'approve' => $approve, - 'reason' => $reason - ] - ], $this->callback); - } - - public function getLoginInfo() { - return CQAPI::processAPI($this->connection, ['action' => $this->getActionName(__FUNCTION__)], $this->callback); - } - - public function getStrangerInfo($user_id, $no_cache = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'user_id' => $user_id, - 'no_cache' => $no_cache - ] - ], $this->callback); - } - - public function getFriendList() { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__) - ], $this->callback); - } - - public function getGroupList() { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__) - ], $this->callback); - } - - public function getGroupInfo($group_id, $no_cache = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'no_cache' => $no_cache - ] - ], $this->callback); - } - - public function getGroupMemberInfo($group_id, $user_id, $no_cache = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'user_id' => $user_id, - 'no_cache' => $no_cache - ] - ], $this->callback); - } - - public function getGroupMemberList($group_id) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id - ] - ]); - } - - public function getCookies($domain = "") { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'domain' => $domain - ] - ]); - } - - public function getCsrfToken() { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__) - ], $this->callback); - } - - public function getCredentials($domain = "") { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'domain' => $domain - ] - ], $this->callback); - } - - public function getRecord($file, $out_format, $full_path = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'file' => $file, - 'out_format' => $out_format, - 'full_path' => $full_path - ] - ], $this->callback); - } - - public function getImage($file) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'file' => $file - ] - ], $this->callback); - } - - public function canSendImage() { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__) - ], $this->callback); - } - - public function canSendRecord() { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__) - ], $this->callback); - } - - public function getStatus() { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__) - ], $this->callback); - } - - public function getVersionInfo() { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__) - ], $this->callback); - } - - public function setRestartPlugin($delay = 0) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'delay' => $delay - ] - ], $this->callback); - } - - public function cleanDataDir($data_dir) { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__), - 'params' => [ - 'data_dir' => $data_dir - ] - ], $this->callback); - } - - public function cleanPluginLog() { - return CQAPI::processAPI($this->connection, [ - 'action' => $this->getActionName(__FUNCTION__) - ], $this->callback); - } - - public function getExperimentAPI() { - return new ZMRobotExperiment($this->connection, $this->callback, $this->prefix); - } - - private function getActionName(string $method) { - $prefix = ($this->prefix == self::API_ASYNC ? '_async' : ($this->prefix == self::API_RATE_LIMITED ? '_rate_limited' : '')); - $func_name = strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $method)); - return $func_name . $prefix; - } -} diff --git a/src/ZM/Utils/ZMRobotExperiment.php b/src/ZM/Utils/ZMRobotExperiment.php deleted file mode 100644 index 2043fc67..00000000 --- a/src/ZM/Utils/ZMRobotExperiment.php +++ /dev/null @@ -1,104 +0,0 @@ -connection = $connection; - $this->callback = $callback; - $this->prefix = $prefix; - } - - public function setCallback($callback = true) { - $this->callback = $callback; - return $this; - } - - public function setPrefix($prefix = ZMRobot::API_NORMAL) { - $this->prefix = $prefix; - return $this; - } - - public function getFriendList($flat = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => '_' . $this->getActionName(__FUNCTION__), - 'params' => [ - 'flat' => $flat - ] - ], $this->callback); - } - - public function getGroupInfo($group_id) { - return CQAPI::processAPI($this->connection, [ - 'action' => '_' . $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id - ] - ], $this->callback); - } - - public function getVipInfo($user_id) { - return CQAPI::processAPI($this->connection, [ - 'action' => '_' . $this->getActionName(__FUNCTION__), - 'params' => [ - 'user_id' => $user_id - ] - ], $this->callback); - } - - public function getGroupNotice($group_id) { - return CQAPI::processAPI($this->connection, [ - 'action' => '_' . $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id - ] - ], $this->callback); - } - - public function sendGroupNotice($group_id, $title, $content) { - return CQAPI::processAPI($this->connection, [ - 'action' => '_' . $this->getActionName(__FUNCTION__), - 'params' => [ - 'group_id' => $group_id, - 'title' => $title, - 'content' => $content - ] - ], $this->callback); - } - - public function setRestart($clean_log = false, $clean_cache = false, $clean_event = false) { - return CQAPI::processAPI($this->connection, [ - 'action' => '_' . $this->getActionName(__FUNCTION__), - 'params' => [ - 'clean_log' => $clean_log, - 'clean_cache' => $clean_cache, - 'clean_event' => $clean_event - ] - ], $this->callback); - } - - private function getActionName(string $method) { - $prefix = ($this->prefix == ZMRobot::API_ASYNC ? '_async' : ($this->prefix == ZMRobot::API_RATE_LIMITED ? '_rate_limited' : '')); - $func_name = strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $method)); - return $prefix . $func_name; - } -} diff --git a/src/ZM/Utils/ZMUtil.php b/src/ZM/Utils/ZMUtil.php index f09982a5..a4a3e949 100644 --- a/src/ZM/Utils/ZMUtil.php +++ b/src/ZM/Utils/ZMUtil.php @@ -5,68 +5,54 @@ namespace ZM\Utils; use Co; -use framework\Console; -use Framework\DataProvider; -use Framework\ZMBuf; -use ZM\API\CQ; +use Exception; +use Swoole\Event; +use Swoole\Timer; +use ZM\Console\Console; +use ZM\Store\LightCache; +use ZM\Store\LightCacheInside; +use ZM\Store\ZMAtomic; +use ZM\Store\ZMBuf; class ZMUtil { - /** - * 检查workerStart是否运行结束 - */ - public static function checkWait() { - if (ZMBuf::isset("wait_start")) { - ZMBuf::append("wait_start", Co::getCid()); - Co::suspend(); + public static function stop() { + Console::warning(Console::setColor("Stopping server...", "red")); + LightCache::savePersistence(); + if (ZMBuf::$terminal !== null) + Event::del(ZMBuf::$terminal); + ZMAtomic::get("stop_signal")->set(1); + try { + LightCache::set('stop', 'OK'); + } catch (Exception $e) { } + server()->shutdown(); + server()->stop(); } - public static function stop($without_shutdown = false) { - Console::info(Console::setColor("Stopping server...", "red")); - foreach ((ZMBuf::$server->connections ?? []) as $v) { - ZMBuf::$server->close($v); - } - DataProvider::saveBuffer(); - if (!$without_shutdown) - ZMBuf::$server->shutdown(); - ZMBuf::$server->stop(); - } - - public static function getHttpCodePage(int $http_code) { - if (isset(ZMBuf::globals("http_default_code_page")[$http_code])) { - return Co::readFile(DataProvider::getResourceFolder() . "html/" . ZMBuf::globals("http_default_code_page")[$http_code]); - } else return null; - } - - public static function reload() { + public static function reload($delay = 800) { Console::info(Console::setColor("Reloading server...", "gold")); - foreach (ZMBuf::get("wait_api", []) as $k => $v) { - if ($v["result"] === null) Co::resume($v["coroutine"]); + usleep($delay * 1000); + foreach ((LightCacheInside::get("wait_api", "wait_api") ?? []) as $k => $v) { + if (($v["result"] ?? false) === null && isset($v["coroutine"])) Co::resume($v["coroutine"]); } - foreach (ZMBuf::$server->connections as $v) { - ZMBuf::$server->close($v); - } - DataProvider::saveBuffer(); - ZMBuf::$server->reload(); - } - - /** - * 解析CQ码 - * @param $msg - * @return array|null - */ - static function getCQ($msg) { - return CQ::getCQ($msg); + LightCacheInside::unset("wait_api", "wait_api"); + LightCache::savePersistence(); + //DataProvider::saveBuffer(); + Timer::clearAll(); + server()->reload(); } public static function getModInstance($class) { if (!isset(ZMBuf::$instance[$class])) { - Console::debug("Class instance $class not exist, so I created it."); - ZMBuf::$instance[$class] = new $class(); - return ZMBuf::$instance[$class]; + //Console::debug("Class instance $class not exist, so I created it."); + return ZMBuf::$instance[$class] = new $class(); } else { return ZMBuf::$instance[$class]; } } + + public static function sendActionToWorker($target_id, $action, $data) { + server()->sendMessage(json_encode(["action" => $action, "data" => $data]), $target_id); + } } diff --git a/src/ZM/Utils/ZMWebSocket.php b/src/ZM/Utils/ZMWebSocket.php deleted file mode 100644 index 6340ba7c..00000000 --- a/src/ZM/Utils/ZMWebSocket.php +++ /dev/null @@ -1,106 +0,0 @@ - true], $header = []) { - $this->parse = parse_url($url); - if (!isset($this->parse["host"])) { - Console::warning("ZMRequest: url must contains scheme such as \"ws(s)://\""); - return; - } - if (!isset($this->parse["path"])) $this->parse["path"] = "/"; - $port = $this->parse["port"] ?? (($this->parse["scheme"] ?? "ws") == "wss" ? 443 : 80); - $this->client = new Client($this->parse["host"], $port, (($this->parse["scheme"] ?? "ws") == "wss" ? true : false)); - $this->client->set($set); - if ($header != []) $this->client->setHeaders($header); - $this->is_available = true; - } - - /** - * @return bool - */ - public function upgrade() { - if (!$this->is_available) return false; - $r = $this->client->upgrade($this->parse["path"] . (isset($this->parse["query"]) ? ("?" . $this->parse["query"]) : "")); - if ($r) { - go(function () { - while (true) { - $result = $this->client->recv(60); - if ($result === false) { - if ($this->client->connected === false) { - go(function () { - call_user_func($this->close_func, $this->client); - }); - break; - } - } elseif ($result instanceof Frame) { - go(function () use ($result) { - $this->is_available = false; - call_user_func($this->message_func, $result, $this->client); - }); - } - } - }); - return true; - } - return false; - } - - /** - * @param callable $callable - * @return $this - */ - public function onMessage(callable $callable) { - $this->message_func = $callable; - return $this; - } - - /** - * @param callable $callable - * @return $this - */ - public function onClose(callable $callable) { - $this->close_func = $callable; - return $this; - } -} - -if (!debug_backtrace()) { - go(function () { - require_once __DIR__ . "/../../Framework/Console.php"; - $cli = new ZMWebSocket("ws://127.0.0.1:20001/"); - if (!$cli->is_available) die("Error!\n"); - $cli->onMessage(function (Frame $frame) { - var_dump($frame); - }); - $cli->onClose(function () { - echo "Connection closed.\n"; - }); - if ($cli->upgrade()) { - echo "成功连接!\n"; - } else { - echo "连接失败!\n"; - } - }); -} diff --git a/src/ZM/global_defines.php b/src/ZM/global_defines.php new file mode 100644 index 00000000..9612da44 --- /dev/null +++ b/src/ZM/global_defines.php @@ -0,0 +1,20 @@ +getConnection()->getName() == 'qq'; +} + +function connectIsDefault() { + return ctx()->getConnection()->getName() == 'default'; +} + +function connectIs($type) { + return ctx()->getConnection()->getName() == $type; +} + +function getAnnotations() { + $s = debug_backtrace()[1]; + //echo json_encode($s, 128|256); + $list = []; + foreach (EventManager::$events as $event => $v) { + foreach ($v as $ks => $vs) { + //echo get_class($vs).": ".$vs->class." => ".$vs->method.PHP_EOL; + if ($vs->class == $s["class"] && $vs->method == $s["function"]) { + $list[get_class($vs)][] = $vs; + } + } + } + return $list; +} + function set_coroutine_params($array) { $cid = Co::getCid(); if ($cid == -1) die("Cannot set coroutine params at none coroutine mode."); - if (isset(ZMBuf::$context[$cid])) ZMBuf::$context[$cid] = array_merge(ZMBuf::$context[$cid], $array); - else ZMBuf::$context[$cid] = $array; - foreach (ZMBuf::$context as $c => $v) { - if (!Co::exists($c)) unset(ZMBuf::$context[$c], ZMBuf::$context_class[$c]); + if (isset(Context::$context[$cid])) Context::$context[$cid] = array_merge(Context::$context[$cid], $array); + else Context::$context[$cid] = $array; + foreach (Context::$context as $c => $v) { + if (!Co::exists($c)) unset(Context::$context[$c], ZMBuf::$context_class[$c]); } } @@ -182,22 +218,30 @@ function context() { */ function ctx() { $cid = Co::getCid(); - $c_class = ZMBuf::globals("context_class"); - if (isset(ZMBuf::$context[$cid])) { + $c_class = ZMConfig::get("global", "context_class"); + if (isset(Context::$context[$cid])) { return ZMBuf::$context_class[$cid] ?? (ZMBuf::$context_class[$cid] = new $c_class($cid)); } else { Console::debug("未找到当前协程的上下文($cid),正在找父进程的上下文"); while (($pcid = Co::getPcid($cid)) !== -1) { $cid = $pcid; - if (isset(ZMBuf::$context[$cid])) return ZMBuf::$context_class[$cid] ?? (ZMBuf::$context_class[$cid] = new $c_class($cid)); + if (isset(Context::$context[$cid])) return ZMBuf::$context_class[$cid] ?? (ZMBuf::$context_class[$cid] = new $c_class($cid)); } return null; } } -function debug($msg) { Console::debug($msg); } +function zm_debug($msg) { Console::debug($msg); } -function zm_sleep($s = 1) { Co::sleep($s); } +function onebot_target_id_name($message_type) { + return ($message_type == "group" ? "group_id" : "user_id"); +} + +function zm_sleep($s = 1) { + if (Coroutine::getCid() != -1) System::sleep($s); + else usleep($s * 1000 * 1000); + return true; +} function zm_exec($cmd): array { return System::exec($cmd); } @@ -209,19 +253,41 @@ function zm_resume(int $cid) { Co::resume($cid); } function zm_timer_after($ms, callable $callable) { go(function () use ($ms, $callable) { - ZMUtil::checkWait(); Swoole\Timer::after($ms, $callable); }); } function zm_timer_tick($ms, callable $callable) { go(function () use ($ms, $callable) { - ZMUtil::checkWait(); Console::debug("Adding extra timer tick of " . $ms . " ms"); Swoole\Timer::tick($ms, $callable); }); } +function zm_data_hash($v) { + return md5($v["user_id"] . "^" . $v["self_id"] . "^" . $v["message_type"] . "^" . ($v[$v["message_type"] . "_id"] ?? $v["user_id"])); +} + +function server() { + return Framework::$server; +} + +/** + * @return ZMRobot + * @throws RobotNotFoundException + * @throws ZMException + */ +function bot() { + if (($conn = LightCacheInside::get("connect", "conn_fd")) == -2) { + return ZMRobot::getRandom(); + } elseif ($conn != -1) { + if (($obj = ManagerGM::get($conn)) !== null) return new ZMRobot($obj); + else throw new RobotNotFoundException("单机器人连接模式可能连接了多个机器人!"); + } else { + throw new RobotNotFoundException("没有任何机器人连接到框架!"); + } +} + diff --git a/test/CoroutinePoolTest.php b/test/CoroutinePoolTest.php new file mode 100644 index 00000000..cefced7b --- /dev/null +++ b/test/CoroutinePoolTest.php @@ -0,0 +1,28 @@ +assertTrue(true); + Console::init(4); + CoroutinePool::setSize("default", 2); + CoroutinePool::defaultSize(50); + for ($i = 0; $i < 59; ++$i) { + CoroutinePool::go(function () use ($i) { + //Console::debug("第 $i 个马上进入睡眠..."); + ZMRequest::get("http://localhost:9002/test/ping"); + Console::verbose(strval($i)); + }); + } + } + + public function testA() { + $this->assertTrue(true); + } +} diff --git a/test/LightCacheTest.php b/test/LightCacheTest.php new file mode 100644 index 00000000..59dff6f3 --- /dev/null +++ b/test/LightCacheTest.php @@ -0,0 +1,26 @@ + 2, + "max_strlen" => 4096, + "hash_conflict_proportion" => 0, + "persistence_path" => "../composer.json" + ]); + //LightCache::set("bool", true); + $this->assertEquals(true, LightCache::set("2048", 123, 3)); + $this->assertArrayHasKey("2048", LightCache::getAll()); + sleep(3); + $this->assertArrayNotHasKey("2048", LightCache::getAll()); + $this->assertEquals("Apache-2.0", LightCache::get("license")); + $this->assertEquals("zhamao/framework", LightCache::get("name")); + //$this->assertTrue(LightCache::set("storage", "asdasd", -2)); + //LightCache::savePersistence(); + } +} diff --git a/test/ZMTest/Mock/Context.php b/test/ZMTest/Mock/Context.php new file mode 100644 index 00000000..5992f718 --- /dev/null +++ b/test/ZMTest/Mock/Context.php @@ -0,0 +1,256 @@ +set参数 */ +$config['swoole'] = [ + 'log_file' => $config['crash_dir'] . 'swoole_error.log', + 'worker_num' => 8, + 'dispatch_mode' => 2, + 'max_coroutine' => 30000, + //'task_worker_num' => 4, + //'task_enable_coroutine' => true +]; + +/** 轻量字符串缓存,默认开启 */ +$config['light_cache'] = [ + "status" => true, + "size" => 2048, //最多允许储存的条数(需要2的倍数) + "max_strlen" => 4096, //单行字符串最大长度(需要2的倍数) + "hash_conflict_proportion" => 0.6 //Hash冲突率(越大越好,但是需要的内存更多) +]; + +/** MySQL数据库连接信息,host留空则启动时不创建sql连接池 */ +$config['sql_config'] = [ + 'sql_host' => '', + 'sql_port' => 3306, + 'sql_username' => 'name', + 'sql_database' => 'db_name', + 'sql_password' => '', + 'sql_enable_cache' => true, + 'sql_reset_cache' => '0300', + 'sql_options' => [ + PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_EMULATE_PREPARES => false + ], + 'sql_no_exception' => false, + 'sql_default_fetch_mode' => PDO::FETCH_ASSOC // added in 1.5.6 +]; + +/** CQHTTP连接约定的token */ +$config["access_token"] = ""; + +/** HTTP服务器固定请求头的返回 */ +$config['http_header'] = [ + 'X-Powered-By' => 'zhamao-framework', + 'Content-Type' => 'text/html; charset=utf-8' +]; + +/** HTTP服务器在指定状态码下回复的页面(默认) */ +$config['http_default_code_page'] = [ + '404' => '404.html' +]; + +/** zhamao-framework在框架启动时初始化的atomic们 */ +$config['init_atomics'] = [ + //'custom_atomic_name' => 0, //自定义添加的Atomic +]; + +/** 终端日志显示等级(0-4) */ +$config["info_level"] = 2; + +/** 自动保存计时器的缓存保存时间(秒) */ +$config['auto_save_interval'] = 900; + +/** 上下文接口类 implemented from ContextInterface */ +$config['context_class'] = \ZMTest\Mock\Context::class; + +/** 静态文件访问 */ +$config['static_file_server'] = [ + 'status' => false, + 'document_root' => realpath(__DIR__ . "/../") . '/resources/html', + 'document_index' => [ + 'index.html' + ] +]; + +/** 注册 Swoole Server 事件注解的类列表 */ +$config['server_event_handler_class'] = [ + \ZM\Event\ServerEventHandler::class, +]; + +/** 注册自定义指令的类 */ +$config['command_register_class'] = [ + //\Custom\Command\CustomCommand::class +]; + +/** 服务器启用的外部第三方和内部插件 */ +$config['modules'] = [ + 'onebot' => true, // QQ机器人事件解析器,如果取消此项则默认为 true 开启状态,否则你手动填写 false 才会关闭 +]; + +return $config; diff --git a/test/ZMTest/Mock/mock.php b/test/ZMTest/Mock/mock.php new file mode 100644 index 00000000..a4abe2da --- /dev/null +++ b/test/ZMTest/Mock/mock.php @@ -0,0 +1,2 @@ +parser = new AnnotationParser(); + $this->parser->addRegisterPath(WORKING_DIR . "/src/Module/", "Module"); + try { + $this->parser->registerMods(); + } catch (ReflectionException $e) { + throw $e; + } + } + + public function testAnnotation() { + ob_start(); + $gen = $this->parser->generateAnnotationEvents(); + $m = $gen[OnStart::class][0]->method; + $class = $gen[OnStart::class][0]->class; + $c = new $class(); + try { + $c->$m(); + } catch (Exception $e) { + } + $result = ob_get_clean(); + echo $result; + $this->assertStringContainsString("我开始了!", $result); + } + + public function testAnnotation2() { + + foreach ($this->parser->generateAnnotationEvents() as $k => $v) { + foreach ($v as $vs) { + $this->assertTrue($vs->method === null || $vs->method != ''); + $this->assertTrue(strlen($vs->class) > 0); + } + } + } + + public function testAnnotationMap() { + $map = $this->parser->getMiddlewareMap(); + $this->assertContainsEquals("timer", $map[Hello::class]["timer"]); + } + + public function testMiddlewares() { + $wares = $this->parser->getMiddlewares(); + $this->assertArrayHasKey("timer", $wares); + } + + public function testReqMapping() { + $mapping = $this->parser->getReqMapping(); + $this->assertEquals("index", $mapping["method"]); + } +} diff --git a/test/ZMTest/Testing/EventDispatcherTest.php b/test/ZMTest/Testing/EventDispatcherTest.php new file mode 100644 index 00000000..26984427 --- /dev/null +++ b/test/ZMTest/Testing/EventDispatcherTest.php @@ -0,0 +1,51 @@ +addRegisterPath(WORKING_DIR . "/src/Module/", "Module"); + try { + $parser->registerMods(); + } catch (ReflectionException $e) { + throw $e; + } + EventManager::loadEventByParser($parser); + $dispatcher = new EventDispatcher(CQCommand::class); + $dispatcher->setReturnFunction(function ($result) { + echo $result . PHP_EOL; + }); + //$dispatcher->setRuleFunction(function ($v) { return $v->match == "qwe"; }); + $dispatcher->setRuleFunction(function ($v) { return $v->match == "你好"; }); + //$dispatcher->setRuleFunction(fn ($v) => $v->match == "qwe"); + ob_start(); + try { + $dispatcher->dispatchEvents(); + } catch (AnnotationException $e) { + } + $r = ob_get_clean(); + echo $r; + $this->assertStringContainsString("你好啊", $r); + } +} diff --git a/test/ZMTest/Testing/ModuleTest.php b/test/ZMTest/Testing/ModuleTest.php new file mode 100644 index 00000000..a42644f6 --- /dev/null +++ b/test/ZMTest/Testing/ModuleTest.php @@ -0,0 +1,27 @@ +randNum(["随机数", "1", "5"]); + $out = ob_get_clean(); + $this->assertEquals("随机数是:1\n", $out); + } +} diff --git a/test/test.php b/test/test.php new file mode 100644 index 00000000..13ae52ed --- /dev/null +++ b/test/test.php @@ -0,0 +1,64 @@ + 'MyController')); +$routes = new RouteCollection(); +$routes->add('route_name', $route); + + + +$route = new Route( + '/archive/{month}', // path + array('controller' => 'showArchive', 'asd' => 'feswf'), // default values + array('month' => '[0-9]{4}-[0-9]{2}', 'subdomain' => 'www|m'), // requirements + array(), // options + '', // host + array(), // schemes + array() // methods +); + +// ... + +$routes->add('date', new Route( + '/archive/{month}', // path + array('controller' => 'showArchive', 'asd' => 'feswf'), // default values + array('month' => '[0-9]{4}-[0-9]{2}', 'subdomain' => 'www|m'), // requirements + array(), // options + '', // host + array(), // schemes + array() // methods +)); + +$route = new Route('/archive/test'); + +$routes->add('qwerty', $route); + +$route = new Route('/'); + +$routes->add('root', $route); + +$context = new RequestContext(); + +$matcher = new UrlMatcher($routes, $context); + +//$parameters = $matcher->match('/test/foo');var_dump($parameters); + +$parameters = $matcher->match('/archive/2012-01'); +var_dump($parameters); +// array( +// 'controller' => 'showArchive', +// 'month' => '2012-01', +// 'subdomain' => 'www', +// '_route' => ... +// ) + +$parameters = $matcher->match('/'); +var_dump($parameters); + +$sub = new RouteCollection();