File: //proc/1124/cwd/frontend/controllers/BotController.18.03.php
<?php
namespace frontend\controllers;
use aki\telegram\types\InputMedia\InputMediaDocument;
use cheatsheet\Time;
use common\models\Bot;
use common\models\BotCommands;
use common\models\BotContactBooks;
use common\models\BotContacts;
use common\models\BotUserAction;
use common\models\Help;
use Yii;
use yii\filters\PageCache;
use yii\web\BadRequestHttpException;
use yii\web\Controller;
use yii\web\Response;
use aki\telegram\base\Command;
use Box\Spout\Reader\Common\Creator\ReaderEntityFactory;
use common\helpers\GoogleSheetHelper;
/**
* Bot controller с автоматической конвертацией XLSX/XLS → CSV через Spout
*/
class BotController extends Controller
{
public $enableCsrfValidation = false;
/**
* @return string
* Вход в приложение
*/
public function actionIndex($id = null)
{
// Отладка: логируем приход update
file_put_contents(Yii::getAlias('@web_path') . '/tg_log.txt', date('Y-m-d H:i:s') . " - id: $id\n", FILE_APPEND);
if (!$id) {
return 'ok';
}
$bot_data = Bot::find()->where(['id' => $id])->one();
if (!$bot_data || !$bot_data->token) {
return 'ok';
}
$token = $bot_data->token;
$user_id = $bot_data->user_id;
// Инициализируем Telegram
\Yii::$app->setComponents([
'telegram' => [
'class' => \aki\telegram\Telegram::class,
'botToken' => $token,
]
]);
$telegram = Yii::$app->telegram;
$message_data = $telegram->input->message ?? null;
if (!$message_data) {
return 'ok';
}
$message_text = $message_data->text ?? null;
$chat_id = $message_data->chat->id ?? null;
$mini_path = null;
$document_id = null;
if (isset($message_data->document['file_id'])) {
$document_id = $message_data->document['file_id'];
} elseif (isset($message_data->photo[2]['file_id'])) {
$document_id = $message_data->photo[2]['file_id'];
} elseif (isset($message_data->voice['file_id'])) {
$document_id = $message_data->voice['file_id'];
$audio_data = $telegram->getFile(['file_id' => $document_id]);
$audio_path = $audio_data['result']['file_path'] ?? null;
}
if ($document_id) {
$file_data = $telegram->getFile(['file_id' => $document_id]);
$file_path = $file_data['result']['file_path'] ?? null;
if ($file_path) {
$file_name = basename($file_path);
$res_file_name = uniqid() . '_' . $file_name;
$path = Yii::getAlias('@storage/web/source/1/') . $res_file_name;
$mini_path = '1/' . $res_file_name;
$file_url = $telegram->getFileUrl(['file_id' => $document_id]);
if ($file_url) {
file_put_contents($path, file_get_contents($file_url));
if (isset($message_data->voice['file_id'])) {
self::actionuserregistration($chat_id, $id, $user_id, $message_text ?? '', $mini_path);
}
}
}
}
// === ОБРАБОТКА /start команды ===
try {
Command::run("/start", function ($telegram, $arr) use ($chat_id, $id, $user_id) {
self::actionuserregistration($chat_id, $id, $user_id, '/start', '---');
return true;
});
} catch (\Throwable $e) {
file_put_contents(
Yii::getAlias('@web_path') . '/tg_error.txt',
$e->getMessage() . "\n" . $e->getTraceAsString() . "\n",
FILE_APPEND
);
}
// === ОБРАБОТКА команд /reg и регистрация пользователя ===
if ($message_text === '/reg') {
Yii::$app->cache->set('reg_step_' . $chat_id, 'ask_name', 3600);
$answer_text = BotCommands::find()
->where(['command' => $message_text, 'bot_id' => $id, 'user_id' => $user_id])
->select('message')
->scalar() ?? "👋 Вітаю! Введіть ваше ім’я:";
$telegram->sendMessage(['chat_id' => $chat_id, 'text' => $answer_text]);
return true;
}
$regStep = Yii::$app->cache->get('reg_step_' . $chat_id);
if ($regStep) {
$data = Yii::$app->cache->get('reg_data_' . $chat_id) ?? ['id' => $chat_id];
$data['id'] = $chat_id;
switch ($regStep) {
case 'ask_name':
$data['name'] = $message_text;
Yii::$app->cache->set('reg_data_' . $chat_id, $data, 3600);
Yii::$app->cache->set('reg_step_' . $chat_id, 'ask_company', 3600);
$telegram->sendMessage(['chat_id' => $chat_id, 'text' => "🏢 Введіть назву вашої компанії:"]);
return true;
case 'ask_company':
$data['company'] = $message_text;
Yii::$app->cache->set('reg_data_' . $chat_id, $data, 3600);
Yii::$app->cache->set('reg_step_' . $chat_id, 'ask_phone', 3600);
$telegram->sendMessage([
'chat_id' => $chat_id,
'text' => "📞 Натисніть кнопку нижче, щоб поділитися номером:",
'reply_markup' => json_encode([
'keyboard' => [[['text' => '📲 Поділитися номером', 'request_contact' => true]]],
'resize_keyboard' => true,
'one_time_keyboard' => true
])
]);
return true;
case 'ask_phone':
$data['phone'] = $message_data->contact['phone_number'] ?? 'Не додано';
$data['allow'] = 0;
// Отправляем в Google Sheets
$this->appendToGoogleSheet($data);
Yii::$app->cache->delete('reg_step_' . $chat_id);
Yii::$app->cache->delete('reg_data_' . $chat_id);
$telegram->sendMessage(['chat_id' => $chat_id, 'text' => "✅ Дякуємо, {$data['name']}! Ваші дані збережено. Дочекайтеся підтверждення доступу."]);
return true;
}
}
// === ОБРАБОТКА /search режима ===
$searchCacheKey = 'bot_search_mode_' . $id . '_' . $chat_id;
$msgLower = mb_strtolower($message_text ?? '', 'UTF-8');
if ($msgLower === '/search') {
Yii::$app->cache->set($searchCacheKey, true, 7200);
}
$searchCmdAllowed = BotCommands::find()->where(['bot_id' => $id, 'command' => '/search'])->exists();
$isSearchModeActive = Yii::$app->cache->get($searchCacheKey);
if ($searchCmdAllowed && $message_text && $message_text[0] !== '/' && $isSearchModeActive) {
try {
$results = $this->actionSearchProduct($message_text, $id);
$this->sendSearchResults($telegram, $chat_id, $results, $token);
self::actionuserregistration($chat_id, $id, $user_id, $message_text, $mini_path);
return true;
} catch (\Throwable $e) {
Yii::error("SEARCH ERROR: " . $e->getMessage());
return true;
}
}
// === ОБРАБОТКА любых других команд ===
if ($message_text) {
$command_message_data = BotCommands::find()
->where(['command' => $message_text, 'bot_id' => $id, 'user_id' => $user_id])
->one();
$answer_text = $command_message_data->message ?? (($message_text === '/start') ? 'Приветствуем Вас' : (str_starts_with($message_text, '/') ? 'Команда не найдена' : null));
if ($answer_text) {
$telegram->sendMessage(['chat_id' => $chat_id, 'text' => $answer_text, 'parse_mode' => 'HTML']);
} elseif ($message_text !== '/start') {
self::actionuserregistration($chat_id, $id, $user_id, $message_text, $mini_path);
}
}
$mss_photo = $message_data->photo ?? null;
$mss_audio = $message_data->voice ?? null;
$mss_document = $message_data->document ?? null;
// === Файлы без текста ===
if (($mss_document || $mss_photo) && !$message_text) {
self::actionuserregistration($chat_id, $id, $user_id, $message_text ?? null, $mini_path);
}
return 'ok';
}
/**
* @param $telegram
* @param $chat_id
* @param $key
*/
public static function actionuserregistration($chat_id = null, $bot_id = null, $user_id = null, $message_text = null, $message_file = null)
{
if ($chat_id && $bot_id && $user_id) {
//проверяем, есть ли такой пользователь в базе
$contact_status = BotContacts::find()->where(['bot_id' => $bot_id])->andWhere(['contact' => $chat_id])->one();
if (!$contact_status) {
//если пользователя нету - проверяем, есть ли дефолтная книга у пользователя
$default_book = BotContactBooks::find()
->where(['bot_id' => $bot_id])
->andWhere(['user_id' => $user_id])
->andWhere(['name' => 'Default'])->one() ?? new BotContactBooks();
$default_book->bot_id = (string)$bot_id;
$default_book->user_id = (string)$user_id;
$default_book->name = (string)'Default';
$default_book->save();
$book_id = $default_book->id;
//если есть дефолтная книга пользователей - добавляем пользователя,
//если нету дефолтной книги для данного бота - создаем и тогда добавляем нашего пользователя
$bt_cntct = new BotContacts();
$bt_cntct->bot_contacts_book_id = (string)$book_id;
$bt_cntct->bot_id = (string)$bot_id;
$bt_cntct->contact = (string)$chat_id;
$bt_cntct->name = (string)'User';
$bt_cntct->other_data = (string)'-';
$bt_cntct->visible = (string)'1';
$bt_cntct->save();
}
//пишем данные
$time_now = date("Y-m-d H:i:s");
if ($message_text || $message_file) {
$bot_action_data = new BotUserAction();
$bot_action_data->user_id = (string)$user_id;
$bot_action_data->message_from = (string)'Пользователь';
$bot_action_data->bot_id = (string)$bot_id;
$bot_action_data->chat_id = (string)$chat_id;
$bot_action_data->message = (string)$message_text;
$bot_action_data->image = NULL;
$bot_action_data->file = (string)$message_file;
$bot_action_data->admin_comment = NULL;
$bot_action_data->created_at = (string)$time_now;
$bot_action_data->save();
}
//отправляем уведомление в чат админу
if ($bot_id) {
//для начала получаем данные по боту - есть ли для бота указан пользователь для получения уведомлений
//если есть - отправляем данные указанному контакту
$bot_data = Bot::find()->where(['id' => $bot_id])->one();
if ($bot_data) {
$admin_chat_id = $bot_data->inform_chat_id;
$admin_chat_bot_token = $bot_data->token;
$admin_chat_bot_name = $bot_data->name;
if ($bot_data->bot_type == 1 && $admin_chat_id && $admin_chat_bot_token) {
//если есть chat_id для уведомлений + есть токен бота = отправляем уведомление о новом событии в боте
\Yii::$app->setComponents(['telegram' => [
'class' => \aki\telegram\Telegram::class,
'botToken' => $admin_chat_bot_token,
]]);
$telegram = Yii::$app->telegram;
if ($admin_chat_bot_name) {
$admin_chat_bot_name = " - " . $admin_chat_bot_name;
} else {
$admin_chat_bot_name = " ";
}
if($admin_chat_bot_name){
$msg_upd = $admin_chat_bot_name;
//добавляем ссылку на наш чат в панели управления
if($bot_action_data->id){
$contact_data = BotContacts::find()
->where(['bot_id' => $bot_id])
->andWhere(['contact' => $chat_id])
->one();
if($contact_data){
$line_id = $contact_data->id;
if($line_id){
$url = env("FRONTEND_HOST_INFO")."/user/botcontactbooks/viewactions?id=".$line_id;
if($url){
$url_tag = "<a href='".$url."'> Перейти в чат </a>";
$admin_chat_bot_name = $admin_chat_bot_name.$url_tag;
}
}
}
}
}
$answear_text = "Новое действие в Вашем боте " . $admin_chat_bot_name." Chat ID - ".$chat_id;
try {
Yii::$app->telegram->sendMessage([
'chat_id' => $admin_chat_id,
'text' => $answear_text,
'parse_mode' => "HTML"
]);
} catch (\Exception $e) {
Yii::error('BOT SENDER ERROR (BotController LN 364) - CDRMN ERR - : ' . $e->getMessage());
return true;
}
return true;
}
return true;
}
}
}
return true;
}
/**
* Конвертація XLSX/XLS → CSV через Spout (швидко і без проблем з пам'яттю)
*/
private function convertExcelToCsvSpout($excelPath, $csvPath)
{
try {
Yii::warning("Spout конвертація: $excelPath → $csvPath");
// Визначаємо тип файлу
$extension = strtolower(pathinfo($excelPath, PATHINFO_EXTENSION));
// Створюємо reader
if ($extension === 'xlsx') {
$reader = ReaderEntityFactory::createXLSXReader();
} elseif ($extension === 'xls') {
$reader = ReaderEntityFactory::createXLSReader();
} else {
throw new \Exception('Непідтримуваний формат');
}
$reader->open($excelPath);
// Відкриваємо CSV для запису
$csvFile = fopen($csvPath, 'w');
if (!$csvFile) {
throw new \Exception('Не вдалося створити CSV');
}
// Читаємо по рядках (streaming!)
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) {
$rowData = [];
$cells = $row->getCells();
foreach ($cells as $cell) {
$rowData[] = $cell->getValue();
}
fputcsv($csvFile, $rowData);
}
break; // Читаємо тільки перший лист
}
fclose($csvFile);
$reader->close();
Yii::warning("Spout конвертація завершена успішно");
return true;
} catch (\Exception $e) {
Yii::error("Spout помилка: " . $e->getMessage());
return false;
}
}
/**
* Отримує CSV (автоматично конвертує XLSX/XLS через Spout)
*/
private function getCsvPath($baseName, $botId)
{
// Папка для файлів бота
$storagePath = Yii::getAlias('@storage/web/source/bot_' . $botId);
// Якщо папки бота немає - використовуємо загальну папку (для зворотної сумісності)
if (!is_dir($storagePath)) {
$storagePath = Yii::getAlias('@storage/web/source');
}
$csvPath = $storagePath . '/' . $baseName . '.csv';
$xlsxPath = $storagePath . '/' . $baseName . '.xlsx';
$xlsPath = $storagePath . '/' . $baseName . '.xls';
// Якщо CSV існує - перевіряємо актуальність
if (file_exists($csvPath)) {
$csvTime = filemtime($csvPath);
// Перевіряємо чи XLSX/XLS новіший
if (file_exists($xlsxPath) && filemtime($xlsxPath) > $csvTime) {
Yii::warning("XLSX новіший, реконвертація через Spout");
$this->convertExcelToCsvSpout($xlsxPath, $csvPath);
} elseif (file_exists($xlsPath) && filemtime($xlsPath) > $csvTime) {
Yii::warning("XLS новіший, реконвертація через Spout");
$this->convertExcelToCsvSpout($xlsPath, $csvPath);
}
return $csvPath;
}
// CSV не існує - конвертуємо XLSX/XLS
if (file_exists($xlsxPath)) {
Yii::warning("Конвертуємо XLSX через Spout");
if ($this->convertExcelToCsvSpout($xlsxPath, $csvPath)) {
return $csvPath;
}
}
if (file_exists($xlsPath)) {
Yii::warning("Конвертуємо XLS через Spout");
if ($this->convertExcelToCsvSpout($xlsPath, $csvPath)) {
return $csvPath;
}
}
throw new \Exception("Файл $baseName не знайдено для бота $botId");
}
public function actionSearchProduct($text, $bot_id)
{
$storagePath = Yii::getAlias('@storage/web/source/bot_' . $bot_id);
$files = glob($storagePath . '/*.csv');
$text = mb_strtolower(trim($text), 'UTF-8');
if (empty($files) || mb_strlen($text) < 2) return [];
$stockData = [];
$results = [];
// ШАГ 1. Сначала ищем файл СКЛАДА (Sklad)
// Загружаем остатки в массив: [Артикул => Кол-во]
foreach ($files as $filePath) {
$fileName = basename($filePath);
// Если в имени файла есть "Sklad" или "Склад"
if (mb_stripos($fileName, 'sklad') !== false || mb_stripos($fileName, 'склад') !== false) {
if (($handle = fopen($filePath, 'r')) !== false) {
fgetcsv($handle); // Пропускаем заголовок
while (($row = fgetcsv($handle)) !== false) {
// Sklad: Col 0 = Артикул, Col 2 = Остаток
if (isset($row[0])) {
$art = trim($row[0]);
// Если колонки 2 нет, ставим 0
$qty = isset($row[2]) ? trim($row[2]) : '0';
$stockData[$art] = $qty;
}
}
fclose($handle);
}
}
}
// ШАГ 2. Ищем в ПРАЙСЕ и других файлах
foreach ($files as $filePath) {
$fileName = basename($filePath);
// Склад мы уже обработали, его пропускаем
if (mb_stripos($fileName, 'sklad') !== false || mb_stripos($fileName, 'склад') !== false) continue;
// Определяем, это ПРАЙС-ЛИСТ или просто левая таблица (tab1)?
// Считаем прайсом, если в имени есть "Pricelist" или "Прайс"
$isPricelist = (mb_stripos($fileName, 'pricelist') !== false || mb_stripos($fileName, 'прайс') !== false);
if (($handle = fopen($filePath, 'r')) !== false) {
$headers = fgetcsv($handle); // Читаем заголовки
// Чистим заголовки
if ($headers) {
foreach ($headers as $k => $h) $headers[$k] = trim($h, "\xEF\xBB\xBF \t\n\r\0\x0B");
}
while (($row = fgetcsv($handle)) !== false) {
// Проверяем совпадение текста во всей строке
$rowStr = implode(' ', $row);
if (mb_stripos($rowStr, $text, 0, 'UTF-8') !== false) {
$item = [];
if ($isPricelist) {
// === ЛОГИКА ДЛЯ ПРАЙСА (ЖЕСТКИЕ ИНДЕКСЫ) ===
// 0-Арт, 1-Назв, 2-Ед, 3-Цена, 4-Упак, 5-Фото
$article = isset($row[0]) ? trim($row[0]) : '';
$item['type'] = 'product'; // Метка для вывода
$item['article'] = $article;
$item['name'] = isset($row[1]) ? trim($row[1]) : 'Без назви';
$item['unit'] = isset($row[2]) ? trim($row[2]) : '';
$item['price'] = isset($row[3]) ? trim($row[3]) : '';
$item['packing'] = isset($row[4]) ? trim($row[4]) : '';
$item['image'] = isset($row[5]) ? trim($row[5]) : null;
// Подтягиваем остаток из Sklad
if ($article && isset($stockData[$article])) {
$item['stock'] = $stockData[$article];
} else {
$item['stock'] = '0';
}
} else {
// === ЛОГИКА ДЛЯ ДРУГИХ ФАЙЛОВ (tab1 контакты) ===
$item['type'] = 'general';
// Берем 6-ю колонку как фото (твое требование)
$item['image'] = (isset($row[5]) && filter_var($row[5], FILTER_VALIDATE_URL)) ? $row[5] : null;
$item['headers'] = $headers;
$item['row'] = $row;
}
$results[] = $item;
if (count($results) >= 10) break 2;
}
}
fclose($handle);
}
}
return $results;
}
private function sendSearchResults($telegram, $chat_id, $results, $bot_token = null)
{
if (empty($results)) {
$telegram->sendMessage([
'chat_id' => $chat_id,
'text' => "🔍 Нічого не знайдено.",
'parse_mode' => "HTML"
]);
return;
}
$getEnv = function($key, $default) {
if (isset($_ENV[$key])) return $_ENV[$key];
$val = getenv($key);
if ($val !== false) return $val;
if (isset($_SERVER[$key])) return $_SERVER[$key];
return $default;
};
$targetToken = $getEnv('TARGET_BOT_TOKEN', '');
if ($bot_token && $targetToken && trim($bot_token) === trim($targetToken)) {
$lbl_article = $getEnv('BOT_LABEL_ARTICLE', '🔢 Артикул: ');
$lbl_price = $getEnv('BOT_LABEL_PRICE', '💰 Ціна: ');
$lbl_unit = $getEnv('BOT_LABEL_UNIT', '📏 Одиниця: ');
$lbl_packing = $getEnv('BOT_LABEL_PACKING', '📦 Норма пакування: ');
$lbl_stock = $getEnv('BOT_LABEL_STOCK', '✅ Доступно: ');
} else {
$lbl_article = '🔢 Артикул: ';
$lbl_price = '💰 Ціна: ';
$lbl_unit = '📏 Одиниця: ';
$lbl_packing = '📦 Норма пакування: ';
$lbl_stock = '✅ Доступно: ';
}
foreach ($results as $item) {
$caption = "";
$photoUrl = isset($item['image']) ? $item['image'] : null;
if (isset($item['type']) && $item['type'] === 'product') {
$caption .= "📦 <b>" . htmlspecialchars($item['name']) . "</b>\n";
// === ИСПРАВЛЕНИЕ ===
// Проверяем: если есть 'article' (из CSV) - берем его, если нет - берем 'vendorCode' (из XML)
$artVal = isset($item['article']) ? $item['article'] : (isset($item['vendorCode']) ? $item['vendorCode'] : '');
$caption .= $lbl_article . "<code>" . htmlspecialchars($artVal) . "</code>\n";
// Дальше идет цена и т.д.
$caption .= $lbl_price . htmlspecialchars($item['price']) . " EUR (без ПДВ)\n";
if (!empty($item['unit'])) {
$caption .= $lbl_unit . htmlspecialchars($item['unit']) . "\n";
}
if (!empty($item['packing'])) {
$caption .= $lbl_packing . htmlspecialchars($item['packing']) . "\n";
}
$stockVal = isset($item['stock']) ? $item['stock'] : (isset($item['available']) ? $item['available'] : '');
$caption .= $lbl_stock . "<b>" . htmlspecialchars($stockVal) . "</b>";
} else {
if (!empty($item['headers']) && !empty($item['row'])) {
foreach ($item['headers'] as $i => $h) {
$val = isset($item['row'][$i]) ? trim($item['row'][$i]) : '';
if ($val === '' || $val === $photoUrl) continue;
$caption .= "<b>" . htmlspecialchars($h) . ":</b> " . htmlspecialchars($val) . "\n";
}
}
}
if ($photoUrl && filter_var($photoUrl, FILTER_VALIDATE_URL)) {
try {
$telegram->sendPhoto([
'chat_id' => $chat_id,
'photo' => $photoUrl,
'caption' => $caption,
'parse_mode' => "HTML"
]);
} catch (\Exception $e) {
$telegram->sendMessage(['chat_id' => $chat_id, 'text' => $caption, 'parse_mode' => "HTML"]);
}
} else {
$telegram->sendMessage(['chat_id' => $chat_id, 'text' => $caption, 'parse_mode' => "HTML"]);
}
}
}
private function appendToGoogleSheet($userData)
{
$client = new \Google_Client();
$client->setAuthConfig(Yii::getAlias('@common/config/google/service-account.json'));
$client->addScope(\Google_Service_Sheets::SPREADSHEETS);
$service = new \Google_Service_Sheets($client);
$spreadsheetId = env('GOOGLE_SPREADSHEET_ID'); // ID таблицы
$range = 'Лист1!A1';
$values = [[$userData['id'], $userData['name'], $userData['phone'], $userData['company'], $userData['allow']]];
$body = new \Google_Service_Sheets_ValueRange([
'values' => $values
]);
$params = [
'valueInputOption' => 'RAW',
'insertDataOption' => 'INSERT_ROWS'
];
$response = $service->spreadsheets_values->append($spreadsheetId, $range, $body, $params);
return true;
}
public function checkOrCreateUserInSheet($userData)
{
$client = new Google_Client();
$client->setAuthConfig(Yii::getAlias('@common/config/google/service-account.json'));
$client->setScopes([Google_Service_Sheets::SPREADSHEETS]);
$service = new Google_Service_Sheets($client);
$spreadsheetId = env('GOOGLE_SPREADSHEET_ID');
$range = 'Лист1!A1';
// 1. Получаем все строки
$response = $service->spreadsheets_values->get($spreadsheetId, $range);
$rows = $response->getValues();
$foundRowIndex = null;
$flagValue = null;
if (!empty($rows)) {
foreach ($rows as $index => $row) {
if (isset($row[0]) && $row[0] == $userData['id']) {
$foundRowIndex = $index;
$flagValue = $row[4] ?? null; // колонка E
break;
}
}
}
// 2. Если НЕ найден — создаем запись
if ($foundRowIndex === null) {
$values = [
[$userData['id'], $userData['name'], $userData['company'], $userData['phone'], false]
];
$body = new Google_Service_Sheets_ValueRange([
'values' => $values
]);
$params = [
'valueInputOption' => 'USER_ENTERED'
];
$service->spreadsheets_values->append(
$spreadsheetId,
'Sheet1!A:E',
$body,
$params
);
return false;
}
// 3. Если найден — проверяем флажок (колонка E)
// В Google Sheets checkbox возвращает TRUE/FALSE или "TRUE"/"FALSE"
if ($flagValue === true || $flagValue === 'TRUE') {
return true;
}
return false;
}
}