Repository: php-telegram-bot/laravel Branch: main Commit: f0ba8e0018ac Files: 44 Total size: 68.8 KB Directory structure: gitextract_qf0nzb7c/ ├── .gitignore ├── .styleci.yml ├── changelog.md ├── composer.json ├── config/ │ └── telegram.php ├── database/ │ └── migrations/ │ ├── 2021_06_14_171118_create_telegram_bot_structure.php │ ├── 2022_02_18_175100_update_to_0.75.0.php │ ├── 2022_04_24_175700_update_to_0.77.0.php │ ├── 2022_10_04_221900_update_to_0.78.0.php │ ├── 2022_11_11_130500_update_to_0.80.0.php │ ├── 2023_05-07_101600_update_to_0.81.0.php │ └── sql/ │ ├── 0.74.0-0.75.0.sql │ ├── 0.76.1-0.77.0.sql │ ├── 0.77.1-0.78.0.sql │ ├── 0.79.0-0.80.0.sql │ ├── 0.80.0-0.81.0.sql │ └── structure-0.73.0.sql ├── license.md ├── phpunit.xml ├── readme.md ├── routes/ │ └── telegram.php └── src/ ├── Console/ │ └── Commands/ │ ├── GeneratorCommand.php │ ├── MakeTelegramCommand.php │ ├── TelegramCloseCommand.php │ ├── TelegramDeleteWebhookCommand.php │ ├── TelegramFetchCommand.php │ ├── TelegramLogoutCommand.php │ ├── TelegramPublishCommand.php │ ├── TelegramSetWebhookCommand.php │ └── stubs/ │ ├── example-start-command.stub │ └── telegram-command.stub ├── Facades/ │ ├── CallbackButton.php │ └── Telegram.php ├── Factories/ │ └── CallbackButton.php ├── Http/ │ └── Middleware/ │ └── TrustTelegramNetwork.php ├── LaravelTelegramBot.php ├── Telegram/ │ ├── Commands/ │ │ ├── CallbackqueryCommand.php │ │ └── GenericmessageCommand.php │ ├── Conversation/ │ │ ├── ConversationWrapper.php │ │ └── LeadsConversation.php │ ├── InlineKeyboardButton/ │ │ ├── CallbackPayload.php │ │ └── RemembersCallbackPayload.php │ └── UsesEffectiveEntities.php └── TelegramServiceProvider.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Created by https://www.toptal.com/developers/gitignore/api/laravel,composer # Edit at https://www.toptal.com/developers/gitignore?templates=laravel,composer ### Composer ### composer.phar /vendor/ # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file composer.lock ### Laravel ### node_modules/ npm-debug.log yarn-error.log # Laravel 4 specific bootstrap/compiled.php app/storage/ # Laravel 5 & Lumen specific public/storage public/hot # Laravel 5 & Lumen specific with changed public path public_html/storage public_html/hot storage/*.key .env Homestead.yaml Homestead.json /.vagrant .phpunit.result.cache # Laravel IDE helper *.meta.* _ide_* # End of https://www.toptal.com/developers/gitignore/api/laravel,composer /.idea ================================================ FILE: .styleci.yml ================================================ preset: laravel ================================================ FILE: changelog.md ================================================ # Changelog All notable changes to `LaravelTelegramBot` will be documented in this file. ## Version 2.1.0 ### Added - Support for Laravel 10 ### Changed - Bump to core version 0.81.0 ## Version 2.0.0 ### Added - Everything ================================================ FILE: composer.json ================================================ { "name": "php-telegram-bot/laravel", "description": "Integrates PHP Telegram Bot into Laravel.", "license": "MIT", "authors": [ { "name": "Avtandil Kikabidze aka LONGMAN", "email": "akalongman@gmail.com", "homepage": "http://longman.me", "role": "Maintainer, Developer" }, { "name": "Tii", "email": "mail@tii.one", "role": "Developer" } ], "homepage": "https://github.com/php-telegram-bot/laravel", "keywords": ["laravel", "telegram", "bot"], "require": { "illuminate/support": "~7|~8|~9|~10|~11|~12", "longman/telegram-bot": "^0.81", "ext-pcntl": "*" }, "require-dev": { "phpunit/phpunit": "~9.0", "symfony/process": "^5.3" }, "autoload": { "psr-4": { "PhpTelegramBot\\Laravel\\": "src/" } }, "extra": { "branch-alias": { "dev-main": "2.1.x-dev" }, "laravel": { "providers": [ "PhpTelegramBot\\Laravel\\TelegramServiceProvider" ], "aliases": { "CallbackButton": "PhpTelegramBot\\Laravel\\Facades\\CallbackButton", "Telegram": "PhpTelegramBot\\Laravel\\Facades\\Telegram" } } }, "minimum-stability": "dev", "prefer-stable": true, "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true } } } ================================================ FILE: config/telegram.php ================================================ [ 'api_token' => env('TELEGRAM_API_TOKEN'), 'username' => env('TELEGRAM_BOT_USERNAME', ''), 'api_url' => env('TELEGRAM_API_URL'), ], 'admins' => env('TELEGRAM_ADMINS', '') ]; ================================================ FILE: database/migrations/2021_06_14_171118_create_telegram_bot_structure.php ================================================ ./tests/ src/ ================================================ FILE: readme.md ================================================ # LaravelTelegramBot [![Latest Version on Packagist][ico-version]][link-packagist] [![Total Downloads][ico-downloads]][link-downloads] ## Installation Install this package through Composer. Run this command in your project's terminal: ``` bash composer require php-telegram-bot/laravel ``` Execute the following command to publish the folder structure to your Laravel application: ```bash php artisan telegram:publish ``` This also includes a dummy `/start` command to give you a quick start. Since we're using the database part of php-telegram-bot you should run the migrations so the database schema gets installed: ```bash php artisan migrate ``` And add the following lines to your .env file: ```dotenv TELEGRAM_API_TOKEN= TELEGRAM_BOT_USERNAME= TELEGRAM_API_URL= TELEGRAM_ADMINS= ``` `TELEGRAM_API_TOKEN` and `TELEGRAM_BOT_USERNAME` should be filled with the corresponding data from [@BotFather](https://t.me/BotFather) `TELEGRAM_API_URL` is optional and can be filled with the URL to your [custom Bot API Server](https://core.telegram.org/bots/api#using-a-local-bot-api-server) if you want to use one. `TELEGRAM_ADMINS` is optional and a comma-separated list of Telegram User IDs that gets passed to the `enableAdmins` command of php-telegram-bot to enable admin commands for those users. After that you can run `php artisan telegram:set-webhook` if your development server is reachable from the outside or you're using a custom bot api server. Or `php artisan telegram:fetch` to start fetching your updates via polling. ⚠️ Be aware that you have to cancel and restart the `telegram:fetch` command, if you change your code. ## Usage For further basic configuration of this Laravel package you do not need to create any configuration files. Artisan terminal commands for the Webhook usage (remember, that you need an HTTPS server for it): ``` bash # Use this method to specify a url and receive incoming updates via an outgoing webhook php artisan telegram:set-webhook # List of available options: # --d|drop-pending-updates : Drop all pending updates # --a|all-update-types : Explicitly allow all updates (including "chat_member") # --allowed-updates= : Define allowed updates (comma-seperated) # Use this method to remove webhook integration if you decide to switch back to getUpdates php artisan telegram:delete-webhook # List of available options: # --d|drop-pending-updates : Pass to drop all pending updates ``` Artisan terminal commands for the Telegram getUpdates method: ``` bash # Fetches Telegram updates periodically php artisan telegram:fetch # List of available options: # --a|all-update-types : Explicitly allow all updates (including "chat_member") # --allowed-updates= : Define allowed updates (comma-seperated) ``` Artisan terminal command for Telegram Server logging out: ``` bash # Sends a logout to the currently registered Telegram Server php artisan telegram:logout ``` Artisan terminal command for closing Telegram Server: ``` bash # Sends a close to the currently registered Telegram Server php artisan telegram:close ``` Artisan terminal command for publishing Telegram command folder structure in your project: ``` bash # Publishes folder structure for Telegram Commands # Default StartCommand class will be created php artisan telegram:publish ``` Artisan terminal command for creating new Telegram command class in your project: ``` bash # Create a new Telegram Bot Command class # e.g. php artisan make:telegram-command Menu --> will make User command class MenuCommand # e.g. php artisan make:telegram-command Genericmessage --system --> will make System command class GenericmessageCommand php artisan make:telegram-command # List of available options: # name : Name of the Telegram Command # --a|admin : Generate a AdminCommand # --s|system : Generate a SystemCommand # Without admin or system option default User command will be created ``` ## Credits - [Avtandil Kikabidze aka LONGMAN](https://github.com/akalongman) - [TiiFuchs](https://github.com/TiiFuchs) - [All Contributors][link-contributors] ## License Please see the [license file](license.md) for more information. [ico-version]: https://img.shields.io/packagist/v/php-telegram-bot/laravel.svg?style=flat-square [ico-downloads]: https://img.shields.io/packagist/dt/php-telegram-bot/laravel.svg?style=flat-square [link-packagist]: https://packagist.org/packages/php-telegram-bot/laravel [link-downloads]: https://packagist.org/packages/php-telegram-bot/laravel [link-contributors]: https://github.com/php-telegram-bot/laravel/contributors ================================================ FILE: routes/telegram.php ================================================ handle(); })->middleware('telegram.network')->name('telegram.webhook'); ================================================ FILE: src/Console/Commands/GeneratorCommand.php ================================================ error("{$basename} already exists!"); return false; } $content = file_get_contents($source); $content = $this->replacePlaceholder($content, $replacements); file_put_contents($destination, $content); return true; } protected function replacePlaceholder(string $content, array $replacements): string { foreach ($replacements as $from => $to) { $content = str_replace($from, $to, $content); } return $content; } protected function getRootNamespace(): string { return rtrim($this->laravel->getNamespace(), '\\'); } } ================================================ FILE: src/Console/Commands/MakeTelegramCommand.php ================================================ argument('name'); // start if (Str::endsWith($name, ['Command', 'command'])) { $name = (string) Str::of($name)->substr(0, -7)->lower(); } else { $name = Str::lower($name); } $class = Str::studly($name) . 'Command'; $success = $this->publish( __DIR__ . '/stubs/telegram-command.stub', app_path("Telegram/Commands/{$class}.php"), [ 'DummyNamespace' => $this->getRootNamespace(), 'DummyParent' => $this->getParentClassName(), 'DummyClass' => $class, '{{name}}' => $name ] ); if ($success) { $this->info('Telegram Command created successfully'); } } protected function getParentClassName() { if ($this->option('admin')) { return 'AdminCommand'; } if ($this->option('system')) { return 'SystemCommand'; } return 'UserCommand'; } } ================================================ FILE: src/Console/Commands/TelegramCloseCommand.php ================================================ isOk()) { $this->error($response->getDescription()); } $this->info($response->getDescription()); } } ================================================ FILE: src/Console/Commands/TelegramDeleteWebhookCommand.php ================================================ option('drop-pending-updates')) { $options['drop_pending_updates'] = true; } $response = Request::deleteWebhook($options); if (! $response->isOk()) { $this->error($response->getDescription()); } $this->info($response->getDescription()); } } ================================================ FILE: src/Console/Commands/TelegramFetchCommand.php ================================================ callSilent('telegram:delete-webhook'); $options = [ 'timeout' => 30 ]; // allowed_updates if ($this->option('all-update-types')) { $options['allowed_updates'] = Update::getUpdateTypes(); } elseif ($allowedUpdates = $this->option('allowed-updates')) { $options['allowed_updates'] = Str::of($allowedUpdates)->explode(','); } $this->info("Start fetching updates...\n(Exit with Ctrl + C)"); if ($this->childPid = pcntl_fork()) { // Parent process while (true) { if ($this->shallExit) { exec('kill -9 ' . $this->childPid); break; } } } else { // Child process while (true) { $response = rescue(fn() => $bot->handleGetUpdates($options)); if ($response !== null && ! $response->isOk()) { $this->error($response->getDescription()); } } } } public function getSubscribedSignals(): array { return [SIGINT]; } public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false { $this->shallExit = true; } } ================================================ FILE: src/Console/Commands/TelegramLogoutCommand.php ================================================ isOk()) { $this->error($response->getDescription()); } $this->info($response->getDescription()); } } ================================================ FILE: src/Console/Commands/TelegramPublishCommand.php ================================================ ensureDirectoryExists(app_path('Telegram/Commands')); $success = $this->publish( __DIR__ . '/stubs/example-start-command.stub', app_path('Telegram/Commands/StartCommand.php'), [ 'DummyRootNamespace' => $this->getRootNamespace(), ] ); if ($success) { $this->info('Publishing complete.'); } } } ================================================ FILE: src/Console/Commands/TelegramSetWebhookCommand.php ================================================ argument('hostname'); if (! $hostname) { $hostname = $this->ask('Which hostname do you like to set?', config('app.url')); } if (! Str::of($hostname)->startsWith('http')) { $schema = match (app()->environment()) { 'local' => 'http', default => 'https' }; $hostname = "{$schema}://{$hostname}"; } $url = $hostname . route('telegram.webhook', [ 'token' => config('telegram.bot.api_token') ], false); $options = []; if ($this->option('drop-pending-updates')) { $options['drop_pending_updates'] = true; } if ($this->option('all-update-types')) { $options['allowed_updates'] = Update::getUpdateTypes(); } elseif ($allowedUpdates = $this->option('allowed-updates')) { $options['allowed_updates'] = Str::of($allowedUpdates)->explode(','); } $response = $bot->setWebhook($url, $options); if (! $response->isOk()) { $this->error($response->getDescription()); } $this->info("Telegram Webhook set to {$url}"); } } ================================================ FILE: src/Console/Commands/stubs/example-start-command.stub ================================================ replyToChat('Hello world! 👋'); } } ================================================ FILE: src/Console/Commands/stubs/telegram-command.stub ================================================ withXxxxx($value) if (Str::startsWith($name, 'with')) { $key = (string) Str::of($name)->after('with')->snake(); $value = head($arguments); return $this->with($key, $value); } throw new \BadMethodCallException("Call to undefined method CallbackButton::{$name}()"); } public function with(string $key, mixed $value): self { $this->data[$key] = $value; return $this; } public function new(): self { return clone $this; } public function returnTo(string $className): self { $this->className = $className; return $this; } public function make(string $text, array $payload = []): InlineKeyboardButton { // Find valid hash do { $hash = Str::random(32); $cacheKey = 'CallbackQuery:'.$hash; } while (Cache::has($cacheKey)); // Save payload $payload = $payload + $this->data; if (isset($this->className)) { $payload['__class'] = $this->className; } Cache::put($cacheKey, $payload); // Assemble button return new InlineKeyboardButton([ 'text' => $text, 'callback_data' => $hash ]); } } ================================================ FILE: src/Http/Middleware/TrustTelegramNetwork.php ================================================ ip(), $this->localIpNets)) { return $next($request); } if (IpUtils::checkIp($request->ip(), $this->trustedIpNets)) { return $next($request); } abort(403); } } ================================================ FILE: src/LaravelTelegramBot.php ================================================ callbacks[] = $callback; } public function call(Update $update): ?ServerResponse { foreach ($this->callbacks as $callback) { $return = $callback($update); if ($return instanceof ServerResponse) { return $return; } elseif ($return === true) { return Request::emptyResponse(); } } return null; } } ================================================ FILE: src/Telegram/Commands/CallbackqueryCommand.php ================================================ getUpdate()); if ($return instanceof ServerResponse) { return $return; } // Check if we have data for that hash in the Cache if ($class = $this->payload()?->get('__class')) { if (class_exists($class) && is_subclass_of($class, Command::class)) { /** @var Command $command */ $command = new $class($this->telegram, $this->update); return $command->preExecute(); } } // Check if conversation is active $user = $this->getEffectiveUser($this->getUpdate()); $chat = $this->getEffectiveChat($this->getUpdate()); $conversation = new Conversation( user_id: $user->getId(), chat_id: $chat->getId() ); if ($conversation->exists() && ($command = $conversation->getCommand())) { return $this->getTelegram()->executeCommand($command); } // Check if own CallbackqueryCommand class is available $class = App::getNamespace() . 'Telegram\\Commands\\CallbackqueryCommand'; if (class_exists($class) && is_subclass_of($class, SystemCommand::class)) { /** @var SystemCommand $command */ $command = new $class($this->telegram, $this->update); return $command->preExecute(); } return Request::emptyResponse(); } } ================================================ FILE: src/Telegram/Commands/GenericmessageCommand.php ================================================ getUpdate()); if ($return instanceof ServerResponse) { return $return; } $user = $this->getEffectiveUser($this->getUpdate()); $chat = $this->getEffectiveChat($this->getUpdate()); // Check Conversation $conversation = new Conversation( user_id: $user->getId(), chat_id: $chat->getId() ); if ($conversation->exists() && ($command = $conversation->getCommand())) { return $this->getTelegram()->executeCommand($command); } // Check if own GenericmessageCommand class is available $class = App::getNamespace() . 'Telegram\\Commands\\GenericmessageCommand'; if (class_exists($class) && is_subclass_of($class, SystemCommand::class)) { /** @var SystemCommand $command */ $command = new $class($this->telegram, $this->update); return $command->preExecute(); } return Request::emptyResponse(); } } ================================================ FILE: src/Telegram/Conversation/ConversationWrapper.php ================================================ getMessage() ?? $update->getEditedMessage()) { $user = $message->getFrom(); $chat = $message->getChat(); } elseif ($callbackQuery = $update->getCallbackQuery()) { $user = $callbackQuery->getFrom(); $chat = $callbackQuery->getMessage()?->getChat(); } // TODO: Use getEffective*() Methods that should be created in \Bot Facade if (! isset($user) || ! isset($chat)) { throw new \InvalidArgumentException('Could not determine user or chat for ConversationWrapper'); } $this->conversation = new Conversation( user_id: $user->getId(), chat_id: $chat->getId(), command: $command ); $notes = &$this->conversation->notes; $notes['vars'] ??= []; $notes['persist'] ??= []; if ($this->conversation->exists()) { // Remove temporary variables foreach ($notes['vars'] as $key => $value) { if (array_search($key, $notes['persist']) === false) { // Is temporary $this->temporary[$key] = $value; unset($notes['vars'][$key]); } } $this->conversation->update(); } } public function all(): array { return $this->conversation->notes['vars'] + $this->temporary; } public function get(string $key, string $default = null): mixed { return data_get($this->conversation->notes['vars'], $key) ?? data_get($this->temporary, $key, $default); } public function has(string $key): bool { return $this->get($key) !== null; } public function getConversation(): Conversation { return $this->conversation; } public function persist(array $data): self { $notes = &$this->conversation->notes; foreach ($data as $key => $value) { $notes['vars'][$key] = $value; $notes['persist'][] = $key; } $this->conversation->update(); return $this; } public function remember(array $data = [], bool $keepPreviousData = false): self { $notes = &$this->conversation->notes; if ($keepPreviousData) { foreach ($this->temporary as $key => $value) { $notes['vars'][$key] = $value; } } foreach ($data as $key => $value) { $notes['vars'][$key] = $value; $index = array_search($key, $notes['persist']); if ($index !== false) { unset($notes['persist'][$index]); } } $notes['persist'] = array_values($notes['persist']); $this->conversation->update(); return $this; } public function exists(): bool { return $this->conversation->exists(); } public function end(): void { $this->conversation->stop(); } public function cancel(): void { $this->conversation->cancel(); } } ================================================ FILE: src/Telegram/Conversation/LeadsConversation.php ================================================ conversation)) { $this->conversation = new ConversationWrapper($this->getUpdate(), $this->getName()); } if (isset($key)) { return $this->conversation->get($key, $default); } return $this->conversation; } } ================================================ FILE: src/Telegram/InlineKeyboardButton/CallbackPayload.php ================================================ payload; } public function get(string $key, string $default = null): mixed { return data_get($this->payload, $key, $default); } public function has(string $key): bool { return $this->get($key) !== null; } } ================================================ FILE: src/Telegram/InlineKeyboardButton/RemembersCallbackPayload.php ================================================ payload)) { $update = $this->getUpdate(); $data = $update?->getCallbackQuery()?->getData(); if ($data === null) { return null; } // TODO: Move CacheKey and initialization in CallbackPayload::__construct() $cacheKey = 'CallbackQuery:'.$data; if (! Cache::has($cacheKey)) { return null; } $payload = Cache::get($cacheKey); $this->payload = new CallbackPayload($payload); } if (isset($key)) { return $this->payload->get($key, $default); } return $this->payload; } } ================================================ FILE: src/Telegram/UsesEffectiveEntities.php ================================================ getUpdateType(); $user = $update->$type['from'] ?? $update->poll_answer['user'] ?? null; return $user ? new User($user) : null; } protected function getEffectiveChat(Update $update): ?Chat { $type = $update->getUpdateType(); $chat = $update->$type['chat'] ?? $update->callback_query['message']['chat'] ?? null; return $chat ? new Chat($chat) : null; } protected function getEffectiveMessage(Update $update): ?Message { $message = $update->edited_channel_post ?? $update->channel_post ?? $update->callback_query['message'] ?? $update->edited_message ?? $update->message ?? null; return $message ? new Message($message) : null; } } ================================================ FILE: src/TelegramServiceProvider.php ================================================ loadMigrationsFrom(__DIR__ . '/../database/migrations'); if (file_exists(base_path('routes/telegram.php'))) { $this->loadRoutesFrom(base_path('routes/telegram.php')); } else { $this->loadRoutesFrom(__DIR__ . '/../routes/telegram.php'); } $router = $this->app->make(Router::class); $router->aliasMiddleware('telegram.network', TrustTelegramNetwork::class); // Publishing is only necessary when using the CLI. if ($this->app->runningInConsole()) { $this->bootForConsole(); } } /** * Register any package services. * * @return void */ public function register(): void { $this->mergeConfigFrom(__DIR__ . '/../config/telegram.php', 'telegram'); $this->configureTelegramBot(); } protected function configureTelegramBot() { $token = config('telegram.bot.api_token'); if (! $token) { return; } $username = config('telegram.bot.username'); $apiUrl = config('telegram.bot.api_url', ''); if (! empty($apiUrl)) { Request::setCustomBotApiUri($apiUrl); } $bot = new Telegram($token, $username); // Commands Discovery $this->discoverTelegramCommands($bot); $bot->addCommandClass(CallbackqueryCommand::class); $bot->addCommandClass(GenericmessageCommand::class); // Set MySQL Connection $connection = app('db')->connection('mysql'); $bot->enableExternalMySql($connection->getPdo(), 'bot_'); // Register admins $this->registerTelegramAdmins($bot); $this->app->instance(Telegram::class, $bot); } /** * Console-specific booting. * * @return void */ protected function bootForConsole(): void { // Publishing the configuration file. $this->publishes([ __DIR__ . '/../config/telegram.php' => config_path('telegram.php'), ], 'telegram-config'); $this->publishes([ __DIR__ . '/../routes/telegram.php' => base_path('routes/telegram.php') ], 'telegram-routes'); // Registering package commands. $this->commands([ MakeTelegramCommand::class, TelegramCloseCommand::class, TelegramDeleteWebhookCommand::class, TelegramFetchCommand::class, TelegramLogoutCommand::class, TelegramPublishCommand::class, TelegramSetWebhookCommand::class, ]); } /** * @param Telegram $bot * @throws \ReflectionException */ protected function discoverTelegramCommands(Telegram $bot): void { $namespace = $this->app->getNamespace(); $commandsPath = app_path('Telegram/Commands'); File::ensureDirectoryExists($commandsPath); foreach ((new Finder)->in($commandsPath)->files() as $command) { $command = $namespace . str_replace( ['/', '.php'], ['\\', ''], \Str::after($command->getRealPath(), realpath(app_path()) . DIRECTORY_SEPARATOR) ); if (is_subclass_of($command, Command::class) && ! (new \ReflectionClass($command))->isAbstract()) { $bot->addCommandClass($command); } } } /** * @param Telegram $bot */ protected function registerTelegramAdmins(Telegram $bot): void { $admins = config('telegram.admins', ''); if (! empty($admins)) { $admins = explode(',', $admins); $bot->enableAdmins($admins); } } }