CLI refactoring, Postman support, breaking changes

This commit is contained in:
Sys 2021-07-24 17:27:47 +02:00
parent 4bf53de7cd
commit ce4836623b
No known key found for this signature in database
GPG Key ID: 3CD2C29F8AB39BFD
9 changed files with 1502 additions and 431 deletions

View File

@ -1,121 +1,22 @@
#!/usr/bin/env php
<?php
use Composer\InstalledVersions;
use Symfony\Component\Console\Application;
use TgScraper\Commands\CreateStubsCommand;
use TgScraper\Commands\ExportSchemaCommand;
require_once __DIR__ . '/../vendor/autoload.php';
use TgScraper\Generator;
$application = new Application('TGScraper', InstalledVersions::getVersion('sysbot/tgscraper'));
$args = getopt('o:u:n:hs:');
if (array_key_exists('h', $args)) {
echo 'Usage: tgscraper -o <output> [-u <url>] [-n <namespace>] [-s <scheme>]' . PHP_EOL . PHP_EOL;
echo 'Options:' . PHP_EOL;
echo ' -n <namespace> namespace to use (for PHP stubs only)' . PHP_EOL;
echo ' -o <output> output file/directory (the directory must exist) for JSON/YAML schema or PHP stubs' . PHP_EOL;
echo ' -s <scheme> path to custom scheme to use (for PHP stubs only)' . PHP_EOL;
echo ' -u <url> URL to use for fetching bot API data' . PHP_EOL . PHP_EOL;
echo 'Note: based on the "-o" value, the script will automatically detect whether to output a JSON/YAML schema or the PHP stubs.' . PHP_EOL;
exit;
}
$output = $args['o'] ?? null;
$namespace = $args['n'] ?? 'Generated';
$url = $args['u'] ?? Generator::BOT_API_URL;
$scheme = $args['s'] ?? null;
if (empty($output)) {
fwrite(STDERR, 'ERROR: "-o" option missing!' . PHP_EOL . PHP_EOL);
fwrite(STDERR, 'Use "tgscraper -h" for help.' . PHP_EOL);
exit(1);
}
if (!empty($url) and !filter_var($url, FILTER_VALIDATE_URL)) {
echo '> WARNING: URL not valid, the default one will be used.' . PHP_EOL;
$url = Generator::BOT_API_URL;
}
$generator = new Generator($url);
$data = null;
if (is_dir($output)) {
if (!empty($scheme)) {
$data = file_get_contents($scheme);
if (false === $data) {
echo sprintf('> WARNING: Cannot read data from "%s", URL will be used instead.%s', $scheme, PHP_EOL);
echo sprintf('> Using "%s" as URL.%s', $url, PHP_EOL);
try {
echo '> Extracting JSON scheme.' . PHP_EOL;
$data = $generator->toJson();
} catch (Throwable $e) {
echo '> ERROR: Unable to extract scheme: ' . $e->getMessage() . PHP_EOL;
exit(1);
}
} else {
echo sprintf('> Loaded data from "%s".%s', $scheme, PHP_EOL);
}
echo sprintf('> Outputting PHP stubs to %s.%s', realpath($output), PHP_EOL);
/** @noinspection PhpUnhandledExceptionInspection */
$result = $generator->toStubs($output, $namespace, $data);
if (!$result) {
echo '> ERROR: unable to create stubs.' . PHP_EOL;
}
exit(!$result);
}
echo sprintf('> Using "%s" as URL.%s', $url, PHP_EOL);
try {
echo '> Extracting JSON scheme.' . PHP_EOL;
$data = $generator->toJson();
} catch (Throwable $e) {
echo '> ERROR: Unable to extract scheme: ' . $e->getMessage() . PHP_EOL;
exit(1);
}
echo sprintf('> Outputting PHP stubs to %s.%s', realpath($output), PHP_EOL);
/** @noinspection PhpUnhandledExceptionInspection */
$result = $generator->toStubs($output, $namespace, $data);
if (!$result) {
echo '> ERROR: unable to create stubs.' . PHP_EOL;
}
exit(!$result);
}
echo sprintf('> Using "%s" as URL.%s', $url, PHP_EOL);
touch($output);
$extension = pathinfo($output, PATHINFO_EXTENSION);
if (in_array($extension, ['yml', 'yaml'])) {
echo sprintf('> Outputting YAML schema to %s.%s', realpath($output), PHP_EOL);
try {
$data = $generator->toYaml();
} catch (Throwable $e) {
echo '> ERROR: Unable to generate scheme:' . $e->getMessage() . PHP_EOL;
exit(1);
}
$error = file_put_contents($output, $data) === false;
if ($error) {
echo '> ERROR: unable to write to file.' . PHP_EOL;
}
exit($error);
}
echo sprintf('> Outputting JSON schema to %s.%s', realpath($output), PHP_EOL);
$application->add(new CreateStubsCommand());
$application->add(new ExportSchemaCommand());
try {
$data = $generator->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
echo '> ERROR: Unable to generate scheme:' . $e->getMessage() . PHP_EOL;
exit(1);
$exitCode = $application->run();
} catch (Exception $e) {
echo $e->getMessage() . PHP_EOL;
}
$error = file_put_contents($output, $data) === false;
if ($error) {
echo '> ERROR: unable to write to file.' . PHP_EOL;
}
exit($error);
exit($exitCode ?? 1);

View File

