├── .env.example ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commands ├── .gitkeep ├── CallbackqueryCommand.php ├── ChannelpostCommand.php ├── DonateCommand.php ├── GenericmessageCommand.php ├── HelpCommand.php ├── IdCommand.php ├── NewchatmembersCommand.php ├── PreCheckoutQueryCommand.php ├── RulesCommand.php └── StartCommand.php ├── composer.json ├── composer.lock ├── cron.php ├── logs └── .gitkeep ├── phpcs.xml.dist ├── public ├── manager.php └── webhooks │ └── github.php ├── src ├── .gitkeep ├── Helpers.php └── Webhooks │ └── Utils.php └── structure.sql /.env.example: -------------------------------------------------------------------------------- 1 | # Environment 2 | TG_ENV='development' 3 | TG_AUTOUPDATE=1 4 | 5 | # Directories 6 | TG_BASE_DIR='/var/www/tg-support-bot' 7 | TG_CACHE_DIR='${TG_BASE_DIR}/cache' 8 | TG_COMMANDS_DIR='${TG_BASE_DIR}/commands' 9 | TG_LOGS_DIR='${TG_BASE_DIR}/logs' 10 | TG_PUBLIC_DIR='${TG_BASE_DIR}/public' 11 | TG_DOWNLOADS_DIR='${TG_BASE_DIR}/downloads' 12 | TG_UPLOADS_DIR='${TG_BASE_DIR}/uploads' 13 | 14 | # Bot vitals 15 | TG_API_KEY='12345:api_key' 16 | TG_BOT_USERNAME='my_cool_bot' 17 | TG_SECRET='super-secret' 18 | 19 | # Bot extras 20 | TG_WEBHOOK='{"url": "https://bot.com/manager.php"}' 21 | TG_ADMINS='[123,456]' 22 | TG_LOGGING='{"error": "${TG_LOGS_DIR}/error.log", "update": "${TG_LOGS_DIR}/update.log"}' 23 | 24 | # Request Client 25 | TG_REQUEST_CLIENT_BASE_URI='https://client-base-uri.com' 26 | TG_REQUEST_CLIENT_PROXY='socks5://client-proxy.com:9050' 27 | 28 | # Database 29 | TG_DB_HOST='localhost' 30 | TG_DB_PORT=3306 31 | TG_DB_USER='root' 32 | TG_DB_PASSWORD='root' 33 | TG_DB_DATABASE='tgbot' 34 | 35 | # Paths 36 | TG_COMMANDS='{"paths": ["${TG_COMMANDS_DIR}"]}' 37 | TG_PATHS='{"download": "${TG_DOWNLOADS_DIR}", "upload": "${TG_UPLOADS_DIR}"}' 38 | 39 | # Webhook secrets 40 | TG_WEBHOOK_SECRET_GITHUB='github-secret' 41 | 42 | # GitHub API token 43 | TG_GITHUB_AUTH_USER='github-user' 44 | TG_GITHUB_AUTH_TOKEN='github-token' 45 | 46 | # Telegram Payments 47 | TG_PAYMENT_PROVIDER_TOKEN='123:TEST:abc' 48 | 49 | # Support group 50 | TG_SUPPORT_GROUP_ID='-12345' 51 | TG_SUPPORT_GROUP_USERNAME='Support_Group' 52 | TG_SUPPORT_GROUP_ACTIVATION_EXPIRE_TIME='15 min' 53 | TG_SUPPORT_GROUP_BAN_TIME='1 day' 54 | 55 | # URLs 56 | TG_URL_DONATE='https://...' 57 | TG_URL_PATREON='https://...' 58 | TG_URL_TIDELIFT='https://...' 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /logs/* 3 | !/logs/.gitkeep 4 | /vendor/ 5 | /.env 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 3 | 4 | Exclamation symbols (:exclamation:) note something of importance e.g. breaking changes. Click them to learn more. 5 | 6 | ## [Unreleased] 7 | ### Added 8 | ### Changed 9 | ### Deprecated 10 | ### Removed 11 | ### Fixed 12 | ### Security 13 | 14 | ## [0.14.0] - 2023-05-27 15 | ### Added 16 | - Notify group on Laravel package releases. 17 | ### Changed 18 | - Bump manager to 2.1. 19 | - Bump core to 0.81. 20 | ### Removed 21 | - Travis CI webhook. 22 | ### Security 23 | - Minimum PHP 8.1. 24 | 25 | ## [0.13.2] - 2022-06-04 26 | ### Changed 27 | - Bump core to 0.77.1 28 | ### Fixed 29 | - Bump service webhook handler 30 | 31 | ## [0.13.1] - 2022-05-27 32 | ### Fixed 33 | - Trigger the release note for `released` action instead of `published`. 34 | ### Security 35 | - Require Guzzle 7.4.3 and up. 36 | 37 | ## [0.13.0] - 2022-03-24 38 | ### Added 39 | - Rule regarding advertisements / job offers. 40 | - Notify about `php-telegram-bot/fluent-keyboard` releases. 41 | ### Changed 42 | - Bumped to core 0.76. 43 | 44 | ## [0.12.0] - 2021-12-29 45 | ### Added 46 | - Notify about `php-telegram-bot/inline-keyboard-pagination` releases. 47 | ### Changed 48 | - Bumped to core 0.75. 49 | 50 | ## [0.11.0] - 2021-07-09 51 | ### Changed 52 | - Bumped to core 0.74. 53 | ### Fixed 54 | - GitHub authentication. 55 | - Self-update. 56 | 57 | ## [0.10.0] - 2021-06-14 58 | ### Added 59 | - Rules notice to use Pastebin instead of posting code directly. 60 | ### Changed 61 | - Bumped to manager 1.7.0 and core 0.73. 62 | - Various code tweaks, make use of PHP 8. 63 | ### Security 64 | - Bumped dependencies. 65 | 66 | ## [0.9.0] - 2021-03-14 67 | ### Changed 68 | - Moved to PHP 8. 69 | - Bump to version 0.71 of core. 70 | 71 | ## [0.8.0] - 2021-01-01 72 | ### Added 73 | - Possibility to set custom Request Client. 74 | ### Changed 75 | - Bumped dependencies, use explicit version 0.70.1 of core. 76 | ### Fixed 77 | - Only kick users that haven't already been kicked. 78 | 79 | ## [0.7.0] - 2020-10-04 80 | ### Added 81 | - Rules must be agreed to before allowing a user to post in the group. (#43) 82 | ### Changed 83 | - Bumped dependencies, use explicit version 0.64.0 of core. 84 | ### Security 85 | - Minimum PHP 7.4. 86 | 87 | ## [0.6.0] - 2020-07-06 88 | ### Added 89 | - New `/donate` command, to allow users to donate via Telegram Payments. (#40) 90 | - GitHub authentication to prevent hitting limits. (#41) 91 | ### Changed 92 | - Link to the `/rules` command in the welcome message. (#42) 93 | 94 | ## [0.5.0] - 2019-11-24 95 | ### Added 96 | - Description for commands. (#35) 97 | - `/id` command, to help users find their user and chat information. (#36) 98 | ### Fixed 99 | - PSR12 compatibility. (#35) 100 | ### Security 101 | - Minimum PHP 7.3. (#35) 102 | - Use master branch of core library. (#35) 103 | 104 | ## [0.4.0] - 2019-08-01 105 | ### Changed 106 | - Only log a single welcome message deletion failure. (#34) 107 | ### Fixed 108 | - Deprecated system commands are now executed via `GenericmessageCommand`. (#33) 109 | 110 | ## [0.3.0] - 2019-07-30 111 | ### Added 112 | - Code checkers to ensure coding standard. (#30) 113 | - When releasing a new version of the Support Bot, automatically fetch the latest code and install with composer. (#31) 114 | - MySQL cache for GitHub client. (#32) 115 | ### Changed 116 | - Bumped Manager to 1.5. (#27) 117 | - Logging is now decoupled with custom Monolog logger. (#28, #29) 118 | 119 | ## [0.2.0] - 2019-06-01 120 | ### Changed 121 | - Bumped Manager to 1.4 122 | ### Fixed 123 | - Only post release message when a new release is actually "published". (#25) 124 | 125 | ## [0.1.0] - 2019-04-15 126 | ### Added 127 | - First minor version that contains the basic functionality. 128 | - Simple logging of incoming webhook requests from GitHub and Travis-CI. 129 | - Post welcome messages to PHP Telegram Bot Support group. 130 | - Post release announcements to PHP Telegram Bot Support group. (#17) 131 | - Extended `.env.example` file. 132 | 133 | [Unreleased]: https://github.com/php-telegram-bot/support-bot/compare/master...develop 134 | [0.14.0]: https://github.com/php-telegram-bot/support-bot/compare/0.13.2...0.14.0 135 | [0.13.2]: https://github.com/php-telegram-bot/support-bot/compare/0.13.1...0.13.2 136 | [0.13.1]: https://github.com/php-telegram-bot/support-bot/compare/0.13.0...0.13.1 137 | [0.13.0]: https://github.com/php-telegram-bot/support-bot/compare/0.12.0...0.13.0 138 | [0.12.0]: https://github.com/php-telegram-bot/support-bot/compare/0.11.0...0.12.0 139 | [0.11.0]: https://github.com/php-telegram-bot/support-bot/compare/0.10.0...0.11.0 140 | [0.10.0]: https://github.com/php-telegram-bot/support-bot/compare/0.9.0...0.10.0 141 | [0.9.0]: https://github.com/php-telegram-bot/support-bot/compare/0.8.0...0.9.0 142 | [0.8.0]: https://github.com/php-telegram-bot/support-bot/compare/0.7.0...0.8.0 143 | [0.7.0]: https://github.com/php-telegram-bot/support-bot/compare/0.6.0...0.7.0 144 | [0.6.0]: https://github.com/php-telegram-bot/support-bot/compare/0.5.0...0.6.0 145 | [0.5.0]: https://github.com/php-telegram-bot/support-bot/compare/0.4.0...0.5.0 146 | [0.4.0]: https://github.com/php-telegram-bot/support-bot/compare/0.3.0...0.4.0 147 | [0.3.0]: https://github.com/php-telegram-bot/support-bot/compare/0.2.0...0.3.0 148 | [0.2.0]: https://github.com/php-telegram-bot/support-bot/compare/0.1.0...0.2.0 149 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 PHP-Telegram-Bot 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Support-Bot 2 | 3 | Assistant bot for the [PHP Telegram Bot Support] group. 4 | 5 | ## Commands 6 | 7 | - `/help`: A short description of the bot and a list of all available commands. 8 | - `/id`: Display the user and chat information. Also, try forwarding any message from a channel to display the channel information. 9 | - `/rules`: Show the rules that apply in the support group. 10 | - `/donate`: Donate to the project using Telegram Payments. 11 | 12 | [PHP Telegram Bot Support]: https://t.me/PHP_Telegram_Bot_Support "@PHP_Telegram_Bot_Support" 13 | -------------------------------------------------------------------------------- /commands/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-telegram-bot/support-bot/20fd468dfff3668f779ac80290b3742766755783/commands/.gitkeep -------------------------------------------------------------------------------- /commands/CallbackqueryCommand.php: -------------------------------------------------------------------------------- 1 | getCallbackQuery(); 51 | parse_str($callback_query->getData(), $callback_data); 52 | 53 | return match ($callback_data['command'] ?? null) { 54 | 'donate' => DonateCommand::handleCallbackQuery($callback_query, $callback_data), 55 | 'rules' => RulesCommand::handleCallbackQuery($callback_query, $callback_data), 56 | default => $callback_query->answer(), 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /commands/ChannelpostCommand.php: -------------------------------------------------------------------------------- 1 | getChannelPost()->getCommand() === 'id') { 48 | return $this->getTelegram()->executeCommand('id'); 49 | } 50 | 51 | return Request::emptyResponse(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /commands/DonateCommand.php: -------------------------------------------------------------------------------- 1 | '; 52 | 53 | /** 54 | * @var string 55 | */ 56 | protected $version = '0.1.1'; 57 | 58 | /** 59 | * @var bool 60 | */ 61 | protected $private_only = true; 62 | 63 | /** 64 | * Handle the callback queries regarding the /donate command. 65 | * 66 | * @param CallbackQuery $callback_query 67 | * @param array $callback_data 68 | * 69 | * @return ServerResponse 70 | */ 71 | public static function handleCallbackQuery(CallbackQuery $callback_query, array $callback_data): ServerResponse 72 | { 73 | self::createPaymentInvoice( 74 | $callback_query->getFrom()->getId(), 75 | (int) $callback_data['amount'], 76 | $callback_data['currency'] 77 | ); 78 | 79 | return $callback_query->answer([ 80 | 'text' => 'Awesome, an invoice has been sent to you.', 81 | ]); 82 | } 83 | 84 | /** 85 | * @return ServerResponse 86 | * @throws TelegramException 87 | */ 88 | public function preExecute(): ServerResponse 89 | { 90 | $this->isPrivateOnly() && $this->removeNonPrivateMessage(); 91 | 92 | // Make sure we only reply to messages. 93 | if (!$this->getMessage()) { 94 | return Request::emptyResponse(); 95 | } 96 | 97 | return $this->execute(); 98 | } 99 | 100 | /** 101 | * Execute command 102 | * 103 | * @return ServerResponse 104 | * @throws TelegramException 105 | */ 106 | public function execute(): ServerResponse 107 | { 108 | $currencies = $this->validateCurrencyFetching(); 109 | if ($currencies instanceof ServerResponse) { 110 | return $currencies; 111 | } 112 | 113 | $message = $this->getMessage(); 114 | $user_id = $message->getFrom()->getId(); 115 | 116 | $text = trim($message->getText(true)); 117 | if ('' === $text) { 118 | return $this->sendBaseDonationMessage(); 119 | } 120 | 121 | // Fetch currency and amount being donated. 122 | // Hack: https://stackoverflow.com/a/1807896 123 | [$amount, $currency_code] = preg_split('/\s+/', "$text "); 124 | 125 | $currency = $this->validateCurrency($currency_code); 126 | if ($currency instanceof ServerResponse) { 127 | return $currency; 128 | } 129 | 130 | $amount = $this->validateAmount($amount, $currency); 131 | if ($amount instanceof ServerResponse) { 132 | return $amount; 133 | } 134 | 135 | return self::createPaymentInvoice($user_id, $amount, $currency['code']); 136 | } 137 | 138 | /** 139 | * Fetch the list of official currencies supported by Telegram Payments. 140 | * 141 | * @return array 142 | */ 143 | protected function fetchCurrenciesFromTelegram(): array 144 | { 145 | try { 146 | $currencies = cache()->get('telegram_bot_currencies.json'); 147 | if (empty($currencies)) { 148 | $currencies = file_get_contents('https://core.telegram.org/bots/payments/currencies.json'); 149 | cache()->set('telegram_bot_currencies.json', $currencies, 86400); 150 | } 151 | 152 | return json_decode($currencies, true, 512, JSON_THROW_ON_ERROR); 153 | } catch (JsonException) { 154 | return []; 155 | } 156 | } 157 | 158 | /** 159 | * Create an invoice for the passed parameters and return the response. 160 | * 161 | * @param int $chat_id 162 | * @param int $amount 163 | * @param string $currency_code 164 | * 165 | * @return ServerResponse 166 | */ 167 | public static function createPaymentInvoice(int $chat_id, int $amount, string $currency_code = self::DEFAULT_CURRENCY): ServerResponse 168 | { 169 | $price = new LabeledPrice(['label' => 'Donation', 'amount' => $amount]); 170 | 171 | return Request::sendInvoice([ 172 | 'chat_id' => $chat_id, 173 | 'title' => 'Donation to the PHP Telegram Bot library', 174 | 'description' => LitEmoji::encodeUnicode( 175 | ':rainbow: Support the well-being of this great project and help it progress.' . PHP_EOL . 176 | PHP_EOL . 177 | ':heart: With much appreciation, your donation will flow back into making the PHP Telegram Bot library even better!' 178 | ), 179 | 'payload' => "donation_{$amount}_{$currency_code}", 180 | 'provider_token' => getenv('TG_PAYMENT_PROVIDER_TOKEN'), 181 | 'start_parameter' => 'donation', 182 | 'currency' => strtoupper($currency_code), 183 | 'prices' => [$price], 184 | 'reply_markup' => new InlineKeyboard([ 185 | ['text' => LitEmoji::encodeUnicode(':money_with_wings: Donate Now'), 'pay' => true], 186 | ['text' => LitEmoji::encodeUnicode(':gem: Become a Patron'), 'url' => getenv('TG_URL_PATREON')], 187 | ]), 188 | ]); 189 | } 190 | 191 | /** 192 | * Make sure the currencies can be retrieved and cached correctly. 193 | * 194 | * @return array|ServerResponse 195 | * @throws TelegramException 196 | */ 197 | protected function validateCurrencyFetching(): array|ServerResponse 198 | { 199 | if ($currencies = $this->fetchCurrenciesFromTelegram()) { 200 | return $currencies; 201 | } 202 | 203 | return $this->replyToUser( 204 | LitEmoji::encodeUnicode( 205 | 'Donations via the Support Bot are not available at this time :confused:' . PHP_EOL . 206 | PHP_EOL . 207 | 'Try again later or see [other ways to donate](' . getenv('TG_URL_DONATE') . ')' 208 | ), 209 | ['parse_mode' => 'markdown'] 210 | ); 211 | } 212 | 213 | /** 214 | * Ensure the currency is valid and return the currency data array. 215 | * 216 | * @param string $currency_code 217 | * 218 | * @return array|ServerResponse 219 | * @throws TelegramException 220 | */ 221 | protected function validateCurrency(string $currency_code): array|ServerResponse 222 | { 223 | $currencies = $this->fetchCurrenciesFromTelegram(); 224 | 225 | '' !== $currency_code || $currency_code = self::DEFAULT_CURRENCY; 226 | $currency_code = strtoupper($currency_code); 227 | 228 | if ($currency = $currencies[$currency_code] ?? null) { 229 | return $currency; 230 | } 231 | 232 | return $this->replyToUser( 233 | "Currency *{$currency_code}* not supported." . PHP_EOL . 234 | PHP_EOL . 235 | '[Check supported currencies](https://core.telegram.org/bots/payments#supported-currencies)', 236 | ['parse_mode' => 'markdown', 'disable_web_page_preview' => true] 237 | ); 238 | } 239 | 240 | /** 241 | * Ensure the amount is valid and return the clean integer to use for the invoice. 242 | * 243 | * @param string $amount 244 | * @param array $currency 245 | * 246 | * @return int|ServerResponse 247 | * @throws TelegramException 248 | */ 249 | protected function validateAmount(string $amount, array $currency): int|ServerResponse 250 | { 251 | $int_amount = (int) ceil((float) $amount); 252 | 253 | // Check that the donation amount is valid. 254 | $multiplier = 10 ** (int) $currency['exp']; 255 | 256 | // Let's ignore the fractions and round to the next whole. 257 | $min_amount = (int) ceil($currency['min_amount'] / $multiplier); 258 | $max_amount = (int) floor($currency['max_amount'] / $multiplier); 259 | 260 | if ($int_amount >= $min_amount && $int_amount <= $max_amount) { 261 | return $int_amount * $multiplier; 262 | } 263 | 264 | return $this->replyToUser( 265 | sprintf( 266 | 'Donations in %1$s must be between %2$s and %3$s.' . PHP_EOL . 267 | PHP_EOL . 268 | '[Check currency limits](https://core.telegram.org/bots/payments#supported-currencies)', 269 | $currency['title'], 270 | $min_amount, 271 | $max_amount 272 | ), 273 | ['parse_mode' => 'markdown', 'disable_web_page_preview' => true] 274 | ); 275 | } 276 | 277 | /** 278 | * Send a message with an inline keyboard listing predefined amounts. 279 | * 280 | * @return ServerResponse 281 | * @throws TelegramException 282 | */ 283 | protected function sendBaseDonationMessage(): ServerResponse 284 | { 285 | return $this->replyToUser( 286 | LitEmoji::encodeUnicode( 287 | ":smiley: So great that you're considering a donation to the PHP Telegram Bot project." . PHP_EOL . 288 | PHP_EOL . 289 | ':+1: Simply select one of the predefined amounts listed below.' . PHP_EOL . 290 | PHP_EOL . 291 | 'Alternatively, you can also define a custom amount using:' . PHP_EOL . 292 | '`' . $this->usage . '`' . PHP_EOL 293 | ) . PHP_EOL . 294 | '[Check supported currencies](https://core.telegram.org/bots/payments#supported-currencies) (Default is: *' . self::DEFAULT_CURRENCY . '*)', 295 | [ 296 | 'parse_mode' => 'markdown', 297 | 'disable_web_page_preview' => true, 298 | 'reply_markup' => new InlineKeyboard([ 299 | ['text' => '5€', 'callback_data' => 'command=donate&amount=500¤cy=EUR'], 300 | ['text' => '10€', 'callback_data' => 'command=donate&amount=1000¤cy=EUR'], 301 | ['text' => '20€', 'callback_data' => 'command=donate&amount=2000¤cy=EUR'], 302 | ['text' => '50€', 'callback_data' => 'command=donate&amount=5000¤cy=EUR'], 303 | ], [ 304 | ['text' => '$5', 'callback_data' => 'command=donate&amount=500¤cy=USD'], 305 | ['text' => '$10', 'callback_data' => 'command=donate&amount=1000¤cy=USD'], 306 | ['text' => '$20', 'callback_data' => 'command=donate&amount=2000¤cy=USD'], 307 | ['text' => '$50', 'callback_data' => 'command=donate&amount=5000¤cy=USD'], 308 | ], [ 309 | ['text' => LitEmoji::encodeUnicode(':gem: Patreon'), 'url' => getenv('TG_URL_PATREON')], 310 | ['text' => LitEmoji::encodeUnicode(':cyclone: Tidelift'), 'url' => getenv('TG_URL_TIDELIFT')], 311 | ['text' => 'More options...', 'url' => getenv('TG_URL_DONATE')], 312 | ]), 313 | ] 314 | ); 315 | } 316 | 317 | /** 318 | * Send "Thank you" message to user who donated. 319 | * 320 | * @param SuccessfulPayment $payment 321 | * @param int $user_id 322 | * 323 | * @return ServerResponse 324 | * @throws TelegramException 325 | */ 326 | public static function handleSuccessfulPayment(SuccessfulPayment $payment, int $user_id): ServerResponse 327 | { 328 | return Request::sendMessage([ 329 | 'chat_id' => $user_id, 330 | 'text' => LitEmoji::encodeUnicode( 331 | ':pray: Thank you for joining our growing list of donors.' . PHP_EOL . 332 | ':star: Your support helps a lot to keep this project alive!' 333 | ), 334 | ]); 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /commands/GenericmessageCommand.php: -------------------------------------------------------------------------------- 1 | getMessage(); 51 | $user_id = $message->getFrom()->getId(); 52 | 53 | // Handle new chat members. 54 | if ($message->getNewChatMembers()) { 55 | $this->deleteThisMessage(); // Service message. 56 | return $this->getTelegram()->executeCommand('newchatmembers'); 57 | } 58 | if ($message->getLeftChatMember()) { 59 | $this->deleteThisMessage(); // Service message. 60 | } 61 | 62 | // Handle successful payment of donation. 63 | if ($payment = $message->getSuccessfulPayment()) { 64 | return DonateCommand::handleSuccessfulPayment($payment, $user_id); 65 | } 66 | 67 | // Handle posts forwarded from channels. 68 | if ($message->getForwardFrom()) { 69 | return $this->getTelegram()->executeCommand('id'); 70 | } 71 | 72 | return parent::execute(); 73 | } 74 | 75 | /** 76 | * Delete the current message. 77 | * 78 | * @return ServerResponse 79 | */ 80 | private function deleteThisMessage(): ServerResponse 81 | { 82 | return Request::deleteMessage([ 83 | 'chat_id' => $this->getMessage()->getChat()->getId(), 84 | 'message_id' => $this->getMessage()->getMessageId(), 85 | ]); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /commands/HelpCommand.php: -------------------------------------------------------------------------------- 1 | '; 42 | 43 | /** 44 | * @var string 45 | */ 46 | protected $version = '0.1.0'; 47 | 48 | /** 49 | * @var bool 50 | */ 51 | protected $private_only = true; 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function execute(): ServerResponse 57 | { 58 | $message = $this->getMessage(); 59 | $command_str = trim($message->getText(true)); 60 | 61 | $text = <<getChat()->tryMention(true)}! 63 | 64 | Please feel free to ask your questions about the PHP Telegram Bot library. 65 | Keep in mind that the support group is English only. 66 | 67 | (Go to group: @PHP\_Telegram\_Bot\_Support) 68 | 69 | Below you can see the available commands of this bot. 70 | 71 | 72 | EOT; 73 | 74 | // Admin commands shouldn't be shown in group chats 75 | $safe_to_show = $message->getChat()->isPrivateChat(); 76 | $extra_data = ['parse_mode' => 'markdown']; 77 | 78 | [$all_commands, $user_commands, $admin_commands] = $this->getUserAdminCommands(); 79 | 80 | // If no command parameter is passed, show the list. 81 | if ($command_str === '') { 82 | $text .= '*Commands List*:' . PHP_EOL; 83 | foreach ($user_commands as $user_command) { 84 | $text .= '/' . $user_command->getName() . ' - ' . $user_command->getDescription() . PHP_EOL; 85 | } 86 | if ($safe_to_show && count($admin_commands) > 0) { 87 | $text .= PHP_EOL . '*Admin Commands List*:' . PHP_EOL; 88 | foreach ($admin_commands as $admin_command) { 89 | $text .= '/' . $admin_command->getName() . ' - ' . $admin_command->getDescription() . PHP_EOL; 90 | } 91 | } 92 | $text .= PHP_EOL . 'For exact command help, type: /help '; 93 | return $this->replyToChat($text, $extra_data); 94 | } 95 | 96 | $command_str = str_replace('/', '', $command_str); 97 | if (isset($all_commands[$command_str]) && ($safe_to_show || !$all_commands[$command_str]->isAdminCommand())) { 98 | $command = $all_commands[$command_str]; 99 | $text = sprintf( 100 | 'Command: %s (v%s)' . PHP_EOL . 101 | 'Description: %s' . PHP_EOL . 102 | 'Usage: %s', 103 | $command->getName(), 104 | $command->getVersion(), 105 | $command->getDescription(), 106 | $command->getUsage() 107 | ); 108 | return $this->replyToChat($text, $extra_data); 109 | } 110 | 111 | $text = "No help available: Command /{$command_str} not found"; 112 | return $this->replyToChat($text, $extra_data); 113 | } 114 | 115 | /** 116 | * Get all available User and Admin commands to display in the help list. 117 | * 118 | * @return Command[][] 119 | * @throws TelegramException 120 | */ 121 | protected function getUserAdminCommands(): array 122 | { 123 | // Only get enabled Admin and User commands that are allowed to be shown. 124 | /** @var Command[] $commands */ 125 | $commands = array_filter($this->telegram->getCommandsList(), static function ($command) { 126 | /** @var Command $command */ 127 | return !$command->isSystemCommand() && $command->showInHelp() && $command->isEnabled(); 128 | }); 129 | ksort($commands); 130 | 131 | $user_commands = array_filter($commands, static function ($command) { 132 | return $command->isUserCommand(); 133 | }); 134 | ksort($user_commands); 135 | 136 | $admin_commands = array_filter($commands, static function ($command) { 137 | return $command->isAdminCommand(); 138 | }); 139 | ksort($admin_commands); 140 | 141 | return [$commands, $user_commands, $admin_commands]; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /commands/IdCommand.php: -------------------------------------------------------------------------------- 1 | isPrivateOnly() && $this->removeNonPrivateMessage(); 58 | 59 | // Make sure we only reply to messages. 60 | if (!$this->getMessage()) { 61 | return Request::emptyResponse(); 62 | } 63 | 64 | return $this->execute(); 65 | } 66 | 67 | /** 68 | * Execute command 69 | * 70 | * @return ServerResponse 71 | * @throws TelegramException 72 | */ 73 | public function execute(): ServerResponse 74 | { 75 | $user_info = '👤 *User Info*' . PHP_EOL . $this->getUserInfo(); 76 | $chat_info = '🗣 *Chat Info*' . PHP_EOL . $this->getChatInfo(); 77 | 78 | return $this->replyToUser($user_info . PHP_EOL . PHP_EOL . $chat_info, ['parse_mode' => 'markdown']); 79 | } 80 | 81 | /** 82 | * Get the information of the user. 83 | * 84 | * @return string 85 | */ 86 | protected function getUserInfo(): string 87 | { 88 | $user = $this->getMessage()->getFrom(); 89 | 90 | return implode(PHP_EOL, [ 91 | "User Id: `{$user->getId()}`", 92 | 'First Name: ' . (($first_name = $user->getFirstName()) ? "`{$first_name}`" : '_n/a_'), 93 | 'Last Name: ' . (($last_name = $user->getLastName()) ? "`{$last_name}`" : '_n/a_'), 94 | 'Username: ' . (($username = $user->getUsername()) ? "`{$username}`" : '_n/a_'), 95 | 'Language Code: ' . (($language_code = $user->getLanguageCode()) ? "`{$language_code}`" : '_n/a_'), 96 | ]); 97 | } 98 | 99 | /** 100 | * Get the information of the chat. 101 | * 102 | * @return string 103 | */ 104 | protected function getChatInfo(): string 105 | { 106 | $message = $this->getMessage(); 107 | $chat = $message->getForwardFromChat() ?? $message->getChat(); 108 | 109 | if (!$chat || $chat->isPrivateChat()) { 110 | return '`Private chat`'; 111 | } 112 | 113 | return implode(PHP_EOL, [ 114 | "Type: `{$chat->getType()}`", 115 | "Chat Id: `{$chat->getId()}`", 116 | 'Title: ' . (($title = $chat->getTitle()) ? "`{$title}`" : '_n/a_'), 117 | 'First Name: ' . (($first_name = $chat->getFirstName()) ? "`{$first_name}`" : '_n/a_'), 118 | 'Last Name: ' . (($last_name = $chat->getLastName()) ? "`{$last_name}`" : '_n/a_'), 119 | 'Username: ' . (($username = $chat->getUsername()) ? "`{$username}`" : '_n/a_'), 120 | ]); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /commands/NewchatmembersCommand.php: -------------------------------------------------------------------------------- 1 | message = $this->getMessage(); 77 | $this->chat_id = $this->message->getChat()->getId(); 78 | $this->user_id = $this->message->getFrom()->getId(); 79 | 80 | $this->group_name = $this->message->getChat()->getTitle(); 81 | 82 | ['users' => $new_users, 'bots' => $new_bots] = $this->getNewUsersAndBots(); 83 | 84 | // Kick bots if they weren't added by an admin. 85 | $this->kickDisallowedBots($new_bots); 86 | 87 | // Restrict all permissions for new users. 88 | $this->restrictNewUsers($new_users); 89 | 90 | // Set the joined date for all new group members. 91 | $this->updateUsersJoinedDate($new_users); 92 | 93 | return $this->refreshWelcomeMessage($new_users); 94 | } 95 | 96 | /** 97 | * Remove existing and send new welcome message. 98 | * 99 | * @param array $new_users 100 | * 101 | * @return ServerResponse 102 | * @throws TelegramException 103 | */ 104 | private function refreshWelcomeMessage(array $new_users): ServerResponse 105 | { 106 | if (empty($new_users)) { 107 | return Request::emptyResponse(); 108 | } 109 | 110 | $new_users_text = implode(', ', array_map(static function (User $new_user) { 111 | return '' . filter_var($new_user->getFirstName(), FILTER_SANITIZE_SPECIAL_CHARS) . ''; 112 | }, $new_users)); 113 | 114 | $text = ":wave: Welcome {$new_users_text} to the {$this->group_name} group\n\n"; 115 | $text .= 'Please read and agree to the rules before posting here, thank you!'; 116 | 117 | $welcome_message_sent = $this->replyToChat( 118 | LitEmoji::encodeUnicode($text), 119 | [ 120 | 'parse_mode' => 'HTML', 121 | 'disable_web_page_preview' => true, 122 | 'disable_notification' => true, 123 | 'reply_markup' => new InlineKeyboard([ 124 | ['text' => LitEmoji::encodeUnicode(':orange_book: Read the Rules'), 'url' => 'https://t.me/' . getenv('TG_BOT_USERNAME') . '?start=rules'], 125 | ]), 126 | ] 127 | ); 128 | if (!$welcome_message_sent->isOk()) { 129 | return Request::emptyResponse(); 130 | } 131 | 132 | /** @var Message $welcome_message */ 133 | $welcome_message = $welcome_message_sent->getResult(); 134 | 135 | $new_message_id = $welcome_message->getMessageId(); 136 | $chat_id = $welcome_message->getChat()->getId(); 137 | 138 | if ($new_message_id && $chat_id) { 139 | Helpers::saveLatestWelcomeMessage($new_message_id); 140 | Helpers::deleteOldWelcomeMessages(); 141 | } 142 | 143 | return $welcome_message_sent; 144 | } 145 | 146 | /** 147 | * Check if the bot has been added by an admin. 148 | * 149 | * @return bool 150 | */ 151 | private function isUserAllowedToAddBot(): bool 152 | { 153 | $chat_member = Request::getChatMember([ 154 | 'chat_id' => $this->chat_id, 155 | 'user_id' => $this->user_id, 156 | ])->getResult(); 157 | 158 | return $chat_member instanceof ChatMemberOwner 159 | || $chat_member instanceof ChatMemberAdministrator; 160 | } 161 | 162 | /** 163 | * Get an array of all newly added users and bots. 164 | * 165 | * @return array 166 | */ 167 | private function getNewUsersAndBots(): array 168 | { 169 | $users = []; 170 | $bots = []; 171 | 172 | foreach ($this->message->getNewChatMembers() as $member) { 173 | if ($member->getIsBot()) { 174 | $bots[] = $member; 175 | continue; 176 | } 177 | 178 | $users[] = $member; 179 | } 180 | 181 | return compact('users', 'bots'); 182 | } 183 | 184 | /** 185 | * Kick bots that weren't added by an admin. 186 | * 187 | * @todo: Maybe notify the admins / user that tried to add the bot(s)? 188 | * 189 | * @param array $bots 190 | */ 191 | private function kickDisallowedBots(array $bots): void 192 | { 193 | if (empty($bots) || $this->isUserAllowedToAddBot()) { 194 | return; 195 | } 196 | 197 | foreach ($bots as $bot) { 198 | Request::banChatMember([ 199 | 'chat_id' => $this->chat_id, 200 | 'user_id' => $bot->getId(), 201 | ]); 202 | } 203 | } 204 | 205 | /** 206 | * Write users join date to DB. 207 | * 208 | * @param User[] $new_users 209 | * 210 | * @return bool 211 | */ 212 | private function updateUsersJoinedDate(array $new_users): bool 213 | { 214 | $new_users_ids = array_map(static function (User $user) { 215 | return $user->getId(); 216 | }, $new_users); 217 | 218 | // Update "Joined Date" for new users. 219 | return DB::getPdo()->prepare(" 220 | UPDATE `user` 221 | SET `joined_at` = ? 222 | WHERE `id` IN (?) 223 | ")->execute([date('Y-m-d H:i:s'), implode(',', $new_users_ids)]); 224 | } 225 | 226 | /** 227 | * Restrict permissions in support group for passed users. 228 | * 229 | * @param User[] $new_users 230 | * 231 | * @return array 232 | */ 233 | private function restrictNewUsers(array $new_users): array 234 | { 235 | $responses = []; 236 | 237 | foreach ($new_users as $new_user) { 238 | $user_id = $new_user->getId(); 239 | $responses[$user_id] = Request::restrictChatMember([ 240 | 'chat_id' => getenv('TG_SUPPORT_GROUP_ID'), 241 | 'user_id' => $user_id, 242 | 'permissions' => new ChatPermissions([ 243 | 'can_send_messages' => false, 244 | 'can_send_media_messages' => false, 245 | 'can_send_polls' => false, 246 | 'can_send_other_messages' => false, 247 | 'can_add_web_page_previews' => false, 248 | 'can_change_info' => false, 249 | 'can_invite_users' => false, 250 | 'can_pin_messages' => false, 251 | ]), 252 | ]); 253 | } 254 | 255 | return $responses; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /commands/PreCheckoutQueryCommand.php: -------------------------------------------------------------------------------- 1 | getPreCheckoutQuery()->answer(true); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /commands/RulesCommand.php: -------------------------------------------------------------------------------- 1 | getMessage(); 63 | $chat_id = $message->getChat()->getId(); 64 | $clicked_user_id = $callback_query->getFrom()->getId(); 65 | 66 | // If the user is already activated, keep the initial activation date. 67 | $activated = DB::getPdo()->prepare(" 68 | UPDATE `user` 69 | SET `activated_at` = ? 70 | WHERE `id` = ? 71 | AND `activated_at` IS NULL 72 | ")->execute([date('Y-m-d H:i:s'), $clicked_user_id]); 73 | 74 | if (!$activated) { 75 | return $callback_query->answer([ 76 | 'text' => 'Something went wrong, please try again later.', 77 | 'show_alert' => true, 78 | ]); 79 | } 80 | 81 | Request::restrictChatMember([ 82 | 'chat_id' => getenv('TG_SUPPORT_GROUP_ID'), 83 | 'user_id' => $clicked_user_id, 84 | 'permissions' => new ChatPermissions([ 85 | 'can_send_messages' => true, 86 | 'can_send_media_messages' => true, 87 | 'can_add_web_page_previews' => true, 88 | 'can_invite_users' => true, 89 | ]), 90 | ]); 91 | 92 | Request::editMessageReplyMarkup([ 93 | 'chat_id' => $chat_id, 94 | 'message_id' => $message->getMessageId(), 95 | 'reply_markup' => new InlineKeyboard([ 96 | ['text' => LitEmoji::encodeUnicode(':white_check_mark: Ok! Go to Bot Support group...'), 'url' => 'https://t.me/' . getenv('TG_SUPPORT_GROUP_USERNAME')], 97 | ]), 98 | ]); 99 | 100 | return $callback_query->answer([ 101 | 'text' => 'Thanks for agreeing to the rules. You may now post in the support group.', 102 | 'show_alert' => true, 103 | ]); 104 | } 105 | 106 | return $callback_query->answer(); 107 | } 108 | 109 | /** 110 | * @inheritdoc 111 | */ 112 | public function execute(): ServerResponse 113 | { 114 | $text = " 115 | Rules: `English only | No Spam | No Bots | No long Code` 116 | 117 | **:uk: English only** 118 | Please keep your conversations in English inside this chatroom, otherwise your message will be deleted. 119 | 120 | **:do_not_litter: No Spamming or Nonsense Posting** 121 | Don't spam or send Messages with useless Content. When repeated, you may be kicked or banned. 122 | 123 | **:robot: No Bots** 124 | Please do not add a Bot inside this Chat without asking the Admins first. Feel free to mention the Bot in a Message 125 | 126 | **:memo: Use Pastebin to post Source Code** 127 | If you want to share your Code for troubleshooting, please upload it to [Pastebin](https://pastebin.com) and post the link. Don't send long code parts directly in the Chat. 128 | 129 | *:no_entry_sign:* No Advertisement 130 | Please do not send any Advertisement in this Chat (eg.: Programming Service for Money) 131 | 132 | Also keep in mind that the [PHP Telegram Bot Support Chat](https://t.me/PHP_Telegram_Bot_Support) applies only for the [PHP Telegram Bot library](https://github.com/php-telegram-bot/core). 133 | "; 134 | 135 | $data = [ 136 | 'parse_mode' => 'markdown', 137 | 'disable_web_page_preview' => true, 138 | ]; 139 | 140 | if (!self::hasUserAgreedToRules($this->getMessage()->getFrom()->getId())) { 141 | $text .= PHP_EOL . 'You **must agree** to these rules to post in the support group. Simply click the button below.'; 142 | $data['reply_markup'] = new InlineKeyboard([ 143 | ['text' => LitEmoji::encodeUnicode(':+1: I Agree to the Rules'), 'callback_data' => 'command=rules&action=agree'], 144 | ]); 145 | } 146 | 147 | return $this->replyToChat(LitEmoji::encodeUnicode($text), $data); 148 | } 149 | 150 | /** 151 | * Check if the passed user has agreed to the rules. 152 | * 153 | * @param int $user_id 154 | * 155 | * @return bool 156 | */ 157 | protected static function hasUserAgreedToRules(int $user_id): bool 158 | { 159 | $statement = DB::getPdo()->prepare(' 160 | SELECT `activated_at` 161 | FROM `user` 162 | WHERE `id` = ? 163 | AND `activated_at` IS NOT NULL 164 | '); 165 | $statement->execute([$user_id]); 166 | $agreed = $statement->fetchAll(PDO::FETCH_COLUMN, 0); 167 | 168 | return !empty($agreed); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /commands/StartCommand.php: -------------------------------------------------------------------------------- 1 | getMessage()->getText(true); 57 | 58 | // Fall back to /help command. 59 | if (!in_array($text, ['activate', 'rules'])) { 60 | $text = 'help'; 61 | } 62 | 63 | return $this->getTelegram()->executeCommand($text); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "telegram-bot/support-bot", 3 | "type": "project", 4 | "description": "Friendly and helpful bot for t.me/PHP_Telegram_Support_Bot", 5 | "keywords": ["telegram", "bot", "manager", "support"], 6 | "license": "MIT", 7 | "homepage": "https://github.com/php-telegram-bot/support-bot", 8 | "support": { 9 | "issues": "https://github.com/php-telegram-bot/support-bot/issues", 10 | "source": "https://github.com/php-telegram-bot/support-bot" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "PHP Telegram Bot Team", 15 | "homepage": "https://github.com/php-telegram-bot/support-bot/graphs/contributors", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1", 21 | "ext-json": "*", 22 | "ext-pdo": "*", 23 | "php-telegram-bot/telegram-bot-manager": "^2.1", 24 | "longman/telegram-bot": "0.81", 25 | "noplanman/service-webhook-handler": "^1.0", 26 | "vlucas/phpdotenv": "^5.5", 27 | "elvanto/litemoji": "^4.3", 28 | "monolog/monolog": "^3.3", 29 | "matthiasmullie/scrapbook": "^1.4", 30 | "knplabs/github-api": "^3.11", 31 | "guzzlehttp/guzzle": "^7.7", 32 | "guzzlehttp/psr7": "^2.5" 33 | }, 34 | "require-dev": { 35 | "roave/security-advisories": "dev-latest", 36 | "squizlabs/php_codesniffer": "^3.7", 37 | "php-parallel-lint/php-parallel-lint": "^1.3", 38 | "symfony/var-dumper": "^6.2" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "TelegramBot\\SupportBot\\": "src" 43 | } 44 | }, 45 | "scripts": { 46 | "check-code": [ 47 | "vendor/bin/parallel-lint . --exclude vendor", 48 | "vendor/bin/phpcs" 49 | ] 50 | }, 51 | "config": { 52 | "allow-plugins": { 53 | "php-http/discovery": true 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cron.php: -------------------------------------------------------------------------------- 1 | load(); 23 | 24 | try { 25 | $telegram = new Telegram(getenv('TG_API_KEY'), getenv('TG_BOT_USERNAME')); 26 | $telegram->enableMySql([ 27 | 'host' => getenv('TG_DB_HOST'), 28 | 'port' => getenv('TG_DB_PORT'), 29 | 'user' => getenv('TG_DB_USER'), 30 | 'password' => getenv('TG_DB_PASSWORD'), 31 | 'database' => getenv('TG_DB_DATABASE'), 32 | ]); 33 | 34 | // Handle expired activations. 35 | Helpers::handleExpiredActivations(); 36 | } catch (\Throwable $e) { 37 | TelegramLog::error($e->getMessage()); 38 | } 39 | -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-telegram-bot/support-bot/20fd468dfff3668f779ac80290b3742766755783/logs/.gitkeep -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | . 8 | */vendor/* 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/manager.php: -------------------------------------------------------------------------------- 1 | load(); 37 | 38 | function cache(): KeyValueStore 39 | { 40 | static $cache; 41 | 42 | return $cache ??= new MySQL(DB::getPdo()); 43 | } 44 | 45 | try { 46 | // Vitals! 47 | $params = [ 48 | 'api_key' => getenv('TG_API_KEY'), 49 | ]; 50 | foreach (['bot_username', 'secret'] as $extra) { 51 | if ($param = getenv('TG_' . strtoupper($extra))) { 52 | $params[$extra] = $param; 53 | } 54 | } 55 | 56 | // Database connection. 57 | if (getenv('TG_DB_HOST')) { 58 | $params['mysql'] = [ 59 | 'host' => getenv('TG_DB_HOST'), 60 | 'port' => getenv('TG_DB_PORT'), 61 | 'user' => getenv('TG_DB_USER'), 62 | 'password' => getenv('TG_DB_PASSWORD'), 63 | 'database' => getenv('TG_DB_DATABASE'), 64 | ]; 65 | } 66 | 67 | // Optional extras. 68 | $extras = ['admins', 'commands', 'cron', 'limiter', 'paths', 'valid_ips', 'webhook']; 69 | foreach ($extras as $extra) { 70 | if ($param = getenv('TG_' . strtoupper($extra))) { 71 | $params[$extra] = json_decode($param, true); 72 | } 73 | } 74 | 75 | initLogging(); 76 | initRequestClient(); 77 | 78 | $bot = new BotManager($params); 79 | $bot->run(); 80 | } catch (Throwable $e) { 81 | TelegramLog::error($e->getMessage()); 82 | } 83 | 84 | /** 85 | * Initialise the logging. 86 | * 87 | * @throws Exception 88 | */ 89 | function initLogging(): void 90 | { 91 | // Logging. 92 | $logging_paths = json_decode(getenv('TG_LOGGING'), true) ?? []; 93 | 94 | $debug_log = $logging_paths['debug'] ?? null; 95 | $error_log = $logging_paths['error'] ?? null; 96 | $update_log = $logging_paths['update'] ?? null; 97 | 98 | // Main logger that handles all 'debug' and 'error' logs. 99 | $logger = ($debug_log || $error_log) ? new Logger('telegram_bot') : new NullLogger(); 100 | $debug_log && $logger->pushHandler((new StreamHandler($debug_log, Level::Debug))->setFormatter(new LineFormatter(null, null, true))); 101 | $error_log && $logger->pushHandler((new StreamHandler($error_log, Level::Error))->setFormatter(new LineFormatter(null, null, true))); 102 | 103 | // Updates logger for raw updates. 104 | $update_logger = new NullLogger(); 105 | if ($update_log) { 106 | $update_logger = new Logger('telegram_bot_updates'); 107 | $update_logger->pushHandler((new StreamHandler($update_log, Level::Info))->setFormatter(new LineFormatter('%message%' . PHP_EOL))); 108 | } 109 | 110 | TelegramLog::initialize($logger, $update_logger); 111 | } 112 | 113 | /** 114 | * Initialise a custom Request Client. 115 | */ 116 | function initRequestClient(): void 117 | { 118 | $config = array_filter([ 119 | 'base_uri' => getenv('TG_REQUEST_CLIENT_BASE_URI') ?: 'https://api.telegram.org', 120 | 'proxy' => getenv('TG_REQUEST_CLIENT_PROXY'), 121 | ]); 122 | 123 | $config && Request::setClient(new Client($config)); 124 | } 125 | -------------------------------------------------------------------------------- /public/webhooks/github.php: -------------------------------------------------------------------------------- 1 | load(); 35 | 36 | // Load the webhook request and check if it's valid. 37 | $webhook = new GitHubHandler(getenv('TG_WEBHOOK_SECRET_GITHUB')); 38 | if (!$webhook->validate()) { 39 | http_response_code(401); 40 | die; 41 | } 42 | 43 | // Save all incoming data to a log file for future reference. 44 | Utils::logWebhookData(getenv('TG_LOGS_DIR') . '/' . getenv('TG_BOT_USERNAME') . '_webhook_github.log'); 45 | 46 | // Limit repos and events to serve. 47 | $allowed_repos_events = [ 48 | 'php-telegram-bot/core' => ['release'], 49 | 'php-telegram-bot/support-bot' => ['release'], 50 | 'php-telegram-bot/telegram-bot-manager' => ['release'], 51 | 'php-telegram-bot/inline-keyboard-pagination' => ['release'], 52 | 'php-telegram-bot/fluent-keyboard' => ['release'], 53 | 'php-telegram-bot/laravel' => ['release'], 54 | ]; 55 | 56 | // Get the incoming webhook data. 57 | $data = $webhook->getData(); 58 | 59 | // Only react to allowed repos and events. 60 | $repo = $data['repository']; 61 | if (!in_array($webhook->getEvent(), $allowed_repos_events[$repo['full_name']] ?? [], true)) { 62 | die; 63 | } 64 | 65 | // Handle event. 66 | if ($webhook->getEvent() === 'release') { 67 | handleRelease($data); 68 | 69 | if ($repo['full_name'] === 'php-telegram-bot/support-bot' && getenv('TG_AUTOUPDATE') === '1') { 70 | pullLatestAndUpdate(); 71 | } 72 | } 73 | 74 | /** 75 | * Handle the "release" event. 76 | * 77 | * @param array $data 78 | */ 79 | function handleRelease(array $data): void 80 | { 81 | $repo = $data['repository']; 82 | $release = $data['release']; 83 | $action = $data['action']; 84 | 85 | if ($action === 'released' && !$release['draft'] && !$release['prerelease']) { 86 | $author = $release['author']['login']; 87 | $author_url = $release['author']['html_url']; 88 | $tag = $release['tag_name']; 89 | $url = $release['html_url']; 90 | $body = parseReleaseBody($release['body'], $repo['owner']['login'], $repo['name']); 91 | 92 | $message = LitEmoji\LitEmoji::encodeUnicode(" 93 | :star: *New Release!* :star: 94 | (_version_ [{$tag}]({$url}) _of_ [{$repo['full_name']}]({$repo['html_url']}) _has just been released by_ [{$author}]({$author_url})) 95 | 96 | {$body} 97 | "); 98 | 99 | // Post the release message! 100 | sendTelegramMessage((string) getenv('TG_SUPPORT_GROUP_ID'), $message); 101 | } 102 | } 103 | 104 | /** 105 | * Make the release message Telegram-friendly and resolve links to GitHub. 106 | * 107 | * @param string $body 108 | * @param string $user 109 | * @param string $repo 110 | * 111 | * @return string 112 | */ 113 | function parseReleaseBody(string $body, string $user, string $repo): string 114 | { 115 | // Replace headers with bold text. 116 | $body = preg_replace_callback('~### (?
.*)~', static function ($matches) { 117 | $header = trim($matches['header']); 118 | return "*{$header}*"; 119 | }, $body); 120 | 121 | $github_client = new Client(); 122 | $github_client->authenticate(getenv('TG_GITHUB_AUTH_USER'), getenv('TG_GITHUB_AUTH_TOKEN'), AuthMethod::CLIENT_ID); 123 | $github_client->addCache(new Pool(new MySQL( 124 | new PDO('mysql:dbname=' . getenv('TG_DB_DATABASE') . ';host=' . getenv('TG_DB_HOST'), getenv('TG_DB_USER'), getenv('TG_DB_PASSWORD')) 125 | ))); 126 | 127 | // Replace any ID links with the corresponding issue or pull request link. 128 | $body = preg_replace_callback('~(?:(?[0-9a-z\-]*)/(?[0-9a-z\-]*))?#(?\d*)~i', static function ($matches) use ($github_client, $user, $repo) { 129 | $text = $matches[0]; 130 | $id = $matches['id']; 131 | $user = $matches['user'] ?: $user; 132 | $repo = $matches['repo'] ?: $repo; 133 | 134 | // Check if this ID is an issue. 135 | try { 136 | /** @var Issue $issue */ 137 | $issue = $github_client->issue()->show($user, $repo, $id); 138 | return "[{$text}]({$issue['html_url']})"; 139 | } catch (Throwable) { 140 | // Silently ignore. 141 | } 142 | 143 | // Check if this ID is a pull request. 144 | try { 145 | /** @var PullRequest $pr */ 146 | $pr = $github_client->pr()->show($user, $repo, $id); 147 | return "[{$text}]({$pr['html_url']})"; 148 | } catch (Throwable) { 149 | // Silently ignore. 150 | } 151 | 152 | return $text; 153 | }, $body); 154 | 155 | return $body; 156 | } 157 | 158 | /** 159 | * Send a text to the passed chat. 160 | * 161 | * @param string $chat_id 162 | * @param string $text 163 | * 164 | * @return ServerResponse|null 165 | */ 166 | function sendTelegramMessage(string $chat_id, string $text): ?ServerResponse 167 | { 168 | try { 169 | new Telegram(getenv('TG_API_KEY')); 170 | 171 | TelegramLog::initialize(new Logger('telegram_bot_releases', [ 172 | (new StreamHandler(getenv('TG_LOGS_DIR') . '/releases.debug.log', Level::Debug))->setFormatter(new LineFormatter(null, null, true)), 173 | (new StreamHandler(getenv('TG_LOGS_DIR') . '/releases.error.log', Level::Error))->setFormatter(new LineFormatter(null, null, true)), 174 | ])); 175 | 176 | $parse_mode = 'markdown'; 177 | 178 | return Request::sendMessage(compact('chat_id', 'text', 'parse_mode')); 179 | } catch (TelegramException $e) { 180 | TelegramLog::error($e->getMessage()); 181 | } catch (Throwable) { 182 | // Silently ignore. 183 | } 184 | 185 | return null; 186 | } 187 | 188 | /** 189 | * Pull the latest code from the repository and install with composer. 190 | */ 191 | function pullLatestAndUpdate(): void 192 | { 193 | exec('/usr/bin/git stash'); 194 | exec('/usr/bin/git fetch'); 195 | exec('/usr/bin/git reset --hard'); 196 | exec('/usr/bin/git rebase'); 197 | exec('/usr/bin/git pull'); 198 | exec('/usr/local/bin/composer install --no-dev'); 199 | } 200 | -------------------------------------------------------------------------------- /src/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-telegram-bot/support-bot/20fd468dfff3668f779ac80290b3742766755783/src/.gitkeep -------------------------------------------------------------------------------- /src/Helpers.php: -------------------------------------------------------------------------------- 1 | query(" 36 | SELECT `value` 37 | FROM `simple_options` 38 | WHERE `name` = '{$name}' 39 | ")->fetchColumn() ?: '', true) ?? $default; 40 | } 41 | 42 | /** 43 | * Set a simple option value. 44 | * 45 | * @todo: Move into core! 46 | * 47 | * @param string $name 48 | * @param mixed $value 49 | * 50 | * @return bool 51 | */ 52 | public static function setSimpleOption(string $name, mixed $value): bool 53 | { 54 | return DB::getPdo()->prepare(" 55 | INSERT INTO `simple_options` 56 | (`name`, `value`) VALUES (?, ?) 57 | ON DUPLICATE KEY UPDATE 58 | `name` = VALUES(`name`), 59 | `value` = VALUES(`value`) 60 | ")->execute([$name, json_encode($value)]); 61 | } 62 | 63 | /** 64 | * Delete any old welcome messages from the group. 65 | */ 66 | public static function deleteOldWelcomeMessages(): void 67 | { 68 | $chat_id = getenv('TG_SUPPORT_GROUP_ID'); 69 | 70 | $welcome_message_ids = self::getSimpleOption('welcome_message_ids', []); 71 | foreach ($welcome_message_ids as $key => $message_id) { 72 | // Be sure to keep the latest one. 73 | if ($key === 'latest') { 74 | continue; 75 | } 76 | 77 | $deletion = Request::deleteMessage(compact('chat_id', 'message_id')); 78 | if (!$deletion->isOk()) { 79 | // Let's just save the error for now if it fails, to see if we can fix this better. 80 | TelegramLog::error(sprintf( 81 | 'Chat ID: %s, Message ID: %s, Error Code: %s, Error Message: %s', 82 | $chat_id, 83 | $message_id, 84 | $deletion->getErrorCode(), 85 | $deletion->getDescription() 86 | )); 87 | } 88 | 89 | unset($welcome_message_ids[$key]); 90 | } 91 | 92 | self::setSimpleOption('welcome_message_ids', $welcome_message_ids); 93 | } 94 | 95 | /** 96 | * Save the latest welcome message to the option. 97 | * 98 | * @param int $welcome_message_id 99 | */ 100 | public static function saveLatestWelcomeMessage(int $welcome_message_id): void 101 | { 102 | $welcome_message_ids = self::getSimpleOption('welcome_message_ids', []); 103 | $new_welcome_message_ids = array_values($welcome_message_ids) + ['latest' => $welcome_message_id]; 104 | self::setSimpleOption('welcome_message_ids', $new_welcome_message_ids); 105 | } 106 | 107 | /** 108 | * Handle expired activations and kick those users. 109 | */ 110 | public static function handleExpiredActivations(): void 111 | { 112 | $expiry_time = strtotime(getenv('TG_SUPPORT_GROUP_ACTIVATION_EXPIRE_TIME') ?: '15 min'); 113 | $expiry_time_in_s = $expiry_time - time(); 114 | 115 | // If the user is already activated, keep the initial activation date. 116 | $users_to_kick = DB::getPdo()->query(" 117 | SELECT `id` 118 | FROM `user` 119 | WHERE `joined_at` < (NOW() - INTERVAL {$expiry_time_in_s} SECOND) 120 | AND `activated_at` IS NULL 121 | AND `kicked_at` IS NULL 122 | "); 123 | foreach ($users_to_kick as $user_to_kick) { 124 | self::kickUser((int) $user_to_kick['id']); 125 | } 126 | } 127 | 128 | /** 129 | * Kick the passed user. 130 | * 131 | * @param int $user_id 132 | * 133 | * @return bool 134 | */ 135 | protected static function kickUser(int $user_id): bool 136 | { 137 | try { 138 | $ban_time = strtotime(getenv('TG_SUPPORT_GROUP_BAN_TIME') ?: '1 day'); 139 | $kick_user = Request::banChatMember([ 140 | 'chat_id' => getenv('TG_SUPPORT_GROUP_ID'), 141 | 'user_id' => $user_id, 142 | 'until_date' => $ban_time, 143 | ]); 144 | if ($kick_user->isOk()) { 145 | return DB::getPdo()->prepare(" 146 | UPDATE `user` 147 | SET `activated_at` = NULL, 148 | `kicked_at` = NOW() 149 | WHERE `id` = ? 150 | ")->execute([$user_id]); 151 | } 152 | } catch (Throwable) { 153 | // Fail silently. 154 | } 155 | 156 | return false; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Webhooks/Utils.php: -------------------------------------------------------------------------------- 1 |