Compare commits

...

11 commits

35 changed files with 1295 additions and 722 deletions

View file

@ -11,6 +11,7 @@
"doctrine/doctrine-bundle": "^2.8",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.14",
"jms/serializer-bundle": "^5.2",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.16",
"sensio/framework-extra-bundle": "^6.1",
@ -41,7 +42,8 @@
"symfony/web-link": "6.2.*",
"symfony/yaml": "6.2.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
"twig/twig": "^2.12|^3.0",
"unreal4u/telegram-api": "*"
},
"config": {
"allow-plugins": {

707
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": "75085b03a22620e56812fd6d68ace9bb",
"content-hash": "3e4ae473183073d1dd231517f5734c3e",
"packages": [
{
"name": "doctrine/annotations",
@ -1457,6 +1457,562 @@
],
"time": "2023-01-14T14:17:03+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.5.8",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "a52f0440530b54fa079ce76e8c5d196a42cad981"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981",
"reference": "a52f0440530b54fa079ce76e8c5d196a42cad981",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.9",
"php": ">=5.5",
"symfony/polyfill-intl-idn": "^1.17"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
"psr/log": "^1.1"
},
"suggest": {
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.5-dev"
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Jeremy Lindblom",
"email": "jeremeamia@gmail.com",
"homepage": "https://github.com/jeremeamia"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle is a PHP HTTP client library",
"homepage": "http://guzzlephp.org/",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"rest",
"web service"
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/6.5.8"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
"type": "tidelift"
}
],
"time": "2022-06-20T22:16:07+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "1.5.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "b94b2807d85443f9719887892882d0329d1e2598"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598",
"reference": "b94b2807d85443f9719887892882d0329d1e2598",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"symfony/phpunit-bridge": "^4.4 || ^5.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.5-dev"
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/1.5.2"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
"type": "tidelift"
}
],
"time": "2022-08-28T14:55:35+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.9.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/e98e3e6d4f86621a9b75f623996e6bbdeb4b9318",
"reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0",
"ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"ext-zlib": "*",
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/1.9.0"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
"type": "tidelift"
}
],
"time": "2022-06-20T21:43:03+00:00"
},
{
"name": "jms/metadata",
"version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/metadata.git",
"reference": "7ca240dcac0c655eb15933ee55736ccd2ea0d7a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/metadata/zipball/7ca240dcac0c655eb15933ee55736ccd2ea0d7a6",
"reference": "7ca240dcac0c655eb15933ee55736ccd2ea0d7a6",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0"
},
"require-dev": {
"doctrine/cache": "^1.0",
"doctrine/coding-standard": "^8.0",
"mikey179/vfsstream": "^1.6.7",
"phpunit/phpunit": "^8.5|^9.0",
"psr/container": "^1.0|^2.0",
"symfony/cache": "^3.1|^4.0|^5.0",
"symfony/dependency-injection": "^3.1|^4.0|^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"Metadata\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "Class/method/property metadata management in PHP",
"keywords": [
"annotations",
"metadata",
"xml",
"yaml"
],
"support": {
"issues": "https://github.com/schmittjoh/metadata/issues",
"source": "https://github.com/schmittjoh/metadata/tree/2.8.0"
},
"time": "2023-02-15T13:44:18+00:00"
},
{
"name": "jms/serializer",
"version": "3.23.0",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/serializer.git",
"reference": "ac0b16ee5317d1aacc41deb91c6c325eae97c176"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/serializer/zipball/ac0b16ee5317d1aacc41deb91c6c325eae97c176",
"reference": "ac0b16ee5317d1aacc41deb91c6c325eae97c176",
"shasum": ""
},
"require": {
"doctrine/annotations": "^1.13 || ^2.0",
"doctrine/instantiator": "^1.0.3",
"doctrine/lexer": "^1.1 || ^2",
"jms/metadata": "^2.6",
"php": "^7.2||^8.0",
"phpstan/phpdoc-parser": "^0.4 || ^0.5 || ^1.0"
},
"require-dev": {
"doctrine/coding-standard": "^8.1",
"doctrine/orm": "~2.1",
"doctrine/persistence": "^1.3.3|^2.0|^3.0",
"doctrine/phpcr-odm": "^1.3|^2.0",
"ext-pdo_sqlite": "*",
"jackalope/jackalope-doctrine-dbal": "^1.1.5",
"ocramius/proxy-manager": "^1.0|^2.0",
"phpbench/phpbench": "^1.0",
"phpstan/phpstan": "^1.0.2",
"phpunit/phpunit": "^8.5.21||^9.0",
"psr/container": "^1.0|^2.0",
"symfony/dependency-injection": "^3.0|^4.0|^5.0|^6.0",
"symfony/expression-language": "^3.2|^4.0|^5.0|^6.0",
"symfony/filesystem": "^3.0|^4.0|^5.0|^6.0",
"symfony/form": "^3.0|^4.0|^5.0|^6.0",
"symfony/translation": "^3.0|^4.0|^5.0|^6.0",
"symfony/uid": "^5.1|^6.0",
"symfony/validator": "^3.1.9|^4.0|^5.0|^6.0",
"symfony/yaml": "^3.3|^4.0|^5.0|^6.0",
"twig/twig": "~1.34|~2.4|^3.0"
},
"suggest": {
"doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.",
"symfony/cache": "Required if you like to use cache functionality.",
"symfony/uid": "Required if you'd like to serialize UID objects.",
"symfony/yaml": "Required if you'd like to use the YAML metadata format."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"JMS\\Serializer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "Library for (de-)serializing data of any complexity; supports XML, and JSON.",
"homepage": "http://jmsyst.com/libs/serializer",
"keywords": [
"deserialization",
"jaxb",
"json",
"serialization",
"xml"
],
"support": {
"issues": "https://github.com/schmittjoh/serializer/issues",
"source": "https://github.com/schmittjoh/serializer/tree/3.23.0"
},
"funding": [
{
"url": "https://github.com/goetas",
"type": "github"
}
],
"time": "2023-02-17T17:40:48+00:00"
},
{
"name": "jms/serializer-bundle",
"version": "5.2.1",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/JMSSerializerBundle.git",
"reference": "c772704a0b3cb772fa391ff5ac7d36fd6cecebf4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/JMSSerializerBundle/zipball/c772704a0b3cb772fa391ff5ac7d36fd6cecebf4",
"reference": "c772704a0b3cb772fa391ff5ac7d36fd6cecebf4",
"shasum": ""
},
"require": {
"jms/metadata": "^2.5",
"jms/serializer": "^3.20",
"php": "^7.2 || ^8.0",
"symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/framework-bundle": "^3.4 || ^4.0 || ^5.0 || ^6.0"
},
"require-dev": {
"doctrine/coding-standard": "^8.1",
"doctrine/orm": "^2.4",
"phpunit/phpunit": "^8.0 || ^9.0",
"symfony/expression-language": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/finder": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/form": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/stopwatch": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/templating": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/twig-bundle": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/uid": "^5.1 || ^6.0",
"symfony/validator": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0"
},
"suggest": {
"symfony/expression-language": "Required for opcache preloading ^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/finder": "Required for cache warmup, supported versions ^3.4 || ^4.0 || ^5.0 || ^6.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"JMS\\SerializerBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "Allows you to easily serialize, and deserialize data of any complexity",
"homepage": "http://jmsyst.com/bundles/JMSSerializerBundle",
"keywords": [
"deserialization",
"json",
"serialization",
"xml"
],
"support": {
"issues": "https://github.com/schmittjoh/JMSSerializerBundle/issues",
"source": "https://github.com/schmittjoh/JMSSerializerBundle/tree/5.2.1"
},
"funding": [
{
"url": "https://github.com/goetas",
"type": "github"
}
],
"time": "2023-02-05T11:03:45+00:00"
},
{
"name": "monolog/monolog",
"version": "3.3.1",
@ -1920,6 +2476,59 @@
},
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "psr/http-message",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/master"
},
"time": "2016-08-06T14:39:51+00:00"
},
{
"name": "psr/link",
"version": "2.0.1",
@ -2026,6 +2635,50 @@
},
"time": "2021-07-14T16:46:02+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "sensio/framework-extra-bundle",
"version": "v6.2.10",
@ -7216,6 +7869,58 @@
],
"time": "2023-02-08T07:49:20+00:00"
},
{
"name": "unreal4u/telegram-api",
"version": "v1.1.1",
"source": {
"type": "git",
"url": "https://github.com/unreal4u/telegram-api.git",
"reference": "501a02062942fb533bbcdf6f9f538368208db65a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/unreal4u/telegram-api/zipball/501a02062942fb533bbcdf6f9f538368208db65a",
"reference": "501a02062942fb533bbcdf6f9f538368208db65a",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "~6.0",
"php": ">=7.0.0"
},
"require-dev": {
"phpmd/phpmd": "@stable",
"phpunit/phpunit": "@stable",
"squizlabs/php_codesniffer": "@stable"
},
"type": "library",
"autoload": {
"psr-4": {
"unreal4u\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Camilo Sperberg",
"email": "me@unreal4u.com",
"homepage": "https://github.com/unreal4u/telegram-api/graphs/contributors"
}
],
"description": "Complete implementation used to communicate with the open-source Telegram API",
"keywords": [
"api",
"telegram",
"telegram bot"
],
"support": {
"issues": "https://github.com/unreal4u/telegram-api/issues",
"source": "https://github.com/unreal4u/telegram-api/tree/master"
},
"time": "2016-01-26T21:46:33+00:00"
},
{
"name": "webmozart/assert",
"version": "1.11.0",

View file

@ -12,4 +12,5 @@ return [
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
];

View file

@ -0,0 +1,30 @@
jms_serializer:
visitors:
xml_serialization:
format_output: '%kernel.debug%'
# metadata:
# auto_detection: false
# directories:
# any-name:
# namespace_prefix: "My\\FooBundle"
# path: "@MyFooBundle/Resources/config/serializer"
# another-name:
# namespace_prefix: "My\\BarBundle"
# path: "@MyBarBundle/Resources/config/serializer"
when@prod:
jms_serializer:
visitors:
json_serialization:
options:
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION
when@dev:
jms_serializer:
visitors:
json_serialization:
options:
- JSON_PRETTY_PRINT
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION

View file

@ -10,6 +10,14 @@ services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
# TODO: fix retrieval
# Telegram Bot API
$telegramToken: ''
# Point API
$pointApiDelay: ''
$pointAppUserId: ''
$pointApiClient: '@app.point.http_client'
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
@ -20,5 +28,10 @@ services:
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
# HTTP client for Point API
Symfony\Component\HttpClient\HttpClient:
alias: 'app.point.http_client'
factory: [null, 'create']
arguments:
base_uri: '%point_base_url%'
timeout: 5.0

View file

@ -1,73 +0,0 @@
<?php
namespace src\PointToolsBundle\Command;
use src\PointToolsBundle\Service\Telegram\MessageSender;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\{InputArgument, InputInterface, InputOption};
use Symfony\Component\Console\Output\OutputInterface;
use function Skobkin\Bundle\PointToolsBundle\Command\mb_strlen;
use function Skobkin\Bundle\PointToolsBundle\Command\mb_substr;
class TelegramSendMessageCommand extends Command
{
/** @var MessageSender */
private $messenger;
public function __construct(MessageSender $messenger)
{
$this->messenger = $messenger;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('telegram:send-message')
->setDescription('Send message via Telegram')
->addOption('chat-id', 'c', InputOption::VALUE_OPTIONAL, 'ID of the chat')
->addOption('stdin', 'i', InputOption::VALUE_NONE, 'Read message from stdin instead of option')
->addArgument('message', InputArgument::OPTIONAL, 'Text of the message')
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('Sending message...');
if ($input->getOption('stdin')) {
$message = file_get_contents('php://stdin');
} elseif (null !== $input->getArgument('message')) {
$message = $input->getArgument('message');
} else {
$output->writeln('<error>Either \'--stdin\' option or \'message\' argument should be specified.</error>');
return 1;
}
if (mb_strlen($message) > 4096) {
$output->writeln('<comment>Message is too long (>4096). Cutting the tail...</comment>');
$message = mb_substr($message, 0, 4090).PHP_EOL.'...';
}
try {
$this->messenger->sendMessageToChat(
(int) $input->getOption('chat-id'),
$message
);
} catch (\Exception $e) {
$output->writeln('Error: '.$e->getMessage());
return 1;
}
return 0;
}
}

View file

@ -1,91 +0,0 @@
<?php
namespace src\PointToolsBundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\{InputArgument, InputInterface};
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use unreal4u\TelegramAPI\Telegram\Methods\{DeleteWebhook, SetWebhook};
use unreal4u\TelegramAPI\TgLog;
/**
* Sets or deletes Telegram bot Web-Hook
* @see https://core.telegram.org/bots/api#setwebhook
*/
class TelegramWebHookCommand extends Command
{
private const MODE_SET = 'set';
private const MODE_DELETE = 'delete';
/** @var TgLog */
private $client;
/** @var UrlGeneratorInterface */
private $router;
/** @var string */
private $token;
/** @var int */
private $maxConnections;
public function __construct(TgLog $client, UrlGeneratorInterface $router, string $telegramToken, int $telegramWebhookMaxConnections)
{
parent::__construct();
$this->client = $client;
$this->router = $router;
$this->token = $telegramToken;
$this->maxConnections = $telegramWebhookMaxConnections;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('telegram:webhook')
->setDescription('Set webhook')
->addArgument('mode', InputArgument::REQUIRED, 'Command mode (set or delete)')
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
if (self::MODE_SET === strtolower($input->getArgument('mode'))) {
$url = $this->router->generate(
'telegram_webhook',
['token' => $this->token],
UrlGeneratorInterface::ABSOLUTE_URL
);
$output->writeln('Setting webhook: '.$url);
$setWebHook = new SetWebhook();
$setWebHook->url = $url;
$setWebHook->max_connections = $this->maxConnections;
$this->client->performApiRequest($setWebHook);
$output->writeln('Done');
} elseif (self::MODE_DELETE === strtolower($input->getArgument('mode'))) {
$output->writeln('Deleting webhook');
$deleteWebHook = new DeleteWebhook();
$this->client->performApiRequest($deleteWebHook);
$output->writeln('Done');
} else {
throw new \InvalidArgumentException(sprintf('Mode must be exactly one of: %s', implode(', ', [self::MODE_SET, self::MODE_DELETE])));
}
return 0;
}
}

View file

@ -1,8 +0,0 @@
<?php
namespace src\PointToolsBundle\Exception\Api;
class ApiException extends \Exception
{
}

View file

@ -1,10 +0,0 @@
<?php
namespace src\PointToolsBundle\Exception\Api;
use src\PointToolsBundle\Exception\Api\ApiException;
class ForbiddenException extends ApiException
{
}

View file

@ -1,10 +0,0 @@
<?php
namespace src\PointToolsBundle\Exception\Api;
use src\PointToolsBundle\Exception\Api\ApiException;
class InvalidResponseException extends ApiException
{
}

View file

@ -1,10 +0,0 @@
<?php
namespace src\PointToolsBundle\Exception\Api;
use src\PointToolsBundle\Exception\Api\ApiException;
class NetworkException extends ApiException
{
}

View file

@ -1,10 +0,0 @@
<?php
namespace src\PointToolsBundle\Exception\Api;
use src\PointToolsBundle\Exception\Api\ApiException;
class NotFoundException extends ApiException
{
}

View file

@ -1,10 +0,0 @@
<?php
namespace src\PointToolsBundle\Exception\Api;
use src\PointToolsBundle\Exception\Api\ApiException;
class ServerProblemException extends ApiException
{
}

View file

@ -1,10 +0,0 @@
<?php
namespace src\PointToolsBundle\Exception\Api;
use src\PointToolsBundle\Exception\Api\ApiException;
class UnauthorizedException extends ApiException
{
}

View file

@ -1,41 +0,0 @@
<?php
namespace src\PointToolsBundle\Exception\Api;
use src\PointToolsBundle\Exception\Api\NotFoundException;
class UserNotFoundException extends NotFoundException
{
/**
* @var int
*/
protected $userId;
/**
* @var string
*/
protected $login;
/**
* {@inheritdoc}
* @param int $userId
*/
public function __construct($message = 'User not found', $code = 0, \Exception $previous = null, $userId = null, $login = null)
{
parent::__construct($message, $code, $previous);
$this->userId = $userId;
$this->login = $login;
}
public function getUserId(): int
{
return $this->userId;
}
public function getLogin(): string
{
return $this->login;
}
}

View file

@ -1,202 +0,0 @@
<?php
namespace src\PointToolsBundle\Service\Api;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\TransferException;
use JMS\Serializer\{DeserializationContext, SerializerInterface};
use Psr\Http\Message\{ResponseInterface, StreamInterface};
use Psr\Log\LoggerInterface;
use Skobkin\Bundle\PointToolsBundle\Exception\Api\{
src\PointToolsBundle\Exception\Api\ForbiddenException, src\PointToolsBundle\Exception\Api\NetworkException, src\PointToolsBundle\Exception\Api\NotFoundException, src\PointToolsBundle\Exception\Api\ServerProblemException, src\PointToolsBundle\Exception\Api\UnauthorizedException};
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
abstract class AbstractApi
{
/**
* @var ClientInterface HTTP-client from Guzzle
*/
protected $client;
/**
* @var SerializerInterface
*/
protected $serializer;
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @var string Authentication token for API
*/
protected $authToken;
/**
* @var string CSRF-token for API
*/
protected $csRfToken;
public function __construct(ClientInterface $httpClient, SerializerInterface $serializer, LoggerInterface $logger)
{
$this->client = $httpClient;
$this->serializer = $serializer;
$this->logger = $logger;
}
/**
* Make GET request and return DTO objects
*
* @return array|object
*/
public function getGetJsonData(string $path, array $parameters = [], string $type, DeserializationContext $context = null)
{
return $this->serializer->deserialize(
$this->getGetResponseBody($path, $parameters),
$type,
'json',
$context
);
}
/**
* Make POST request and return DTO objects
*
* @return array|object
*/
public function getPostJsonData(string $path, array $parameters = [], string $type, DeserializationContext $context = null)
{
return $this->serializer->deserialize(
$this->getPostResponseBody($path, $parameters),
$type,
'json',
$context
);
}
/**
* Make GET request and return response body
*/
public function getGetResponseBody($path, array $parameters = []): StreamInterface
{
return $this->sendGetRequest($path, $parameters)->getBody();
}
/**
* Make POST request and return response body
*/
public function getPostResponseBody(string $path, array $parameters = []): StreamInterface
{
return $this->sendPostRequest($path, $parameters)->getBody();
}
/**
* @param string $path Request path
* @param array $parameters Key => Value array of query parameters
*
* @return ResponseInterface
*
* @throws \src\PointToolsBundle\Exception\Api\NetworkException
*/
private function sendGetRequest(string $path, array $parameters = []): ResponseInterface
{
$this->logger->debug('Sending GET request', ['path' => $path, 'parameters' => $parameters]);
return $this->sendRequest('GET', $path, ['query' => $parameters]);
}
/**
* @param string $path Request path
* @param array $parameters Key => Value array of request data
*
* @return ResponseInterface
*
* @throws \src\PointToolsBundle\Exception\Api\NetworkException
*/
private function sendPostRequest(string $path, array $parameters = []): ResponseInterface
{
$this->logger->debug('Sending POST request', ['path' => $path, 'parameters' => $parameters]);
return $this->sendRequest('POST', $path, ['form_params' => $parameters]);
}
private function sendRequest(string $method, string $path, array $parameters): ResponseInterface
{
try {
$response = $this->client->request($method, $path, $parameters);
$this->checkResponse($response);
return $response;
} catch (TransferException $e) {
$this->processTransferException($e);
throw new \src\PointToolsBundle\Exception\Api\NetworkException('Request error', $e->getCode(), $e);
}
}
/**
* @param \Exception $e
*
* @throws \src\PointToolsBundle\Exception\Api\ForbiddenException
* @throws \src\PointToolsBundle\Exception\Api\NotFoundException
* @throws \src\PointToolsBundle\Exception\Api\ServerProblemException
* @throws \src\PointToolsBundle\Exception\Api\UnauthorizedException
* @todo refactor with $this->checkResponse()
*
*/
private function processTransferException(\Exception $e): void
{
switch ($e->getCode()) {
case SymfonyResponse::HTTP_UNAUTHORIZED:
throw new \src\PointToolsBundle\Exception\Api\UnauthorizedException('Unauthorized', SymfonyResponse::HTTP_UNAUTHORIZED, $e);
case SymfonyResponse::HTTP_NOT_FOUND:
throw new \src\PointToolsBundle\Exception\Api\NotFoundException('Resource not found', SymfonyResponse::HTTP_NOT_FOUND, $e);
case SymfonyResponse::HTTP_FORBIDDEN:
throw new \src\PointToolsBundle\Exception\Api\ForbiddenException('Forbidden', SymfonyResponse::HTTP_FORBIDDEN, $e);
case SymfonyResponse::HTTP_INTERNAL_SERVER_ERROR:
case SymfonyResponse::HTTP_NOT_IMPLEMENTED:
case SymfonyResponse::HTTP_BAD_GATEWAY:
case SymfonyResponse::HTTP_SERVICE_UNAVAILABLE:
case SymfonyResponse::HTTP_GATEWAY_TIMEOUT:
throw new \src\PointToolsBundle\Exception\Api\ServerProblemException('Server error', SymfonyResponse::HTTP_INTERNAL_SERVER_ERROR, $e);
}
}
/**
* @throws \src\PointToolsBundle\Exception\Api\ForbiddenException
* @throws \src\PointToolsBundle\Exception\Api\NotFoundException
* @throws \src\PointToolsBundle\Exception\Api\ServerProblemException
* @throws \src\PointToolsBundle\Exception\Api\UnauthorizedException
*/
private function checkResponse(ResponseInterface $response): void
{
$code = $response->getStatusCode();
$reason = $response->getReasonPhrase();
// @todo remove after fix
// Temporary fix until @arts fixes this bug
if ('{"error": "UserNotFound"}' === (string) $response->getBody()) {
throw new \src\PointToolsBundle\Exception\Api\NotFoundException('Not found', SymfonyResponse::HTTP_NOT_FOUND);
} elseif ('{"message": "Forbidden", "code": 403, "error": "Forbidden"}' === (string) $response->getBody()) {
throw new \src\PointToolsBundle\Exception\Api\ForbiddenException('Forbidden', SymfonyResponse::HTTP_FORBIDDEN);
}
switch ($code) {
case SymfonyResponse::HTTP_UNAUTHORIZED:
throw new \src\PointToolsBundle\Exception\Api\UnauthorizedException($reason, $code);
case SymfonyResponse::HTTP_FORBIDDEN:
throw new \src\PointToolsBundle\Exception\Api\ForbiddenException($reason, $code);
case SymfonyResponse::HTTP_NOT_FOUND:
throw new \src\PointToolsBundle\Exception\Api\NotFoundException($reason, $code);
case SymfonyResponse::HTTP_INTERNAL_SERVER_ERROR:
case SymfonyResponse::HTTP_NOT_IMPLEMENTED:
case SymfonyResponse::HTTP_BAD_GATEWAY:
case SymfonyResponse::HTTP_SERVICE_UNAVAILABLE:
case SymfonyResponse::HTTP_GATEWAY_TIMEOUT:
throw new \src\PointToolsBundle\Exception\Api\ServerProblemException($reason, $code);
}
}
}

View file

@ -1,28 +0,0 @@
<?php
namespace src\PointToolsBundle\Service\Api;
use GuzzleHttp\ClientInterface;
use JMS\Serializer\SerializerInterface;
use Psr\Log\LoggerInterface;
use src\PointToolsBundle\Service\Api\AbstractApi;
use src\PointToolsBundle\Service\Factory\Blogs\PostFactory;
/**
* Basic Point.im user API functions from /api/post
*/
class PostApi extends AbstractApi
{
/**
* @var PostFactory
*/
private $postFactory;
public function __construct(ClientInterface $httpClient, SerializerInterface $serializer, LoggerInterface $logger, PostFactory $postFactory)
{
parent::__construct($httpClient, $serializer, $logger);
$this->postFactory = $postFactory;
}
}

View file

@ -18,11 +18,11 @@ use Symfony\Component\Console\Style\SymfonyStyle;
class RestoreRemovedUsersCommand extends Command
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly LoggerInterface $logger,
private readonly EntityManagerInterface $em,
private readonly UserRepository $userRepo,
private readonly UserApi $userApi,
private readonly int $apiDelay,
private readonly UserRepository $userRepo,
private readonly UserApi $userApi,
private readonly int $pointApiDelay,
) {
parent::__construct();
}
@ -32,7 +32,7 @@ class RestoreRemovedUsersCommand extends Command
$io = new SymfonyStyle($input, $output);
foreach ($this->userRepo->findBy(['removed' => true]) as $removedUser) {
\usleep($this->apiDelay);
\usleep($this->pointApiDelay);
try {
$remoteUser = $this->userApi->getUserById($removedUser->getId());

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Service\Telegram\MessageSender;
use Symfony\Component\Console\Attribute\AsCommand;
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\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(name: 'app:telegram:message', description: 'Send message via Telegram')]
class TelegramSendMessageCommand extends Command
{
public function __construct(
private readonly MessageSender $messenger,
) {
parent::__construct();
}
protected function configure()
{
$this
->addOption('chat-id', 'c', InputOption::VALUE_OPTIONAL, 'ID of the chat')
->addOption('stdin', 'i', InputOption::VALUE_NONE, 'Read message from stdin instead of option')
->addArgument('message', InputArgument::OPTIONAL, 'Text of the message')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
if ($input->getOption('stdin')) {
$message = \file_get_contents('php://stdin');
} elseif (null !== $input->getArgument('message')) {
$message = $input->getArgument('message');
} else {
$io->error('Either \'--stdin\' option or \'message\' argument should be specified.')
return Command::FAILURE;
}
if (mb_strlen($message) > 4096) {
$io->comment('Message is too long (>4096). Cutting the tail...');
$message = \mb_substr($message, 0, 4090) . PHP_EOL . '...';
}
try {
$this->messenger->sendMessageToChat(
(int) $input->getOption('chat-id'),
$message
);
} catch (\Exception $e) {
$io->error($e->getMessage());
return Command::FAILURE;
}
return Command::SUCCESS;
}
}

View file

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use unreal4u\Telegram\Methods\SetWebhook;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use unreal4u\TgLog;
#[AsCommand(name: 'app:telegram:webhook', description: 'Set webhook')]
class TelegramWebhookCommand extends Command
{
private const MODE_SET = 'set';
private const MODE_DELETE = 'delete';
public function __construct(
private readonly TgLog $client,
private readonly UrlGeneratorInterface $router,
private readonly string $telegramToken,
private readonly int $telegramWebhookMaxConnections,
) {
parent::__construct();
}
protected function configure()
{
$this
->addArgument('mode', InputArgument::REQUIRED, 'Command mode (set or delete)')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
if (self::MODE_SET === strtolower($input->getArgument('mode'))) {
$url = $this->router->generate(
'telegram_webhook',
['token' => $this->telegramToken],
UrlGeneratorInterface::ABSOLUTE_URL
);
$io->info('Setting webhook: ' . $url);
$setWebHook = new SetWebhook();
$setWebHook->url = $url;
$setWebHook->max_connections = $this->telegramWebhookMaxConnections;
$this->client->performApiRequest($setWebHook);
$output->writeln('Done');
} elseif (self::MODE_DELETE === strtolower($input->getArgument('mode'))) {
$io->warning('Unsupported until moving to another library.');
} else {
throw new \InvalidArgumentException(sprintf('Mode must be exactly one of: %s', implode(', ', [self::MODE_SET, self::MODE_DELETE])));
}
return Command::SUCCESS;
}
}

View file

@ -1,77 +1,40 @@
<?php
declare(strict_types=1);
namespace src\PointToolsBundle\Command;
namespace App\Command;
use App\Repository\UserRepository;
use App\Entity\{Subscription, User};
use App\Exception\Api\UserNotFoundException;
use App\Service\Api\UserApi;
use App\Service\SubscriptionsManager;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use src\PointToolsBundle\Entity\User;
use src\PointToolsBundle\Service\SubscriptionsManager;
use src\PointToolsBundle\Entity\{Subscription};
use src\PointToolsBundle\Exception\Api\UserNotFoundException;
use src\PointToolsBundle\Repository\UserRepository;
use src\PointToolsBundle\Service\{Api\UserApi};
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\{InputInterface, InputOption};
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* @todo https://symfony.com/doc/current/console/lockable_trait.html
*/
#[AsCommand(name: 'app:subscriptions:update', description: 'Update subscriptions of users subscribed to service')]
class UpdateSubscriptionsCommand extends Command
{
/** @var EntityManagerInterface */
private $em;
/** @var LoggerInterface */
private $logger;
/** @var UserRepository */
private $userRepo;
/** @var InputInterface */
private $input;
/** @var UserApi */
private $api;
/** @var int */
private $apiDelay = 500000;
/** @var int */
private $appUserId;
/** @var SubscriptionsManager */
private $subscriptionManager;
/** @var ProgressBar */
private $progress;
public function __construct(
EntityManagerInterface $em,
LoggerInterface $logger,
UserRepository $userRepo,
UserApi $api,
SubscriptionsManager $subscriptionManager,
int $apiDelay,
int $appUserId
private readonly EntityManagerInterface $em,
private readonly LoggerInterface $logger,
private readonly UserRepository $userRepo,
private readonly UserApi $api,
private readonly SubscriptionsManager $subscriptionManager,
private readonly int $pointApiDelay,
private readonly int $pointAppUserId,
) {
parent::__construct();
$this->em = $em;
$this->logger = $logger;
$this->userRepo = $userRepo;
$this->api = $api;
$this->subscriptionManager = $subscriptionManager;
$this->apiDelay = $apiDelay;
$this->appUserId = $appUserId;
}
protected function configure()
{
$this
->setName('point:update:subscriptions')
->setDescription('Update subscriptions of users subscribed to service')
->addOption(
'all-users',
null,
@ -87,46 +50,46 @@ class UpdateSubscriptionsCommand extends Command
;
}
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->input = $input;
$io = new SymfonyStyle($input, $output);
$this->logger->debug('UpdateSubscriptionsCommand started.');
$this->progress = new ProgressBar($output);
$this->progress->setFormat('debug');
$progress = $io->createProgressBar();
$progress->setFormat(ProgressBar::FORMAT_DEBUG);
if (!$input->getOption('check-only')) { // Beginning transaction for all changes
$this->em->beginTransaction();
}
try {
$usersForUpdate = $this->getUsersForUpdate();
$usersForUpdate = $this->getUsersForUpdate($input);
} catch (\Exception $e) {
$this->logger->error('Error while getting service subscribers', ['exception' => get_class($e), 'message' => $e->getMessage()]);
return 1;
return Command::FAILURE;
}
if (0 === count($usersForUpdate)) {
$this->logger->info('No local subscribers. Finishing.');
return 0;
return Command::SUCCESS;
}
$this->logger->info('Processing users subscribers');
$this->progress->start(count($usersForUpdate));
$progress->start(count($usersForUpdate));
foreach ($usersForUpdate as $user) {
usleep($this->apiDelay);
usleep($this->pointApiDelay);
$this->progress->advance();
$progress->advance();
$this->logger->info('Processing @'.$user->getLogin());
$this->updateUser($user);
}
$this->progress->finish();
$progress->finish();
// Flushing all changes at once to the database
if (!$input->getOption('check-only')) {
@ -136,7 +99,7 @@ class UpdateSubscriptionsCommand extends Command
$this->logger->debug('Finished');
return 0;
return Command::SUCCESS;
}
private function updateUser(User $user): void
@ -183,18 +146,18 @@ class UpdateSubscriptionsCommand extends Command
}
}
private function getUsersForUpdate(): array
private function getUsersForUpdate(InputInterface $input): array
{
$usersForUpdate = [];
if ($this->input->getOption('all-users')) {
if ($input->getOption('all-users')) {
$usersForUpdate = $this->userRepo->findBy(['removed' => false]);
} else {
/** @var User $serviceUser */
try {
$serviceUser = $this->userRepo->findActiveUserWithSubscribers($this->appUserId);
$serviceUser = $this->userRepo->findActiveUserWithSubscribers($this->pointAppUserId);
} catch (\Exception $e) {
$this->logger->error('Error while getting active user with subscribers', ['app_user_id' => $this->appUserId]);
$this->logger->error('Error while getting active user with subscribers', ['app_user_id' => $this->pointAppUserId]);
throw $e;
}
@ -203,7 +166,7 @@ class UpdateSubscriptionsCommand extends Command
$this->logger->warning('Service user not found or marked as removed. Falling back to API.');
try {
$serviceUser = $this->api->getUserById($this->appUserId);
$serviceUser = $this->api->getUserById($this->pointAppUserId);
} catch (UserNotFoundException $e) {
throw new \RuntimeException('Service user not found in the database and could not be retrieved from API.');
}
@ -212,7 +175,7 @@ class UpdateSubscriptionsCommand extends Command
$this->logger->info('Getting service subscribers');
try {
$usersForUpdate = $this->api->getUserSubscribersById($this->appUserId);
$usersForUpdate = $this->api->getUserSubscribersById($this->pointAppUserId);
} catch (UserNotFoundException $e) {
$this->logger->critical('Service user deleted or API response is invalid');
@ -258,4 +221,4 @@ class UpdateSubscriptionsCommand extends Command
return $usersForUpdate;
}
}
}

View file

@ -1,66 +1,40 @@
<?php
declare(strict_types=1);
namespace src\PointToolsBundle\Command;
namespace App\Command;
use App\Entity\User;
use App\Exception\Api\ForbiddenException;
use App\Exception\Api\UserNotFoundException;
use App\Repository\UserRepository;
use App\Service\Api\UserApi;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use src\PointToolsBundle\Entity\User;
use src\PointToolsBundle\Exception\Api\ForbiddenException;
use src\PointToolsBundle\Entity\{Subscription};
use src\PointToolsBundle\Exception\Api\{UserNotFoundException};
use src\PointToolsBundle\Repository\UserRepository;
use src\PointToolsBundle\Service\Api\UserApi;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\{InputInterface, InputOption};
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(name: 'app:privacy:update', description: 'Check removed users status and restore if user was deleted by error.')]
class UpdateUsersPrivacyCommand extends Command
{
/** @var EntityManagerInterface */
private $em;
/** @var LoggerInterface */
private $logger;
/** @var UserRepository */
private $userRepo;
/** @var InputInterface */
private $input;
/** @var UserApi */
private $api;
/** @var int */
private $apiDelay = 500000;
/** @var int */
private $appUserId;
/** @var ProgressBar */
private $progress;
public function __construct(EntityManagerInterface $em, LoggerInterface $logger, UserRepository $userRepo, UserApi $api, int $apiDelay, int $appUserId)
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly LoggerInterface $logger,
private readonly UserRepository $userRepo,
private readonly UserApi $api,
private readonly int $pointApiDelay,
private readonly int $pointAppUserId,
) {
parent::__construct();
$this->em = $em;
$this->logger = $logger;
$this->userRepo = $userRepo;
$this->api = $api;
$this->apiDelay = $apiDelay;
$this->appUserId = $appUserId;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('point:update:privacy')
->setDescription('Update users privacy')
->addOption(
'all-users',
null,
@ -70,47 +44,43 @@ class UpdateUsersPrivacyCommand extends Command
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->input = $input;
$io = new SymfonyStyle($input, $output);
$this->logger->debug(static::class.' started.');
$this->progress = new ProgressBar($output);
$this->progress->setFormat('debug');
$progress = $io->createProgressBar();
$progress->setFormat(ProgressBar::FORMAT_DEBUG);
try {
/** @var User[] $usersForUpdate */
$usersForUpdate = $this->getUsersForUpdate();
$usersForUpdate = $this->getUsersForUpdate($input);
} catch (\Exception $e) {
$this->logger->error('Error while getting service subscribers', ['exception' => get_class($e), 'message' => $e->getMessage()]);
return 1;
return Command::FAILURE;
}
$this->logger->info('Processing users privacy.');
$this->progress->start(count($usersForUpdate));
$progress->start(count($usersForUpdate));
foreach ($usersForUpdate as $idx => $user) {
usleep($this->apiDelay);
foreach ($usersForUpdate as $user) {
usleep($this->pointApiDelay);
$this->progress->advance();
$progress->advance();
$this->logger->info('Processing @'.$user->getLogin());
$this->updateUser($user);
}
$this->progress->finish();
$progress->finish();
$this->em->flush();
$this->logger->debug('Finished');
return 0;
return Command::SUCCESS;
}
private function updateUser(User $user): void
@ -150,17 +120,18 @@ class UpdateUsersPrivacyCommand extends Command
}
}
private function getUsersForUpdate(): array
/** @return User[] */
private function getUsersForUpdate(InputInterface $input): array
{
if ($this->input->getOption('all-users')) {
if ($input->getOption('all-users')) {
return $this->userRepo->findBy(['removed' => false]);
}
/** @var User $serviceUser */
try {
$serviceUser = $this->userRepo->findActiveUserWithSubscribers($this->appUserId);
$serviceUser = $this->userRepo->findActiveUserWithSubscribers($this->pointAppUserId);
} catch (\Exception $e) {
$this->logger->error('Error while getting active user with subscribers', ['app_user_id' => $this->appUserId]);
$this->logger->error('Error while getting active user with subscribers', ['app_user_id' => $this->pointAppUserId]);
throw $e;
}
@ -169,7 +140,7 @@ class UpdateUsersPrivacyCommand extends Command
$this->logger->warning('Service user not found or marked as removed. Falling back to API.');
try {
$serviceUser = $this->api->getUserById($this->appUserId);
$serviceUser = $this->api->getUserById($this->pointAppUserId);
} catch (UserNotFoundException $e) {
throw new \RuntimeException('Service user not found in the database and could not be retrieved from API.');
}
@ -178,7 +149,7 @@ class UpdateUsersPrivacyCommand extends Command
$this->logger->info('Getting service subscribers');
try {
return $this->api->getUserSubscribersById($this->appUserId);
return $this->api->getUserSubscribersById($this->pointAppUserId);
} catch (UserNotFoundException $e) {
$this->logger->critical('Service user deleted or API response is invalid');
@ -197,7 +168,6 @@ class UpdateUsersPrivacyCommand extends Command
$localSubscribers = [];
/** @var Subscription $subscription */
foreach ($serviceUser->getSubscribers() as $subscription) {
$localSubscribers[] = $subscription->getSubscriber();
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Exception\Api;
class ApiException extends \Exception
{
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Exception\Api;
class ForbiddenException extends ApiException
{
public function __construct(
string $message = 'Forbidden',
int $code = 403,
?\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace App\Exception\Api;
class InvalidResponseException extends ApiException
{
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Exception\Api;
class NetworkException extends ApiException
{
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Exception\Api;
class NotFoundException extends ApiException
{
public function __construct(
string $message = 'Resource not found',
int $code = 404,
?\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Exception\Api;
class ServerProblemException extends ApiException
{
public function __construct(
string $message = 'Server error',
int $code = 500,
?\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Exception\Api;
class UnauthorizedException extends ApiException
{
public function __construct(
string $message = 'Unauthorized',
int $code = 401,
?\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Exception\Api;
class UserNotFoundException extends NotFoundException
{
public function __construct(
$message = 'User not found',
$code = 0,
\Exception $previous = null,
private readonly ?int $userId = null,
private readonly ?string $login = null,
) {
parent::__construct($message, $code, $previous);
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getLogin(): ?string
{
return $this->login;
}
}

View file

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace src\PointToolsBundle\Exception;
namespace App\Exception;
class SubscriptionManagerException extends \Exception
{

View file

@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace App\Service\Api;
use App\Exception\Api\{ApiException,
ForbiddenException,
NetworkException,
NotFoundException,
UnauthorizedException,
ServerProblemException};
use JMS\Serializer\SerializerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
class AbstractApi
{
protected HttpClientInterface $client;
// TODO: check if these are still needed
protected string $authToken;
protected string $csRfToken;
public function __construct(
HttpClientInterface $pointApiClient,
protected readonly LoggerInterface $logger,
private readonly SerializerInterface $serializer,
) {
$this->client = $pointApiClient;
}
/** Make GET request and return DTO objects */
public function getGetJsonData(string $path, array $parameters = [], string $type, DeserializationContext $context = null): array|object|null
{
return $this->serializer->deserialize(
$this->getGetResponseBody($path, $parameters),
$type,
'json',
$context
);
}
/** Make POST request and return DTO objects */
public function getPostJsonData(string $path, array $parameters = [], string $type, DeserializationContext $context = null): array|object|null
{
return $this->serializer->deserialize(
$this->getPostResponseBody($path, $parameters),
$type,
'json',
$context
);
}
/** Make GET request and return response body */
public function getGetResponseBody($path, array $parameters = []): string
{
return $this->sendGetRequest($path, $parameters)->getContent();
}
/** Make POST request and return response body */
public function getPostResponseBody(string $path, array $parameters = []): string
{
return $this->sendPostRequest($path, $parameters)->getContent();
}
private function sendGetRequest(string $path, array $parameters = []): ResponseInterface
{
$this->logger->debug('Sending GET request', ['path' => $path, 'parameters' => $parameters]);
return $this->sendRequest('GET', $path, ['query' => $parameters]);
}
private function sendPostRequest(string $path, array $parameters = []): ResponseInterface
{
$this->logger->debug('Sending POST request', ['path' => $path, 'parameters' => $parameters]);
return $this->sendRequest('POST', $path, ['form_params' => $parameters]);
}
private function sendRequest(string $method, string $path, array $parameters): ResponseInterface
{
try {
$response = $this->client->request($method, $path, $parameters);
$this->checkResponse($response);
return $response;
} catch (TransportExceptionInterface $e) {
$this->throwIfCodeMatches($e->getCode(), $e->getPrevious());
throw new NetworkException('Request error', $e->getCode(), $e);
}
}
/** @throws ApiException */
private function checkResponse(ResponseInterface $response): void
{
$code = $response->getStatusCode();
// @todo remove after fix
// Temporary fix until @arts fixes this bug
if ('{"error": "UserNotFound"}' === $response->getContent()) {
throw new NotFoundException('Not found', SymfonyResponse::HTTP_NOT_FOUND);
} elseif ('{"message": "Forbidden", "code": 403, "error": "Forbidden"}' === (string) $response->getBody()) {
throw new ForbiddenException('Forbidden', SymfonyResponse::HTTP_FORBIDDEN);
}
$this->throwIfCodeMatches($code);
}
private function throwIfCodeMatches(int $code, ?\Throwable $previous = null): void
{
$e = $this->matchException($code, $previous);
if ($e) {
throw $e;
}
}
private function matchException(int $code, ?\Throwable $previous = null): ?ApiException
{
return match ($code) {
SymfonyResponse::HTTP_UNAUTHORIZED => new UnauthorizedException(previous: $previous),
SymfonyResponse::HTTP_NOT_FOUND => new NotFoundException(previous: $previous),
SymfonyResponse::HTTP_FORBIDDEN => new ForbiddenException(previous: $previous),
SymfonyResponse::HTTP_INTERNAL_SERVER_ERROR,
SymfonyResponse::HTTP_NOT_IMPLEMENTED,
SymfonyResponse::HTTP_BAD_GATEWAY,
SymfonyResponse::HTTP_SERVICE_UNAVAILABLE,
SymfonyResponse::HTTP_GATEWAY_TIMEOUT => new ServerProblemException(previous: $previous),
default => null,
};
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Service\Api;
use JMS\Serializer\SerializerInterface;
use Psr\Log\LoggerInterface;
use App\Service\Factory\Blogs\PostFactory;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/** Basic Point.im user API functions from /api/post */
class PostApi extends AbstractApi
{
public function __construct(
HttpClientInterface $pointApiClient,
SerializerInterface $serializer,
LoggerInterface $logger,
private readonly PostFactory $postFactory
) {
parent::__construct($pointApiClient, $logger, $serializer);
}
}

View file

@ -1,36 +1,32 @@
<?php
declare(strict_types=1);
namespace src\PointToolsBundle\Service\Api;
namespace App\Service\Api;
use GuzzleHttp\ClientInterface;
use JMS\Serializer\{
DeserializationContext, SerializerInterface
use App\DTO\Api\{Auth as AuthDTO, User as UserDTO};
use App\Entity\User;
use App\Exception\Api\{ForbiddenException,
InvalidResponseException,
NotFoundException,
UserNotFoundException
};
use App\Service\Factory\UserFactory;
use JMS\Serializer\{DeserializationContext, SerializerInterface};
use Psr\Log\LoggerInterface;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\{src\PointToolsBundle\DTO\Api\Auth, src\PointToolsBundle\DTO\Api\User as UserDTO};
use src\PointToolsBundle\Entity\User;
use Skobkin\Bundle\PointToolsBundle\Exception\Api\{
src\PointToolsBundle\Exception\Api\ForbiddenException, src\PointToolsBundle\Exception\Api\InvalidResponseException, src\PointToolsBundle\Exception\Api\NotFoundException, src\PointToolsBundle\Exception\Api\UserNotFoundException};
use src\PointToolsBundle\Service\Api\AbstractApi;
use src\PointToolsBundle\Service\Factory\UserFactory;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* Basic Point.im user API functions from /api/user/*
*/
/** Basic Point.im user API functions from /api/user/* */
class UserApi extends AbstractApi
{
private const PREFIX = '/api/user/';
/**
* @var UserFactory
*/
private $userFactory;
public function __construct(ClientInterface $httpClient, SerializerInterface $serializer, LoggerInterface $logger, UserFactory $userFactory)
{
parent::__construct($httpClient, $serializer, $logger);
$this->userFactory = $userFactory;
public function __construct(
HttpClientInterface $pointApiClient,
SerializerInterface $serializer,
LoggerInterface $logger,
private readonly UserFactory $userFactory,
) {
parent::__construct($pointApiClient, $logger, $serializer);
}
public function isLoginAndPasswordValid(string $login, string $password): bool
@ -50,7 +46,7 @@ class UserApi extends AbstractApi
return false;
}
public function authenticate(string $login, string $password): \src\PointToolsBundle\DTO\Api\Auth
public function authenticate(string $login, string $password): AuthDTO
{
$this->logger->debug('Trying to authenticate user via Point.im API', ['login' => $login]);
@ -61,17 +57,15 @@ class UserApi extends AbstractApi
'login' => $login,
'password' => $password,
],
\src\PointToolsBundle\DTO\Api\Auth::class
AuthDTO::class
);
} catch (\src\PointToolsBundle\Exception\Api\NotFoundException $e) {
throw new \src\PointToolsBundle\Exception\Api\InvalidResponseException('API method not found', 0, $e);
} catch (NotFoundException $e) {
throw new InvalidResponseException('API method not found', 0, $e);
}
}
/**
* @throws \src\PointToolsBundle\Exception\Api\InvalidResponseException
*/
public function logout(\src\PointToolsBundle\DTO\Api\Auth $auth): bool
/** @throws InvalidResponseException */
public function logout(AuthDTO $auth): bool
{
$this->logger->debug('Trying to log user out via Point.im API');
@ -79,20 +73,14 @@ class UserApi extends AbstractApi
$this->getPostResponseBody('/api/logout', ['csrf_token' => $auth->getCsRfToken()]);
return true;
} catch (\src\PointToolsBundle\Exception\Api\NotFoundException $e) {
throw new \src\PointToolsBundle\Exception\Api\InvalidResponseException('API method not found', 0, $e);
} catch (\src\PointToolsBundle\Exception\Api\ForbiddenException $e) {
} catch (NotFoundException $e) {
throw new InvalidResponseException('API method not found', 0, $e);
} catch (ForbiddenException $e) {
return true;
}
}
/**
* Get user subscribers by user login
*
* @return User[]
*
* @throws \src\PointToolsBundle\Exception\Api\UserNotFoundException
*/
/** @return User[] */
public function getUserSubscribersByLogin(string $login): array
{
$this->logger->debug('Trying to get user subscribers by login', ['login' => $login]);
@ -102,22 +90,16 @@ class UserApi extends AbstractApi
self::PREFIX.urlencode($login).'/subscribers',
[],
'array<'.UserDTO::class.'>',
DeserializationContext::create()->setGroups(['user_short'])
DeserializationContext::create()->setGroups(['user_short']),
);
} catch (\src\PointToolsBundle\Exception\Api\NotFoundException $e) {
throw new \src\PointToolsBundle\Exception\Api\UserNotFoundException('User not found', 0, $e, null, $login);
} catch (NotFoundException $e) {
throw new UserNotFoundException('User not found', 0, $e, null, $login);
}
return $this->userFactory->findOrCreateFromDTOArray($usersList);
}
/**
* Get user subscribers by user id
*
* @return User[]
*
* @throws \src\PointToolsBundle\Exception\Api\UserNotFoundException
*/
/** @return User[] */
public function getUserSubscribersById(int $id): array
{
$this->logger->debug('Trying to get user subscribers by id', ['id' => $id]);
@ -127,18 +109,15 @@ class UserApi extends AbstractApi
self::PREFIX.'id/'.$id.'/subscribers',
[],
'array<'.UserDTO::class.'>',
DeserializationContext::create()->setGroups(['user_short'])
DeserializationContext::create()->setGroups(['user_short']),
);
} catch (\src\PointToolsBundle\Exception\Api\NotFoundException $e) {
throw new \src\PointToolsBundle\Exception\Api\UserNotFoundException('User not found', 0, $e, $id);
} catch (NotFoundException $e) {
throw new UserNotFoundException('User not found', 0, $e, $id);
}
return $this->userFactory->findOrCreateFromDTOArray($usersList);
}
/**
* Get full user info by login
*/
public function getUserByLogin(string $login): User
{
$this->logger->debug('Trying to get user by login', ['login' => $login]);
@ -149,18 +128,15 @@ class UserApi extends AbstractApi
self::PREFIX.'login/'.urlencode($login),
[],
UserDTO::class,
DeserializationContext::create()->setGroups(['user_full'])
DeserializationContext::create()->setGroups(['user_full']),
);
} catch (\src\PointToolsBundle\Exception\Api\NotFoundException $e) {
throw new \src\PointToolsBundle\Exception\Api\UserNotFoundException('User not found', 0, $e, null, $login);
} catch (NotFoundException $e) {
throw new UserNotFoundException('User not found', 0, $e, null, $login);
}
return $this->userFactory->findOrCreateFromDTO($userInfo);
}
/**
* Get full user info by id
*/
public function getUserById(int $id): User
{
$this->logger->debug('Trying to get user by id', ['id' => $id]);
@ -171,10 +147,10 @@ class UserApi extends AbstractApi
self::PREFIX.'id/'.$id,
[],
UserDTO::class,
DeserializationContext::create()->setGroups(['user_full'])
DeserializationContext::create()->setGroups(['user_full']),
);
} catch (\src\PointToolsBundle\Exception\Api\NotFoundException $e) {
throw new \src\PointToolsBundle\Exception\Api\UserNotFoundException('User not found', 0, $e, $id);
} catch (NotFoundException $e) {
throw new UserNotFoundException('User not found', 0, $e, $id);
}
// Not catching ForbiddenException right now

View file

@ -35,6 +35,18 @@
"migrations/.gitignore"
]
},
"jms/serializer-bundle": {
"version": "5.2",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "4.0",
"ref": "cc04e10cf7171525b50c18b36004edf64cb478be"
},
"files": [
"config/packages/jms_serializer.yaml"
]
},
"phpunit/phpunit": {
"version": "9.6",
"recipe": {