From 1781487f6a0b1cdf72beeef12f3770b177e59763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Mon, 18 May 2026 08:59:30 +0900 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20po?= =?UTF-8?q?lling-=D0=B2=D0=BE=D1=80=D0=BA=D0=B5=D1=80=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20Telegram-=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/polling.php | 389 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 scripts/polling.php diff --git a/scripts/polling.php b/scripts/polling.php new file mode 100644 index 0000000..f332649 --- /dev/null +++ b/scripts/polling.php @@ -0,0 +1,389 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + + return $pdo; +} + +function q(string $sql, array $params = []): PDOStatement +{ + $st = db()->prepare($sql); + $st->execute($params); + return $st; +} + +function one(string $sql, array $params = []): ?array +{ + $row = q($sql, $params)->fetch(); + return $row ?: null; +} + +function allRows(string $sql, array $params = []): array +{ + return q($sql, $params)->fetchAll(); +} + +function tgRequest(array $bot, string $method, array $payload = []): array +{ + $url = 'https://api.telegram.org/bot' . $bot['token'] . '/' . $method; + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_TIMEOUT => 35, + ]); + $raw = curl_exec($ch); + if ($raw === false) { + return ['ok' => false, 'description' => curl_error($ch)]; + } + $json = json_decode($raw, true); + return is_array($json) ? $json : ['ok' => false, 'description' => 'Bad Telegram response']; +} + +function sendMessage(array $bot, int $chatId, string $text, array $keyboard = []): void +{ + if ($chatId <= 0) { + return; + } + $payload = [ + 'chat_id' => $chatId, + 'text' => $text, + 'parse_mode' => 'HTML', + ]; + if ($keyboard) { + $payload['reply_markup'] = json_encode(['inline_keyboard' => $keyboard], JSON_UNESCAPED_UNICODE); + } + tgRequest($bot, 'sendMessage', $payload); +} + +function answerCallback(array $bot, string $callbackId): void +{ + tgRequest($bot, 'answerCallbackQuery', ['callback_query_id' => $callbackId]); +} + +function buttons(array $rows, string $prefix, string $labelField = 'name'): array +{ + $kb = []; + foreach ($rows as $r) { + $kb[] = [[ + 'text' => (string)$r[$labelField], + 'callback_data' => $prefix . ':' . $r['id'], + ]]; + } + return $kb; +} + +function getBotUser(array $bot, array $from): array +{ + $tgId = (int)$from['id']; + $row = one('SELECT * FROM bot_users WHERE bot_id=? AND telegram_id=?', [$bot['id'], $tgId]); + if (!$row) { + q( + 'INSERT INTO bot_users (bot_id,telegram_id,username,first_name,last_name,last_seen_at) VALUES (?,?,?,?,?,NOW())', + [$bot['id'], $tgId, $from['username'] ?? null, $from['first_name'] ?? null, $from['last_name'] ?? null] + ); + $row = one('SELECT * FROM bot_users WHERE bot_id=? AND telegram_id=?', [$bot['id'], $tgId]); + } else { + q( + 'UPDATE bot_users SET username=?, first_name=?, last_name=?, last_seen_at=NOW() WHERE id=?', + [$from['username'] ?? null, $from['first_name'] ?? null, $from['last_name'] ?? null, $row['id']] + ); + } + return $row; +} + +function setState(int $botUserId, string $state, array $data = []): void +{ + q('UPDATE bot_users SET state=?, state_data=? WHERE id=?', [ + $state, + json_encode($data, JSON_UNESCAPED_UNICODE), + $botUserId, + ]); +} + +function stateData(array $botUser): array +{ + $d = json_decode((string)($botUser['state_data'] ?? ''), true); + return is_array($d) ? $d : []; +} + +function showCities(array $bot, int $chatId, array $botUser): void +{ + $rows = allRows('SELECT * FROM shop_cities WHERE bot_id=? AND is_active=1 ORDER BY sort_order,name', [$bot['id']]); + if (!$rows) { + sendMessage($bot, $chatId, 'Пока нет доступных городов. Добавьте город в админ-панели.'); + return; + } + setState((int)$botUser['id'], 'choose_city'); + sendMessage($bot, $chatId, $bot['welcome_text'] ?: 'Выберите город:', buttons($rows, 'city')); +} + +function generateAmount(int $botId, int $base, int $min, int $max): array +{ + $busy = allRows("SELECT final_amount FROM shop_orders WHERE bot_id=? AND status='waiting_pay' AND expires_at>NOW()", [$botId]); + $busyMap = array_flip(array_map(fn($r) => (int)$r['final_amount'], $busy)); + + for ($i = 0; $i < 100; $i++) { + $add = random_int($min, $max); + $final = $base + $add; + if (!isset($busyMap[$final])) { + return [$add, $final]; + } + } + + throw new RuntimeException('Нет свободной уникальной суммы. Попробуйте позже.'); +} + +function createOrder(array $bot, array $botUser, array $data): array +{ + foreach (['city_id', 'district_id', 'product_id', 'variant_id'] as $key) { + if (empty($data[$key])) { + throw new RuntimeException('Неполные данные заказа. Начните заново через /start.'); + } + } + + $variant = one('SELECT * FROM shop_product_variants WHERE id=? AND bot_id=? AND is_active=1', [$data['variant_id'], $bot['id']]); + if (!$variant) { + throw new RuntimeException('Вариант товара не найден.'); + } + + $method = one('SELECT * FROM shop_payment_methods WHERE bot_id=? AND is_active=1 AND is_online=1 ORDER BY id ASC LIMIT 1', [$bot['id']]); + if (!$method) { + throw new RuntimeException('Нет активных реквизитов оплаты.'); + } + + [$add, $final] = generateAmount((int)$bot['id'], (int)$variant['price'], (int)$bot['min_add'], (int)$bot['max_add']); + $ttl = max(5, (int)$bot['order_ttl_minutes']); + + q( + 'INSERT INTO shop_orders (bot_id,bot_user_id,telegram_id,city_id,district_id,product_id,variant_id,payment_method_id,base_amount,random_increment,final_amount,expires_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,DATE_ADD(NOW(), INTERVAL ' . $ttl . ' MINUTE))', + [$bot['id'], $botUser['id'], $botUser['telegram_id'], $data['city_id'], $data['district_id'], $data['product_id'], $data['variant_id'], $method['id'], $variant['price'], $add, $final] + ); + + return one('SELECT * FROM shop_orders WHERE id=?', [(int)db()->lastInsertId()]); +} + +function textSafe(?string $value): string +{ + return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); +} + +function orderText(array $order): string +{ + $city = one('SELECT name FROM shop_cities WHERE id=?', [$order['city_id']]); + $district = one('SELECT name FROM shop_districts WHERE id=?', [$order['district_id']]); + $product = one('SELECT name FROM shop_products WHERE id=?', [$order['product_id']]); + $variant = one('SELECT name FROM shop_product_variants WHERE id=?', [$order['variant_id']]); + $method = one('SELECT * FROM shop_payment_methods WHERE id=?', [$order['payment_method_id']]); + + $text = "Заказ №{$order['id']}\n"; + $text .= 'Город: ' . textSafe($city['name'] ?? '') . "\n"; + $text .= 'Район: ' . textSafe($district['name'] ?? '') . "\n"; + $text .= 'Товар: ' . textSafe($product['name'] ?? '') . "\n"; + $text .= 'Вариант: ' . textSafe($variant['name'] ?? '') . "\n"; + $text .= "Цена: {$order['base_amount']} ₽\n"; + $text .= "Уникальная добавка: +{$order['random_increment']} ₽\n\n"; + $text .= "К оплате: {$order['final_amount']} ₽\n\n"; + + if ($method) { + $text .= "Реквизиты:\n"; + if ($method['bank_name']) { + $text .= 'Банк: ' . textSafe($method['bank_name']) . "\n"; + } + if ($method['card_number']) { + $text .= 'Карта: ' . textSafe($method['card_number']) . "\n"; + } + if ($method['sbp_phone']) { + $text .= 'СБП: ' . textSafe($method['sbp_phone']) . "\n"; + } + if ($method['recipient_name']) { + $text .= 'Получатель: ' . textSafe($method['recipient_name']) . "\n"; + } + } + + $text .= "\nОплатите точную сумму. После проверки администратор подтвердит заказ."; + return $text; +} + +function processUpdate(array $bot, array $payload): void +{ + q('INSERT INTO telegram_updates (bot_id, update_id, payload) VALUES (?,?,?)', [ + $bot['id'], + $payload['update_id'] ?? null, + json_encode($payload, JSON_UNESCAPED_UNICODE), + ]); + + $message = $payload['message'] ?? null; + $callback = $payload['callback_query'] ?? null; + $chatId = 0; + + try { + if ($message) { + $chatId = (int)$message['chat']['id']; + $botUser = getBotUser($bot, $message['from'] ?? $message['chat']); + showCities($bot, $chatId, $botUser); + return; + } + + if (!$callback) { + return; + } + + answerCallback($bot, $callback['id']); + $chatId = (int)$callback['message']['chat']['id']; + $botUser = getBotUser($bot, $callback['from']); + [$action, $id] = array_pad(explode(':', (string)$callback['data'], 2), 2, null); + $data = stateData($botUser); + + if ($action === 'city') { + $data = ['city_id' => (int)$id]; + setState((int)$botUser['id'], 'choose_district', $data); + $rows = allRows('SELECT * FROM shop_districts WHERE bot_id=? AND city_id=? AND is_active=1 ORDER BY sort_order,name', [$bot['id'], $id]); + if (!$rows) { + sendMessage($bot, $chatId, 'Для этого города пока нет районов.'); + return; + } + sendMessage($bot, $chatId, 'Выберите район:', buttons($rows, 'district')); + return; + } + + if ($action === 'district') { + $data['district_id'] = (int)$id; + setState((int)$botUser['id'], 'choose_product', $data); + $rows = allRows('SELECT * FROM shop_products WHERE bot_id=? AND is_active=1 ORDER BY sort_order,name', [$bot['id']]); + if (!$rows) { + sendMessage($bot, $chatId, 'Пока нет доступных товаров.'); + return; + } + sendMessage($bot, $chatId, 'Выберите товар:', buttons($rows, 'product')); + return; + } + + if ($action === 'product') { + $data['product_id'] = (int)$id; + setState((int)$botUser['id'], 'choose_variant', $data); + $rows = allRows('SELECT id, CONCAT(name, " — ", price, " ₽") AS name FROM shop_product_variants WHERE bot_id=? AND product_id=? AND is_active=1 ORDER BY sort_order,name', [$bot['id'], $id]); + if (!$rows) { + sendMessage($bot, $chatId, 'Для этого товара пока нет вариантов.'); + return; + } + sendMessage($bot, $chatId, 'Выберите вариант:', buttons($rows, 'variant')); + return; + } + + if ($action === 'variant') { + $data['variant_id'] = (int)$id; + setState((int)$botUser['id'], 'confirm', $data); + sendMessage($bot, $chatId, 'Подтвердить заказ?', [ + [['text' => 'Подтвердить', 'callback_data' => 'confirm:1']], + [['text' => 'Отмена', 'callback_data' => 'cancel:1']], + ]); + return; + } + + if ($action === 'confirm') { + $order = createOrder($bot, $botUser, $data); + setState((int)$botUser['id'], 'ordered', ['order_id' => $order['id']]); + sendMessage($bot, $chatId, orderText($order)); + return; + } + + if ($action === 'cancel') { + setState((int)$botUser['id'], 'start'); + showCities($bot, $chatId, $botUser); + } + } catch (Throwable $e) { + sendMessage($bot, $chatId, 'Ошибка: ' . $e->getMessage()); + fwrite(STDERR, '[' . date('c') . '] Bot #' . $bot['id'] . ': ' . $e->getMessage() . PHP_EOL); + } +} + +function loadOffsets(): array +{ + if (!is_file(OFFSET_FILE)) { + return []; + } + $json = json_decode((string)file_get_contents(OFFSET_FILE), true); + return is_array($json) ? $json : []; +} + +function saveOffsets(array $offsets): void +{ + file_put_contents(OFFSET_FILE, json_encode($offsets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); +} + +fwrite(STDOUT, '[' . date('c') . "] BotShop polling started\n"); +$offsets = loadOffsets(); +$webhookDeleted = []; + +while (true) { + try { + q("UPDATE shop_orders SET status='expired' WHERE status='waiting_pay' AND expires_at 'false']); + $webhookDeleted[$botId] = true; + } + + $payload = [ + 'timeout' => 10, + 'limit' => 30, + 'allowed_updates' => json_encode(['message', 'callback_query']), + ]; + if (isset($offsets[$botId])) { + $payload['offset'] = (int)$offsets[$botId]; + } + + $response = tgRequest($bot, 'getUpdates', $payload); + if (!($response['ok'] ?? false)) { + fwrite(STDERR, '[' . date('c') . '] getUpdates error for bot #' . $botId . ': ' . ($response['description'] ?? 'unknown') . PHP_EOL); + continue; + } + + foreach (($response['result'] ?? []) as $update) { + processUpdate($bot, $update); + if (isset($update['update_id'])) { + $offsets[$botId] = ((int)$update['update_id']) + 1; + } + } + + saveOffsets($offsets); + } + + if (!$bots) { + sleep(3); + } + } catch (Throwable $e) { + fwrite(STDERR, '[' . date('c') . '] polling loop error: ' . $e->getMessage() . PHP_EOL); + sleep(5); + } +}