diff --git a/README.md b/README.md index 84c0f08..77eb757 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ * Пример: если бот доступен по url `https://example.com/bot`, привязать его можно выполнив запрос: `https://api.telegram.org/bot{BOT_TOKEN}/setWebhook?url=https://example.com/bot` (наличие ssl - **обязательно**). * Проверить статус бота можно, выполнив запрос: `https://api.telegram.org/bot{BOT_TOKEN}/getWebhook` -## Функционал ботов +## Реализованные боты ### simple-bot @@ -53,7 +53,21 @@ > [Пример работающего TbaPhpSdkBot бота](https://t.me/TbaPhpSdkBot "Пример работающего TbaPhpSdkBot бота") +### translation-bot-v1 + +Бот переводчик, перевод фраз осуществляется с помощью `dejurin/php-google-translate-for-free`. Доступные переводы: + +* С Английского на Русский +* С Русского на английский + +Есть возможность переводить как слова, так и словосочетания и предложения. Реализована обработка ошибок, если в бот отправить +фото, видео, стикер или какой-то документ, пользователю вернется сообщение, что бот работает только с текстом. + +> [Пример работающего ya_translation_bot бота](https://t.me/ya_translation_bot "Пример работающего ya_translation_bot бота") + +--- + ### Для отладки в ботах реализован функционал записи в логи * Возникающие исключения отлавливаются в `try/catch` и пишутся в файл `try_catch_logs.txt`, который находится в папке с ботом. -* Для записи ответов с телеграмм бота и запросов с вашей стороны реализован методы `addToLogs()` / `writeToLogs()`. \ No newline at end of file +* Для записи ответов с телеграмм бота и запросов с вашей стороны реализован методы `addToLogs()` / `writeToLogs()`. diff --git a/composer.json b/composer.json index dfa1829..c0d19fd 100644 --- a/composer.json +++ b/composer.json @@ -1,16 +1,18 @@ { - "require-dev": { - "symfony/var-dumper": "^6.0" - }, - "require": { - "guzzlehttp/guzzle": "^7.4", - "irazasyed/telegram-bot-sdk": "^3.4" - }, - "autoload": { - "psr-4": { - "SimpleBot\\": "simple-bot/", - "TbaPhpSdk\\": "tba-php-sdk/", - "YaTranslationBot\\": "translation-bot/" - } + "require-dev": { + "symfony/var-dumper": "^6.0" + }, + "require": { + "guzzlehttp/guzzle": "^7.4", + "irazasyed/telegram-bot-sdk": "^3.4", + "dejurin/php-google-translate-for-free": "^1.0", + "ext-pdo": "*" + }, + "autoload": { + "psr-4": { + "SimpleBot\\": "simple-bot/", + "TbaPhpSdk\\": "tba-php-sdk/", + "YaTranslationBot\\": "translation-bot-v1/" } + } } diff --git a/composer.lock b/composer.lock index 914d812..7077223 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,50 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ce2aa1397155c754016d27dfe87831ab", + "content-hash": "d7c230385438c2df08088842215141a9", "packages": [ + { + "name": "dejurin/php-google-translate-for-free", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/dejurin/php-google-translate-for-free.git", + "reference": "db5e3d0ac66e711dc41ed59618595aa3e3a7e475" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dejurin/php-google-translate-for-free/zipball/db5e3d0ac66e711dc41ed59618595aa3e3a7e475", + "reference": "db5e3d0ac66e711dc41ed59618595aa3e3a7e475", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dejurin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0+" + ], + "description": "Library for free use Google Translator. With attempts connecting on failure and array support.", + "homepage": "https://github.com/dejurin/php-google-translate-for-free", + "keywords": [ + "api", + "free", + "google", + "translate", + "translator" + ], + "support": { + "issues": "https://github.com/dejurin/php-google-translate-for-free/issues", + "source": "https://github.com/dejurin/php-google-translate-for-free/releases" + }, + "time": "2018-03-02T19:36:37+00:00" + }, { "name": "doctrine/inflector", "version": "2.0.8", diff --git a/config.php.example b/config.php.example index 5106191..9620423 100644 --- a/config.php.example +++ b/config.php.example @@ -1,6 +1,7 @@ >> Token for local telegram bots -/** - * Токен используется для ботов из директории: local -*/ -const FIRST_BOT_TOKEN = '...'; -// Token for local telegram bots <<< - - -// >>> Token for remote telegram bots /** * Логин: ... * - * Привязка URL к вебхуку: - * echo BASE_URL . EXAMPLE_TOKEN . '/setWebhook?url={{ URL YOUR SITE, https - required }}'; + * Привязка URL к вебхука: * https://api.telegram.org/bot{{EXAMPLE_TOKEN }}/setWebhook?url={{ URL YOUR SITE, https - required }} * * Проверка статуса вебхука: - * https://api.telegram.org/bot{{EXAMPLE_TOKEN }}/getWebhookInfo?url={{ URL YOUR SITE, https - required }} + * https://api.telegram.org/bot{{EXAMPLE_TOKEN }}/getWebhookInfo + * + * Удалить вебхук: + * https://api.telegram.org/bot{{EXAMPLE_TOKEN }}/setWebhook */ -const SIMPLE_BOT_TOKEN = '...'; // Токен используется для ботов из директории: remote/echo-bot -const TBA_PHP_SDK_TOKEN = '...'; // Токен используется для ботов из директории: remote/tba-php-sdk -// Token for remote telegram bots <<< \ No newline at end of file +const EXAMPLE_TOKEN = '...'; // Токен (создается в BotFather) + +// Для подключения к БД (используется в некоторых ботах, например бот переводчик) +const PARAMS_DB = [ + 'type' => 'mysql', + 'host' => 'localhost', + 'name' => '{DB_NAME}', + 'port' => '{DB_PORT}', + 'user' => '{DB_USER}', + 'password' => '{DB_PASSWORD}', + 'options' => [ + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + //PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION // формирование исключений для PHP < v.8 + ], +]; + +function db(): DB +{ + return DB::getInstance()->getConnection(PARAMS_DB); +} diff --git a/translation-bot-v1/DB.php b/translation-bot-v1/DB.php new file mode 100644 index 0000000..f4047d6 --- /dev/null +++ b/translation-bot-v1/DB.php @@ -0,0 +1,138 @@ +connection instanceof PDO) { + return $this; + } + + $dsn = "{$dbConfig['type']}:host={$dbConfig['host']};port={$dbConfig['port']};dbname={$dbConfig['name']}"; + + try { + $this->connection = new PDO($dsn, $dbConfig['user'], $dbConfig['password'], $dbConfig['options']); + } catch (PDOException $exception) { + file_put_contents( + __DIR__ . '/try_catch_db_logs.txt', + date('d.m.Y H:i:s') . PHP_EOL . print_r($exception, true), + FILE_APPEND + ); + die; + } + + return $this; + } + + public function query(string $query, array $params = []): false|DB + { + try { + $this->stmt = $this->connection->prepare($query); + $this->stmt->execute($params); + } catch (PDOException $exception) { + file_put_contents( + __DIR__ . '/try_catch_db_logs.txt', + date('d.m.Y H:i:s') . PHP_EOL . print_r($exception, true), + FILE_APPEND + ); + die; + } + + return $this; + } + + public function findAll(): array + { + return $this + ->stmt + ->fetchAll(); + } + + public function find(): ?array + { + $result = $this + ->stmt + ->fetch(); + + if (!$result) { + return null; + } + + return $result; + } + + public function getChatId(int $chatId): ?array + { + return $this + ->query('SELECT * FROM chat WHERE chat_id=:chatId', ['chatId' => $chatId]) + ->find(); + } + + public function setChat( + int $chatId, + string $firstName, + string $lastName, + string $username, + string $date, + string $lang + ): void { + $this + ->query( + 'INSERT INTO chat + (chat_id, first_name, last_name, username, date, lang) + VALUES (:chatId, :firstName, :lastName, :username, :date, :lang)', + [ + 'chatId' => $chatId, + 'firstName' => $firstName, + 'lastName' => $lastName, + 'username' => $username, + 'date' => $date, + 'lang' => $lang + ] + ); + } + + public function updateChat(int $chatId, string $lang, string $date): void + { + $this + ->query( + 'UPDATE chat + SET lang=:lang, date=:date + WHERE chat_id=:chatId', + [ + 'chatId' => $chatId, + 'lang' => $lang, + 'date' => $date + ] + ); + } +} diff --git a/translation-bot-v1/FAQ/README.md b/translation-bot-v1/FAQ/README.md new file mode 100644 index 0000000..7f2f667 --- /dev/null +++ b/translation-bot-v1/FAQ/README.md @@ -0,0 +1,18 @@ +Создание таблицы `chat` в БД `telegram_bot_translation`: + +```sql +CREATE TABLE `telegram_bot_translation`.`chat` +( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `chat_id` BIGINT UNSIGNED NOT NULL, + `first_name` VARCHAR(510) NOT NULL, + `last_name` VARCHAR(510) NOT NULL, + `username` VARCHAR(510) NOT NULL, + `date` DATETIME NOT NULL, + `lang` VARCHAR(50) NOT NULL, + + PRIMARY KEY (`id`), + UNIQUE (`chat_id`) +) + ENGINE = InnoDB; +``` diff --git a/translation-bot-v1/Utils/DifferentTypesKeyboards.php b/translation-bot-v1/Utils/DifferentTypesKeyboards.php new file mode 100644 index 0000000..96fbd56 --- /dev/null +++ b/translation-bot-v1/Utils/DifferentTypesKeyboards.php @@ -0,0 +1,103 @@ +>> SIMPLE KEYBOARDS + protected static function simpleKeyboards(): array + { + return [ + ['Кнопка 1'], + ['Кнопка 2'], + ['Кнопка 3'], + ]; + } + + protected static function simpleKeyboardsWithComplexBtn(): array + { + return [ + [ + [ + 'text' => 'Отправить контакт', + 'request_contact' => true, + ], + [ + 'text' => 'Отправить локацию', + 'request_location' => true, + ] + ], + ['Открыть продвинутую клавиатуру'], + ['Убрать клавиатуру'], + ]; + } + + protected static function complexKeyboards(): array + { + return [ + ['Кнопка 1', 'Кнопка 2', 'Кнопка 3'], + ['Кнопка 4', 'Кнопка 5'], + ['Вернуться на стартовую клавиатуру'], + ['Убрать клавиатуру'], + ]; + } + // SIMPLE KEYBOARDS <<< + + // >>> INLINE KEYBOARDS + protected static function getInlineKeyboardForTranslationBot(string $lang): array + { + if ('ru' === $lang) { + $buttons = [ + [ + 'text' => 'Английский', + 'callback_data' => 'en', + ], + [ + 'text' => '☑️ Русский', + 'callback_data' => 'ru', + ], + ]; + } else { + $buttons = [ + [ + 'text' => '☑️ English', + 'callback_data' => 'en', + ], + [ + 'text' => 'Russian', + 'callback_data' => 'ru', + ], + ]; + } + + return [$buttons]; + } + // INLINE KEYBOARDS <<< + + protected static function preparedSelectedKeyboards( + array $keyBoards, + array $additionalParams = [], + bool $inlineKeyboards = false + ): Keyboard { + $typeKeyboard = $inlineKeyboards ? 'inline_keyboard' : 'keyboard'; + + $params = [ + $typeKeyboard => $keyBoards, + ]; + + if (count($additionalParams) > 0) { + $params = array_merge($params, $additionalParams); + } + + return Keyboard::make($params); + } + + protected static function removeSelectedKeyboard(): Keyboard + { + return Keyboard::make([ + 'remove_keyboard' => true, + ]); + } +} diff --git a/translation-bot-v1/Utils/TelegramBotApiHelper.php b/translation-bot-v1/Utils/TelegramBotApiHelper.php new file mode 100644 index 0000000..1b61b59 --- /dev/null +++ b/translation-bot-v1/Utils/TelegramBotApiHelper.php @@ -0,0 +1,269 @@ +answerCallbackQuery возвращаемый тип MessageObject|bool, + * а не Telegram\Bot\Objects\Message ( ->answerCallbackQuery - возвращает bool) + */ + public static function definedTypeMessage( + TelegramBotApi $telegram, + Update $update, + string $nameArrMessage = 'message' + ): MessageObject|bool { + switch ($nameArrMessage) { + case 'message': + $typeMessage = $update['message']; + $chatId = (int)$typeMessage['chat']['id']; + $incomingText = isset($typeMessage['text']) ? strtolower(trim($typeMessage['text'])) : ''; + break; + case 'edited_message': + $typeMessage = $update['edited_message']; + $chatId = (int)$typeMessage['chat']['id']; + $incomingText = isset($typeMessage['text']) ? strtolower(trim($typeMessage['text'])) : ''; + break; + case 'callback_query': + $typeMessage = $update['callback_query']; + $chatId = (int)$typeMessage['message']['chat']['id']; + $incomingText = ''; + break; + default: + $typeMessage = []; + $chatId = -1; + $incomingText = ''; + break; + } + + if ('/start' === $incomingText) { + $data = db()->getChatId($chatId); + $lang = 'ru'; + $firstName = self::getFirstName($typeMessage); + $lastName = self::getLastName($typeMessage); + $username = self::getUsername($typeMessage); + $date = self::getTimestampToDateTime($typeMessage); + + if (null === $data) { + db()->setChat( + $chatId, + $firstName, + $lastName, + $username, + $date, + $lang + ); + } else { + $lang = $data['lang']; + } + + $response = self::sendMessage( + telegram: $telegram, + chatId: $chatId, + message: 'Оставьте отмеченный язык для перевода с него или выберите другой', + additionalParams: [ + 'parse_mode' => 'Markdown', + 'reply_markup' => self::preparedSelectedKeyboards( + keyBoards: self::getInlineKeyboardForTranslationBot($lang), + inlineKeyboards: true + ) + ] + ); + } elseif ('callback_query' === $nameArrMessage) { + $btnInlineKeyBoard = $typeMessage['message']['reply_markup']['inline_keyboard'][0]; + $now = new DateTime(); + $nowStr = $now->format('Y-m-d H:i:s'); + + foreach ($btnInlineKeyBoard as $btn) { + $isoCode = match ($btn['text']) { + 'Русский', 'Russian' => 'ru', + 'English', 'Английский' => 'en', + default => '', + }; + + if ($isoCode === $typeMessage['data']) { + db()->updateChat( + $chatId, + $typeMessage['data'], + $nowStr, + ); + + $telegram->answerCallbackQuery(['callback_query_id' => $typeMessage['id']]); + + $response = self::sendMessage( + telegram: $telegram, + chatId: $chatId, + message: 'Можете вводить слово или фразу для перевода с выбранного языка', + additionalParams: [ + 'parse_mode' => 'Markdown', + 'reply_markup' => self::preparedSelectedKeyboards( + keyBoards: self::getInlineKeyboardForTranslationBot($typeMessage['data']), + inlineKeyboards: true + ) + ] + ); + + break; + } + } + + $telegram->answerCallbackQuery([ + 'callback_query_id' => $typeMessage['id'], + 'text' => 'Это уже активный язык', + 'show_alert' => false, // Вызывает модальное окно, требуется клик/тап, что бы убрать + ]); + + $response = true; + } elseif ('' !== $incomingText) { + $data = db()->getChatId($chatId); + + $source = ($data['lang'] === 'en') ? 'en' : 'ru'; + $target = ($data['lang'] === 'ru') ? 'en' : 'ru'; + $attempts = 5; + + $result = GoogleTranslateForFree::translate($source, $target, trim($incomingText), $attempts); + + self::writeToLogs( + [ + 'chatId' => $data['chat_id'], + 'full_name' => $data['first_name'] . ' ' . $data['last_name'], + 'username' => $data['username'], + 'textToTranslate' => $incomingText, + 'translatedText' => $result, + ], + __DIR__ . '/../translations.txt' + ); + + if ($result) { + $response = self::sendMessage( + telegram: $telegram, + chatId: $chatId, + message: $result, + ); + } else { + $response = self::sendMessage( + telegram: $telegram, + chatId: $chatId, + message: 'Сервис временно недоступен, попробуйте повторить позже', + ); + } + } else { + $response = self::sendMessage( + telegram: $telegram, + chatId: $chatId, + message: 'Бот умеет работать только с текстом', + ); + } + + return $response; + } + + public static function writeToLogs(array $update, string $pathToFile): void + { + if (count($update) > 0) { + ob_start(); + if (array_key_exists('message', $update)) { + echo '===[' . date('d-m-Y H:i:s', $update['message']['date']) . ']===' . PHP_EOL; + } elseif (array_key_exists('edited_message', $update)) { + echo '===[' . date('d-m-Y H:i:s', $update['edited_message']['edit_date']) . ']===' . PHP_EOL; + } elseif (array_key_exists('callback_query', $update)) { + echo '===[' . date('d-m-Y H:i:s', $update['callback_query']['message']['date']) . ']===' . PHP_EOL; + } else { + echo '===[' . date('d-m-Y H:i:s', $update['date']) . ']===' . PHP_EOL; + } + + print_r($update); + echo '------------------' . PHP_EOL; + $log = ob_get_clean(); + + if ('' !== $log) { + file_put_contents($pathToFile, $log, FILE_APPEND); + } + } + } + + /** + * @throws TelegramSDKException + */ + private static function sendMessage( + TelegramBotApi $telegram, + int $chatId, + string $message, + array $additionalParams = [], + int $delayMicroSecond = 0 + ): MessageObject { + $params = [ + 'chat_id' => $chatId, + 'text' => $message, + ]; + + if (count($additionalParams) > 0) { + $params = array_merge($params, $additionalParams); + } + + if (0 !== $delayMicroSecond) { + usleep($delayMicroSecond); + return $telegram->sendMessage($params); + } + + return $telegram->sendMessage($params); + } + + private static function getChatInfo(array $typeMessage): ?array + { + return $typeMessage['chat'] ?? null; + } + + private static function getFirstName(array $typeMessage): string + { + $chat = self::getChatInfo($typeMessage); + + if (null === $chat) { + return ''; + } + + return array_key_exists('first_name', $chat) ? $chat['first_name'] : ''; + } + + private static function getLastName(array $typeMessage): string + { + $chat = self::getChatInfo($typeMessage); + + if (null === $chat) { + return ''; + } + + return array_key_exists('last_name', $chat) ? $chat['last_name'] : ''; + } + + private static function getUsername(array $typeMessage): string + { + $chat = array_key_exists('chat', $typeMessage) ? $typeMessage['chat'] : ''; + + if ('' === $chat) { + return ''; + } + + return array_key_exists('username', $chat) ? $chat['username'] : ''; + } + + private static function getTimestampToDateTime(array $typeMessage): string + { + $date = new DateTime(); + $date->setTimestamp($typeMessage['date']); + + return $date->format('Y-m-d H:i:s'); + } +} diff --git a/translation-bot-v1/index.php b/translation-bot-v1/index.php new file mode 100644 index 0000000..cdf0132 --- /dev/null +++ b/translation-bot-v1/index.php @@ -0,0 +1,33 @@ +>> getUpdates + $update = $telegram->getWebhookUpdate(); + Helper::writeToLogs($update->getRawResponse(), __DIR__ . '/update_logs.txt'); + // getUpdates <<< + + // >>> sendMessage + if ($update->count() > 0) { + $response = match (true) { + array_key_exists('edited_message', $update->getRawResponse()) => Helper::definedTypeMessage($telegram, $update, 'edited_message'), + array_key_exists('callback_query', $update->getRawResponse()) => Helper::definedTypeMessage($telegram, $update, 'callback_query'), + default => Helper::definedTypeMessage($telegram, $update), + }; + } + // sendMessage <<< +} catch (Throwable $e) { + file_put_contents(__DIR__ . '/try_catch_logs.txt', date('d.m.Y H:i:s') . PHP_EOL . print_r($e, true), FILE_APPEND); +} + +//die('Silence is golden'); diff --git a/translation-bot/index.php b/translation-bot/index.php deleted file mode 100644 index cd7c7fd..0000000 --- a/translation-bot/index.php +++ /dev/null @@ -1,9 +0,0 @@ -