├── 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 |
9 | - Name: {{ order.name }}
10 | - Phone: {{ order.phone }}
11 | - Email: {{ order.email }}
12 | - Message: {{ order.message | nl2br }}
13 |
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 |
--------------------------------------------------------------------------------