├── CHANGELOG.md ├── LICENSE ├── LOGIN_WITH_TELEGRAM.md ├── README.md ├── composer.json └── src ├── Authenticator ├── TelegramAuthenticator.php ├── TelegramLoginValidator.php ├── UserFactoryInterface.php └── UserLoaderInterface.php ├── BoShurikTelegramBotBundle.php ├── Command ├── UpdatesCommand.php └── Webhook │ ├── InfoCommand.php │ ├── SetCommand.php │ └── UnsetCommand.php ├── Controller └── WebhookController.php ├── DependencyInjection ├── BoShurikTelegramBotExtension.php ├── Compiler │ └── CommandCompilerPass.php └── Configuration.php ├── Event ├── UpdateEvent.php └── WebhookEvent.php ├── EventListener └── CommandListener.php ├── Exception └── AuthenticationException.php ├── Messenger ├── MessageHandler.php └── TelegramMessage.php ├── Resources └── config │ ├── authenticator.php │ ├── routing.php │ └── services.php └── Telegram ├── BotLocator.php ├── Command ├── AbstractCommand.php ├── CommandInterface.php ├── HelpCommand.php ├── PublicCommandInterface.php └── Registry │ ├── CommandRegistry.php │ └── CommandRegistryLocator.php └── Telegram.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 6.0.0 (2024-09-27) 5 | ------------------ 6 | 7 | * Allow multiple bots 8 | * Improve the webhook:set command so that it accepts the hostname. The webhook URL will be generated automatically. If url or hostname is not passed then command tries to generate url based on [request context](https://symfony.com/doc/current/routing.html#generating-urls-in-commands) 9 | * Move config from yaml to php files 10 | * Allow to set update type for webhook command 11 | * Allow to set timeout for api instances 12 | * Use symfony/http-client if available 13 | * Add Symfony 7 support 14 | 15 | 5.0.0 (2022-01-21) 16 | ------------------ 17 | 18 | * Drop php < 8 19 | * Add Symfony 6 support 20 | * Drop Symfony < 5.4 21 | * Remove deprecated `tracker_token` parameter 22 | * Rename `guard` parameter to `authenticator` 23 | * Update authenticator to use new Symfony security system 24 | 25 | 4.2.0 (2021-09-07) 26 | ------------------ 27 | 28 | * Add support of php8 29 | * Add callback query support for `AbstractCommand` 30 | * Add `AbstractCommand::getCommandParameters()` method 31 | 32 | 4.1.0 (2020-06-20) 33 | ------------------ 34 | 35 | * Add "Login with Telegram" feature (@bigfoot90) 36 | * Add `telegram:webhook:info` command 37 | * Add `symfony/messenger` support 38 | 39 | 4.0.0 (2019-11-21) 40 | ------------------ 41 | 42 | * Drop support of `symfony/symfony` < 4.4 43 | * Move `\BoShurik\TelegramBotBundle\Event\Telegram\UpdateEvent` to `\BoShurik\TelegramBotBundle\Event\UpdateEvent` 44 | * Move `\BoShurik\TelegramBotBundle\Event\TelegramEvents` to `\BoShurik\TelegramBotBundle\Event\WebhookEvent` 45 | * Removed `\BoShurik\TelegramBotBundle\Event\TelegramEvents` 46 | 47 | 3.1.0 (2019-10-16) 48 | ------------------ 49 | 50 | * Drop support of php7.0 51 | * Drop support of php7.1 52 | * Add support of php7.4 53 | * Deprecate `boshurik_telegram_bot.name` as it is not used in the bundle. Inject bot name in your commands if needed. 54 | 55 | 3.0.0 (2019-04-09) 56 | ------------------ 57 | 58 | * Command system now works with `Update` object instead of `Message` (#14) 59 | * Drop support of php5 60 | * Drop support of `symfony/symfony` < 3.4 61 | * Change bundle alias from `bo_shurik_telegram_bot` to `boshurik_telegram_bot` 62 | * Split `bin/console telegram:webhook` command to `bin/console telegram:webhook:set` 63 | and `bin/console telegram:webhook:unset` 64 | * Support autoconfigure for `BoShurik\TelegramBotBundle\Telegram\Command\CommandInterface` interface 65 | * Remove `boshurik_telegram_bot.api` service alias. Use `TelegramBot\Api\BotApi` instead 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LOGIN_WITH_TELEGRAM.md: -------------------------------------------------------------------------------- 1 | # Login with Telegram 2 | 3 | 4 | ## Setup your bot 5 | 6 | First you have to link your domain to the bot, 7 | send the `/setdomain` command to **@Botfather**. 8 | 9 | 10 | ## Configure bundle 11 | 12 | Next enable the Telegram Authenticator: 13 | **config/packages/boshurik_telegram_bot.yaml** 14 | ```yaml 15 | boshurik_telegram_bot: 16 | # ... 17 | authenticator: 18 | default_target_route: user_profile # redirect after login success 19 | guard_route: _telegram_login # guard route 20 | login_route: your_login_route # optional, if login fails user will be redirected there 21 | ``` 22 | 23 | ## Generate login widget 24 | 25 | In the end you can place a login widget in your login page: 26 | - Open https://core.telegram.org/widgets/login#widget-configuration 27 | - In `Authorization Type` choose `Redirect to URL` option and insert your callback url matching `guard_route` configured before. 28 | *(**Help**: to discover your guard callback url run `bin/console debug:router _telegram_login`)* 29 | - Copy&Paste the generated snippet code into your `login.html.twig` template. 30 | 31 | 32 | ## Configure Symfony's firewall 33 | 34 | **config/packages/security.yaml** 35 | ```yaml 36 | security: 37 | firewalls: 38 | telegram_bot: 39 | pattern: ^/_telegram/$ 40 | security: false 41 | main: 42 | custom_authenticators: 43 | - BoShurik\TelegramBotBundle\Authenticator\TelegramAuthenticator 44 | # ... 45 | ``` 46 | 47 | If you are using both classic (username and password form) and Telegram login widget, 48 | you need to add both authenticators to the guard 49 | ```yaml 50 | security: 51 | firewalls: 52 | # ... 53 | main: 54 | custom_authenticators: 55 | - App\Security\LoginFormAuthenticator 56 | - BoShurik\TelegramBotBundle\Authenticator\TelegramAuthenticator 57 | entry_point: App\Security\LoginFormAuthenticator 58 | # ... 59 | ``` 60 | 61 | ## Implement UserProvider 62 | 63 | Authenticator should return an `Symfony\Component\Security\Core\User\UserInterface` instance 64 | 65 | **UserProvider.php** 66 | 67 | ```php 68 | entityManager = $entityManager; 85 | } 86 | 87 | public function loadByTelegramId(string $id): ?UserInterface 88 | { 89 | return $this->entityManager->getRepository(User::class)->findOneBy(['telegram.id' => $id]); 90 | } 91 | 92 | public function createFromTelegram(array $data): UserInterface 93 | { 94 | $user = new User( 95 | $data['id'], 96 | $data['first_name'].' '.$data['last_name'], 97 | $data['username'] ?? null, 98 | $data['photo_url'] ?? null 99 | ); 100 | 101 | $this->entityManager->persist($user); 102 | $this->entityManager->flush(); 103 | 104 | return $user; 105 | } 106 | } 107 | ``` 108 | 109 | **Note:** Implementing `UserFactoryInterface` is optional, 110 | it is required if you want to allow to login all your bot users, 111 | else the login will be limited to the only already stored in your database. 112 | 113 | 114 | ## Autowiring 115 | 116 | **config/services.yaml** 117 | ```yaml 118 | services: 119 | BoShurik\TelegramBotBundle\Authenticator\UserLoaderInterface: '@App\Security\UserProvider' 120 | BoShurik\TelegramBotBundle\Authenticator\UserFactoryInterface: '@App\Security\UserProvider' 121 | ``` 122 | 123 | ## Bot 124 | 125 | Set domain in @BotFather for your bot with `/setdomain` command 126 | 127 | ## Widget 128 | 129 | Place widget obtained from [https://core.telegram.org/widgets/login](https://core.telegram.org/widgets/login) on your site 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TelegramBotBundle 2 | 3 | Telegram bot bundle on top of [`telegram-bot/api`][1] library 4 | 5 | ## Examples 6 | 7 | See [example project][5] 8 | 9 | ## Installation 10 | 11 | #### Composer 12 | 13 | ``` bash 14 | $ composer require boshurik/telegram-bot-bundle 15 | ``` 16 | 17 | If you are using [symfony/flex][6] all you need is to set `TELEGRAM_BOT_TOKEN` environment variable 18 | 19 | #### Register the bundle 20 | 21 | ``` php 22 | get('first'); 91 | } 92 | ``` 93 | or use argument with type `TelegramBot\Api\BotApi` and name pattern `/\${name}(Bot|BotApi|Api)?$/` 94 | ```php 95 | use TelegramBot\Api\BotApi; 96 | public function __construct(private BotApi $firstBotApi) 97 | ``` 98 | 99 | For more info see [Usage][2] section in [`telegram-bot/api`][1] library 100 | 101 | #### Getting updates 102 | 103 | ``` bash 104 | bin/console telegram:updates 105 | bin/console telegram:updates first 106 | ``` 107 | 108 | For more information see [official documentation][3] 109 | 110 | #### Webhook 111 | 112 | ##### Set 113 | 114 | ``` bash 115 | bin/console telegram:webhook:set [url-or-hostname] [] 116 | bin/console telegram:webhook:set [url-or-hostname] [] --bot first 117 | ``` 118 | 119 | If `url-or-hostname` is not set command will generate url based on [request context](https://symfony.com/doc/current/routing.html#generating-urls-in-commands) 120 | 121 | ##### Unset 122 | 123 | ``` bash 124 | bin/console telegram:webhook:unset 125 | bin/console telegram:webhook:unset first 126 | ``` 127 | 128 | For more information see [official documentation][4] 129 | 130 | #### Async command processing 131 | 132 | To improve performance, you can leverage [Messenger][7] to process webhooks later via a Messenger transport. 133 | 134 | ```bash 135 | composer req symfony/messenger 136 | ``` 137 | 138 | ```yaml 139 | # config/packages/messenger.yaml 140 | framework: 141 | messenger: 142 | transports: 143 | async: "%env(MESSENGER_TRANSPORT_DSN)%" 144 | 145 | routing: 146 | 'BoShurik\TelegramBotBundle\Messenger\TelegramMessage': async 147 | ``` 148 | 149 | #### Adding commands 150 | 151 | Commands must implement `\BoShurik\TelegramBotBundle\Telegram\Command\CommandInterface` 152 | 153 | There is `\BoShurik\TelegramBotBundle\Telegram\Command\AbstractCommand` you can start with 154 | 155 | To register command: add tag `boshurik_telegram_bot.command` to service definition 156 | ``` yaml 157 | app.telegram.command: 158 | class: AppBundle\Telegram\Command\SomeCommand 159 | tags: 160 | - { name: boshurik_telegram_bot.command } 161 | ``` 162 | 163 | If you use `autoconfigure` tag will be added automatically 164 | 165 | For application with multiple bots you need to pass bot id: 166 | ``` yaml 167 | app.telegram.command: 168 | class: AppBundle\Telegram\Command\SomeCommand 169 | tags: 170 | - { name: boshurik_telegram_bot.command, bot: first } 171 | ``` 172 | If you need to use same command for multiple bots you must add multiple tags for each bot: 173 | ``` yaml 174 | app.telegram.command: 175 | class: AppBundle\Telegram\Command\SomeCommand 176 | tags: 177 | - { name: boshurik_telegram_bot.command, bot: first } 178 | - { name: boshurik_telegram_bot.command, bot: second } 179 | ``` 180 | 181 | There is predefined `\BoShurik\TelegramBotBundle\Telegram\Command\HelpCommand`. 182 | It displays commands which additionally implement `\BoShurik\TelegramBotBundle\Telegram\Command\PublicCommandInterface` 183 | 184 | You need to register it: 185 | ``` yaml 186 | app.telegram.command.help: 187 | class: BoShurik\TelegramBotBundle\Telegram\Command\HelpCommand 188 | arguments: 189 | - '@boshurik_telegram_bot.command.registry.default' 190 | tags: 191 | - { name: boshurik_telegram_bot.command } 192 | ``` 193 | or for multiple bots: 194 | ``` yaml 195 | app.telegram.command.help: 196 | class: BoShurik\TelegramBotBundle\Telegram\Command\HelpCommand 197 | arguments: 198 | - '@boshurik_telegram_bot.command.registry.first' 199 | tags: 200 | - { name: boshurik_telegram_bot.command, bot: first } 201 | ``` 202 | 203 | #### Events 204 | 205 | For more complex application (e.g. conversations) you can listen for `BoShurik\TelegramBotBundle\Event\UpdateEvent` event 206 | ``` php 207 | /** 208 | * @param UpdateEvent $event 209 | */ 210 | public function onUpdate(UpdateEvent $event) 211 | { 212 | $update = $event->getUpdate(); 213 | $message = $update->getMessage(); 214 | } 215 | ``` 216 | 217 | ## Login with Telegram 218 | 219 | This bundle supports login through Telegram Api 220 | 221 | If you want to allow your Bot's users to login without requiring them to register again 222 | follow these [instructions](LOGIN_WITH_TELEGRAM.md). 223 | 224 | [1]: https://github.com/TelegramBot/Api 225 | [2]: https://github.com/TelegramBot/Api#usage 226 | [3]: https://core.telegram.org/bots/api#getupdates 227 | [4]: https://core.telegram.org/bots/api#setwebhook 228 | [5]: https://github.com/BoShurik/telegram-bot-example 229 | [6]: https://flex.symfony.com 230 | [7]: https://symfony.com/doc/current/messenger.html 231 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boshurik/telegram-bot-bundle", 3 | "license": "MIT", 4 | "type": "symfony-bundle", 5 | "description": "Telegram bot bundle", 6 | "authors": [ 7 | { 8 | "name": "Alexander Borisov", 9 | "email": "boshurik@gmail.com" 10 | }, 11 | { 12 | "name": "Community", 13 | "homepage": "https://github.com/BoShurik/TelegramBotBundle/graphs/contributors" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "BoShurik\\TelegramBotBundle\\": "src/" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "BoShurik\\TelegramBotBundle\\Tests\\": "tests/" 24 | } 25 | }, 26 | "require": { 27 | "php": "^8.0", 28 | "ext-curl": "*", 29 | "ext-json": "*", 30 | "symfony/console": "^5.4 || ^6.0 || ^7.0", 31 | "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", 32 | "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0", 33 | "telegram-bot/api": "^2.3.14" 34 | }, 35 | "require-dev": { 36 | "friendsofphp/php-cs-fixer": "~3.23.0", 37 | "symfony/phpunit-bridge": "^7.0.1", 38 | "symfony/security-http": "^5.4 || ^6.0 || ^7.0", 39 | "symfony/http-client": "^5.4 || ^6.0 || ^7.0", 40 | "symfony/messenger": "^5.4 || ^6.0 || ^7.0", 41 | "symfony/yaml": "^5.4 || ^6.0 || ^7.0", 42 | "vimeo/psalm": "~4.30.0", 43 | "psalm/plugin-symfony": "^4.0" 44 | }, 45 | "suggest": { 46 | "symfony/security-guard": "Required to implement user authentication through Telegram", 47 | "symfony/http-client": "Required to use third party http client", 48 | "symfony/messenger": "Required to handle messages with queues" 49 | }, 50 | "scripts": { 51 | "test": "vendor/bin/simple-phpunit --colors=always", 52 | "coverage": "XDEBUG_MODE=coverage vendor/bin/simple-phpunit --coverage-html build/coverage", 53 | "cs-check": "vendor/bin/php-cs-fixer fix --allow-risky=yes --diff --ansi --dry-run", 54 | "cs-fix": "vendor/bin/php-cs-fixer fix --allow-risky=yes --diff --ansi", 55 | "psalm": "vendor/bin/psalm", 56 | "dev": [ 57 | "@cs-fix", 58 | "@psalm", 59 | "@test" 60 | ], 61 | "checks": [ 62 | "@cs-check", 63 | "@psalm", 64 | "@test" 65 | ] 66 | }, 67 | "extra": { 68 | "branch-alias": { 69 | "dev-master": "6.0.x-dev" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Authenticator/TelegramAuthenticator.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 BoShurik\TelegramBotBundle\Authenticator; 13 | 14 | use Symfony\Component\HttpFoundation\RedirectResponse; 15 | use Symfony\Component\HttpFoundation\Request; 16 | use Symfony\Component\HttpFoundation\Response; 17 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 18 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 19 | use Symfony\Component\Security\Core\Exception\AuthenticationException; 20 | use Symfony\Component\Security\Core\Exception\BadCredentialsException; 21 | use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; 22 | use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; 23 | use Symfony\Component\Security\Http\Authenticator\Passport\Passport; 24 | use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; 25 | use Symfony\Component\Security\Http\Util\TargetPathTrait; 26 | 27 | final class TelegramAuthenticator extends AbstractAuthenticator 28 | { 29 | use TargetPathTrait; 30 | 31 | public function __construct( 32 | private TelegramLoginValidator $validator, 33 | private UserLoaderInterface $userLoader, 34 | private ?UserFactoryInterface $userFactory, 35 | private UrlGeneratorInterface $urlGenerator, 36 | private string $guardRoute, 37 | private string $defaultTargetRoute, 38 | private ?string $loginRoute = null 39 | ) { 40 | } 41 | 42 | public function supports(Request $request): ?bool 43 | { 44 | $route = $request->attributes->get('_route'); 45 | 46 | return $route === $this->guardRoute; 47 | } 48 | 49 | public function authenticate(Request $request): Passport 50 | { 51 | $credentials = $request->query->all(); 52 | 53 | $this->validator->validate($credentials); 54 | $user = $this->userLoader->loadByTelegramId($credentials['id']); 55 | 56 | if (!$user && $this->userFactory) { 57 | $user = $this->userFactory->createFromTelegram($credentials); 58 | } 59 | 60 | if (!$user) { 61 | throw new BadCredentialsException(); 62 | } 63 | 64 | return new SelfValidatingPassport(new UserBadge($credentials['id'], function ($id) { 65 | return $this->userLoader->loadByTelegramId($id); 66 | })); 67 | } 68 | 69 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response 70 | { 71 | if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { 72 | return new RedirectResponse($targetPath); 73 | } 74 | 75 | return new RedirectResponse($this->urlGenerator->generate($this->defaultTargetRoute)); 76 | } 77 | 78 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response 79 | { 80 | if ($this->loginRoute) { 81 | return new RedirectResponse($this->urlGenerator->generate($this->loginRoute)); 82 | } 83 | 84 | return null; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Authenticator/TelegramLoginValidator.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 BoShurik\TelegramBotBundle\Authenticator; 13 | 14 | use BoShurik\TelegramBotBundle\Exception\AuthenticationException; 15 | 16 | /** 17 | * @final 18 | */ 19 | /* final */ class TelegramLoginValidator 20 | { 21 | private const EXPIRING_TIMEOUT = 3600; 22 | 23 | private const REQUIRED_FIELDS = [ 24 | 'id', 25 | 'auth_date', 26 | 'hash', 27 | ]; 28 | 29 | private string $secret; 30 | 31 | public function __construct(string $telegramBotToken) 32 | { 33 | $this->secret = hash('sha256', $telegramBotToken, true); 34 | } 35 | 36 | /** 37 | * Return TRUE if all required fields are present 38 | */ 39 | private static function hasAllRequiredFields(array $data): bool 40 | { 41 | return array_intersect(self::REQUIRED_FIELDS, array_keys($data)) === self::REQUIRED_FIELDS; 42 | } 43 | 44 | /** 45 | * Validate login data 46 | * 47 | * @throws AuthenticationException if something goes wrong 48 | */ 49 | public function validate(array $data): void 50 | { 51 | if (!self::hasAllRequiredFields($data)) { 52 | throw new AuthenticationException('Login data missing'); 53 | } 54 | 55 | // Check for data expiration 56 | // This is optional, but HIGHLY recommended step 👍 57 | if ((time() - $data['auth_date']) > self::EXPIRING_TIMEOUT) { 58 | throw new AuthenticationException('Login data expired'); 59 | } 60 | 61 | $hash = $data['hash']; 62 | unset($data['hash']); 63 | 64 | // Check for data integrity 65 | $hmac = hash_hmac('sha256', $this->serialize($data), $this->secret); 66 | if ($hmac !== $hash) { 67 | throw new AuthenticationException('Invalid data checksum'); 68 | } 69 | } 70 | 71 | private function serialize(array $data): string 72 | { 73 | ksort($data); 74 | 75 | return implode("\n", array_map( 76 | function ($key, $value) { 77 | return "$key=$value"; 78 | }, 79 | array_keys($data), 80 | $data 81 | )); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Authenticator/UserFactoryInterface.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 BoShurik\TelegramBotBundle\Authenticator; 13 | 14 | use Symfony\Component\Security\Core\User\UserInterface; 15 | 16 | interface UserFactoryInterface 17 | { 18 | /** 19 | * @param array $data contains id, first_name, last_name, username, photo_url, auth_date and hash fields 20 | */ 21 | public function createFromTelegram(array $data): UserInterface; 22 | } 23 | -------------------------------------------------------------------------------- /src/Authenticator/UserLoaderInterface.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 BoShurik\TelegramBotBundle\Authenticator; 13 | 14 | use Symfony\Component\Security\Core\User\UserInterface; 15 | 16 | interface UserLoaderInterface 17 | { 18 | public function loadByTelegramId(string $id): ?UserInterface; 19 | } 20 | -------------------------------------------------------------------------------- /src/BoShurikTelegramBotBundle.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 BoShurik\TelegramBotBundle; 13 | 14 | use BoShurik\TelegramBotBundle\DependencyInjection\BoShurikTelegramBotExtension; 15 | use BoShurik\TelegramBotBundle\DependencyInjection\Compiler\CommandCompilerPass; 16 | use Symfony\Component\DependencyInjection\ContainerBuilder; 17 | use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; 18 | use Symfony\Component\HttpKernel\Bundle\Bundle; 19 | 20 | final class BoShurikTelegramBotBundle extends Bundle 21 | { 22 | public function build(ContainerBuilder $container): void 23 | { 24 | parent::build($container); 25 | 26 | $container->addCompilerPass(new CommandCompilerPass()); 27 | } 28 | 29 | public function getContainerExtension(): ?ExtensionInterface 30 | { 31 | if (null === $this->extension) { 32 | $this->extension = new BoShurikTelegramBotExtension(); 33 | } 34 | 35 | return $this->extension; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Command/UpdatesCommand.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 BoShurik\TelegramBotBundle\Command; 13 | 14 | use BoShurik\TelegramBotBundle\Telegram\Telegram; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | final class UpdatesCommand extends Command 21 | { 22 | public function __construct(private Telegram $telegram) 23 | { 24 | parent::__construct(); 25 | } 26 | 27 | protected function configure(): void 28 | { 29 | $this 30 | ->setName('telegram:updates') 31 | ->addArgument('bot', InputArgument::OPTIONAL, 'Bot') 32 | ->setDescription('Get updates') 33 | ; 34 | } 35 | 36 | protected function execute(InputInterface $input, OutputInterface $output): int 37 | { 38 | /** @var string|null $bot */ 39 | $bot = $input->getArgument('bot'); 40 | if ($bot) { 41 | $this->telegram->processUpdates($bot); 42 | } else { 43 | $this->telegram->processAllUpdates(); 44 | } 45 | 46 | return Command::SUCCESS; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Command/Webhook/InfoCommand.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 BoShurik\TelegramBotBundle\Command\Webhook; 13 | 14 | use BoShurik\TelegramBotBundle\Telegram\BotLocator; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | use Symfony\Component\Console\Style\SymfonyStyle; 20 | use TelegramBot\Api\BotApi; 21 | 22 | final class InfoCommand extends Command 23 | { 24 | public function __construct(private BotLocator $botLocator) 25 | { 26 | parent::__construct(); 27 | } 28 | 29 | protected function configure(): void 30 | { 31 | $this 32 | ->setName('telegram:webhook:info') 33 | ->addArgument('bot', InputArgument::OPTIONAL, 'Bot') 34 | ->setDescription('Webhook info') 35 | ; 36 | } 37 | 38 | protected function execute(InputInterface $input, OutputInterface $output): int 39 | { 40 | $io = new SymfonyStyle($input, $output); 41 | 42 | /** @var string|null $bot */ 43 | $bot = $input->getArgument('bot'); 44 | if ($bot) { 45 | $api = $this->botLocator->get($bot); 46 | 47 | $this->printWebhookInfo($io, $bot, $api); 48 | } else { 49 | foreach ($this->botLocator->all() as $name => $api) { 50 | $this->printWebhookInfo($io, $name, $api); 51 | } 52 | } 53 | 54 | return Command::SUCCESS; 55 | } 56 | 57 | private function printWebhookInfo(SymfonyStyle $io, string $name, BotApi $api): void 58 | { 59 | $io->block(sprintf('Bot "%s"', $name)); 60 | 61 | $info = $api->getWebhookInfo(); 62 | 63 | $values = []; 64 | $values[] = [ 65 | 'Webhook URL', 66 | $info->getUrl(), 67 | ]; 68 | $values[] = [ 69 | 'Custom Certificate', 70 | $info->hasCustomCertificate() ? 'yes' : 'no', 71 | ]; 72 | $values[] = [ 73 | 'Pending Update Count', 74 | $info->getPendingUpdateCount(), 75 | ]; 76 | $lastErrorDate = $info->getLastErrorDate(); 77 | $values[] = [ 78 | 'Last Error Date', 79 | $lastErrorDate ? date('Y-m-d H:i:s', $lastErrorDate) : '-', 80 | ]; 81 | $values[] = [ 82 | 'Last Error Message', 83 | $info->getLastErrorMessage() ?? '-', 84 | ]; 85 | $values[] = [ 86 | 'Max Connections', 87 | $info->getMaxConnections(), 88 | ]; 89 | $allowedUpdates = $info->getAllowedUpdates(); 90 | $values[] = [ 91 | 'Allowed Updates', 92 | \is_array($allowedUpdates) ? implode(', ', $allowedUpdates) : '-', 93 | ]; 94 | 95 | $io->table([ 96 | 'Name', 97 | 'Value', 98 | ], $values); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Command/Webhook/SetCommand.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 BoShurik\TelegramBotBundle\Command\Webhook; 13 | 14 | use BoShurik\TelegramBotBundle\Telegram\BotLocator; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Input\InputOption; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | use Symfony\Component\Console\Style\SymfonyStyle; 21 | use Symfony\Component\Routing\Exception\RouteNotFoundException; 22 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 23 | use TelegramBot\Api\BotApi; 24 | 25 | final class SetCommand extends Command 26 | { 27 | private const MAX_CONNECTIONS = 40; 28 | private const ALLOWED_UPDATE_TYPES = ['message', 'edited_message', 'channel_post', 'edited_channel_post', 29 | 'inline_query', 'chosen_inline_result', 'callback_query', 'shipping_query', 'pre_checkout_query', 'poll', 30 | 'poll_answer', 'my_chat_member', 'chat_member', 'chat_join_request']; 31 | 32 | public function __construct(private BotLocator $botLocator, private UrlGeneratorInterface $urlGenerator) 33 | { 34 | parent::__construct(); 35 | } 36 | 37 | protected function configure(): void 38 | { 39 | $this 40 | ->setName('telegram:webhook:set') 41 | ->addArgument('urlOrHostname', InputArgument::OPTIONAL, 'Webhook URL or the host name of your site. if you specify only a host name (without https://), path will be generated for you.') 42 | ->addArgument('certificate', InputArgument::OPTIONAL, 'Path to public key certificate') 43 | ->addOption( 44 | 'allowedUpdateType', 45 | null, 46 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 47 | 'Allowed update type. Add option multiple times to add several values.' 48 | ) 49 | ->addOption('bot', null, InputOption::VALUE_REQUIRED, 'Bot') 50 | ->setDescription('Set webhook') 51 | ; 52 | } 53 | 54 | protected function execute(InputInterface $input, OutputInterface $output): int 55 | { 56 | $io = new SymfonyStyle($input, $output); 57 | 58 | $certificateFile = null; 59 | if ($certificate = $input->getArgument('certificate')) { 60 | if (!is_file($certificate) || !is_readable($certificate)) { 61 | throw new \RuntimeException(sprintf('Can\'t read certificate file "%s"', $certificate)); 62 | } 63 | 64 | $certificateFile = new \CURLFile($certificate); 65 | } 66 | 67 | /** @var string|null $urlOrHostname */ 68 | $urlOrHostname = $input->getArgument('urlOrHostname'); 69 | /** @var string|null $bot */ 70 | $bot = $input->getOption('bot'); 71 | 72 | $allowedUpdates = $input->getOption('allowedUpdateType'); 73 | foreach ($allowedUpdates as $update) { 74 | if (!\in_array($update, self::ALLOWED_UPDATE_TYPES)) { 75 | $io->error('Incorrect update type: '.$update); 76 | 77 | return self::FAILURE; 78 | } 79 | } 80 | 81 | if ($bot) { 82 | $api = $this->botLocator->get($bot); 83 | if (!$this->setWebhook($io, $bot, $api, $urlOrHostname, $certificateFile, $allowedUpdates)) { 84 | return self::FAILURE; 85 | } 86 | } else { 87 | if ($urlOrHostname && str_starts_with($urlOrHostname, 'https://') && !$this->botLocator->isSingle()) { 88 | $io->error('Can\'t set single url for multiple bots. Pass hostname to generate urls automatically'); 89 | 90 | return self::FAILURE; 91 | } 92 | 93 | foreach ($this->botLocator->all() as $name => $api) { 94 | if (!$this->setWebhook($io, $name, $api, $urlOrHostname, $certificateFile, $allowedUpdates)) { 95 | return self::FAILURE; 96 | } 97 | } 98 | } 99 | 100 | return self::SUCCESS; 101 | } 102 | 103 | private function setWebhook( 104 | SymfonyStyle $io, 105 | string $name, 106 | BotApi $api, 107 | string $urlOrHostname = null, 108 | \CURLFile $certificateFile = null, 109 | array $allowedUpdates = null 110 | ): bool { 111 | $io->block(sprintf('Bot "%s"', $name)); 112 | 113 | if (!$urlOrHostname) { 114 | $url = $this->urlGenerator->generate('_telegram_bot_webhook', [ 115 | 'bot' => $name, 116 | ], UrlGeneratorInterface::ABSOLUTE_URL); 117 | if (str_contains($url, '://localhost')) { 118 | $io->error('Can\'t generate url: request context is not set'); 119 | 120 | return false; 121 | } 122 | } elseif (!str_starts_with($urlOrHostname, 'https://')) { 123 | try { 124 | $url = 'https://'.rtrim($urlOrHostname, '/').$this->urlGenerator->generate('_telegram_bot_webhook', [ 125 | 'bot' => $name, 126 | ]); 127 | } catch (RouteNotFoundException $e) { 128 | $helpUrl = 'https://github.com/BoShurik/TelegramBotBundle#add-routing-for-webhook'; 129 | $message = "We could not find the webhook route. Read on\n%s\nhow to add the route or use symfony/flex."; 130 | $io->block(messages: sprintf($message, $helpUrl), escape: false); 131 | 132 | return false; 133 | } 134 | } else { 135 | $url = $urlOrHostname; 136 | } 137 | 138 | $api->setWebhook($url, $certificateFile, null, self::MAX_CONNECTIONS, json_encode($allowedUpdates)); 139 | 140 | $message = sprintf('Webhook URL has been set to %s', $url); 141 | $io->block($message, 'OK', 'fg=black;bg=green', ' ', true, false); 142 | 143 | return true; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Command/Webhook/UnsetCommand.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 BoShurik\TelegramBotBundle\Command\Webhook; 13 | 14 | use BoShurik\TelegramBotBundle\Telegram\BotLocator; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | use Symfony\Component\Console\Style\SymfonyStyle; 20 | use TelegramBot\Api\BotApi; 21 | 22 | final class UnsetCommand extends Command 23 | { 24 | public function __construct(private BotLocator $botLocator) 25 | { 26 | parent::__construct(); 27 | } 28 | 29 | protected function configure(): void 30 | { 31 | $this 32 | ->setName('telegram:webhook:unset') 33 | ->addArgument('bot', InputArgument::OPTIONAL, 'Bot') 34 | ->setDescription('Unset webhook') 35 | ; 36 | } 37 | 38 | protected function execute(InputInterface $input, OutputInterface $output): int 39 | { 40 | $io = new SymfonyStyle($input, $output); 41 | 42 | /** @var string|null $bot */ 43 | $bot = $input->getArgument('bot'); 44 | if ($bot) { 45 | $api = $this->botLocator->get($bot); 46 | $this->deleteWebhook($io, $bot, $api); 47 | } else { 48 | foreach ($this->botLocator->all() as $name => $api) { 49 | $this->deleteWebhook($io, $name, $api); 50 | } 51 | } 52 | 53 | return Command::SUCCESS; 54 | } 55 | 56 | private function deleteWebhook(SymfonyStyle $io, string $name, BotApi $api): void 57 | { 58 | $io->block(sprintf('Bot "%s"', $name)); 59 | 60 | $api->deleteWebhook(); 61 | 62 | $io->success('Webhook URL has been unset'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Controller/WebhookController.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 BoShurik\TelegramBotBundle\Controller; 13 | 14 | use BoShurik\TelegramBotBundle\Event\WebhookEvent; 15 | use BoShurik\TelegramBotBundle\Messenger\TelegramMessage; 16 | use BoShurik\TelegramBotBundle\Telegram\Telegram; 17 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 18 | use Symfony\Component\HttpFoundation\Request; 19 | use Symfony\Component\HttpFoundation\Response; 20 | use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; 21 | use Symfony\Component\Messenger\MessageBusInterface; 22 | use TelegramBot\Api\BotApi; 23 | use TelegramBot\Api\Types\Update; 24 | 25 | final class WebhookController 26 | { 27 | public function __construct( 28 | private Telegram $telegram, 29 | private EventDispatcherInterface $eventDispatcher, 30 | private ?MessageBusInterface $bus = null 31 | ) { 32 | } 33 | 34 | public function indexAction(string $bot, Request $request): Response 35 | { 36 | if ($content = $request->getContent()) { 37 | if ($data = BotApi::jsonValidate($content, true)) { 38 | /** @var array $data */ 39 | $update = Update::fromResponse($data); 40 | if ($this->bus === null) { 41 | $this->telegram->processUpdate($bot, $update); 42 | } else { 43 | $this->bus->dispatch(new TelegramMessage($bot, $update)); 44 | } 45 | } 46 | } 47 | 48 | if (!isset($update)) { 49 | throw new BadRequestHttpException('Empty data'); 50 | } 51 | 52 | $event = $this->eventDispatcher->dispatch(new WebhookEvent($bot, $request, $update)); 53 | 54 | return $event->getResponse() ? $event->getResponse() : new Response(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/DependencyInjection/BoShurikTelegramBotExtension.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 BoShurik\TelegramBotBundle\DependencyInjection; 13 | 14 | use BoShurik\TelegramBotBundle\DependencyInjection\Compiler\CommandCompilerPass; 15 | use BoShurik\TelegramBotBundle\Telegram\Command\CommandInterface; 16 | use Symfony\Component\Config\FileLocator; 17 | use Symfony\Component\DependencyInjection\ChildDefinition; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | use Symfony\Component\DependencyInjection\Definition; 20 | use Symfony\Component\DependencyInjection\Loader; 21 | use Symfony\Component\DependencyInjection\Parameter; 22 | use Symfony\Component\DependencyInjection\Reference; 23 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 24 | use Symfony\Contracts\HttpClient\HttpClientInterface as SymfonyHttpClientInterface; 25 | use TelegramBot\Api\BotApi; 26 | use TelegramBot\Api\Http\CurlHttpClient; 27 | use TelegramBot\Api\Http\HttpClientInterface; 28 | use TelegramBot\Api\Http\SymfonyHttpClient; 29 | 30 | final class BoShurikTelegramBotExtension extends Extension 31 | { 32 | private const BOT_API_ID_TEMPLATE = 'boshurik_telegram_bot.api.bot.%s'; 33 | 34 | public function load(array $configs, ContainerBuilder $container): void 35 | { 36 | $configuration = new Configuration(); 37 | $config = $this->processConfiguration($configuration, $configs); 38 | 39 | $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 40 | 41 | $loader->load('services.php'); 42 | 43 | $container->setParameter('boshurik_telegram_bot.api.proxy', $config['api']['proxy']); 44 | $container->setParameter('boshurik_telegram_bot.api.timeout', $config['api']['timeout']); 45 | 46 | $defaultBot = $config['api']['default_bot']; 47 | 48 | if (interface_exists(HttpClientInterface::class)) { 49 | if (interface_exists(SymfonyHttpClientInterface::class)) { 50 | $httpClient = new Definition(SymfonyHttpClient::class, [ 51 | new Reference(SymfonyHttpClientInterface::class), 52 | ]); 53 | } else { 54 | $httpClient = new Definition(CurlHttpClient::class); 55 | $httpClient->addMethodCall('setProxy', [new Parameter('boshurik_telegram_bot.api.proxy')]); 56 | $httpClient->addMethodCall('setOption', [\CURLOPT_TIMEOUT, new Parameter('boshurik_telegram_bot.api.timeout')]); 57 | } 58 | 59 | $container->setDefinition(HttpClientInterface::class, $httpClient); 60 | } 61 | 62 | $bots = []; 63 | $registries = []; 64 | foreach ($config['api']['bots'] as $name => $bot) { 65 | $botId = sprintf(self::BOT_API_ID_TEMPLATE, $name); 66 | $registryId = sprintf(CommandCompilerPass::REGISTRY_ID_TEMPLATE, $name); 67 | 68 | $container 69 | ->setDefinition( 70 | $botId, 71 | new ChildDefinition('boshurik_telegram_bot.api.abstract_bot') 72 | ) 73 | ->setArguments([ 74 | '$token' => $bot['token'], 75 | ]) 76 | ; 77 | 78 | $container 79 | ->setDefinition( 80 | $registryId, 81 | new ChildDefinition('boshurik_telegram_bot.command.abstract_registry') 82 | ) 83 | ->addTag(CommandCompilerPass::REGISTRY_TAG, [ 84 | 'bot' => $name, 85 | ]) 86 | ; 87 | 88 | $bots[$name] = new Reference($botId); 89 | $registries[$name] = new Reference($registryId); 90 | if ($name === $defaultBot) { 91 | $container->setAlias(BotApi::class, $botId); 92 | } 93 | $container->registerAliasForArgument($botId, BotApi::class, $name); 94 | $container->registerAliasForArgument($botId, BotApi::class, $name.'Bot'); 95 | $container->registerAliasForArgument($botId, BotApi::class, $name.'BotApi'); 96 | $container->registerAliasForArgument($botId, BotApi::class, $name.'Api'); 97 | } 98 | 99 | $container 100 | ->getDefinition('boshurik_telegram_bot.api.bot_locator') 101 | ->setArguments([$bots]) 102 | ; 103 | $container 104 | ->getDefinition('boshurik_telegram_bot.command.registry_locator') 105 | ->setArguments([$registries]) 106 | ; 107 | 108 | if ($config['authenticator']['enabled']) { 109 | $loader->load('authenticator.php'); 110 | 111 | $authenticatorBot = $config['authenticator']['bot'] ?? $defaultBot; 112 | $authenticatorToken = $config['api']['bots'][$authenticatorBot]; 113 | 114 | $container->setParameter('boshurik_telegram_bot.authenticator.token', $authenticatorToken); 115 | $container->setParameter('boshurik_telegram_bot.guard.guard_route', $config['authenticator']['guard_route']); 116 | $container->setParameter('boshurik_telegram_bot.guard.default_target_route', $config['authenticator']['default_target_route']); 117 | $container->setParameter('boshurik_telegram_bot.guard.login_route', $config['authenticator']['login_route'] ?? null); 118 | } 119 | 120 | $container 121 | ->registerForAutoconfiguration(CommandInterface::class) 122 | ->addTag(CommandCompilerPass::COMMAND_TAG) 123 | ; 124 | } 125 | 126 | public function getAlias(): string 127 | { 128 | return 'boshurik_telegram_bot'; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/CommandCompilerPass.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 BoShurik\TelegramBotBundle\DependencyInjection\Compiler; 13 | 14 | use BoShurik\TelegramBotBundle\Telegram\Command\CommandInterface; 15 | use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; 16 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 17 | use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | use Symfony\Component\DependencyInjection\Exception\LogicException; 20 | 21 | final class CommandCompilerPass implements CompilerPassInterface 22 | { 23 | use PriorityTaggedServiceTrait; 24 | public const COMMAND_TAG = 'boshurik_telegram_bot.command'; 25 | public const REGISTRY_TAG = 'boshurik_telegram_bot.command.registry'; 26 | 27 | public const REGISTRY_ID_TEMPLATE = 'boshurik_telegram_bot.command.registry.%s'; 28 | 29 | public function process(ContainerBuilder $container): void 30 | { 31 | $commands = []; 32 | 33 | foreach ($this->findAndSortTaggedServices(new TaggedIteratorArgument(self::COMMAND_TAG), $container) as $command) { 34 | $definition = $container->getDefinition((string) $command); 35 | if (!$class = $definition->getClass()) { 36 | throw new LogicException(sprintf('Unknown class for service "%s"', (string) $command)); 37 | } 38 | $interfaces = class_implements($class); 39 | if (!isset($interfaces[CommandInterface::class])) { 40 | throw new LogicException(sprintf('Can\'t apply tag "%s" to %s class. It must implement %s interface', self::COMMAND_TAG, $class, CommandInterface::class)); 41 | } 42 | 43 | $tags = $definition->getTag(self::COMMAND_TAG); 44 | foreach ($tags as $tag) { 45 | $bot = $tag['bot'] ?? 'default'; 46 | 47 | $commands[$bot][] = $command; 48 | } 49 | } 50 | 51 | foreach ($container->findTaggedServiceIds(self::REGISTRY_TAG) as $id => $tags) { 52 | foreach ($tags as $tag) { 53 | $bot = $tag['bot']; 54 | 55 | if (isset($commands[$bot])) { 56 | $definition = $container->findDefinition($id); 57 | foreach ($commands[$bot] as $command) { 58 | $definition->addMethodCall('addCommand', [$command]); 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.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 BoShurik\TelegramBotBundle\DependencyInjection; 13 | 14 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 15 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 16 | use Symfony\Component\Config\Definition\ConfigurationInterface; 17 | 18 | final class Configuration implements ConfigurationInterface 19 | { 20 | public function getConfigTreeBuilder(): TreeBuilder 21 | { 22 | $treeBuilder = new TreeBuilder('boshurik_telegram_bot'); 23 | /** @var ArrayNodeDefinition $rootNode */ 24 | $rootNode = $treeBuilder->getRootNode(); 25 | 26 | /** @psalm-suppress all */ 27 | $rootNode 28 | ->children() 29 | ->arrayNode('api')->isRequired() 30 | ->beforeNormalization() 31 | ->ifTrue(static function ($v) { 32 | return \is_array($v) && !\array_key_exists('bots', $v) && !\array_key_exists('bot', $v); 33 | }) 34 | ->then(static function ($v) { 35 | // Key that should not be rewritten to the connection config 36 | $excludedKeys = ['default_bot' => true, 'proxy' => true, 'timeout' => true]; 37 | $connection = []; 38 | foreach ($v as $key => $value) { 39 | if (isset($excludedKeys[$key])) { 40 | continue; 41 | } 42 | 43 | $connection[$key] = $v[$key]; 44 | unset($v[$key]); 45 | } 46 | 47 | $v['default_bot'] = isset($v['default_bot']) ? (string) $v['default_bot'] : 'default'; 48 | $v['bots'] = [$v['default_bot'] => $connection]; 49 | 50 | return $v; 51 | }) 52 | ->end() 53 | ->validate() 54 | ->ifTrue(static function ($v) { 55 | $defaultBot = $v['default_bot'] ?? null; 56 | 57 | return !isset($v['bots'][$defaultBot]); 58 | }) 59 | ->thenInvalid('Default bot not found') 60 | ->end() 61 | ->children() 62 | ->scalarNode('default_bot')->isRequired()->end() 63 | ->end() 64 | ->children() 65 | ->scalarNode('proxy')->defaultValue('')->end() 66 | ->scalarNode('timeout')->defaultValue(10)->end() 67 | ->end() 68 | ->children() 69 | ->arrayNode('bots') 70 | ->useAttributeAsKey('name') 71 | ->prototype('array') 72 | ->beforeNormalization() 73 | ->ifString() 74 | ->then(static function ($v) { 75 | return ['token' => $v]; 76 | }) 77 | ->end() 78 | ->children() 79 | ->scalarNode('token')->isRequired()->end() 80 | ->end() 81 | ->end() 82 | ->end() 83 | ->end() 84 | ->end() 85 | ->arrayNode('authenticator')->canBeEnabled() 86 | ->children() 87 | ->scalarNode('bot')->defaultNull()->end() 88 | ->scalarNode('login_route')->defaultNull()->cannotBeEmpty()->end() 89 | ->scalarNode('default_target_route')->isRequired()->cannotBeEmpty()->end() 90 | ->scalarNode('guard_route')->isRequired()->cannotBeEmpty()->end() 91 | ->end() 92 | ->end() 93 | ->end() 94 | ; 95 | 96 | return $treeBuilder; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Event/UpdateEvent.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 BoShurik\TelegramBotBundle\Event; 13 | 14 | use Symfony\Contracts\EventDispatcher\Event; 15 | use TelegramBot\Api\Types\Update; 16 | 17 | final class UpdateEvent extends Event 18 | { 19 | private bool $processed; 20 | 21 | public function __construct(private string $bot, private Update $update) 22 | { 23 | $this->processed = false; 24 | } 25 | 26 | public function getBot(): string 27 | { 28 | return $this->bot; 29 | } 30 | 31 | public function getUpdate(): Update 32 | { 33 | return $this->update; 34 | } 35 | 36 | public function isProcessed(): bool 37 | { 38 | return $this->processed; 39 | } 40 | 41 | public function setProcessed(): void 42 | { 43 | $this->processed = true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Event/WebhookEvent.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 BoShurik\TelegramBotBundle\Event; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\Response; 16 | use Symfony\Contracts\EventDispatcher\Event; 17 | use TelegramBot\Api\Types\Update; 18 | 19 | final class WebhookEvent extends Event 20 | { 21 | private ?Response $response; 22 | 23 | public function __construct(private string $bot, private Request $request, private Update $update) 24 | { 25 | $this->response = null; 26 | } 27 | 28 | public function getBot(): string 29 | { 30 | return $this->bot; 31 | } 32 | 33 | public function getRequest(): Request 34 | { 35 | return $this->request; 36 | } 37 | 38 | public function getUpdate(): Update 39 | { 40 | return $this->update; 41 | } 42 | 43 | public function getResponse(): ?Response 44 | { 45 | return $this->response; 46 | } 47 | 48 | public function setResponse(?Response $response): void 49 | { 50 | $this->response = $response; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/EventListener/CommandListener.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 BoShurik\TelegramBotBundle\EventListener; 13 | 14 | use BoShurik\TelegramBotBundle\Event\UpdateEvent; 15 | use BoShurik\TelegramBotBundle\Telegram\BotLocator; 16 | use BoShurik\TelegramBotBundle\Telegram\Command\Registry\CommandRegistryLocator; 17 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 18 | 19 | final class CommandListener implements EventSubscriberInterface 20 | { 21 | public static function getSubscribedEvents(): array 22 | { 23 | return [ 24 | UpdateEvent::class => 'onUpdate', 25 | ]; 26 | } 27 | 28 | public function __construct(private BotLocator $botLocator, private CommandRegistryLocator $registryLocator) 29 | { 30 | } 31 | 32 | public function onUpdate(UpdateEvent $event): void 33 | { 34 | $api = $this->botLocator->get($event->getBot()); 35 | $registry = $this->registryLocator->get($event->getBot()); 36 | 37 | foreach ($registry->getCommands() as $command) { 38 | if (!$command->isApplicable($event->getUpdate())) { 39 | continue; 40 | } 41 | 42 | $command->execute($api, $event->getUpdate()); 43 | $event->setProcessed(); 44 | 45 | break; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Exception/AuthenticationException.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 BoShurik\TelegramBotBundle\Exception; 13 | 14 | use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; 15 | 16 | final class AuthenticationException extends CustomUserMessageAuthenticationException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Messenger/MessageHandler.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 BoShurik\TelegramBotBundle\Messenger; 13 | 14 | use BoShurik\TelegramBotBundle\Telegram\Telegram; 15 | 16 | final class MessageHandler 17 | { 18 | public function __construct(private Telegram $telegram) 19 | { 20 | } 21 | 22 | public function __invoke(TelegramMessage $message) 23 | { 24 | $this->telegram->processUpdate($message->getBot(), $message->getUpdate()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Messenger/TelegramMessage.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 BoShurik\TelegramBotBundle\Messenger; 13 | 14 | use TelegramBot\Api\Types\Update; 15 | 16 | final class TelegramMessage 17 | { 18 | public function __construct(private string $bot, private Update $update) 19 | { 20 | } 21 | 22 | public function getBot(): string 23 | { 24 | return $this->bot; 25 | } 26 | 27 | public function getUpdate(): Update 28 | { 29 | return $this->update; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Resources/config/authenticator.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 Symfony\Component\DependencyInjection\Loader\Configurator; 13 | 14 | use BoShurik\TelegramBotBundle\Authenticator\TelegramAuthenticator; 15 | use BoShurik\TelegramBotBundle\Authenticator\TelegramLoginValidator; 16 | use BoShurik\TelegramBotBundle\Authenticator\UserFactoryInterface; 17 | use BoShurik\TelegramBotBundle\Authenticator\UserLoaderInterface; 18 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 19 | 20 | return static function (ContainerConfigurator $containerConfigurator): void { 21 | $services = $containerConfigurator->services(); 22 | 23 | $services->set(TelegramAuthenticator::class) 24 | ->args([ 25 | service(TelegramLoginValidator::class), 26 | service(UserLoaderInterface::class), 27 | service(UserFactoryInterface::class), 28 | service(UrlGeneratorInterface::class), 29 | '%boshurik_telegram_bot.guard.guard_route%', 30 | '%boshurik_telegram_bot.guard.default_target_route%', 31 | '%boshurik_telegram_bot.guard.login_route%', 32 | ]); 33 | 34 | $services->set(TelegramLoginValidator::class) 35 | ->args([ 36 | service('%boshurik_telegram_bot.authenticator.token%'), 37 | ]); 38 | }; 39 | -------------------------------------------------------------------------------- /src/Resources/config/routing.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 | use BoShurik\TelegramBotBundle\Controller\WebhookController; 13 | use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; 14 | 15 | return static function (RoutingConfigurator $routes) { 16 | $routes->add('_telegram_bot_webhook', '/') 17 | ->controller([WebhookController::class, 'indexAction']) 18 | ->defaults(['bot' => 'default']) 19 | ; 20 | }; 21 | -------------------------------------------------------------------------------- /src/Resources/config/services.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 Symfony\Component\DependencyInjection\Loader\Configurator; 13 | 14 | use BoShurik\TelegramBotBundle\Command\UpdatesCommand; 15 | use BoShurik\TelegramBotBundle\Command\Webhook\InfoCommand; 16 | use BoShurik\TelegramBotBundle\Command\Webhook\SetCommand; 17 | use BoShurik\TelegramBotBundle\Command\Webhook\UnsetCommand; 18 | use BoShurik\TelegramBotBundle\Controller\WebhookController; 19 | use BoShurik\TelegramBotBundle\EventListener\CommandListener; 20 | use BoShurik\TelegramBotBundle\Messenger\MessageHandler; 21 | use BoShurik\TelegramBotBundle\Telegram\BotLocator; 22 | use BoShurik\TelegramBotBundle\Telegram\Command\Registry\CommandRegistry; 23 | use BoShurik\TelegramBotBundle\Telegram\Command\Registry\CommandRegistryLocator; 24 | use BoShurik\TelegramBotBundle\Telegram\Telegram; 25 | use Symfony\Component\DependencyInjection\ServiceLocator; 26 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 27 | use TelegramBot\Api\BotApi; 28 | use TelegramBot\Api\Http\HttpClientInterface; 29 | 30 | return static function (ContainerConfigurator $containerConfigurator): void { 31 | $services = $containerConfigurator->services(); 32 | 33 | $abstractBot = $services->set('boshurik_telegram_bot.api.abstract_bot', BotApi::class) 34 | ->abstract() 35 | ; 36 | 37 | if (interface_exists(HttpClientInterface::class)) { 38 | $abstractBot->arg('$httpClient', service(HttpClientInterface::class)); 39 | } else { 40 | $abstractBot 41 | ->call('setProxy', ['%boshurik_telegram_bot.api.proxy%']) 42 | ->call('setCurlOption', [\CURLOPT_TIMEOUT, '%boshurik_telegram_bot.api.timeout%']) 43 | ; 44 | } 45 | 46 | $services->set('boshurik_telegram_bot.api.bot_locator', ServiceLocator::class) 47 | ->args([[]]) 48 | ->tag('container.service_locator') 49 | ; 50 | 51 | $services->set(BotLocator::class) 52 | ->args([ 53 | service('boshurik_telegram_bot.api.bot_locator'), 54 | ]) 55 | ; 56 | 57 | $services->set('boshurik_telegram_bot.telegram', Telegram::class) 58 | ->args([ 59 | service(BotLocator::class), 60 | service('event_dispatcher'), 61 | ]); 62 | 63 | $services 64 | ->set('boshurik_telegram_bot.command.abstract_registry', CommandRegistry::class) 65 | ->abstract() 66 | ; 67 | 68 | $services->set('boshurik_telegram_bot.command.registry_locator', ServiceLocator::class) 69 | ->args([[]]) 70 | ->tag('container.service_locator') 71 | ; 72 | 73 | $services->set('boshurik_telegram_bot.command.registries', CommandRegistryLocator::class) 74 | ->args([ 75 | service('boshurik_telegram_bot.command.registry_locator'), 76 | ]) 77 | ; 78 | 79 | $services->set('boshurik_telegram_bot.command.listener', CommandListener::class) 80 | ->args([ 81 | service(BotLocator::class), 82 | service('boshurik_telegram_bot.command.registries'), 83 | ]) 84 | ->tag('kernel.event_subscriber'); 85 | 86 | $services->set(WebhookController::class) 87 | ->public() 88 | ->args([ 89 | service('boshurik_telegram_bot.telegram'), 90 | service('event_dispatcher'), 91 | service('messenger.default_bus')->nullOnInvalid(), 92 | ]); 93 | 94 | $services->set('boshurik_telegram_bot.command.updates', UpdatesCommand::class) 95 | ->args([ 96 | service('boshurik_telegram_bot.telegram'), 97 | ]) 98 | ->tag('console.command'); 99 | 100 | $services->set('boshurik_telegram_bot.command.webhook.set', SetCommand::class) 101 | ->args([ 102 | service(BotLocator::class), 103 | service(UrlGeneratorInterface::class), 104 | ]) 105 | ->tag('console.command'); 106 | 107 | $services->set('boshurik_telegram_bot.command.webhook.unset', UnsetCommand::class) 108 | ->args([ 109 | service(BotLocator::class), 110 | ]) 111 | ->tag('console.command'); 112 | 113 | $services->set('boshurik_telegram_bot.command.webhook.info', InfoCommand::class) 114 | ->args([ 115 | service(BotLocator::class), 116 | ]) 117 | ->tag('console.command'); 118 | 119 | $services->set('boshurik_telegram_bot.messenger.handler', MessageHandler::class) 120 | ->args([ 121 | service('boshurik_telegram_bot.telegram'), 122 | ]) 123 | ->tag('messenger.message_handler'); 124 | }; 125 | -------------------------------------------------------------------------------- /src/Telegram/BotLocator.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 BoShurik\TelegramBotBundle\Telegram; 13 | 14 | use Symfony\Contracts\Service\ServiceProviderInterface; 15 | use TelegramBot\Api\BotApi; 16 | 17 | final class BotLocator 18 | { 19 | public function __construct(private ServiceProviderInterface $locator) 20 | { 21 | } 22 | 23 | public function get(string $bot): BotApi 24 | { 25 | $api = $this->locator->get($bot); 26 | if (!$api instanceof BotApi) { 27 | throw new \RuntimeException(sprintf('Expect "%s", instance of "%s" given', BotApi::class, $api::class)); 28 | } 29 | 30 | return $api; 31 | } 32 | 33 | /** 34 | * @return \Generator 35 | */ 36 | public function all(): \Generator 37 | { 38 | foreach ($this->locator->getProvidedServices() as $name => $bot) { 39 | yield $name => $this->get($name); 40 | } 41 | } 42 | 43 | public function isSingle(): bool 44 | { 45 | return \count($this->locator->getProvidedServices()) === 1; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Telegram/Command/AbstractCommand.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 BoShurik\TelegramBotBundle\Telegram\Command; 13 | 14 | use TelegramBot\Api\Types\Update; 15 | 16 | abstract class AbstractCommand implements CommandInterface 17 | { 18 | protected const TARGET_MESSAGE = 1; 19 | protected const TARGET_CALLBACK = 2; 20 | protected const TARGET_ALL = -1; 21 | 22 | /** 23 | * RegExp for bot commands 24 | */ 25 | public const REGEXP = '/^([^\s@]+)(@\S+)?\s?(.*)$/'; 26 | 27 | abstract public function getName(): string; 28 | 29 | public function getAliases(): array 30 | { 31 | return []; 32 | } 33 | 34 | public function isApplicable(Update $update): bool 35 | { 36 | $callbackQuery = $update->getCallbackQuery(); 37 | if ($this->isTargetCallback() && $callbackQuery) { 38 | $data = $callbackQuery->getData(); 39 | if ($this->matchCommandName((string) $data, $this->getName())) { 40 | return true; 41 | } 42 | 43 | return false; 44 | } 45 | $message = $update->getMessage(); 46 | if ($this->isTargetMessage() && $message) { 47 | if ($this->matchCommandName((string) $message->getText(), $this->getName())) { 48 | return true; 49 | } 50 | 51 | foreach ($this->getAliases() as $alias) { 52 | if ($this->matchCommandName((string) $message->getText(), $alias)) { 53 | return true; 54 | } 55 | } 56 | 57 | return false; 58 | } 59 | 60 | return false; 61 | } 62 | 63 | protected function getTarget(): int 64 | { 65 | return self::TARGET_MESSAGE; 66 | } 67 | 68 | protected function matchCommandName(string $text, string $name): bool 69 | { 70 | preg_match(self::REGEXP, $text, $matches); 71 | 72 | return !empty($matches) && $matches[1] == $name; 73 | } 74 | 75 | protected function getCommandParameters(Update $update): ?string 76 | { 77 | $matches = []; 78 | $callbackQuery = $update->getCallbackQuery(); 79 | if ($this->isTargetCallback() && $callbackQuery) { 80 | preg_match(self::REGEXP, (string) $callbackQuery->getData(), $matches); 81 | } 82 | $message = $update->getMessage(); 83 | if ($this->isTargetMessage() && $message) { 84 | preg_match(self::REGEXP, (string) $message->getText(), $matches); 85 | } 86 | 87 | if (empty($matches)) { 88 | return null; 89 | } 90 | 91 | return $matches[3] !== '' ? $matches[3] : null; 92 | } 93 | 94 | private function isTargetMessage(): bool 95 | { 96 | return ($this->getTarget() & self::TARGET_MESSAGE) === self::TARGET_MESSAGE; 97 | } 98 | 99 | private function isTargetCallback(): bool 100 | { 101 | return ($this->getTarget() & self::TARGET_CALLBACK) === self::TARGET_CALLBACK; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Telegram/Command/CommandInterface.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 BoShurik\TelegramBotBundle\Telegram\Command; 13 | 14 | use TelegramBot\Api\BotApi; 15 | use TelegramBot\Api\Types\Update; 16 | 17 | interface CommandInterface 18 | { 19 | public function execute(BotApi $api, Update $update): void; 20 | 21 | public function isApplicable(Update $update): bool; 22 | } 23 | -------------------------------------------------------------------------------- /src/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 BoShurik\TelegramBotBundle\Telegram\Command; 13 | 14 | use BoShurik\TelegramBotBundle\Telegram\Command\Registry\CommandRegistry; 15 | use TelegramBot\Api\BotApi; 16 | use TelegramBot\Api\Types\Message; 17 | use TelegramBot\Api\Types\Update; 18 | 19 | class HelpCommand extends AbstractCommand implements PublicCommandInterface 20 | { 21 | public function __construct( 22 | private CommandRegistry $commandRegistry, 23 | private string $description = 'Help', 24 | private array $aliases = [] 25 | ) { 26 | } 27 | 28 | public function execute(BotApi $api, Update $update): void 29 | { 30 | $commands = $this->commandRegistry->getCommands(); 31 | /** @var Message $message */ 32 | $message = $update->getMessage(); 33 | 34 | $reply = ''; 35 | foreach ($commands as $command) { 36 | if (!$command instanceof PublicCommandInterface) { 37 | continue; 38 | } 39 | 40 | $reply .= sprintf("%s - %s\n", $command->getName(), $command->getDescription()); 41 | } 42 | 43 | $api->sendMessage($message->getChat()->getId(), $reply); 44 | } 45 | 46 | public function getName(): string 47 | { 48 | return '/help'; 49 | } 50 | 51 | public function getAliases(): array 52 | { 53 | return $this->aliases; 54 | } 55 | 56 | public function getDescription(): string 57 | { 58 | return $this->description; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Telegram/Command/PublicCommandInterface.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 BoShurik\TelegramBotBundle\Telegram\Command; 13 | 14 | interface PublicCommandInterface extends CommandInterface 15 | { 16 | public function getName(): string; 17 | 18 | public function getDescription(): string; 19 | } 20 | -------------------------------------------------------------------------------- /src/Telegram/Command/Registry/CommandRegistry.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 BoShurik\TelegramBotBundle\Telegram\Command\Registry; 13 | 14 | use BoShurik\TelegramBotBundle\Telegram\Command\CommandInterface; 15 | 16 | final class CommandRegistry 17 | { 18 | /** 19 | * @var CommandInterface[] 20 | */ 21 | private array $commands; 22 | 23 | public function __construct() 24 | { 25 | $this->commands = []; 26 | } 27 | 28 | public function addCommand(CommandInterface $command): void 29 | { 30 | $this->commands[] = $command; 31 | } 32 | 33 | /** 34 | * @return CommandInterface[] 35 | */ 36 | public function getCommands(): array 37 | { 38 | return $this->commands; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Telegram/Command/Registry/CommandRegistryLocator.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 BoShurik\TelegramBotBundle\Telegram\Command\Registry; 13 | 14 | use Symfony\Contracts\Service\ServiceProviderInterface; 15 | 16 | final class CommandRegistryLocator 17 | { 18 | public function __construct(private ServiceProviderInterface $locator) 19 | { 20 | } 21 | 22 | public function get(string $bot): CommandRegistry 23 | { 24 | $registry = $this->locator->get($bot); 25 | if (!$registry instanceof CommandRegistry) { 26 | throw new \RuntimeException(sprintf('Expect "%s", instance of "%s" given', CommandRegistry::class, $registry::class)); 27 | } 28 | 29 | return $registry; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Telegram/Telegram.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 BoShurik\TelegramBotBundle\Telegram; 13 | 14 | use BoShurik\TelegramBotBundle\Event\UpdateEvent; 15 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 16 | use TelegramBot\Api\Types\Update; 17 | 18 | /** 19 | * @final 20 | */ 21 | /* final */ class Telegram 22 | { 23 | public function __construct( 24 | private BotLocator $botLocator, 25 | private EventDispatcherInterface $eventDispatcher 26 | ) { 27 | } 28 | 29 | public function processAllUpdates(): void 30 | { 31 | foreach ($this->botLocator->all() as $name => $id) { 32 | $this->processUpdates($name); 33 | } 34 | } 35 | 36 | public function processUpdates(string $bot): void 37 | { 38 | $api = $this->botLocator->get($bot); 39 | 40 | $updates = $api->getUpdates(); 41 | 42 | $lastUpdateId = null; 43 | foreach ($updates as $update) { 44 | $lastUpdateId = $update->getUpdateId(); 45 | $this->processUpdate($bot, $update); 46 | } 47 | 48 | if ($lastUpdateId) { 49 | $api->getUpdates($lastUpdateId + 1, 1); 50 | } 51 | } 52 | 53 | public function processUpdate(string $bot, Update $update): void 54 | { 55 | $event = new UpdateEvent($bot, $update); 56 | $this->eventDispatcher->dispatch($event); 57 | } 58 | } 59 | --------------------------------------------------------------------------------