├── config ├── packages │ ├── mailer.yaml │ ├── dev │ │ └── routing.yaml │ ├── test │ │ ├── routing.yaml │ │ └── validator.yaml │ ├── sensio_framework_extra.yaml │ ├── twig.yaml │ ├── validator.yaml │ ├── misd_phone_number.yaml │ ├── boshurik_telegram_bot.yaml │ ├── routing.yaml │ ├── messenger.yaml │ ├── cache.yaml │ ├── framework.yaml │ └── security.yaml ├── routes.yaml ├── routes │ ├── framework.yaml │ └── boshurik_telegram_bot.yaml ├── preload.php ├── bundles.php ├── bootstrap.php └── services.yaml ├── templates ├── login │ └── site │ │ ├── private.html.twig │ │ ├── widget.html.twig │ │ └── public.html.twig ├── order │ └── email.html.twig └── base.html.twig ├── public └── index.php ├── .gitignore ├── README.md ├── src ├── Kernel.php ├── Telegram │ └── Location │ │ ├── Command │ │ ├── LocationCommandInterface.php │ │ ├── AbstractLocationCommand.php │ │ └── LocationCommand.php │ │ └── LocationHandler.php ├── Order │ ├── Event │ │ └── OrderEvent.php │ ├── EventListener │ │ └── OrderListener.php │ ├── Model │ │ └── Order.php │ └── Telegram │ │ ├── OrderHandler.php │ │ └── Command │ │ └── OrderCommand.php ├── Help │ └── Telegram │ │ └── Command │ │ └── HelpCommand.php ├── Login │ ├── Security │ │ ├── UserLoader.php │ │ ├── UserFactory.php │ │ ├── User.php │ │ ├── UserManager.php │ │ └── UserProvider.php │ └── Controller │ │ └── SiteController.php ├── Post │ ├── Model │ │ └── Post.php │ ├── Repository │ │ └── PostRepository.php │ └── Telegram │ │ └── Command │ │ └── PostCommand.php ├── Mail │ └── Mailer.php ├── Hello │ └── Telegram │ │ └── Command │ │ └── HelloCommand.php └── Office │ ├── Model │ └── Office.php │ ├── Repository │ └── OfficeRepository.php │ └── Telegram │ └── Command │ └── OfficesCommand.php ├── bin └── console ├── psalm.xml ├── LICENSE ├── .env ├── .php-cs-fixer.dist.php ├── composer.json └── symfony.lock /config/packages/mailer.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | mailer: 3 | dsn: '%env(MAILER_DSN)%' 4 | -------------------------------------------------------------------------------- /config/packages/dev/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/test/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /config/packages/test/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | not_compromised_password: false 4 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | 4 | when@test: 5 | twig: 6 | strict_variables: true 7 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | login_controllers: 2 | resource: ../src/Login/Controller/ 3 | type: annotation 4 | 5 | login_guard: 6 | path: /_telegram/login 7 | -------------------------------------------------------------------------------- /config/routes/framework.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | _errors: 3 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 4 | prefix: /_error 5 | -------------------------------------------------------------------------------- /config/routes/boshurik_telegram_bot.yaml: -------------------------------------------------------------------------------- 1 | boshurik_telegram_bot_routing: 2 | resource: "@BoShurikTelegramBotBundle/Resources/config/routing.yml" 3 | prefix: "/_telegram/%telegram_route_secret%" 4 | -------------------------------------------------------------------------------- /templates/login/site/private.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body %} 4 |

Private area

5 |