@ -5,8 +5,11 @@
"require": {
"php": ">=8.0",
"ext-json": "*",
"composer-runtime-api": "^2.0",
"nette/php-generator": "^3.5",
"paquettg/php-html-parser": "^3.1",
"psr/log": "^1.1",
"symfony/console": "^5.3",
"symfony/yaml": "^5.3"
},
"autoload": {

814
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ce91774666a1ea15b98376aed25cee10",
"content-hash": "e8ae86c9b854e3496c7ed12c9ea0d6c4",
"packages": [
{
"name": "guzzlehttp/guzzle",
@ -241,16 +241,16 @@
},
{
"name": "myclabs/php-enum",
"version": "1.8.0",
"version": "1.8.3",
"source": {
"type": "git",
"url": "https://github.com/myclabs/php-enum.git",
"reference": "46cf3d8498b095bd33727b13fd5707263af99421"
"reference": "b942d263c641ddb5190929ff840c68f78713e937"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/php-enum/zipball/46cf3d8498b095bd33727b13fd5707263af99421",
"reference": "46cf3d8498b095bd33727b13fd5707263af99421",
"url": "https://api.github.com/repos/myclabs/php-enum/zipball/b942d263c641ddb5190929ff840c68f78713e937",
"reference": "b942d263c641ddb5190929ff840c68f78713e937",
"shasum": ""
},
"require": {
@ -260,7 +260,7 @@
"require-dev": {
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "1.*",
"vimeo/psalm": "^4.5.1"
"vimeo/psalm": "^4.6.2"
},
"type": "library",
"autoload": {
@ -285,7 +285,7 @@
],
"support": {
"issues": "https://github.com/myclabs/php-enum/issues",
"source": "https://github.com/myclabs/php-enum/tree/1.8.0"
"source": "https://github.com/myclabs/php-enum/tree/1.8.3"
},
"funding": [
{
@ -297,20 +297,20 @@
"type": "tidelift"
}
],
"time": "2021-02-15T16:11:48+00:00"
"time": "2021-07-05T08:18:36+00:00"
},
{
"name": "nette/php-generator",
"version": "v3.5.3",
"version": "v3.5.4",
"source": {
"type": "git",
"url": "https://github.com/nette/php-generator.git",
"reference": "119f01a7bd590469cb01b538f20a125a28853626"
"reference": "59bb35ed6e8da95854fbf7b7d47dce6156b42915"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/php-generator/zipball/119f01a7bd590469cb01b538f20a125a28853626",
"reference": "119f01a7bd590469cb01b538f20a125a28853626",
"url": "https://api.github.com/repos/nette/php-generator/zipball/59bb35ed6e8da95854fbf7b7d47dce6156b42915",
"reference": "59bb35ed6e8da95854fbf7b7d47dce6156b42915",
"shasum": ""
},
"require": {
@ -353,7 +353,7 @@
"homepage": "https://nette.org/contributors"
}
],
"description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.4 features.",
"description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.0 features.",
"homepage": "https://nette.org",
"keywords": [
"code",
@ -363,9 +363,9 @@
],
"support": {
"issues": "https://github.com/nette/php-generator/issues",
"source": "https://github.com/nette/php-generator/tree/v3.5.3"
"source": "https://github.com/nette/php-generator/tree/v3.5.4"
},
"time": "2021-02-24T18:40:21+00:00"
"time": "2021-07-05T12:02:42+00:00"
},
{
"name": "nette/utils",
@ -689,6 +689,54 @@
},
"time": "2020-07-07T09:29:14+00:00"
},
{
"name": "psr/container",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/1.1.1"
},
"time": "2021-03-05T17:36:06+00:00"
},
{
"name": "psr/http-client",
"version": "1.0.1",
@ -794,6 +842,56 @@
},
"time": "2016-08-06T14:39:51+00:00"
},
{
"name": "psr/log",
"version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/1.1.4"
},
"time": "2021-05-03T11:20:27+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
@ -838,6 +936,104 @@
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "symfony/console",
"version": "v5.3.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/649730483885ff2ca99ca0560ef0e5f6b03f2ac1",
"reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php73": "^1.8",
"symfony/polyfill-php80": "^1.15",
"symfony/service-contracts": "^1.1|^2",
"symfony/string": "^5.1"
},
"conflict": {
"symfony/dependency-injection": "<4.4",
"symfony/dotenv": "<5.1",
"symfony/event-dispatcher": "<4.4",
"symfony/lock": "<4.4",
"symfony/process": "<4.4"
},
"provide": {
"psr/log-implementation": "1.0"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/config": "^4.4|^5.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/event-dispatcher": "^4.4|^5.0",
"symfony/lock": "^4.4|^5.0",
"symfony/process": "^4.4|^5.0",
"symfony/var-dumper": "^4.4|^5.0"
},
"suggest": {
"psr/log": "For using the console logger",
"symfony/event-dispatcher": "",
"symfony/lock": "",
"symfony/process": ""
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Eases the creation of beautiful and testable command line interfaces",
"homepage": "https://symfony.com",
"keywords": [
"cli",
"command line",
"console",
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.3.2"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-06-12T09:42:48+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v2.4.0",
@ -985,17 +1181,586 @@
"time": "2021-02-19T12:13:01+00:00"
},
{
"name": "symfony/yaml",
"version": "v5.3.2",
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "71719ab2409401711d619765aa255f9d352a59b2"
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "24b72c6baa32c746a4d0840147c9715e42bb68ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/71719ab2409401711d619765aa255f9d352a59b2",
"reference": "71719ab2409401711d619765aa255f9d352a59b2",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/24b72c6baa32c746a4d0840147c9715e42bb68ab",
"reference": "24b72c6baa32c746a4d0840147c9715e42bb68ab",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's grapheme_* functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"grapheme",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-05-27T09:17:38+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8",
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's Normalizer class and related functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"intl",
"normalizer",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-02-19T12:13:01+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
"reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-05-27T09:27:20+00:00"
},
{
"name": "symfony/polyfill-php73",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php73.git",
"reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010",
"reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php73\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php73/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-02-19T12:13:01+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/eca0bf41ed421bed1b57c4958bab16aa86b757d0",
"reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-02-19T12:13:01+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
"reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"psr/container": "^1.1"
},
"suggest": {
"symfony/service-implementation": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.4-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Service\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to writing services",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v2.4.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-04-01T10:43:52+00:00"
},
{
"name": "symfony/string",
"version": "v5.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1",
"reference": "bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php80": "~1.15"
},
"require-dev": {
"symfony/error-handler": "^4.4|^5.0",
"symfony/http-client": "^4.4|^5.0",
"symfony/translation-contracts": "^1.1|^2",
"symfony/var-exporter": "^4.4|^5.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\String\\": ""
},
"files": [
"Resources/functions.php"
],
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
"homepage": "https://symfony.com",
"keywords": [
"grapheme",
"i18n",
"string",
"unicode",
"utf-8",
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v5.3.3"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-06-27T11:44:38+00:00"
},
{
"name": "symfony/yaml",
"version": "v5.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "485c83a2fb5893e2ff21bf4bfc7fdf48b4967229"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/485c83a2fb5893e2ff21bf4bfc7fdf48b4967229",
"reference": "485c83a2fb5893e2ff21bf4bfc7fdf48b4967229",
"shasum": ""
},
"require": {
@ -1041,7 +1806,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v5.3.2"
"source": "https://github.com/symfony/yaml/tree/v5.3.3"
},
"funding": [
{
@ -1057,7 +1822,7 @@
"type": "tidelift"
}
],
"time": "2021-06-06T09:51:56+00:00"
"time": "2021-06-24T08:13:00+00:00"
}
],
"packages-dev": [],
@ -1068,7 +1833,8 @@
"prefer-lowest": false,
"platform": {
"php": ">=8.0",
"ext-json": "*"
"ext-json": "*",
"composer-runtime-api": "^2.0"
},
"platform-dev": [],
"plugin-api-version": "2.0.0"

