diff --git a/.gitignore b/.gitignore index f7c4739..caae864 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,5 @@ composer.phar /vendor/ /.vscode test.php -# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control -# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file -# composer.lock +config.json +/test \ No newline at end of file diff --git a/README.md b/README.md index 8dc8169..79f3f8c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ the recomended way is to use amphp's server to run all your bots #### server.php ```php -$server = new Server("127.0.0.1:8080"); // create server instance running on port 8080 +$server = new Server("127.0.0.1:8080"); // create server instance listening to port 8080 $server->load_file("bot.php", "index"); // load the handlers in "bot.php" and store them in "index" path $server->load_folder("folder", true); // load all files in a folder. the second param is whether to load recursivly or not $server->run(); @@ -44,7 +44,7 @@ a lot more hanlder, config and server options in examples folder. there is 4 main objects in the library -1. Server: extends Loader. load files and runing the http-server and activating handlers. +1. Server: extends Loader. load files, runing the http-server and activating handlers. 2. Config: configuration. 3. Update: extends API and HTTP. contains all the method to send request to bot api. 4. Handler: create handlers that will run asyncronicly. @@ -86,7 +86,7 @@ All bot api method and some more in this class. instance of this class is passed telegram api methods - https://core.telegram.org/bots/api#available-methods -Added methods: +#### Added methods: - reply: reply to the message. - delete: delete the message. @@ -101,6 +101,7 @@ Added methods: Also there is a lot of preset variables to many update parts. see update.php file. +#### variables Partial list: - chat: the chat where the message sent. @@ -112,7 +113,7 @@ Partial list: you can access the update as object or as array. `$u->message->chat->id` or `$u['message']['chat']['id']`. -you can skip the update type (message is above example). +you can skip the update type (message in above example). `$u->data` will be the callback data in callback update. `$u->message_id` is the message_id. diff --git a/examples/server.php b/examples/server.php index 7e7a96c..979e2ec 100644 --- a/examples/server.php +++ b/examples/server.php @@ -14,8 +14,6 @@ $server->load_file("manager.php", "index.php", true); // this file is load with extra access. $server->load_folder("folder_full_of_bots"); -// you can choose whether run the server with one thread or multiple +// you can choose whether run the server with one thread or multiple. pass 'true' param to run method to run cluster // if you use cluster you have to run the server with vendor/bin/cluster server.php -$server->run(); -// OR -$server->runCluster(); \ No newline at end of file +$server->run(); \ No newline at end of file diff --git a/src/api.php b/src/api.php index edd2f11..0e1d2e0 100644 --- a/src/api.php +++ b/src/api.php @@ -368,7 +368,7 @@ public function text_adjust($text) } function __call($func, $args){ - $camelCaseFunc = str_replace(' ', '', ucwords(str_replace('-', ' ', $func))); + $camelCaseFunc = str_replace(' ', '', ucwords(str_replace(['_', '-'], ' ', $func))); if(method_exists($this, $camelCaseFunc)){ return $this->$camelCaseFunc(...$args); }else{ diff --git a/src/config.php b/src/config.php index 164bc4e..4f4ddd6 100644 --- a/src/config.php +++ b/src/config.php @@ -13,7 +13,7 @@ class Config{ public $ParseMode; /** - * @var $server the base url of the server to send requests + * @var $server the base url of the server to send api requests. */ public $server = 'https://api.telegram.org/bot'; @@ -32,7 +32,6 @@ class Config{ */ public $async = true; - /** * @var $debug show debug info */ @@ -51,7 +50,7 @@ class Config{ public $apiErrorHandler = null; /** - * set timeout and inactivity time upload and download requests. + * timeout and inactivity time for upload and download requests. */ public $fileRequstsTimeout = 30; diff --git a/src/handler.php b/src/handler.php index a18ec56..5dd2700 100644 --- a/src/handler.php +++ b/src/handler.php @@ -19,8 +19,7 @@ public function __construct(Update|null $update = null){ $this->update = $update; } - public function run($data){ - list($config, $update) = $data; + public function activate($config, $update){ if ($config->token === null) throw new \Error('token not set'); @@ -68,7 +67,8 @@ public function run($data){ } return $res; } catch (\Throwable $e) { - print $e->getMessage() . ' when running handlers in ' . $e->getFile() . ' line ' . $e->getLine() . PHP_EOL; + // TODO: get backtrace to the file where the error coming from + print $e->getMessage() . ', when running handlers in ' . $e->getFile() . ' line ' . $e->getLine() . PHP_EOL; if (isset($this->on_error)) { return $this->on_error->runHandler($e, $config->async) ?? []; } diff --git a/src/http.php b/src/http.php index fc477a8..d01af11 100644 --- a/src/http.php +++ b/src/http.php @@ -90,7 +90,7 @@ public function Request($url, $data = [], $async = true) * * `promise|request` - to yield amp's response object. * - * `update` - to yield resulse in update objcet. + * `update` - to yield result in update object. * * by default the Response promise yields Update object. * @@ -99,6 +99,27 @@ public function Request($url, $data = [], $async = true) class Response implements \Amp\Promise { public function __construct(private $request, private $config){ } + public function __get($key) + { + switch ($key) { + case 'result': + case 'response': + return $this->get_res(); + break; + case 'decode': + case 'array': + return $this->get_decoded_res(true); + break; + case 'promise': + case 'request': + return $this->request; + break; + case 'update': + default: + return $this->get_update(); + } + } + private function get_update(){ $return_update = function($req, $conf){ $res = yield $req; @@ -123,26 +144,6 @@ private function get_decoded_res($array = false){ }; return call($return_decoded_response, $this->request); } - - public function __get($key){ - switch($key){ - case 'result': - case 'response': - return $this->get_res(); - break; - case 'decode': - case 'array': - return $this->get_decoded_res(true); - break; - case 'promise': - case 'request': - return $this->request; - break; - case 'update': - default: - return $this->get_update(); - } - } public function onResolve(callable $cb) { diff --git a/src/loader.php b/src/loader.php index f4591ed..d7fa941 100644 --- a/src/loader.php +++ b/src/loader.php @@ -41,7 +41,7 @@ public function load_folder($path, $recursive = false){ public function load_file($file_name, $as = null, $extraAccess = false){ if(is_file($file_name)){ if($extraAccess) list($handler, $config) = $this->include_file_this($file_name); - else list( $handler, $config) = self::include_file_static($file_name); + else list($handler, $config) = self::include_file_static($file_name); $path = $file_name; if($as) @@ -49,7 +49,7 @@ public function load_file($file_name, $as = null, $extraAccess = false){ $this->files[$path] = ['file_name' => $file_name, 'active' => 1, 'handler' => $handler, 'config' => $config]; }else{ - print 'file $file_name not fount' . PHP_EOL; + print 'file '. $file_name .' not fount' . PHP_EOL; } } diff --git a/src/server.php b/src/server.php index 1bd9d04..1c7cc94 100644 --- a/src/server.php +++ b/src/server.php @@ -7,9 +7,6 @@ use Amp\ByteStream; use Amp\Cluster\Cluster; -use Amp\Promise; -use Amp\ByteStream\ResourceOutputStream; -use Amp\ByteStream\ResourceInputStream; use Amp\Http\Server\HttpServer; use Amp\Http\Server\RequestHandler\CallableRequestHandler; use Amp\Http\Server\Request; @@ -26,147 +23,147 @@ class Server extends Loader { // public array $files = []; - set in Loader + /** http server instance */ + private $server; + public function __construct($servers = null) { $this->servers = $servers; } - private function prepareServers(bool $cluster = false){ - if (gettype($this->servers) == 'array') { - $listening_servers = array_map(function ($elem)use($cluster) { - if($cluster) return Cluster::listen($elem); - return Socket\Server::listen($elem); - }, $this->servers); - } elseif (gettype($this->servers) == 'string') { - if($cluster) $listening_servers = [Cluster::listen($this->servers)]; - else $listening_servers = [Socket\Server::listen($this->servers)]; - } else { - if($cluster) $listening_servers = [Cluster::listen('127.0.0.1:1337')]; - else $listening_servers = [Socket\Server::listen('127.0.0.1:1337')]; - } - return $listening_servers; - } - - public function run(){ - Loop::run(function () { + public function run($as_cluster = false){ // TODO: replace with auto determiantion by command (php or bin/cluster) + Loop::run(function () use($as_cluster) { try{ - $servers = $this->prepareServers(); + // TODO: fix this + $servers = $as_cluster ? (yield $this->prepareServers($as_cluster)) : $this->prepareServers($as_cluster); - $logHandler = new StreamHandler(new ResourceOutputStream(STDOUT)); - $logHandler->setFormatter(new ConsoleFormatter); - $logger = new Logger('bots server'); - $logger->pushHandler($logHandler); - + $logger = $this->get_logger($as_cluster); + + // Set up a request handler. $server = new HttpServer($servers, new CallableRequestHandler(function (Request $request) use ($logger) { - try{ + try { \Amp\asyncCall([$this, 'requestHandler'], $request, $logger); - }catch(\Throwable $e){ - print $e->getMessage() . ' when handleing request to ' . $request->getUri() . ' on ' . $e->getFile() . ':' . $e->getLine() . PHP_EOL; + } catch (\Throwable $e) { + print $e->getMessage() . ' when handleing request to ' . $request->getUri() . ' on ' . $e->getFile().':'.$e->getLine() . PHP_EOL; } return new Response(Status::OK, [ 'content-type' => 'text/plain; charset=utf-8' ], 'ok'); }), $logger); - + + // Start the HTTP server yield $server->start(); - - Loop::onSignal(\SIGINT, static function (string $watcherId) use ($server) { - Loop::cancel($watcherId); - yield $server->stop(); - exit(); - }); - - // cli-options - // get info about running bots from console - $in = new ResourceInputStream(STDIN); - - while (($chunk = yield $in->read()) !== null) { - $flag = false; - switch (trim($chunk)) { - case 'ls': - foreach ($this->files as $file_name => $h) { - print $file_name . ' active: ' . $h['active'] . '\n'; - } - break; - case 'll': - foreach ($this->files as $file_name => $h) { - print $file_name . PHP_EOL; - foreach ($h['handler'] as $key => $hh) { - if ($key == 'handlers') { - continue; - } - print $key . PHP_EOL; - } - foreach ($h['handler']->handlers as $h) { - print var_export($h) . PHP_EOL; - } - } - break; - case 'reload': - // TODO: how to reload the files too - print 'restarting server...' . PHP_EOL; - yield $server->stop(2000); - Loop::stop(); - $flag = true; - break; - case 'exit': - exit(); - break; - default: - print 'can\'t understand you. available options are: exit/reload/ls/ll/^C' . PHP_EOL; - } - if ($flag) break; + $this->server = $server; + + \Amp\call(\Closure::fromCallable([$this, 'cli_options'])); + + if($as_cluster){ + // Stop the server when the worker is terminated. + Cluster::onTerminate(function () use ($server) { + return $server->stop(); + }); + }else{ + Loop::onSignal(\SIGINT, static function (string $watcherId) use ($server) { + Loop::cancel($watcherId); + yield $server->stop(); + Loop::stop(); + }); } + }catch(\Throwable $e){ - print $e->getMessage() . ' in ' . $e->getFile() . ' line ' . $e->getLine() . PHP_EOL; + print $e->getMessage() . ' in event-loop. file: ' . $e->getFile() . ' line ' . $e->getLine() . ' exiting loop and server ' . PHP_EOL; yield $server->stop(); Loop::stop(); } }); } - public function runCluster(){ - Loop::run(function () { - try{ - $servers = yield $this->prepareServers(true); - - // Creating a log handler in this way allows the script to be run in a cluster or standalone. - if (Cluster::isWorker()) { - $handler = Cluster::createLogHandler(); - } else { - $handler = new StreamHandler(ByteStream\getStdout()); - $handler->setFormatter(new ConsoleFormatter); - } + private function prepareServers(bool $cluster = false){ + if (gettype($this->servers) == 'array') { + $listening_servers = array_map(function ($elem)use($cluster) { + if($cluster) return Cluster::listen($elem); + return Socket\Server::listen($elem); + }, $this->servers); + + } elseif (gettype($this->servers) == 'string') { + if($cluster) $listening_servers = [Cluster::listen($this->servers)]; + else $listening_servers = [Socket\Server::listen($this->servers)]; + + } else { + if($cluster) $listening_servers = [Cluster::listen('127.0.0.1:1337')]; + else $listening_servers = [Socket\Server::listen('127.0.0.1:1337')]; + } - $logger = new Logger('worker-' . Cluster::getId()); - $logger->pushHandler($handler); + return $listening_servers; + } - // Set up a simple request handler. - $server = new HttpServer($servers, new CallableRequestHandler(function (Request $request) use ($logger) { - try { - \Amp\asyncCall([$this, 'requestHandler'], $request, $logger); - } catch (\Throwable $e) { - print $e->getMessage() . ' when handleing request to ' . $request->getUri() . ' on ' . $e->getFile().':'.$e->getLine() . PHP_EOL; - } - return new Response(Status::OK, [ - 'content-type' => 'text/plain; charset=utf-8' - ], 'ok'); - }), $logger); + private function get_logger($cluster = false){ + if($cluster){ + // Creating a log handler in this way allows the script to be run in a cluster or standalone. + if (Cluster::isWorker()) { + $handler = Cluster::createLogHandler(); + } else { + $handler = new StreamHandler(ByteStream\getStdout()); + $handler->setFormatter(new ConsoleFormatter); + } - // Start the HTTP server - yield $server->start(); + $logger = new Logger('worker-' . Cluster::getId()); + $logger->pushHandler($handler); + return $logger; + }else{ + $logHandler = new StreamHandler(ByteStream\getStdout()); + $logHandler->setFormatter(new ConsoleFormatter); + $logger = new Logger('bots server'); + $logger->pushHandler($logHandler); + return $logger; + } + } - // Stop the server when the worker is terminated. - Cluster::onTerminate(function () use ($server): Promise { - return $server->stop(); - }); - - }catch(\Throwable $e){ - print $e->getMessage() . ' in ' . $e->getFile() . ' line ' . $e->getLine() . PHP_EOL; - yield $server->stop(); - Loop::stop(); + /** + * cli-options + * + * get info about running bots and handlers from console + */ + private function cli_options(){ + $in = ByteStream\getStdin(); + + while (($chunk = yield $in->read()) !== null) { + $flag = false; + switch (trim($chunk)) { + case 'ls': + foreach ($this->files as $file_name => $h) { + print $file_name . ' active: ' . $h['active'] . '\n'; + } + break; + case 'll': + foreach ($this->files as $file_name => $h) { + print $file_name . PHP_EOL; + foreach ($h['handler'] as $key => $hh) { + if ($key == 'handlers') { + continue; + } + print $key . PHP_EOL; + } + foreach ($h['handler']->handlers as $h) { + print var_export($h) . PHP_EOL; + } + } + break; + // case 'reload': + // // TODO: how to reload the files too + // print 'restarting server...' . PHP_EOL; + // yield $server->stop(2000); + // Loop::stop(); + // $flag = true; + // break; + case 'exit': + exit(); + break; + default: + print 'can\'t understand you. available options are: exit/reload/ls/ll/^C' . PHP_EOL; } - }); + if ($flag) break; + } } /** @@ -176,36 +173,34 @@ public function runCluster(){ */ public function requestHandler($request, $logger){ $path = ltrim($request->getUri()->getPath(), '/'); - if (isset($this->files[$path])) { - if ($this->files[$path]['active']) { - // debug - $this->files[$path]['config']->debug && $logger->info('running file: ' . $path); - - try { - $time = \Amp\Loop::now(); - - $file = $this->files[$path]; - - $update_string = yield $request->getBody()->buffer(); - $update = new Update($file['config'], $update_string); - - $run_info = [$file['config'], $update]; - - parse_str($request->getUri()->getQuery(), $query); - if (isset($query['token'])) - $run_info[0]->token = $query['token']; - - // $res = yield \Amp\call([$this, 'runHandler'], $run_info); - $res = yield \Amp\call([$file['handler'], 'run'], $run_info); - $file['config']->debug && $logger->info('took: '.\Amp\Loop::now() - $time.'. handlers result', $res ?? []); - - } catch (\Throwable $e) { - $logger->error( $e->getMessage() . ' in file ' . $e->getFile() . ' in line ' . $e->getLine() . '. path ' . $path . ' - disabled!'); - $this->files[$path]['active'] = 0; - } - } - } else { + + if (!isset($this->files[$path])) { $logger->notice('file ' . $path . ' not exist'); + + }elseif ($this->files[$path]['active']) { + // debug + $this->files[$path]['config']->debug && $logger->info('running file: ' . $path); + + try { + $time = \Amp\Loop::now(); + + $file = $this->files[$path]; + + $update_string = yield $request->getBody()->buffer(); + $update = new Update($file['config'], $update_string); + + parse_str($request->getUri()->getQuery(), $query); + if (isset($query['token'])) + $file['config']->token = $query['token']; + + $res = yield \Amp\call([$file['handler'], 'activate'], $file['config'], $update); + + $file['config']->debug && $logger->info('took: '.\Amp\Loop::now() - $time.'. handlers result', $res ?? []); + + } catch (\Throwable $e) { + $logger->error( $e->getMessage() . ' when activate handlers in file ' . $e->getFile() . ' in line ' . $e->getLine() . '. path ' . $path . ' - disabled!'); + $this->files[$path]['active'] = 0; + } } } } diff --git a/src/update.php b/src/update.php index 5dce24e..cf4f26e 100644 --- a/src/update.php +++ b/src/update.php @@ -75,7 +75,7 @@ public function __construct(public Config $config, string $update = null) public function delete() { if(isset($this->update)) - return $this->deleteMessage($this->chat->id, $this->message->id); + return $this->deleteMessage($this->chat->id, $this->message_id); } public function edit($newMessage, $replyMarkup = null, $ent = null) @@ -162,7 +162,7 @@ public function editButton($button_data, $new_button = null) public function ban($id = null) { if ($this->chatType != 'private') - $this->kickChatMember($this->chat->id, $id ?? $this->from->id); + $this->banChatMember($this->chat->id, $id ?? $this->from->id); else return $this; } @@ -199,11 +199,11 @@ private function init_vars($update){ } $this->forward = $update_obj->$updateType->forward_from ?? $update_obj->$updateType->forward_from_chat ?? null; - $this->chat = $update_obj->$updateType->chat ?? null; + $this->chat = $this->__get('chat') ?? null; $this->from = $this->update_arr['callback_query']['from'] ?? $update_obj->$updateType->sender_chat ?? $update_obj->$updateType->from ?? $this->chat ?? null; $this->reply = $update_obj->$updateType->reply_to_message ?? null; $this->text = $update[$updateType]['text'] ?? $update[$updateType]['caption'] ?? $update[$updateType]['query'] ?? null; - $this->chatType = $this->chat->type; + $this->chatType = $this->chat->type ?? null; $this->ent = $update[$updateType]['entities'] ?? null; @@ -227,7 +227,7 @@ private function init_vars($update){ $this->media = $media; // if thete ent in text its revers it to markdown and add `/```/*/_ to text - $realtext = $update[$updateType]['text']; + $realtext = $this->text; if ($this->ent != null) { $i = 0; foreach ($this->ent as $e) { @@ -290,6 +290,9 @@ public function __get($value) return $this->update_obj->$value; if (isset($this->update_obj->{$this->updateType}->$value)) return $this->update_obj->{$this->updateType}->$value; + // in cbq update the message is inside cbq object + if (isset($this->message->$value)) + return $this->message->$value; } public function offsetSet($offset, $value)