{{ app.user.userIdentifier }} (#{{ app.user.id }})

6 | {% endblock %} -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | 5 | # Enables validator auto-mapping support. 6 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 7 | #auto_mapping: 8 | # App\Entity\: [] 9 | -------------------------------------------------------------------------------- /config/packages/misd_phone_number.yaml: -------------------------------------------------------------------------------- 1 | # To persist libphonenumber\PhoneNumber objects, add the Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType mapping to your application's config. 2 | # This requires: doctrine/doctrine-bundle 3 | #doctrine: 4 | # dbal: 5 | # types: 6 | # phone_number: Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ###> symfony/framework-bundle ### 3 | /.env.local 4 | /.env.local.php 5 | /.env.*.local 6 | /config/secrets/prod/prod.decrypt.private.php 7 | /public/bundles/ 8 | /var/ 9 | /vendor/ 10 | ###< symfony/framework-bundle ### 11 | 12 | ###> friendsofphp/php-cs-fixer ### 13 | /.php-cs-fixer.php 14 | /.php-cs-fixer.cache 15 | ###< friendsofphp/php-cs-fixer ### 16 | -------------------------------------------------------------------------------- /config/packages/boshurik_telegram_bot.yaml: -------------------------------------------------------------------------------- 1 | boshurik_telegram_bot: 2 | api: 3 | token: "%env(TELEGRAM_BOT_TOKEN)%" 4 | authenticator: 5 | default_target_route: login_private # redirect after login success 6 | guard_route: login_guard # guard route 7 | login_route: login_public # optional, if login fails user will be redirected there 8 | -------------------------------------------------------------------------------- /templates/login/site/public.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body %} 4 |

Public area

5 | {% if is_granted('ROLE_USER') %} 6 | Go to private area 7 | {% else %} 8 | {{ render(controller('App\\Login\\Controller\\SiteController::widgetAction')) }} 9 | {% endif %} 10 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | telegram-bot-example 2 | ==================== 3 | 4 | Example of creating telegram bot on top of [Symfony][1] and [BoShurikTelegramBotBundle][2] 5 | 6 | - Hello world command 7 | - Work with location 8 | - Work with multiple steps command 9 | - Paging with inline keyboard 10 | - Login with telegram 11 | 12 | [1]: http://symfony.com 13 | [2]: https://github.com/BoShurik/TelegramBotBundle -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | 5 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 6 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 7 | #default_uri: http://localhost 8 | 9 | when@prod: 10 | framework: 11 | router: 12 | strict_requirements: null 13 | -------------------------------------------------------------------------------- /templates/order/email.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | New order 6 | 7 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App; 13 | 14 | use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; 15 | use Symfony\Component\HttpKernel\Kernel as BaseKernel; 16 | 17 | class Kernel extends BaseKernel 18 | { 19 | use MicroKernelTrait; 20 | } 21 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | BoShurik\TelegramBotBundle\BoShurikTelegramBotBundle::class => ['all' => true], 6 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 7 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 8 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 9 | Misd\PhoneNumberBundle\MisdPhoneNumberBundle::class => ['all' => true], 10 | ]; 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Telegram\Location\Command; 13 | 14 | use TelegramBot\Api\BotApi; 15 | use TelegramBot\Api\Types\Update; 16 | 17 | interface LocationCommandInterface 18 | { 19 | public function getId(): string; 20 | 21 | public function locationExecute(BotApi $api, Update $update): void; 22 | } 23 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /config/packages/messenger.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. 4 | # failure_transport: failed 5 | 6 | transports: 7 | # https://symfony.com/doc/current/messenger.html#transport-configuration 8 | # async: '%env(MESSENGER_TRANSPORT_DSN)%' 9 | # failed: 'doctrine://default?queue_name=failed' 10 | # sync: 'sync://' 11 | 12 | routing: 13 | # Route your messages to the transports 14 | # 'BoShurik\TelegramBotBundle\Messenger\TelegramMessage': async 15 | -------------------------------------------------------------------------------- /src/Order/Event/OrderEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Order\Event; 13 | 14 | use App\Order\Model\Order; 15 | use Symfony\Contracts\EventDispatcher\Event; 16 | 17 | class OrderEvent extends Event 18 | { 19 | public function __construct(private Order $order) 20 | { 21 | } 22 | 23 | public function getOrder(): Order 24 | { 25 | return $this->order; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Help/Telegram/Command/HelpCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Help\Telegram\Command; 13 | 14 | use BoShurik\TelegramBotBundle\Telegram\Command\HelpCommand as BaseCommand; 15 | use TelegramBot\Api\Types\Update; 16 | 17 | class HelpCommand extends BaseCommand 18 | { 19 | public function isApplicable(Update $update): bool 20 | { 21 | return $update->getMessage() !== null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Welcome!{% endblock %} 6 | 7 | {# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #} 8 | {% block stylesheets %} 9 | {# {{ encore_entry_link_tags('app') }}#} 10 | {% endblock %} 11 | 12 | {% block javascripts %} 13 | {# {{ encore_entry_script_tags('app') }}#} 14 | {% endblock %} 15 | 16 | 17 | {% block body %}{% endblock %} 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Login/Security/UserLoader.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Login\Security; 13 | 14 | use BoShurik\TelegramBotBundle\Authenticator\UserLoaderInterface; 15 | use Symfony\Component\Security\Core\User\UserInterface; 16 | 17 | class UserLoader implements UserLoaderInterface 18 | { 19 | public function __construct(private UserManager $userManager) 20 | { 21 | } 22 | 23 | public function loadByTelegramId(string $id): ?UserInterface 24 | { 25 | return $this->userManager->find($id); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | #csrf_protection: true 5 | http_method_override: false 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: null 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | storage_factory_id: session.storage.factory.native 14 | 15 | #esi: true 16 | #fragments: true 17 | php_errors: 18 | log: true 19 | 20 | when@test: 21 | framework: 22 | test: true 23 | session: 24 | storage_factory_id: session.storage.factory.mock_file 25 | -------------------------------------------------------------------------------- /src/Login/Security/UserFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Login\Security; 13 | 14 | use BoShurik\TelegramBotBundle\Authenticator\UserFactoryInterface; 15 | use Symfony\Component\Security\Core\User\UserInterface; 16 | 17 | class UserFactory implements UserFactoryInterface 18 | { 19 | public function __construct(private UserManager $userManager) 20 | { 21 | } 22 | 23 | public function createFromTelegram(array $data): UserInterface 24 | { 25 | $user = new User($data['username'], $data['id']); 26 | $this->userManager->save($user); 27 | 28 | return $user; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Post/Model/Post.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Post\Model; 13 | 14 | class Post 15 | { 16 | public function __construct(private string $description) 17 | { 18 | } 19 | 20 | public function getName(): string 21 | { 22 | $words = explode(' ', $this->getSentence()); 23 | 24 | return implode(' ', \array_slice($words, 0, 2)); 25 | } 26 | 27 | public function getDescription(): string 28 | { 29 | return $this->description; 30 | } 31 | 32 | public function getSentence(): string 33 | { 34 | $length = strpos($this->description, '.'); 35 | 36 | return substr($this->description, 0, $length !== false ? $length : null); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Mail/Mailer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Mail; 13 | 14 | use Symfony\Component\Mailer\MailerInterface; 15 | use Symfony\Component\Mime\Email; 16 | 17 | class Mailer 18 | { 19 | public function __construct(private MailerInterface $mailer, private string $from) 20 | { 21 | } 22 | 23 | /** 24 | * @param string|string[] $to 25 | */ 26 | public function send(string $subject, string $body, $to, array $attachments = [], ?string $from = null): void 27 | { 28 | $email = (new Email()) 29 | ->from($from ?? $this->from) 30 | ->to(...(array) $to) 31 | ->subject($subject) 32 | ->html($body) 33 | ; 34 | 35 | $this->mailer->send($email); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | =1.2) 9 | if (is_array($env = @include dirname(__DIR__).'/.env.local.php')) { 10 | foreach ($env as $name => $value) { 11 | putenv("$name=$value"); 12 | } 13 | $_SERVER += $env; 14 | $_ENV += $env; 15 | } elseif (!class_exists(Dotenv::class)) { 16 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); 17 | } else { 18 | // load all the .env files 19 | (new Dotenv())->loadEnv(dirname(__DIR__).'/.env'); 20 | } 21 | 22 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 23 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; 24 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 - 2020 Alexander Borisov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Login/Security/User.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Login\Security; 13 | 14 | use Symfony\Component\Security\Core\User\UserInterface; 15 | 16 | class User implements UserInterface 17 | { 18 | public function __construct(private string $username, private string $id) 19 | { 20 | } 21 | 22 | public function getId(): string 23 | { 24 | return $this->id; 25 | } 26 | 27 | public function getRoles(): array 28 | { 29 | return [ 30 | 'ROLE_USER', 31 | ]; 32 | } 33 | 34 | public function getPassword() 35 | { 36 | return null; 37 | } 38 | 39 | public function getSalt() 40 | { 41 | return null; 42 | } 43 | 44 | public function getUserIdentifier(): string 45 | { 46 | return $this->username; 47 | } 48 | 49 | public function eraseCredentials() 50 | { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Login/Security/UserManager.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Login\Security; 13 | 14 | use Psr\Cache\CacheItemInterface; 15 | use Psr\Cache\CacheItemPoolInterface; 16 | 17 | class UserManager 18 | { 19 | public function __construct(private CacheItemPoolInterface $cache) 20 | { 21 | } 22 | 23 | public function save(User $user): void 24 | { 25 | $item = $this->getCacheItem($user->getId()); 26 | $item->set($user); 27 | $this->cache->save($item); 28 | } 29 | 30 | public function find(string $id): ?User 31 | { 32 | $item = $this->getCacheItem($id); 33 | if ($item->isHit()) { 34 | return $item->get(); 35 | } 36 | 37 | return null; 38 | } 39 | 40 | private function getCacheItem(string $id): CacheItemInterface 41 | { 42 | return $this->cache->getItem(sprintf('user-%s', $id)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Order/EventListener/OrderListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Order\EventListener; 13 | 14 | use App\Mail\Mailer; 15 | use App\Order\Event\OrderEvent; 16 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 17 | use Twig\Environment; 18 | 19 | class OrderListener implements EventSubscriberInterface 20 | { 21 | public static function getSubscribedEvents(): array 22 | { 23 | return [ 24 | OrderEvent::class => 'onOrderSubmit', 25 | ]; 26 | } 27 | 28 | public function __construct(private Mailer $mailer, private Environment $twig, private string $to) 29 | { 30 | } 31 | 32 | public function onOrderSubmit(OrderEvent $event): void 33 | { 34 | $order = $event->getOrder(); 35 | $body = $this->twig->render('order/email.html.twig', [ 36 | 'order' => $order, 37 | ]); 38 | 39 | $this->mailer->send('New order', $body, $this->to); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Hello/Telegram/Command/HelloCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Hello\Telegram\Command; 13 | 14 | use BoShurik\TelegramBotBundle\Telegram\Command\AbstractCommand; 15 | use BoShurik\TelegramBotBundle\Telegram\Command\PublicCommandInterface; 16 | use TelegramBot\Api\BotApi; 17 | use TelegramBot\Api\Types\Update; 18 | 19 | class HelloCommand extends AbstractCommand implements PublicCommandInterface 20 | { 21 | public function getName(): string 22 | { 23 | return '/hello'; 24 | } 25 | 26 | public function getDescription(): string 27 | { 28 | return 'Example command'; 29 | } 30 | 31 | public function execute(BotApi $api, Update $update): void 32 | { 33 | preg_match(self::REGEXP, $update->getMessage()->getText(), $matches); 34 | $who = !empty($matches[3]) ? $matches[3] : 'World'; 35 | 36 | $text = sprintf('Hello *%s*', $who); 37 | $api->sendMessage($update->getMessage()->getChat()->getId(), $text, 'markdown'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Login/Security/UserProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Login\Security; 13 | 14 | use Symfony\Component\Security\Core\Exception\UserNotFoundException; 15 | use Symfony\Component\Security\Core\User\UserInterface; 16 | use Symfony\Component\Security\Core\User\UserProviderInterface; 17 | 18 | class UserProvider implements UserProviderInterface 19 | { 20 | public function __construct(private UserManager $userManager) 21 | { 22 | } 23 | 24 | public function loadUserByIdentifier(string $identifier): UserInterface 25 | { 26 | if (!$user = $this->userManager->find($identifier)) { 27 | $exception = new UserNotFoundException(); 28 | $exception->setUserIdentifier($identifier); 29 | 30 | throw $exception; 31 | } 32 | 33 | return $user; 34 | } 35 | 36 | public function refreshUser(UserInterface $user): UserInterface 37 | { 38 | return $user; 39 | } 40 | 41 | public function supportsClass(string $class): bool 42 | { 43 | return $class === User::class; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Telegram/Location/Command/AbstractLocationCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Telegram\Location\Command; 13 | 14 | use App\Telegram\Location\LocationHandler; 15 | use BoShurik\TelegramBotBundle\Telegram\Command\AbstractCommand; 16 | use TelegramBot\Api\BotApi; 17 | use TelegramBot\Api\Types\Update; 18 | 19 | abstract class AbstractLocationCommand extends AbstractCommand implements LocationCommandInterface 20 | { 21 | public function __construct(private LocationHandler $locationHandler) 22 | { 23 | } 24 | 25 | public function execute(BotApi $api, Update $update): void 26 | { 27 | $this->locationHandler->setLocationCommand((string) $update->getMessage()->getChat()->getId(), $this->getId()); 28 | $api->sendMessage($update->getMessage()->getChat()->getId(), $this->getMessage()); 29 | } 30 | 31 | public function getId(): string 32 | { 33 | return $this->getName(); 34 | } 35 | 36 | protected function getMessage(): string 37 | { 38 | return "Пожалуйста, отправьте Ваше местоположение:\n• Нажмите \xF0\x9F\x93\x8E\n• Выберите \"Location\"\n• Нажмите \"Send my current location\""; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Telegram/Location/LocationHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Telegram\Location; 13 | 14 | use Psr\Cache\CacheItemPoolInterface; 15 | 16 | class LocationHandler 17 | { 18 | public function __construct(private CacheItemPoolInterface $cache, private int $lifetime = 0) 19 | { 20 | } 21 | 22 | public function hasLocationCommand(string $chat): bool 23 | { 24 | return $this->cache->hasItem($chat); 25 | } 26 | 27 | public function getLocationCommand(string $chat): ?string 28 | { 29 | if (!$this->hasLocationCommand($chat)) { 30 | return null; 31 | } 32 | 33 | $item = $this->cache->getItem($chat); 34 | 35 | return $item->get(); 36 | } 37 | 38 | public function setLocationCommand(string $chat, string $id): void 39 | { 40 | $item = $this->cache->getItem($chat); 41 | $item->set($id); 42 | if ($this->lifetime > 0) { 43 | $item->expiresAfter($this->lifetime); 44 | } 45 | 46 | $this->cache->save($item); 47 | } 48 | 49 | public function clearLocationCommand(string $chat): void 50 | { 51 | $this->cache->deleteItem($chat); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # In all environments, the following files are loaded if they exist, 2 | # the latter taking precedence over the former: 3 | # 4 | # * .env contains default values for the environment variables needed by the app 5 | # * .env.local uncommitted file with local overrides 6 | # * .env.$APP_ENV committed environment-specific defaults 7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides 8 | # 9 | # Real environment variables win over .env files. 10 | # 11 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. 12 | # 13 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). 14 | # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration 15 | 16 | APP_TELEGRAM_BOT_NAME=TelegramBotName 17 | 18 | APP_EMAIL_FROM=bot@example.com 19 | APP_EMAIL_TO=admin@example.com 20 | 21 | ###> symfony/framework-bundle ### 22 | APP_ENV=dev 23 | APP_SECRET=27a680d7624e2822ca7aef97105576b1 24 | ###< symfony/framework-bundle ### 25 | 26 | ###> boshurik/telegram-bot-bundle ### 27 | TELEGRAM_BOT_TOKEN=bot-token 28 | ###< boshurik/telegram-bot-bundle ### 29 | ###> symfony/messenger ### 30 | # Choose one of the transports below 31 | # MESSENGER_TRANSPORT_DSN=doctrine://default 32 | # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages 33 | # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages 34 | ###< symfony/messenger ### 35 | 36 | ###> symfony/mailer ### 37 | # MAILER_DSN=smtp://localhost 38 | ###< symfony/mailer ### 39 | -------------------------------------------------------------------------------- /src/Office/Model/Office.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Office\Model; 13 | 14 | class Office 15 | { 16 | public function __construct(private string $name, private float $latitude, private float $longitude) 17 | { 18 | } 19 | 20 | public function getDistance(float $latitude, float $longitude): float 21 | { 22 | $earthRadius = 6371000; // Meters 23 | 24 | $latitudeFromRad = deg2rad($latitude); 25 | $longitudeFromRad = deg2rad($longitude); 26 | $latitudeToRad = deg2rad($this->latitude); 27 | $longitudeToRad = deg2rad($this->longitude); 28 | 29 | $longitudeDelta = $longitudeToRad - $longitudeFromRad; 30 | 31 | $a = (cos($latitudeToRad) * sin($longitudeDelta)) ** 2 + (cos($latitudeFromRad) * sin($latitudeToRad) - sin($latitudeFromRad) * cos($latitudeToRad) * cos($longitudeDelta)) ** 2; 32 | $b = sin($latitudeFromRad) * sin($latitudeToRad) + cos($latitudeFromRad) * cos($latitudeToRad) * cos($longitudeDelta); 33 | $angle = atan2(sqrt($a), $b); 34 | 35 | return $angle * $earthRadius; 36 | } 37 | 38 | public function getName(): string 39 | { 40 | return $this->name; 41 | } 42 | 43 | public function getLatitude(): float 44 | { 45 | return $this->latitude; 46 | } 47 | 48 | public function getLongitude(): float 49 | { 50 | return $this->longitude; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Login/Controller/SiteController.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Login\Controller; 13 | 14 | use App\Login\Security\User; 15 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; 16 | use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; 17 | use Symfony\Component\HttpFoundation\Response; 18 | use Symfony\Component\Routing\Annotation\Route; 19 | use TelegramBot\Api\BotApi; 20 | 21 | #[Route(name: 'login_')] 22 | class SiteController extends AbstractController 23 | { 24 | #[Route(path: '/', name: 'public')] 25 | public function publicAction(): Response 26 | { 27 | return $this->render('login/site/public.html.twig'); 28 | } 29 | 30 | #[Route(path: '/private', name: 'private')] 31 | #[IsGranted('ROLE_USER')] 32 | public function privateAction(BotApi $api): Response 33 | { 34 | $user = $this->getUser(); 35 | if (!$user instanceof User) { 36 | throw new \LogicException(); 37 | } 38 | if ($user->getId()) { 39 | $api->sendMessage($user->getId(), 'Hello from private area!'); 40 | } 41 | 42 | return $this->render('login/site/private.html.twig'); 43 | } 44 | 45 | public function widgetAction(string $telegramBotName): Response 46 | { 47 | return $this->render('login/site/widget.html.twig', [ 48 | 'telegram_bot_name' => $telegramBotName, 49 | ]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Office/Repository/OfficeRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Office\Repository; 13 | 14 | use App\Office\Model\Office; 15 | 16 | class OfficeRepository 17 | { 18 | /** 19 | * @var Office[]|null 20 | */ 21 | private ?array $offices; 22 | 23 | public function __construct() 24 | { 25 | $this->offices = null; 26 | } 27 | 28 | /** 29 | * @return Office[] 30 | */ 31 | public function findNearest(float $latitude, float $longitude, int $count = 3): array 32 | { 33 | $this->init(); 34 | 35 | $offices = []; 36 | foreach ((array) $this->offices as $office) { 37 | $offices[(string) $office->getDistance($latitude, $longitude)] = $office; 38 | } 39 | 40 | ksort($offices); 41 | 42 | return \array_slice($offices, 0, $count); 43 | } 44 | 45 | private function init() 46 | { 47 | if (null !== $this->offices) { 48 | return; 49 | } 50 | 51 | $this->offices = [ 52 | new Office('Moscow', 55.7494733, 37.3523182), 53 | new Office('Saint Petersburg', 59.9390094, 29.5303031), 54 | new Office('Novosibirsk', 54.969655, 82.6692275), 55 | new Office('Yekaterinburg', 56.8135772, 60.3747574), 56 | new Office('Nizhny Novgorod', 56.2926609, 43.7866631), 57 | new Office('Vladimir', 56.1376417, 40.343441), 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Order/Model/Order.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Order\Model; 13 | 14 | use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber; 15 | use Symfony\Component\Validator\Constraints as Assert; 16 | 17 | class Order 18 | { 19 | #[Assert\NotBlank(groups: ['step1'])] 20 | private ?string $name; 21 | 22 | #[Assert\NotBlank(groups: ['step2'])] 23 | #[PhoneNumber(groups: ['step2'])] 24 | private ?string $phone; 25 | 26 | #[Assert\NotBlank(groups: ['step3'])] 27 | #[Assert\Email(groups: ['step3'])] 28 | private ?string $email; 29 | 30 | private ?string $message; 31 | 32 | public function getName(): ?string 33 | { 34 | return $this->name; 35 | } 36 | 37 | public function setName(?string $name): void 38 | { 39 | $this->name = $name; 40 | } 41 | 42 | public function getPhone(): ?string 43 | { 44 | return $this->phone; 45 | } 46 | 47 | public function setPhone(?string $phone): void 48 | { 49 | $this->phone = $phone; 50 | } 51 | 52 | public function getEmail(): ?string 53 | { 54 | return $this->email; 55 | } 56 | 57 | public function setEmail(?string $email): void 58 | { 59 | $this->email = $email; 60 | } 61 | 62 | public function getMessage(): ?string 63 | { 64 | return $this->message; 65 | } 66 | 67 | public function setMessage(?string $message): void 68 | { 69 | $this->message = $message; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | # This file is the entry point to configure your own services. 2 | # Files in the packages/ subdirectory configure your dependencies. 3 | 4 | # Put parameters here that don't need to change on each machine where the app is deployed 5 | # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration 6 | parameters: 7 | telegram_route_secret: '%env(TELEGRAM_BOT_TOKEN)%' 8 | 9 | services: 10 | # default configuration for services in *this* file 11 | _defaults: 12 | autowire: true # Automatically injects dependencies in your services. 13 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 14 | bind: 15 | $from: '%env(APP_EMAIL_FROM)%' 16 | $to: '%env(APP_EMAIL_TO)%' 17 | $telegramBotName: '%env(APP_TELEGRAM_BOT_NAME)%' 18 | 19 | # makes classes in src/ available to be used as services 20 | # this creates a service per class whose id is the fully-qualified class name 21 | App\: 22 | resource: '../src/' 23 | exclude: 24 | - '../src/DependencyInjection/' 25 | - '../src/Entity/' 26 | - '../src/Kernel.php' 27 | - '../src/Tests/' 28 | 29 | # add more service definitions when explicit configuration is needed 30 | # please note that last definitions always *replace* previous ones 31 | 32 | # Set lowest priority to help command 33 | App\Help\Telegram\Command\HelpCommand: 34 | tags: 35 | - { name: boshurik_telegram_bot.command, priority: -1024 } 36 | 37 | BoShurik\TelegramBotBundle\Authenticator\UserLoaderInterface: '@App\Login\Security\UserLoader' 38 | BoShurik\TelegramBotBundle\Authenticator\UserFactoryInterface: '@App\Login\Security\UserFactory' -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | For the full copyright and license information, please view the LICENSE 9 | file that was distributed with this source code. 10 | HEADER; 11 | 12 | $finder = \PhpCsFixer\Finder::create() 13 | ->in([ 14 | 'src', 15 | ]) 16 | ->name([ 17 | '*.php', 18 | ]) 19 | ; 20 | 21 | return (new PhpCsFixer\Config()) 22 | ->setFinder($finder) 23 | ->setRules([ 24 | '@PSR2' => true, 25 | '@Symfony' => true, 26 | '@Symfony:risky' => true, 27 | '@PHP70Migration' => true, 28 | '@PHP70Migration:risky' => true, 29 | '@PHP71Migration' => true, 30 | '@PHP71Migration:risky' => true, 31 | '@PHP73Migration' => true, 32 | 'list_syntax' => ['syntax' => 'short'], 33 | 'array_syntax' => ['syntax' => 'short'], 34 | 'compact_nullable_typehint' => true, 35 | 'logical_operators' => true, 36 | 'no_null_property_initialization' => true, 37 | 'no_php4_constructor' => true, 38 | 'no_superfluous_elseif' => true, 39 | 'no_useless_else' => true, 40 | 'no_useless_return' => true, 41 | 'combine_consecutive_issets' => true, 42 | 'blank_line_before_statement' => ['statements' => [ 43 | 'break', 44 | 'continue', 45 | 'return', 46 | 'throw', 47 | ]], 48 | 49 | 'header_comment' => [ 50 | 'header' => $header, 51 | 'comment_type' => 'comment', 52 | 'separate' => 'both', 53 | ], 54 | 'phpdoc_summary' => false, 55 | 'yoda_style' => false, 56 | 'declare_strict_types' => false, 57 | 'void_return' => false, 58 | 'phpdoc_align' => [], 59 | 'phpdoc_to_comment' => false, 60 | ]) 61 | ; 62 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | enable_authenticator_manager: true 3 | # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords 4 | password_hashers: 5 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' 6 | # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider 7 | providers: 8 | telegram: 9 | id: App\Login\Security\UserProvider 10 | firewalls: 11 | dev: 12 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 13 | security: false 14 | main: 15 | lazy: true 16 | provider: telegram 17 | custom_authenticators: 18 | - BoShurik\TelegramBotBundle\Authenticator\TelegramAuthenticator 19 | 20 | # activate different ways to authenticate 21 | # https://symfony.com/doc/current/security.html#the-firewall 22 | 23 | # https://symfony.com/doc/current/security/impersonating_user.html 24 | # switch_user: true 25 | 26 | # Easy way to control access for large sections of your site 27 | # Note: Only the *first* access control that matches will be used 28 | access_control: 29 | # - { path: ^/admin, roles: ROLE_ADMIN } 30 | # - { path: ^/profile, roles: ROLE_USER } 31 | 32 | when@test: 33 | security: 34 | password_hashers: 35 | # By default, password hashers are resource intensive and take time. This is 36 | # important to generate secure password hashes. In tests however, secure hashes 37 | # are not important, waste resources and increase test times. The following 38 | # reduces the work factor to the lowest possible values. 39 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 40 | algorithm: auto 41 | cost: 4 # Lowest possible value for bcrypt 42 | time_cost: 3 # Lowest possible value for argon 43 | memory_cost: 10 # Lowest possible value for argon 44 | -------------------------------------------------------------------------------- /src/Office/Telegram/Command/OfficesCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Office\Telegram\Command; 13 | 14 | use App\Office\Model\Office; 15 | use App\Office\Repository\OfficeRepository; 16 | use App\Telegram\Location\Command\AbstractLocationCommand; 17 | use App\Telegram\Location\LocationHandler; 18 | use BoShurik\TelegramBotBundle\Telegram\Command\PublicCommandInterface; 19 | use TelegramBot\Api\BotApi; 20 | use TelegramBot\Api\Types\Location; 21 | use TelegramBot\Api\Types\Update; 22 | 23 | class OfficesCommand extends AbstractLocationCommand implements PublicCommandInterface 24 | { 25 | public function __construct(LocationHandler $locationHandler, private OfficeRepository $officeRepository) 26 | { 27 | parent::__construct($locationHandler); 28 | } 29 | 30 | public function getName(): string 31 | { 32 | return '/offices'; 33 | } 34 | 35 | public function getDescription(): string 36 | { 37 | return 'Nearest offices'; 38 | } 39 | 40 | public function locationExecute(BotApi $api, Update $update): void 41 | { 42 | $location = $update->getMessage()->getLocation(); 43 | $offices = $this->getOffices($location); 44 | 45 | foreach ($offices as $office) { 46 | $reply = sprintf( 47 | "*%s*\n*Distance*: _%s_ м", 48 | $office->getName(), 49 | number_format($office->getDistance($location->getLatitude(), $location->getLongitude()), 2, ',', ' ') 50 | ); 51 | 52 | $api->sendMessage($update->getMessage()->getChat()->getId(), $reply, 'markdown'); 53 | $api->sendLocation($update->getMessage()->getChat()->getId(), $office->getLatitude(), $office->getLongitude()); 54 | } 55 | } 56 | 57 | /** 58 | * @return Office[] 59 | */ 60 | public function getOffices(Location $location): array 61 | { 62 | return $this->officeRepository->findNearest($location->getLatitude(), $location->getLongitude()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Telegram/Location/Command/LocationCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Telegram\Location\Command; 13 | 14 | use App\Telegram\Location\LocationHandler; 15 | use BoShurik\TelegramBotBundle\Telegram\Command\CommandInterface; 16 | use BoShurik\TelegramBotBundle\Telegram\Command\CommandRegistry; 17 | use TelegramBot\Api\BotApi; 18 | use TelegramBot\Api\Types\Location; 19 | use TelegramBot\Api\Types\Message; 20 | use TelegramBot\Api\Types\Update; 21 | 22 | class LocationCommand implements CommandInterface 23 | { 24 | public function __construct(private CommandRegistry $commandRegistry, private LocationHandler $locationHandler) 25 | { 26 | } 27 | 28 | public function execute(BotApi $api, Update $update): void 29 | { 30 | /** @var LocationCommandInterface $command */ 31 | $command = $this->getLocationCommand($update->getMessage()); 32 | $command->locationExecute($api, $update); 33 | $this->locationHandler->clearLocationCommand((string) $update->getMessage()->getChat()->getId()); 34 | } 35 | 36 | public function isApplicable(Update $update): bool 37 | { 38 | if (!$update->getMessage()) { 39 | return false; 40 | } 41 | if (!$update->getMessage()->getLocation() instanceof Location) { 42 | return false; 43 | } 44 | if (!$this->locationHandler->hasLocationCommand((string) $update->getMessage()->getChat()->getId())) { 45 | return false; 46 | } 47 | if (!$this->getLocationCommand($update->getMessage())) { 48 | return false; 49 | } 50 | 51 | return true; 52 | } 53 | 54 | private function getLocationCommand(Message $message): ?LocationCommandInterface 55 | { 56 | $id = $this->locationHandler->getLocationCommand((string) $message->getChat()->getId()); 57 | foreach ($this->commandRegistry->getCommands() as $command) { 58 | if (!$command instanceof LocationCommandInterface) { 59 | continue; 60 | } 61 | if ($command->getId() !== $id) { 62 | continue; 63 | } 64 | 65 | return $command; 66 | } 67 | 68 | return null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "license": "proprietary", 4 | "require": { 5 | "php": "^8.0", 6 | "ext-ctype": "*", 7 | "ext-iconv": "*", 8 | "boshurik/telegram-bot-bundle": "^5.0", 9 | "odolbeau/phone-number-bundle": "^3.6", 10 | "sensio/framework-extra-bundle": "^6.2", 11 | "symfony/console": "6.0.*", 12 | "symfony/dotenv": "6.0.*", 13 | "symfony/flex": "^2.0", 14 | "symfony/framework-bundle": "6.0.*", 15 | "symfony/mailer": "6.0.*", 16 | "symfony/messenger": "6.0.*", 17 | "symfony/runtime": "6.0.*", 18 | "symfony/security-bundle": "6.0.*", 19 | "symfony/twig-bundle": "6.0.*", 20 | "symfony/validator": "6.0.*", 21 | "symfony/yaml": "6.0.*" 22 | }, 23 | "require-dev": { 24 | "friendsofphp/php-cs-fixer": "^3.0", 25 | "vimeo/psalm": "^4.0", 26 | "psalm/plugin-symfony": "^3.0" 27 | }, 28 | "config": { 29 | "preferred-install": { 30 | "*": "dist" 31 | }, 32 | "sort-packages": true 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "App\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "App\\Tests\\": "tests/" 42 | } 43 | }, 44 | "replace": { 45 | "paragonie/random_compat": "2.*", 46 | "symfony/polyfill-ctype": "*", 47 | "symfony/polyfill-iconv": "*", 48 | "symfony/polyfill-php71": "*", 49 | "symfony/polyfill-php70": "*", 50 | "symfony/polyfill-php56": "*" 51 | }, 52 | "scripts": { 53 | "auto-scripts": { 54 | "cache:clear": "symfony-cmd", 55 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 56 | }, 57 | "post-install-cmd": [ 58 | "@auto-scripts" 59 | ], 60 | "post-update-cmd": [ 61 | "@auto-scripts" 62 | ], 63 | "cs-check": "vendor/bin/php-cs-fixer fix --allow-risky=yes --diff --ansi --dry-run", 64 | "cs-fix": "vendor/bin/php-cs-fixer fix --allow-risky=yes --diff --ansi", 65 | "psalm": "vendor/bin/psalm", 66 | "checks": [ 67 | "@cs-check", 68 | "@psalm" 69 | ] 70 | }, 71 | "conflict": { 72 | "symfony/symfony": "*" 73 | }, 74 | "extra": { 75 | "symfony": { 76 | "allow-contrib": true, 77 | "require": "6.0.*" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Order/Telegram/OrderHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Order\Telegram; 13 | 14 | use App\Order\Model\Order; 15 | use Psr\Cache\CacheItemPoolInterface; 16 | 17 | class OrderHandler 18 | { 19 | public const PREFIX_ORDER = 'order_'; 20 | public const PREFIX_STEP = 'step_'; 21 | 22 | public function __construct(private CacheItemPoolInterface $cache, private int $lifetime = 0) 23 | { 24 | } 25 | 26 | public function hasData(string $id): bool 27 | { 28 | $key = $this->getKey(self::PREFIX_STEP, $id); 29 | 30 | return $this->cache->hasItem($key); 31 | } 32 | 33 | public function clearData(string $id): void 34 | { 35 | $stepKey = $this->getKey(self::PREFIX_STEP, $id); 36 | $orderKey = $this->getKey(self::PREFIX_ORDER, $id); 37 | 38 | $this->cache->deleteItems([$stepKey, $orderKey]); 39 | } 40 | 41 | public function getCurrentStep(string $id): int 42 | { 43 | $key = $this->getKey(self::PREFIX_STEP, $id); 44 | if (!$this->cache->hasItem($key)) { 45 | return 0; 46 | } 47 | 48 | $item = $this->cache->getItem($key); 49 | 50 | return (int) $item->get(); 51 | } 52 | 53 | public function setCurrentStep(string $id, int $step): void 54 | { 55 | $key = $this->getKey(self::PREFIX_STEP, $id); 56 | 57 | $item = $this->cache->getItem($key); 58 | $item->set($step); 59 | if ($this->lifetime > 0) { 60 | $item->expiresAfter($this->lifetime); 61 | } 62 | 63 | $this->cache->save($item); 64 | } 65 | 66 | public function getOrder(string $id): Order 67 | { 68 | $key = $this->getKey(self::PREFIX_ORDER, $id); 69 | if (!$this->cache->hasItem($key)) { 70 | return $this->createOrder(); 71 | } 72 | 73 | $item = $this->cache->getItem($key); 74 | 75 | return $item->get(); 76 | } 77 | 78 | public function setOrder(string $id, Order $order): void 79 | { 80 | $key = $this->getKey(self::PREFIX_ORDER, $id); 81 | 82 | $item = $this->cache->getItem($key); 83 | $item->set($order); 84 | if ($this->lifetime > 0) { 85 | $item->expiresAfter($this->lifetime); 86 | } 87 | 88 | $this->cache->save($item); 89 | } 90 | 91 | public function createOrder(): Order 92 | { 93 | return new Order(); 94 | } 95 | 96 | private function getKey(string $prefix, string $id): string 97 | { 98 | return sprintf('%s%s', $prefix, $id); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Post/Repository/PostRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Post\Repository; 13 | 14 | use App\Post\Model\Post; 15 | 16 | class PostRepository 17 | { 18 | /** 19 | * @return Post[] 20 | */ 21 | public function findAll(): array 22 | { 23 | return [ 24 | new Post('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nibh nulla, rhoncus sed est nec, viverra rhoncus massa. Maecenas eros velit, mollis quis mi quis, finibus mollis metus. Curabitur et blandit ante, at aliquet ipsum. In metus elit, ullamcorper in consequat ac, facilisis at leo. Vestibulum vel justo at est commodo semper auctor eget lorem. Donec rutrum ante ut libero dignissim, eu ullamcorper eros dignissim. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec aliquet nibh justo. Vivamus semper pharetra pellentesque. In eget bibendum magna. Nunc at tellus non diam gravida varius ut pellentesque nunc. Nam id sem a nunc ultrices placerat vitae sit amet mi. Curabitur accumsan eros ac porttitor ullamcorper. '), 25 | new Post('Aenean sagittis placerat odio, vitae dictum augue accumsan at. Suspendisse sed rhoncus turpis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin quis euismod libero. Nullam placerat, augue sed consequat lacinia, orci arcu tristique urna, quis tincidunt turpis purus ut ex. Maecenas elementum justo eget augue finibus lobortis. Quisque volutpat convallis tellus sed gravida. Aenean venenatis a tortor vitae volutpat. Proin mollis pharetra dui non vulputate. In quis mauris ac sapien egestas interdum. Morbi sit amet ultricies turpis, sit amet volutpat dolor. Ut dapibus lacus eu ex scelerisque egestas. Etiam ultricies maximus elit, non finibus leo molestie sit amet. Nullam ac quam finibus, hendrerit massa sit amet, semper neque. Nunc condimentum posuere pellentesque. Sed faucibus nisi pharetra, vulputate leo vel, fringilla ligula. '), 26 | new Post('Fusce faucibus, elit at laoreet condimentum, quam diam congue neque, a suscipit enim mi vel enim. Morbi condimentum leo nec tincidunt rutrum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vestibulum volutpat, velit sit amet condimentum blandit, sem tortor posuere arcu, at maximus dolor arcu nec velit. Aliquam porttitor fringilla ipsum in hendrerit. Cras laoreet tellus et odio egestas, et dictum ipsum placerat. Donec vel lorem nec nibh porta aliquam nec eu purus. '), 27 | new Post('Nunc ornare pellentesque diam, sed interdum ex lobortis eu. Duis blandit nisi non tellus viverra volutpat. Sed erat arcu, ultricies quis eleifend non, finibus nec nisi. Duis nibh nisi, sagittis ut urna eget, tempor bibendum mi. Vestibulum tempus augue dignissim tortor ultricies volutpat. Sed massa dolor, posuere aliquam gravida eget, volutpat eget massa. Sed pretium rhoncus nibh nec tincidunt. Sed nec ante nibh. Vestibulum vestibulum, mi eget vehicula molestie, libero lacus cursus nisl, id luctus ligula ante in mauris. Ut aliquet neque nibh, sed fringilla enim ullamcorper ut. Aliquam lacinia risus ac erat rutrum condimentum. '), 28 | new Post('Vivamus imperdiet felis viverra, tristique dui sed, pulvinar nibh. Nam euismod venenatis tempor. Vivamus a augue bibendum erat accumsan condimentum. Nam nec ante risus. Donec rhoncus libero non placerat facilisis. Etiam pharetra porta dui ut faucibus. Ut rutrum elit arcu, quis sagittis ipsum luctus et. '), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Post/Telegram/Command/PostCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Post\Telegram\Command; 13 | 14 | use App\Post\Model\Post; 15 | use App\Post\Repository\PostRepository; 16 | use BoShurik\TelegramBotBundle\Telegram\Command\AbstractCommand; 17 | use BoShurik\TelegramBotBundle\Telegram\Command\PublicCommandInterface; 18 | use TelegramBot\Api\BotApi; 19 | use TelegramBot\Api\Types\Inline\InlineKeyboardMarkup; 20 | use TelegramBot\Api\Types\Update; 21 | 22 | class PostCommand extends AbstractCommand implements PublicCommandInterface 23 | { 24 | private const REGEX_INDEX = '#/post_(\d+)#'; 25 | 26 | public function __construct(private PostRepository $repository) 27 | { 28 | } 29 | 30 | public function getName(): string 31 | { 32 | return '/post'; 33 | } 34 | 35 | public function getDescription(): string 36 | { 37 | return 'Post list'; 38 | } 39 | 40 | public function execute(BotApi $api, Update $update): void 41 | { 42 | $posts = $this->repository->findAll(); 43 | $index = (int) $this->getIndex($update); 44 | $index = isset($posts[$index]) ? $index : 0; 45 | 46 | $messageId = $chatId = null; 47 | if ($update->getCallbackQuery()) { 48 | $chat = $update->getCallbackQuery()->getMessage()->getChat(); 49 | $messageId = $update->getCallbackQuery()->getMessage()->getMessageId(); 50 | } else { 51 | $chat = $update->getMessage()->getChat(); 52 | } 53 | 54 | $this->post($api, $posts[$index], $index, (string) $chat->getId(), $messageId); 55 | } 56 | 57 | public function isApplicable(Update $update): bool 58 | { 59 | if (parent::isApplicable($update)) { 60 | return true; 61 | } 62 | 63 | return $this->getIndex($update) !== null; 64 | } 65 | 66 | private function getIndex(Update $update): ?int 67 | { 68 | if ($update->getMessage() && preg_match(self::REGEX_INDEX, $update->getMessage()->getText(), $matches)) { 69 | return (int) $matches[1]; 70 | } 71 | if ($update->getCallbackQuery() && preg_match(self::REGEX_INDEX, $update->getCallbackQuery()->getData(), $matches)) { 72 | return (int) $matches[1]; 73 | } 74 | 75 | return null; 76 | } 77 | 78 | private function post(BotApi $api, Post $post, int $index, string $chatId, int $messageId = null): void 79 | { 80 | $prev = $next = null; 81 | if ($index - 1 >= 0) { 82 | $prev = $index - 1; 83 | } 84 | if ($index + 1 < 5) { 85 | $next = $index + 1; 86 | } 87 | 88 | $buttons = []; 89 | if ($prev !== null) { 90 | $buttons[] = ['text' => 'Prev', 'callback_data' => '/post_'.$prev]; 91 | } 92 | if ($next !== null) { 93 | $buttons[] = ['text' => 'Next', 'callback_data' => '/post_'.$next]; 94 | } 95 | 96 | $text = sprintf("%d *%s*\n%s", $index, $post->getName(), $post->getDescription()); 97 | 98 | if ($messageId) { 99 | $api->editMessageText( 100 | $chatId, 101 | $messageId, 102 | $text, 103 | 'markdown', 104 | false, 105 | new InlineKeyboardMarkup([$buttons]) 106 | ); 107 | } else { 108 | $api->sendMessage( 109 | $chatId, 110 | $text, 111 | 'markdown', 112 | false, 113 | null, 114 | new InlineKeyboardMarkup([$buttons]) 115 | ); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Order/Telegram/Command/OrderCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace App\Order\Telegram\Command; 13 | 14 | use App\Order\Event\OrderEvent; 15 | use App\Order\Model\Order; 16 | use App\Order\Telegram\OrderHandler; 17 | use BoShurik\TelegramBotBundle\Telegram\Command\AbstractCommand; 18 | use BoShurik\TelegramBotBundle\Telegram\Command\PublicCommandInterface; 19 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 20 | use Symfony\Component\Validator\ConstraintViolationInterface; 21 | use Symfony\Component\Validator\ConstraintViolationListInterface; 22 | use Symfony\Component\Validator\Validator\ValidatorInterface; 23 | use TelegramBot\Api\BotApi; 24 | use TelegramBot\Api\Types\Message; 25 | use TelegramBot\Api\Types\Update; 26 | 27 | class OrderCommand extends AbstractCommand implements PublicCommandInterface 28 | { 29 | public function __construct( 30 | private OrderHandler $orderHandler, 31 | private ValidatorInterface $validator, 32 | private EventDispatcherInterface $eventDispatcher 33 | ) { 34 | } 35 | 36 | public function getName(): string 37 | { 38 | return '/order'; 39 | } 40 | 41 | public function getDescription(): string 42 | { 43 | return 'Send order'; 44 | } 45 | 46 | public function execute(BotApi $api, Update $update): void 47 | { 48 | $id = (string) $update->getMessage()->getChat()->getId(); 49 | 50 | if ($this->isCancelStep($update)) { 51 | $this->cancelStep($api, $update->getMessage(), $id); 52 | 53 | return; 54 | } 55 | 56 | if (parent::isApplicable($update)) { 57 | $step = 0; 58 | $order = $this->orderHandler->createOrder(); 59 | } else { 60 | $step = $this->orderHandler->getCurrentStep($id); 61 | $order = $this->orderHandler->getOrder($id); 62 | } 63 | 64 | $method = sprintf('step%d', $step); 65 | $nextMethod = sprintf('step%d', $step + 1); 66 | 67 | $result = $this->$method($api, $update->getMessage(), $id, $order); 68 | if (!$result) { 69 | return; 70 | } 71 | 72 | if (method_exists($this, $nextMethod)) { 73 | $this->orderHandler->setOrder($id, $order); 74 | $this->orderHandler->setCurrentStep($id, $step + 1); 75 | } else { 76 | $this->finalStep($api, $update->getMessage(), $id, $order); 77 | $this->orderHandler->clearData($id); 78 | } 79 | } 80 | 81 | public function isApplicable(Update $update): bool 82 | { 83 | if (parent::isApplicable($update)) { 84 | return true; 85 | } 86 | if (!$update->getMessage()) { 87 | return false; 88 | } 89 | 90 | return $this->orderHandler->hasData((string) $update->getMessage()->getChat()->getId()); 91 | } 92 | 93 | protected function step0(BotApi $api, Message $message, string $chatId, Order $order): bool 94 | { 95 | $api->sendMessage($chatId, 'To cancel type "/order cancel"'); 96 | $api->sendMessage($chatId, 'Enter your name'); 97 | 98 | return true; 99 | } 100 | 101 | protected function step1(BotApi $api, Message $message, string $chatId, Order $order): bool 102 | { 103 | $order->setName($message->getText()); 104 | 105 | $violations = $this->validateOrder($order, __FUNCTION__); 106 | if ($violations->count() > 0) { 107 | $this->sendErrorMessage($chatId, $api, $violations); 108 | 109 | return false; 110 | } 111 | 112 | $api->sendMessage($chatId, 'Enter your phone'); 113 | 114 | return true; 115 | } 116 | 117 | protected function step2(BotApi $api, Message $message, string $chatId, Order $order): bool 118 | { 119 | $order->setPhone($message->getText()); 120 | 121 | $violations = $this->validateOrder($order, __FUNCTION__); 122 | if ($violations->count() > 0) { 123 | $this->sendErrorMessage($chatId, $api, $violations); 124 | 125 | return false; 126 | } 127 | 128 | $api->sendMessage($chatId, 'Enter your email'); 129 | 130 | return true; 131 | } 132 | 133 | protected function step3(BotApi $api, Message $message, string $chatId, Order $order): bool 134 | { 135 | $order->setEmail($message->getText()); 136 | 137 | $violations = $this->validateOrder($order, __FUNCTION__); 138 | if ($violations->count() > 0) { 139 | $this->sendErrorMessage($chatId, $api, $violations); 140 | 141 | return false; 142 | } 143 | 144 | $api->sendMessage($chatId, 'Enter message'); 145 | 146 | return true; 147 | } 148 | 149 | protected function step4(BotApi $api, Message $message, string $chatId, Order $order): bool 150 | { 151 | $order->setMessage($message->getText()); 152 | 153 | $violations = $this->validateOrder($order, __FUNCTION__); 154 | if ($violations->count() > 0) { 155 | $this->sendErrorMessage($chatId, $api, $violations); 156 | 157 | return false; 158 | } 159 | 160 | return true; 161 | } 162 | 163 | protected function finalStep(BotApi $api, Message $message, string $chatId, Order $order): void 164 | { 165 | $this->eventDispatcher->dispatch(new OrderEvent($order)); 166 | $api->sendMessage($chatId, 'Thank you!'); 167 | } 168 | 169 | protected function cancelStep(BotApi $api, Message $message, string $chatId): void 170 | { 171 | $this->orderHandler->clearData($chatId); 172 | } 173 | 174 | protected function validateOrder(Order $order, string $group): ConstraintViolationListInterface 175 | { 176 | return $this->validator->validate($order, null, [$group]); 177 | } 178 | 179 | protected function sendErrorMessage(string $chatId, BotApi $api, ConstraintViolationListInterface $violations): void 180 | { 181 | $messages = []; 182 | /** @var ConstraintViolationInterface $violation */ 183 | foreach ($violations as $violation) { 184 | $messages[] = sprintf('%s - %s', $violation->getInvalidValue(), (string) $violation->getMessage()); 185 | } 186 | $api->sendMessage($chatId, implode("\n", $messages)); 187 | } 188 | 189 | protected function isCancelStep(Update $update): bool 190 | { 191 | if (!parent::isApplicable($update)) { 192 | return false; 193 | } 194 | 195 | preg_match(self::REGEXP, $update->getMessage()->getText(), $matches); 196 | 197 | return 'cancel' == mb_strtolower($matches[3]); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "amphp/amp": { 3 | "version": "v2.4.4" 4 | }, 5 | "amphp/byte-stream": { 6 | "version": "v1.7.3" 7 | }, 8 | "boshurik/telegram-bot-bundle": { 9 | "version": "4.1", 10 | "recipe": { 11 | "repo": "github.com/symfony/recipes-contrib", 12 | "branch": "master", 13 | "version": "3.1", 14 | "ref": "0e172664f2f361f8f3932f32cea845f56650dd01" 15 | }, 16 | "files": [ 17 | "config/packages/boshurik_telegram_bot.yaml", 18 | "config/routes/boshurik_telegram_bot.yaml" 19 | ] 20 | }, 21 | "composer/pcre": { 22 | "version": "1.0.0" 23 | }, 24 | "composer/semver": { 25 | "version": "1.5.1" 26 | }, 27 | "composer/xdebug-handler": { 28 | "version": "1.4.2" 29 | }, 30 | "dnoegel/php-xdg-base-dir": { 31 | "version": "v0.1.1" 32 | }, 33 | "doctrine/annotations": { 34 | "version": "1.0", 35 | "recipe": { 36 | "repo": "github.com/symfony/recipes", 37 | "branch": "master", 38 | "version": "1.0", 39 | "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" 40 | }, 41 | "files": [ 42 | "config/routes/annotations.yaml" 43 | ] 44 | }, 45 | "doctrine/cache": { 46 | "version": "v1.8.0" 47 | }, 48 | "doctrine/collections": { 49 | "version": "v1.6.1" 50 | }, 51 | "doctrine/event-manager": { 52 | "version": "v1.0.0" 53 | }, 54 | "doctrine/lexer": { 55 | "version": "1.2.1" 56 | }, 57 | "doctrine/persistence": { 58 | "version": "v1.1.0" 59 | }, 60 | "doctrine/reflection": { 61 | "version": "v1.0.0" 62 | }, 63 | "egulias/email-validator": { 64 | "version": "2.1.18" 65 | }, 66 | "felixfbecker/advanced-json-rpc": { 67 | "version": "v3.1.1" 68 | }, 69 | "felixfbecker/language-server-protocol": { 70 | "version": "v1.4.0" 71 | }, 72 | "friendsofphp/php-cs-fixer": { 73 | "version": "3.5", 74 | "recipe": { 75 | "repo": "github.com/symfony/recipes", 76 | "branch": "master", 77 | "version": "3.0", 78 | "ref": "be2103eb4a20942e28a6dd87736669b757132435" 79 | }, 80 | "files": [ 81 | ".php-cs-fixer.dist.php" 82 | ] 83 | }, 84 | "giggsey/libphonenumber-for-php": { 85 | "version": "8.12.41" 86 | }, 87 | "giggsey/locale": { 88 | "version": "2.1" 89 | }, 90 | "netresearch/jsonmapper": { 91 | "version": "v2.1.0" 92 | }, 93 | "nikic/php-parser": { 94 | "version": "v4.5.0" 95 | }, 96 | "ocramius/package-versions": { 97 | "version": "1.4.2" 98 | }, 99 | "odolbeau/phone-number-bundle": { 100 | "version": "3.6", 101 | "recipe": { 102 | "repo": "github.com/symfony/recipes-contrib", 103 | "branch": "master", 104 | "version": "3.0", 105 | "ref": "4388686329b81291918a948cd42891829fb1de71" 106 | }, 107 | "files": [ 108 | "config/packages/misd_phone_number.yaml" 109 | ] 110 | }, 111 | "openlss/lib-array2xml": { 112 | "version": "1.0.0" 113 | }, 114 | "php": { 115 | "version": "7.2" 116 | }, 117 | "php-cs-fixer/diff": { 118 | "version": "v1.3.0" 119 | }, 120 | "phpdocumentor/reflection-common": { 121 | "version": "2.1.0" 122 | }, 123 | "phpdocumentor/reflection-docblock": { 124 | "version": "5.1.0" 125 | }, 126 | "phpdocumentor/type-resolver": { 127 | "version": "1.1.0" 128 | }, 129 | "psalm/plugin-symfony": { 130 | "version": "v1.3.0" 131 | }, 132 | "psr/cache": { 133 | "version": "1.0.1" 134 | }, 135 | "psr/container": { 136 | "version": "1.0.0" 137 | }, 138 | "psr/event-dispatcher": { 139 | "version": "1.0.0" 140 | }, 141 | "psr/log": { 142 | "version": "1.1.3" 143 | }, 144 | "psr/simple-cache": { 145 | "version": "1.0.1" 146 | }, 147 | "sebastian/diff": { 148 | "version": "3.0.2" 149 | }, 150 | "sensio/framework-extra-bundle": { 151 | "version": "5.2", 152 | "recipe": { 153 | "repo": "github.com/symfony/recipes", 154 | "branch": "master", 155 | "version": "5.2", 156 | "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b" 157 | }, 158 | "files": [ 159 | "config/packages/sensio_framework_extra.yaml" 160 | ] 161 | }, 162 | "symfony/amqp-messenger": { 163 | "version": "v5.1.2" 164 | }, 165 | "symfony/cache": { 166 | "version": "v5.1.2" 167 | }, 168 | "symfony/cache-contracts": { 169 | "version": "v2.1.2" 170 | }, 171 | "symfony/config": { 172 | "version": "v5.1.2" 173 | }, 174 | "symfony/console": { 175 | "version": "6.0", 176 | "recipe": { 177 | "repo": "github.com/symfony/recipes", 178 | "branch": "master", 179 | "version": "5.3", 180 | "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047" 181 | }, 182 | "files": [ 183 | "bin/console" 184 | ] 185 | }, 186 | "symfony/contracts": { 187 | "version": "v1.0.2" 188 | }, 189 | "symfony/debug": { 190 | "version": "v4.2.5" 191 | }, 192 | "symfony/dependency-injection": { 193 | "version": "v5.1.2" 194 | }, 195 | "symfony/deprecation-contracts": { 196 | "version": "v2.1.2" 197 | }, 198 | "symfony/doctrine-messenger": { 199 | "version": "v5.1.2" 200 | }, 201 | "symfony/dotenv": { 202 | "version": "v5.1.2" 203 | }, 204 | "symfony/error-handler": { 205 | "version": "v5.1.2" 206 | }, 207 | "symfony/event-dispatcher": { 208 | "version": "v5.1.2" 209 | }, 210 | "symfony/event-dispatcher-contracts": { 211 | "version": "v2.1.2" 212 | }, 213 | "symfony/filesystem": { 214 | "version": "v5.1.2" 215 | }, 216 | "symfony/finder": { 217 | "version": "v5.1.2" 218 | }, 219 | "symfony/flex": { 220 | "version": "1.0", 221 | "recipe": { 222 | "repo": "github.com/symfony/recipes", 223 | "branch": "master", 224 | "version": "1.0", 225 | "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" 226 | }, 227 | "files": [ 228 | ".env" 229 | ] 230 | }, 231 | "symfony/framework-bundle": { 232 | "version": "6.0", 233 | "recipe": { 234 | "repo": "github.com/symfony/recipes", 235 | "branch": "master", 236 | "version": "5.4", 237 | "ref": "d4131812e20853626928e73d3effef44014944c0" 238 | }, 239 | "files": [ 240 | "config/packages/cache.yaml", 241 | "config/packages/framework.yaml", 242 | "config/preload.php", 243 | "config/routes/framework.yaml", 244 | "config/services.yaml", 245 | "public/index.php", 246 | "src/Controller/.gitignore", 247 | "src/Kernel.php" 248 | ] 249 | }, 250 | "symfony/http-client-contracts": { 251 | "version": "v2.3.1" 252 | }, 253 | "symfony/http-foundation": { 254 | "version": "v5.1.2" 255 | }, 256 | "symfony/http-kernel": { 257 | "version": "v5.1.2" 258 | }, 259 | "symfony/intl": { 260 | "version": "v6.0.1" 261 | }, 262 | "symfony/mailer": { 263 | "version": "6.0", 264 | "recipe": { 265 | "repo": "github.com/symfony/recipes", 266 | "branch": "master", 267 | "version": "4.3", 268 | "ref": "bbfc7e27257d3a3f12a6fb0a42540a42d9623a37" 269 | }, 270 | "files": [ 271 | "config/packages/mailer.yaml" 272 | ] 273 | }, 274 | "symfony/messenger": { 275 | "version": "6.0", 276 | "recipe": { 277 | "repo": "github.com/symfony/recipes", 278 | "branch": "master", 279 | "version": "4.3", 280 | "ref": "25e3c964d3aee480b3acc3114ffb7940c89edfed" 281 | }, 282 | "files": [ 283 | "config/packages/messenger.yaml" 284 | ] 285 | }, 286 | "symfony/mime": { 287 | "version": "v5.1.2" 288 | }, 289 | "symfony/options-resolver": { 290 | "version": "v5.1.2" 291 | }, 292 | "symfony/password-hasher": { 293 | "version": "v6.0.2" 294 | }, 295 | "symfony/polyfill-intl-grapheme": { 296 | "version": "v1.17.0" 297 | }, 298 | "symfony/polyfill-intl-idn": { 299 | "version": "v1.17.0" 300 | }, 301 | "symfony/polyfill-intl-normalizer": { 302 | "version": "v1.17.0" 303 | }, 304 | "symfony/polyfill-mbstring": { 305 | "version": "v1.17.0" 306 | }, 307 | "symfony/polyfill-php72": { 308 | "version": "v1.17.0" 309 | }, 310 | "symfony/polyfill-php73": { 311 | "version": "v1.17.0" 312 | }, 313 | "symfony/polyfill-php80": { 314 | "version": "v1.17.0" 315 | }, 316 | "symfony/polyfill-php81": { 317 | "version": "v1.24.0" 318 | }, 319 | "symfony/process": { 320 | "version": "v5.1.2" 321 | }, 322 | "symfony/property-access": { 323 | "version": "v5.1.2" 324 | }, 325 | "symfony/property-info": { 326 | "version": "v5.1.2" 327 | }, 328 | "symfony/redis-messenger": { 329 | "version": "v5.1.2" 330 | }, 331 | "symfony/routing": { 332 | "version": "6.0", 333 | "recipe": { 334 | "repo": "github.com/symfony/recipes", 335 | "branch": "master", 336 | "version": "6.0", 337 | "ref": "ab9ad892b7bba7ac584f6dc2ccdb659d358c63c5" 338 | }, 339 | "files": [ 340 | "config/packages/routing.yaml", 341 | "config/routes.yaml" 342 | ] 343 | }, 344 | "symfony/runtime": { 345 | "version": "v6.0.0" 346 | }, 347 | "symfony/security-bundle": { 348 | "version": "6.0", 349 | "recipe": { 350 | "repo": "github.com/symfony/recipes", 351 | "branch": "master", 352 | "version": "5.3", 353 | "ref": "09b5e809bd7a992061febd05b797c64a2d93b5cd" 354 | }, 355 | "files": [ 356 | "config/packages/security.yaml" 357 | ] 358 | }, 359 | "symfony/security-core": { 360 | "version": "v5.1.2" 361 | }, 362 | "symfony/security-csrf": { 363 | "version": "v5.1.2" 364 | }, 365 | "symfony/security-guard": { 366 | "version": "v5.1.2" 367 | }, 368 | "symfony/security-http": { 369 | "version": "v5.1.2" 370 | }, 371 | "symfony/service-contracts": { 372 | "version": "v2.1.2" 373 | }, 374 | "symfony/stopwatch": { 375 | "version": "v5.1.2" 376 | }, 377 | "symfony/string": { 378 | "version": "v5.1.2" 379 | }, 380 | "symfony/translation-contracts": { 381 | "version": "v2.1.2" 382 | }, 383 | "symfony/twig-bridge": { 384 | "version": "v5.1.2" 385 | }, 386 | "symfony/twig-bundle": { 387 | "version": "6.0", 388 | "recipe": { 389 | "repo": "github.com/symfony/recipes", 390 | "branch": "master", 391 | "version": "5.4", 392 | "ref": "bffbb8f1a849736e64006735afae730cb428b6ff" 393 | }, 394 | "files": [ 395 | "config/packages/twig.yaml", 396 | "templates/base.html.twig" 397 | ] 398 | }, 399 | "symfony/validator": { 400 | "version": "6.0", 401 | "recipe": { 402 | "repo": "github.com/symfony/recipes", 403 | "branch": "master", 404 | "version": "4.3", 405 | "ref": "3eb8df139ec05414489d55b97603c5f6ca0c44cb" 406 | }, 407 | "files": [ 408 | "config/packages/test/validator.yaml", 409 | "config/packages/validator.yaml" 410 | ] 411 | }, 412 | "symfony/var-dumper": { 413 | "version": "v5.1.2" 414 | }, 415 | "symfony/var-exporter": { 416 | "version": "v5.1.2" 417 | }, 418 | "symfony/yaml": { 419 | "version": "v5.1.2" 420 | }, 421 | "telegram-bot/api": { 422 | "version": "2.3.15" 423 | }, 424 | "twig/twig": { 425 | "version": "v3.0.3" 426 | }, 427 | "vimeo/psalm": { 428 | "version": "3.11.6" 429 | }, 430 | "webmozart/assert": { 431 | "version": "1.9.0" 432 | }, 433 | "webmozart/glob": { 434 | "version": "4.1.0" 435 | }, 436 | "webmozart/path-util": { 437 | "version": "2.3.0" 438 | } 439 | } 440 | --------------------------------------------------------------------------------