View File

@ -0,0 +1,96 @@
<?php
namespace TgScraper\Commands;
use Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Logger\ConsoleLogger;
use Symfony\Component\Console\Output\OutputInterface;
use TgScraper\Constants\Versions;
use TgScraper\Generator;
use Throwable;
class CreateStubsCommand extends Command
{
protected static $defaultName = 'app:create-stubs';
protected function validateData(string|false $data): bool
{
return false !== $data and !empty($data);
}
protected function configure(): void
{
$this
->setDescription('Create stubs from bot API schema.')
->setHelp('This command allows you to create class stubs for all types of the Telegram bot API.')
->addArgument('destination', InputArgument::REQUIRED, 'Destination directory')
->addOption('namespace-prefix', null, InputOption::VALUE_REQUIRED, 'Namespace prefix for stubs', 'TelegramApi')
->addOption(
'json',
null,
InputOption::VALUE_REQUIRED,
'Path to JSON file to use instead of fetching from URL (this option takes precedence over "--layer")'
)
->addOption(
'yaml',
null,
InputOption::VALUE_REQUIRED,
'Path to YAML file to use instead of fetching from URL (this option takes precedence over "--layer" and "--json")'
)
->addOption('layer', 'l', InputOption::VALUE_REQUIRED, 'Bot API version to use', 'latest');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$logger = new ConsoleLogger($output);
$url = Versions::getVersionFromText($input->getOption('layer'));
$yamlPath = $input->getOption('yaml');
if (empty($yamlPath)) {
$jsonPath = $input->getOption('json');
if (empty($jsonPath)) {
$logger->info('Using URL: ' . $url);
try {
$output->writeln('Fetching data from URL...');
$generator = new Generator($logger, $url);
} catch (Throwable) {
return Command::FAILURE;
}
} else {
$data = file_get_contents($jsonPath);
if (!$this->validateData($data)) {
$logger->critical('Invalid JSON file provided');
return Command::INVALID;
}
$logger->info('Using JSON schema: ' . $jsonPath);
/** @noinspection PhpUnhandledExceptionInspection */
$generator = Generator::fromJson($logger, $data);
}
} else {
$data = file_get_contents($yamlPath);
if (!$this->validateData($data)) {
$logger->critical('Invalid YAML file provided');
return Command::INVALID;
}
$logger->info('Using YAML schema: ' . $yamlPath);
/** @noinspection PhpUnhandledExceptionInspection */
$generator = Generator::fromYaml($logger, $data);
}
try {
$output->writeln('Creating stubs...');
$generator->toStubs($input->getArgument('destination'), $input->getOption('namespace-prefix'));
} catch (Exception) {
$logger->critical('Could not create stubs.');
return Command::FAILURE;
}
$output->writeln('Done!');
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace TgScraper\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Logger\ConsoleLogger;
use Symfony\Component\Console\Output\OutputInterface;
use TgScraper\Constants\Versions;
use TgScraper\Generator;
use Throwable;
class ExportSchemaCommand extends Command
{
protected static $defaultName = 'app:export-schema';
protected function configure(): void
{
$this
->setDescription('Export schema as JSON or YAML.')
->setHelp('This command allows you to create a schema for a specific version of the Telegram bot API.')
->addArgument('destination', InputArgument::REQUIRED, 'Destination file')
->addOption(
'yaml',
null,
InputOption::VALUE_NONE,
'Export schema as YAML instead of JSON (this option takes precedence over "--postman")'
)
->addOption('postman', null, InputOption::VALUE_NONE, 'Export schema as a Postman-compatible JSON')
->addOption('options', 'o', InputOption::VALUE_REQUIRED, 'Encoder options', 0)
->addOption('readable', 'r', InputOption::VALUE_NONE, '(JSON only) Generate a human-readable JSON')
->addOption('inline', null, InputOption::VALUE_REQUIRED, '(YAML only) Inline level', 6)
->addOption('indent', null, InputOption::VALUE_REQUIRED, '(YAML only) Indent level', 4)
->addOption('layer', 'l', InputOption::VALUE_REQUIRED, 'Bot API version to use', 'latest');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$logger = new ConsoleLogger($output);
$url = Versions::getVersionFromText($input->getOption('layer'));
$logger->info('Using URL: ' . $url);
try {
$output->writeln('Fetching data from URL...');
$generator = new Generator($logger, $url);
} catch (Throwable) {
return Command::FAILURE;
}
$output->writeln('Exporting schema from data...');
$options = $input->getOption('options');
$useYaml = $input->getOption('yaml');
if ($useYaml) {
$data = $generator->toYaml($input->getOption('inline'), $input->getOption('indent'), $options);
}
$destination = $input->getArgument('destination');
if ($input->getOption('readable')) {
$options |= JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
}
$data ??= $input->getOption('postman') ? $generator->toPostman($options) : $generator->toJson($options);
$output->writeln('Saving scheme to file...');
$result = file_put_contents($destination, $data);
if (false === $result) {
$logger->critical('Unable to save file to ' . $destination);
return Command::FAILURE;
}
$output->writeln('Done!');
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,271 @@
<?php
namespace TgScraper\Common;
use PHPHtmlParser\Dom;
use PHPHtmlParser\Exceptions\ChildNotFoundException;
use PHPHtmlParser\Exceptions\CircularException;
use PHPHtmlParser\Exceptions\ContentLengthException;
use PHPHtmlParser\Exceptions\LogicalException;
use PHPHtmlParser\Exceptions\NotLoadedException;
use PHPHtmlParser\Exceptions\ParentNotFoundException;
use PHPHtmlParser\Exceptions\StrictException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Log\LoggerInterface;
use TgScraper\Constants\Versions;
use Throwable;
/**
* Class SchemaExtractor
* @package TgScraper\Common
*/
class SchemaExtractor
{
/**
* Additional methods with boolean return value.
*/
private const BOOL_RETURNS = [
'answerShippingQuery',
'answerPreCheckoutQuery'
];
/**
* SchemaExtractor constructor.
* @param LoggerInterface $logger
* @param string $url
*/
public function __construct(private LoggerInterface $logger, private string $url = Versions::LATEST)
{
}
/**
* @return array
* @throws ChildNotFoundException
* @throws CircularException
* @throws ContentLengthException
* @throws LogicalException
* @throws NotLoadedException
* @throws ParentNotFoundException
* @throws StrictException
* @throws ClientExceptionInterface
* @throws Throwable
*/
public function extract(): array
{
$dom = new Dom;
try {
$dom->loadFromURL($this->url);
} catch (Throwable $e) {
$this->logger->critical(sprintf('Unable to load data from URL "%s": %s', $this->url, $e->getMessage()));
throw $e;
}
try {
$elements = $dom->find('h4');
} catch (Throwable $e) {
$this->logger->critical(sprintf('Unable to load data from URL "%s": %s', $this->url, $e->getMessage()));
throw $e;
}
$data = [];
/* @var Dom\Node\AbstractNode $element */
foreach ($elements as $element) {
if (!str_contains($name = $element->text, ' ')) {
$isMethod = lcfirst($name) == $name;
$path = $isMethod ? 'methods' : 'types';
$temp = $element;
$description = '';
$table = null;
while (true) {
try {
$element = $element->nextSibling();
} catch (ChildNotFoundException) {
break;
}
$tag = $element->tag->name() ?? null;
if (empty($temp->text()) or empty($tag) or $tag == 'text') {
continue;
} elseif (str_starts_with($tag, 'h')) {
break;
} elseif ($tag == 'p') {
$description .= PHP_EOL . $element->innerHtml();
} elseif ($tag == 'table') {
$table = $element->find('tbody')->find('tr');
break;
}
}
/* @var Dom\Node\AbstractNode $element */
$data[$path][] = self::generateElement(
$name,
trim($description),
$table,
$isMethod
);
}
}
return $data;
}
/**
* @param string $name
* @param string $description
* @param Dom\Node\Collection|null $unparsedFields
* @param bool $isMethod
* @return array
* @throws ChildNotFoundException
* @throws CircularException
* @throws ContentLengthException
* @throws LogicalException
* @throws NotLoadedException
* @throws StrictException
*/
private static function generateElement(
string $name,
string $description,
?Dom\Node\Collection $unparsedFields,
bool $isMethod
): array {
$fields = self::parseFields($unparsedFields, $isMethod);
$result = [
'name' => $name,
'description' => htmlspecialchars_decode(strip_tags($description), ENT_QUOTES),
'fields' => $fields
];
if ($isMethod) {
$returnTypes = self::parseReturnTypes($description);
if (empty($returnTypes) and in_array($name, self::BOOL_RETURNS)) {
$returnTypes[] = 'bool';
}
$result['return_types'] = $returnTypes;
return $result;
}
return $result;
}
/**
* @param Dom\Node\Collection|null $fields
* @param bool $isMethod
* @return array
* @throws ChildNotFoundException
* @throws NotLoadedException
*/
private static function parseFields(?Dom\Node\Collection $fields, bool $isMethod): array
{
$parsedFields = [];
$fields = $fields ?? [];
foreach ($fields as $field) {
/* @var Dom $field */
$fieldData = $field->find('td');
$name = $fieldData[0]->text;
if (empty($name)) {
continue;
}
$parsedData = [
'name' => $name,
'type' => strip_tags($fieldData[1]->innerHtml)
];
$parsedData['types'] = self::parseFieldTypes($parsedData['type']);
unset($parsedData['type']);
if ($isMethod) {
$parsedData['required'] = $fieldData[2]->text == 'Yes';
$parsedData['description'] = htmlspecialchars_decode(
strip_tags($fieldData[3]->innerHtml ?? $fieldData[3]->text ?? ''),
ENT_QUOTES
);
} else {
$description = htmlspecialchars_decode(strip_tags($fieldData[2]->innerHtml), ENT_QUOTES);
$parsedData['optional'] = str_starts_with($description, 'Optional.');
$parsedData['description'] = $description;
}
$parsedFields[] = $parsedData;
}
return $parsedFields;
}
/**
* @param string $rawType
* @return array
*/
private static function parseFieldTypes(string $rawType): array
{
$types = [];
foreach (explode(' or ', $rawType) as $rawOrType) {
if (stripos($rawOrType, 'array') === 0) {
$types[] = str_replace(' and', ',', $rawOrType);
continue;
}
foreach (explode(' and ', $rawOrType) as $unparsedType) {
$types[] = $unparsedType;
}
}
$parsedTypes = [];
foreach ($types as $type) {
$type = trim(str_replace(['number', 'of'], '', $type));
$multiplesCount = substr_count(strtolower($type), 'array');
$parsedType = trim(
str_replace(
['Array', 'Integer', 'String', 'Boolean', 'Float', 'True'],
['', 'int', 'string', 'bool', 'float', 'bool'],
$type
)
);
for ($i = 0; $i < $multiplesCount; $i++) {
$parsedType = sprintf('Array<%s>', $parsedType);
}
$parsedTypes[] = $parsedType;
}
return $parsedTypes;
}
/**
* @param string $description
* @return array
* @throws ChildNotFoundException
* @throws CircularException
* @throws NotLoadedException
* @throws StrictException
* @throws ContentLengthException
* @throws LogicalException
* @noinspection PhpUndefinedFieldInspection
*/
private static function parseReturnTypes(string $description): array
{
$returnTypes = [];
$phrases = explode('.', $description);
$phrases = array_filter(
$phrases,
function ($phrase) {
return (false !== stripos($phrase, 'returns') or false !== stripos($phrase, 'is returned'));
}
);
foreach ($phrases as $phrase) {
$dom = new Dom;
$dom->loadStr($phrase);
$a = $dom->find('a');
$em = $dom->find('em');
foreach ($a as $element) {
if ($element->text == 'Messages') {
$returnTypes[] = 'Array<Message>';
continue;
}
$multiplesCount = substr_count(strtolower($phrase), 'array');
$returnType = $element->text;
for ($i = 0; $i < $multiplesCount; $i++) {
$returnType = sprintf('Array<%s>', $returnType);
}
$returnTypes[] = $returnType;
}
foreach ($em as $element) {
if (in_array($element->text, ['False', 'force', 'Array'])) {
continue;
}
$type = str_replace(['True', 'Int', 'String'], ['bool', 'int', 'string'], $element->text);
$returnTypes[] = $type;
}
}
return $returnTypes;
}
}

View File

@ -2,12 +2,11 @@
/** @noinspection PhpInternalEntityUsedInspection */
namespace TgScraper;
namespace TgScraper\Common;
use InvalidArgumentException;
use JetBrains\PhpStorm\ArrayShape;
use JetBrains\PhpStorm\Pure;
use Nette\PhpGenerator\Helpers;
use Nette\PhpGenerator\PhpFile;
use Nette\PhpGenerator\PhpNamespace;
@ -15,7 +14,7 @@ use Nette\PhpGenerator\Type;
/**
* Class StubCreator
* @package TgScraper
* @package TgScraper\Common
*/
class StubCreator
{
@ -27,10 +26,10 @@ class StubCreator
/**
* StubCreator constructor.
* @param array $scheme
* @param array $schema
* @param string $namespace
*/
public function __construct(private array $scheme, string $namespace = '')
public function __construct(private array $schema, string $namespace = '')
{
if (str_ends_with($namespace, '\\')) {
$namespace = substr($namespace, 0, -1);
@ -40,6 +39,9 @@ class StubCreator
throw new InvalidArgumentException('Namespace invalid');
}
}
if (!is_array($this->schema['methods']) or !is_array($this->schema['types'])) {
throw new InvalidArgumentException('Schema invalid');
}
$this->namespace = $namespace;
}
@ -53,9 +55,11 @@ class StubCreator
* @param PhpNamespace $phpNamespace
* @return array
*/
#[Pure] #[ArrayShape(['types' => "string", 'comments' => "string"])]
private function parseFieldTypes(array $fieldTypes, PhpNamespace $phpNamespace): array
{
#[ArrayShape(['types' => "string", 'comments' => "string"])]
private function parseFieldTypes(
array $fieldTypes,
PhpNamespace $phpNamespace
): array {
$types = [];
$comments = [];
foreach ($fieldTypes as $fieldType) {
@ -82,8 +86,10 @@ class StubCreator
* @return array
*/
#[ArrayShape(['types' => "string", 'comments' => "string"])]
private function parseApiFieldTypes(array $apiTypes, PhpNamespace $phpNamespace): array
{
private function parseApiFieldTypes(
array $apiTypes,
PhpNamespace $phpNamespace
): array {
$types = [];
$comments = [];
foreach ($apiTypes as $apiType) {
@ -110,8 +116,9 @@ class StubCreator
* @return PhpFile[]
*/
#[ArrayShape(['Response' => "\Nette\PhpGenerator\PhpFile"])]
private function generateDefaultTypes(string $namespace): array
{
private function generateDefaultTypes(
string $namespace
): array {
$file = new PhpFile;
$phpNamespace = $file->addNamespace($namespace);
$response = $phpNamespace->addClass('Response')
@ -146,7 +153,7 @@ class StubCreator
{
$namespace = $this->namespace . '\\Types';
$types = $this->generateDefaultTypes($namespace);
foreach ($this->scheme['types'] as $type) {
foreach ($this->schema['types'] as $type) {
$file = new PhpFile;
$phpNamespace = $file->addNamespace($namespace);
$typeClass = $phpNamespace->addClass($type['name'])
@ -193,7 +200,7 @@ class StubCreator
->setType(Type::STRING);
$sendRequest->addParameter('args')
->setType(Type::ARRAY);
foreach ($this->scheme['methods'] as $method) {
foreach ($this->schema['methods'] as $method) {
$function = $apiClass->addMethod($method['name'])
->setPublic()
->addBody('$args = get_defined_vars();')

View File

@ -0,0 +1,57 @@
<?php
namespace TgScraper\Constants;
class Versions
{
public const V100 = 'https://web.archive.org/web/20150714025308/https://core.telegram.org/bots/api/';
public const V110 = 'https://web.archive.org/web/20150812125616/https://core.telegram.org/bots/api';
public const V140 = 'https://web.archive.org/web/20150909214252/https://core.telegram.org/bots/api';
public const V150 = 'https://web.archive.org/web/20150921091215/https://core.telegram.org/bots/api/';
public const V160 = 'https://web.archive.org/web/20151023071257/https://core.telegram.org/bots/api';
public const V180 = 'https://web.archive.org/web/20160112101045/https://core.telegram.org/bots/api';
public const V182 = 'https://web.archive.org/web/20160126005312/https://core.telegram.org/bots/api';
public const V183 = 'https://web.archive.org/web/20160305132243/https://core.telegram.org/bots/api';
public const V200 = 'https://web.archive.org/web/20160413101342/https://core.telegram.org/bots/api';
public const V210 = 'https://web.archive.org/web/20160912130321/https://core.telegram.org/bots/api';
public const V211 = self::V210;
public const V220 = 'https://web.archive.org/web/20161004150232/https://core.telegram.org/bots/api';
public const V230 = 'https://web.archive.org/web/20161124162115/https://core.telegram.org/bots/api';
public const V231 = 'https://web.archive.org/web/20161204181811/https://core.telegram.org/bots/api';
public const V300 = 'https://web.archive.org/web/20170612094628/https://core.telegram.org/bots/api';
public const V310 = 'https://web.archive.org/web/20170703123052/https://core.telegram.org/bots/api';
public const V320 = 'https://web.archive.org/web/20170819054238/https://core.telegram.org/bots/api';
public const V330 = 'https://web.archive.org/web/20170914060628/https://core.telegram.org/bots/api';
public const V350 = 'https://web.archive.org/web/20171201065426/https://core.telegram.org/bots/api';
public const V360 = 'https://web.archive.org/web/20180217001114/https://core.telegram.org/bots/api';
public const V400 = 'https://web.archive.org/web/20180728174553/https://core.telegram.org/bots/api';
public const V410 = 'https://web.archive.org/web/20180828155646/https://core.telegram.org/bots/api';
public const V420 = 'https://web.archive.org/web/20190417160652/https://core.telegram.org/bots/api';
public const V430 = 'https://web.archive.org/web/20190601122107/https://core.telegram.org/bots/api';
public const V440 = 'https://web.archive.org/web/20190731114703/https://core.telegram.org/bots/api';
public const V450 = 'https://web.archive.org/web/20200107090812/https://core.telegram.org/bots/api';
public const V460 = 'https://web.archive.org/web/20200208225346/https://core.telegram.org/bots/api';
public const V470 = 'https://web.archive.org/web/20200401052001/https://core.telegram.org/bots/api';
public const V480 = 'https://web.archive.org/web/20200429054924/https://core.telegram.org/bots/api';
public const V490 = 'https://web.archive.org/web/20200611131321/https://core.telegram.org/bots/api';
public const V500 = 'https://web.archive.org/web/20201104151640/https://core.telegram.org/bots/api';
public const V510 = 'https://web.archive.org/web/20210315055600/https://core.telegram.org/bots/api';
public const V520 = 'https://web.archive.org/web/20210428145432/https://core.telegram.org/bots/api';
public const V530 = 'https://web.archive.org/web/20210626142851/https://core.telegram.org/bots/api';
public const LATEST = 'https://core.telegram.org/bots/api';
public static function getVersionFromText(string $text): string
{
$text = str_replace('.', '', $text);
$const = sprintf('%s::V%s', self::class, $text);
if (defined($const)) {
return constant($const);
}
return self::LATEST;
}
}

View File

@ -3,8 +3,8 @@
namespace TgScraper;
use Exception;
use JsonException;
use PHPHtmlParser\Dom;
use InvalidArgumentException;
use JetBrains\PhpStorm\ArrayShape;
use PHPHtmlParser\Exceptions\ChildNotFoundException;
use PHPHtmlParser\Exceptions\CircularException;
use PHPHtmlParser\Exceptions\ContentLengthException;
@ -13,59 +13,126 @@ use PHPHtmlParser\Exceptions\NotLoadedException;
use PHPHtmlParser\Exceptions\ParentNotFoundException;
use PHPHtmlParser\Exceptions\StrictException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Yaml\Yaml;
use TgScraper\Common\SchemaExtractor;
use TgScraper\Common\StubCreator;
use TgScraper\Constants\Versions;
use Throwable;
/**
* Class Generator
* @package TgScraper
*/
class Generator
{
private const BOOL_RETURNS = [
'answerShippingQuery',
'answerPreCheckoutQuery'
];
/**
* @var array
*/
private array $schema;
public const BOT_API_URL = 'https://core.telegram.org/bots/api';
/**
* Generator constructor.
* @param LoggerInterface $logger
* @param string $url
* @param array|null $schema
* @throws ChildNotFoundException
* @throws CircularException
* @throws ClientExceptionInterface
* @throws ContentLengthException
* @throws LogicalException
* @throws NotLoadedException
* @throws ParentNotFoundException
* @throws StrictException
* @throws Throwable
*/
public function __construct(
private LoggerInterface $logger,
private string $url = Versions::LATEST,
?array $schema = null
) {
if (empty($schema)) {
$extractor = new SchemaExtractor($this->logger, $this->url);
try {
$this->logger->info('Schema not provided, extracting from URL.');
$schema = $extractor->extract();
} catch (Throwable $e) {
$this->logger->critical(
'An exception occurred while trying to extract the schema: ' . $e->getMessage()
);
throw $e;
}
}
/** @var array $schema */
$this->schema = $schema;
}
public function __construct(private string $url = self::BOT_API_URL)
/**
* @throws ChildNotFoundException
* @throws CircularException
* @throws ParentNotFoundException
* @throws StrictException
* @throws ClientExceptionInterface
* @throws NotLoadedException
* @throws ContentLengthException
* @throws LogicalException
* @throws Throwable
*/
public static function fromYaml(LoggerInterface $logger, string $yaml): self
{
$data = Yaml::parse($yaml);
return new self($logger, schema: $data);
}
/**
* @throws ChildNotFoundException
* @throws ParentNotFoundException
* @throws CircularException
* @throws StrictException
* @throws ClientExceptionInterface
* @throws NotLoadedException
* @throws ContentLengthException
* @throws LogicalException
* @throws Throwable
*/
public static function fromJson(LoggerInterface $logger, string $json): self
{
$data = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
return new self($logger, schema: $data);
}
/**
* @param string $directory
* @param string $namespace
* @param string|null $scheme
* @return bool
* @throws ClientExceptionInterface
* @return void
* @throws Exception
*/
public function toStubs(string $directory = '', string $namespace = '', string $scheme = null): bool
public function toStubs(string $directory = '', string $namespace = 'TelegramApi'): void
{
try {
$directory = self::getTargetDirectory($directory);
} catch (Exception $e) {
echo 'Unable to use target directory:' . $e->getMessage() . PHP_EOL;
return false;
$this->logger->critical(
'An exception occurred while trying to get the target directory: ' . $e->getMessage()
);
throw $e;
}
mkdir($directory . '/Types', 0755);
try {
if (!empty($scheme)) {
try {
$data = json_decode($scheme, true, flags: JSON_THROW_ON_ERROR);
} /** @noinspection PhpRedundantCatchClauseInspection */ catch (JsonException) {
$data = null;
}
}
$data = $data ?? $this->extractScheme();
$creator = new StubCreator($data, $namespace);
$code = $creator->generateCode();
foreach ($code['types'] as $className => $type) {
$filename = sprintf('%s/Types/%s.php', $directory, $className);
file_put_contents($filename, $type);
}
file_put_contents($directory . '/API.php', $code['api']);
} catch (Exception $e) {
echo $e->getMessage() . PHP_EOL;
return false;
$creator = new StubCreator($this->schema, $namespace);
} catch (InvalidArgumentException $e) {
$this->logger->critical(
'An exception occurred while trying to parse the schema: ' . $e->getMessage()
);
throw $e;
}
return true;
$code = $creator->generateCode();
foreach ($code['types'] as $className => $type) {
$this->logger->info('Generating class for Type: ' . $className);
$filename = sprintf('%s/Types/%s.php', $directory, $className);
file_put_contents($filename, $type);
}
file_put_contents($directory . '/API.php', $code['api']);
}
/**
@ -75,8 +142,8 @@ class Generator
*/
private static function getTargetDirectory(string $path): string
{
$path = realpath($path);
if (false === $path) {
$result = realpath($path);
if (false === $result) {
if (!mkdir($path)) {
$path = __DIR__ . '/../generated';
if (!file_exists($path)) {
@ -84,268 +151,97 @@ class Generator
}
}
}
if (realpath($path) === false) {
$result = realpath($path);
if (false === $result) {
throw new Exception('Could not create target directory');
}
return $path;
}
/**
* @return array
* @throws ChildNotFoundException
* @throws CircularException
* @throws ContentLengthException
* @throws LogicalException
* @throws NotLoadedException
* @throws ParentNotFoundException
* @throws StrictException
* @throws ClientExceptionInterface
*/
private function extractScheme(): array
{
$dom = new Dom;
$dom->loadFromURL($this->url);
$elements = $dom->find('h4');
$data = [];
/* @var Dom\Node\AbstractNode $element */
foreach ($elements as $element) {
if (!str_contains($name = $element->text, ' ')) {
$isMethod = self::isMethod($name);
$path = $isMethod ? 'methods' : 'types';
$temp = $element;
$description = '';
$table = null;
while (true) {
try {
$element = $element->nextSibling();
} catch (ChildNotFoundException) {
break;
}
$tag = $element->tag->name() ?? null;
if (empty($temp->text()) or empty($tag) or $tag == 'text') {
continue;
} elseif (str_starts_with($tag, 'h')) {
break;
} elseif ($tag == 'p') {
$description .= PHP_EOL . $element->innerHtml();
} elseif ($tag == 'table') {
$table = $element->find('tbody')->find('tr');
break;
}
}
/* @var Dom\Node\AbstractNode $element */
$data[$path][] = self::generateElement(
$name,
trim($description),
$table,
$isMethod
);
}
}
return $data;
}
private static function isMethod(string $name): bool
{
return lcfirst($name) == $name;
}
/**
* @param string $name
* @param string $description
* @param Dom\Node\Collection|null $unparsedFields
* @param bool $isMethod
* @return array
* @throws ChildNotFoundException
* @throws CircularException
* @throws ContentLengthException
* @throws LogicalException
* @throws NotLoadedException
* @throws StrictException
*/
private static function generateElement(
string $name,
string $description,
?Dom\Node\Collection $unparsedFields,
bool $isMethod
): array {
$fields = self::parseFields($unparsedFields, $isMethod);
if (!$isMethod) {
return [
'name' => $name,
'description' => htmlspecialchars_decode(strip_tags($description), ENT_QUOTES),
'fields' => $fields
];
}
$returnTypes = self::parseReturnTypes($description);
if (empty($returnTypes) and in_array($name, self::BOOL_RETURNS)) {
$returnTypes[] = 'bool';
}
return [
'name' => $name,
'description' => htmlspecialchars_decode(strip_tags($description), ENT_QUOTES),
'fields' => $fields,
'return_types' => $returnTypes
];
}
/**
* @param Dom\Node\Collection|null $fields
* @param bool $isMethod
* @return array
* @throws ChildNotFoundException
* @throws NotLoadedException
*/
private static function parseFields(?Dom\Node\Collection $fields, bool $isMethod): array
{
$parsedFields = [];
$fields = $fields ?? [];
foreach ($fields as $field) {
/* @var Dom $field */
$fieldData = $field->find('td');
$name = $fieldData[0]->text;
if (empty($name)) {
continue;
}
$parsedData = [
'name' => $name,
'type' => strip_tags($fieldData[1]->innerHtml)
];
$parsedData['types'] = self::parseFieldTypes($parsedData['type']);
unset($parsedData['type']);
if ($isMethod) {
$parsedData['required'] = $fieldData[2]->text == 'Yes';
$parsedData['description'] = htmlspecialchars_decode(
strip_tags($fieldData[3]->innerHtml ?? $fieldData[3]->text ?? ''),
ENT_QUOTES
);
} else {
$description = htmlspecialchars_decode(strip_tags($fieldData[2]->innerHtml), ENT_QUOTES);
$parsedData['optional'] = str_starts_with($description, 'Optional.');
$parsedData['description'] = $description;
}
$parsedFields[] = $parsedData;
}
return $parsedFields;
}
/**
* @param string $rawType
* @return array
*/
private static function parseFieldTypes(string $rawType): array
{
$types = [];
foreach (explode(' or ', $rawType) as $rawOrType) {
if (stripos($rawOrType, 'array') === 0) {
$types[] = str_replace(' and', ',', $rawOrType);
continue;
}
foreach (explode(' and ', $rawOrType) as $unparsedType) {
$types[] = $unparsedType;
}
}
$parsedTypes = [];
foreach ($types as $type) {
$type = trim(str_replace(['number', 'of'], '', $type));
$multiplesCount = substr_count(strtolower($type), 'array');
$parsedType = trim(
str_replace(
['Array', 'Integer', 'String', 'Boolean', 'Float', 'True'],
['', 'int', 'string', 'bool', 'float', 'bool'],
$type
)
);
for ($i = 0; $i < $multiplesCount; $i++) {
$parsedType = sprintf('Array<%s>', $parsedType);
}
$parsedTypes[] = $parsedType;
}
return $parsedTypes;
}
/**
* @param string $description
* @return array
* @throws ChildNotFoundException
* @throws CircularException
* @throws NotLoadedException
* @throws StrictException
* @throws ContentLengthException
* @throws LogicalException
* @noinspection PhpUndefinedFieldInspection
*/
private static function parseReturnTypes(string $description): array
{
$returnTypes = [];
$phrases = explode('.', $description);
$phrases = array_filter(
$phrases,
function ($phrase) {
return (false !== stripos($phrase, 'returns') or false !== stripos($phrase, 'is returned'));
}
);
foreach ($phrases as $phrase) {
$dom = new Dom;
$dom->loadStr($phrase);
$a = $dom->find('a');
$em = $dom->find('em');
foreach ($a as $element) {
if ($element->text == 'Messages') {
$returnTypes[] = 'Array<Message>';
continue;
}
$multiplesCount = substr_count(strtolower($phrase), 'array');
$returnType = $element->text;
for ($i = 0; $i < $multiplesCount; $i++) {
$returnType = sprintf('Array<%s>', $returnType);
}
$returnTypes[] = $returnType;
}
foreach ($em as $element) {
if (in_array($element->text, ['False', 'force', 'Array'])) {
continue;
}
$type = str_replace(['True', 'Int', 'String'], ['bool', 'int', 'string'], $element->text);
$returnTypes[] = $type;
}
}
return $returnTypes;
@mkdir($result . '/Types', 0755);
return $result;
}
/**
* @param int $options
* @return string
* @throws ChildNotFoundException
* @throws CircularException
* @throws ClientExceptionInterface
* @throws ContentLengthException
* @throws LogicalException
* @throws NotLoadedException
* @throws ParentNotFoundException
* @throws StrictException
*/
public function toJson(int $options = 0): string
{
$scheme = $this->extractScheme();
return json_encode($scheme, $options);
return json_encode($this->schema, $options);
}
/**
* @throws ChildNotFoundException
* @throws CircularException
* @throws ParentNotFoundException
* @throws StrictException
* @throws ClientExceptionInterface
* @throws NotLoadedException
* @throws ContentLengthException
* @throws LogicalException
* @param int $inline
* @param int $indent
* @param int $flags
* @return string
*/
public function toYaml(int $inline = 6, int $indent = 4, int $flags = 0): string
{
$scheme = $this->extractScheme();
return Yaml::dump($scheme, $inline, $indent, $flags);
return Yaml::dump($this->schema, $inline, $indent, $flags);
}
/**
* Thanks to davtur19 (https://github.com/davtur19/TuriBotGen/blob/master/postman.php)
* @param int $options
* @return string
*/
#[ArrayShape(['info' => "string[]", 'variable' => "string[]", 'item' => "array[]"])]
public function toPostman(
int $options = 0
): string {
$result = [
'info' => [
'name' => 'Telegram Bot API',
'schema' => 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
],
'variable' => [
'key' => 'token',
'value' => '1234:AAbbcc',
'type' => 'string'
]
];
foreach ($this->schema['methods'] as $method) {
$formData = [];
if (!empty($method['fields'])) {
foreach ($method['fields'] as $field) {
$formData[] = [
'key' => $field['name'],
'value' => '',
'description' => sprintf(
'%s. %s',
$field['required'] ? 'Required' : 'Optional',
$field['description']
),
'type' => 'text'
];
}
}
$result['item'][] = [
'name' => $method['name'],
'request' => [
'method' => 'POST',
'body' => [
'mode' => 'formdata',
'formdata' => $formData
],
'url' => [
'raw' => 'https://api.telegram.org/bot{{token}}/' . $method['name'],
'protocol' => 'https',
'host' => [
'api',
'telegram',
'org'
],
'path' => [
'bot{{token}}',
$method['name']
]
],
'description' => $method['description']
]
];
}
return json_encode($result, $options);
}
}