' : ''; }
function botOptions(?int $selected = null): string {
$html = '';
foreach (allRows('SELECT id,name FROM bots ORDER BY id DESC') as $b) {
$html .= '';
}
return $html;
}
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=>10]);
$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 {
$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'=>$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 {
$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 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']]);
$pay = "\n\nК оплате: {$order['final_amount']} ₽\n";
if ($method) {
$pay .= "\nРеквизиты:\n";
if ($method['bank_name']) $pay .= 'Банк: ' . e($method['bank_name']) . "\n";
if ($method['card_number']) $pay .= 'Карта: ' . e($method['card_number']) . "\n";
if ($method['sbp_phone']) $pay .= 'СБП: ' . e($method['sbp_phone']) . "\n";
if ($method['recipient_name']) $pay .= 'Получатель: ' . e($method['recipient_name']) . "\n";
}
return "Заказ №{$order['id']}\nГород: ".e($city['name']??'')."\nРайон: ".e($district['name']??'')."\nТовар: ".e($product['name']??'')."\nВариант: ".e($variant['name']??'')."\nЦена: {$order['base_amount']} ₽\nУникальная добавка: +{$order['random_increment']} ₽" . $pay . "\nОплатите точную сумму. После проверки администратор подтвердит заказ.";
}
function handleWebhook(string $secret): void {
$bot = one('SELECT * FROM bots WHERE webhook_secret=? AND status="active"', [$secret]);
if (!$bot) { http_response_code(404); echo 'bot not found'; return; }
$payload = json_decode(file_get_contents('php://input') ?: '{}', true) ?: [];
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;
try {
if ($message) {
$chatId = (int)$message['chat']['id'];
$botUser = getBotUser($bot, $message['from'] ?? $message['chat']);
showCities($bot, $chatId, $botUser);
} elseif ($callback) {
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]);
sendMessage($bot,$chatId,'Выберите район:', buttons($rows,'district'));
} elseif ($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']]);
sendMessage($bot,$chatId,'Выберите товар:', buttons($rows,'product'));
} elseif ($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]);
sendMessage($bot,$chatId,'Выберите вариант:', buttons($rows,'variant'));
} elseif ($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']]]);
} elseif ($action === 'confirm') {
$order = createOrder($bot,$botUser,$data); setState((int)$botUser['id'],'ordered',['order_id'=>$order['id']]);
sendMessage($bot,$chatId, orderText($order));
} elseif ($action === 'cancel') {
setState((int)$botUser['id'],'start'); showCities($bot,$chatId,$botUser);
}
}
} catch (Throwable $e) {
sendMessage($bot, (int)($chatId ?? 0), 'Ошибка: ' . $e->getMessage());
}
echo 'ok';
}
function installPage(): void {
if (installed()) redirect('/login');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$host=$_POST['db_host']??'localhost'; $name=$_POST['db_name']??''; $user=$_POST['db_user']??''; $pass=$_POST['db_pass']??'';
$pdo = new PDO('mysql:host='.$host.';dbname='.$name.';charset=utf8mb4', $user, $pass, [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
$schema = file_get_contents(ROOT.'/database/schema.sql'); $pdo->exec($schema);
$adminName=$_POST['admin_name']?:'Admin'; $email=$_POST['admin_email']; $hash=password_hash($_POST['admin_pass'], PASSWORD_DEFAULT);
$st=$pdo->prepare('INSERT INTO users (name,email,password_hash,role) VALUES (?,?,?,"admin")'); $st->execute([$adminName,$email,$hash]);
if (!is_dir(ROOT.'/storage')) mkdir(ROOT.'/storage',0775,true); if (!is_dir(ROOT.'/storage/logs')) mkdir(ROOT.'/storage/logs',0775,true);
$cfg = "$host,'db_name'=>$name,'db_user'=>$user,'db_pass'=>$pass], true) . ";\n";
file_put_contents(CONFIG_FILE, $cfg); redirect('/login');
}
layout('Установка', '
Установка BotShop
');
}
function simpleCreate(string $table, array $fields, string $redirect): void {
checkCsrf(); $cols=[];$vals=[];$ph=[]; foreach($fields as $f){$cols[]=$f;$vals[]=$_POST[$f]??null;$ph[]='?';}
q('INSERT INTO '.$table.' ('.implode(',',$cols).') VALUES ('.implode(',',$ph).')',$vals); flash('Сохранено'); redirect($redirect);
}
function loginPage(): void {
if ($_SERVER['REQUEST_METHOD']==='POST') { $u=one('SELECT * FROM users WHERE email=?',[$_POST['email']??'']); if($u && password_verify($_POST['password']??'', $u['password_hash'])){$_SESSION['user']=['id'=>$u['id'],'email'=>$u['email'],'name'=>$u['name']]; redirect('/');} flash('Неверный логин или пароль'); }
layout('Вход', '
BotShop
'.notice().'
');
}
function dashboard(): void { requireAuth();
$stats = [
'Боты'=>one('SELECT COUNT(*) c FROM bots')['c']??0,
'Пользователи'=>one('SELECT COUNT(*) c FROM bot_users')['c']??0,
'Ожидают оплату'=>one("SELECT COUNT(*) c FROM shop_orders WHERE status='waiting_pay'")['c']??0,
'Оплачено'=>one("SELECT COUNT(*) c FROM shop_orders WHERE status='paid'")['c']??0,
];
$html=notice().'
'; foreach($stats as $k=>$v)$html.='
'.e($k).'
'.e((string)$v).'
'; $html.='
BotShop v'.APP_VERSION.'
MVP мультибот-магазина без отправки чеков. Оплата подтверждается оператором в панели.
'; layout('Дашборд',$html);
}
function botsPage(): void { requireAuth();
if ($_SERVER['REQUEST_METHOD']==='POST') {
checkCsrf(); $token=trim($_POST['token']??''); $secret=bin2hex(random_bytes(16));
$bot=['token'=>$token]; $me=tgRequest($bot,'getMe'); $username=$me['result']['username']??null;
q('INSERT INTO bots (name,username,token,webhook_secret,status,welcome_text,pay_text,min_add,max_add,order_ttl_minutes) VALUES (?,?,?,?,?,?,?,?,?,?)', [$_POST['name'],$username,$token,$secret,$_POST['status']??'paused',$_POST['welcome_text']??null,$_POST['pay_text']??null,(int)$_POST['min_add'],(int)$_POST['max_add'],(int)$_POST['order_ttl_minutes']]);
flash('Бот добавлен'); redirect('/bots');
}
if (isset($_GET['setwebhook'])) { $b=one('SELECT * FROM bots WHERE id=?',[(int)$_GET['setwebhook']]); if($b){$url=(isset($_SERVER['HTTPS'])?'https':'http').'://'.$_SERVER['HTTP_HOST'].'/webhook/'.$b['webhook_secret']; $r=tgRequest($b,'setWebhook',['url'=>$url]); flash(($r['ok']??false)?'Webhook установлен: '.$url:'Ошибка webhook: '.($r['description']??'unknown'));} redirect('/bots'); }
$rows=allRows('SELECT * FROM bots ORDER BY id DESC'); $html=notice().'
'; layout('Боты',$html);
}
function listPage(string $title, string $table, array $fields, array $labels): void { requireAuth(); if($_SERVER['REQUEST_METHOD']==='POST') simpleCreate($table,$fields,path());
$rows=allRows('SELECT * FROM '.$table.' ORDER BY id DESC LIMIT 200'); $html=notice().'
Добавить
'.e($title).'
'; foreach($labels as $l)$html.='
'.e($l).'
'; $html.='
'; foreach($rows as $r){$html.='
'; foreach(array_keys($labels) as $k)$html.='
'.e((string)($r[$k]??'')).'
'; $html.='
'; } $html.='
'; layout($title,$html);
}
function ordersPage(): void { requireAuth();
if(isset($_GET['paid'])||isset($_GET['reject'])){ $id=(int)($_GET['paid']??$_GET['reject']); $status=isset($_GET['paid'])?'paid':'rejected'; q('UPDATE shop_orders SET status=?, paid_at=IF(?="paid",NOW(),paid_at) WHERE id=?',[$status,$status,$id]); $o=one('SELECT * FROM shop_orders WHERE id=?',[$id]); if($o){$bot=one('SELECT * FROM bots WHERE id=?',[$o['bot_id']]); if($bot) sendMessage($bot,(int)$o['telegram_id'],$status==='paid'?'Оплата подтверждена. Заказ №'.$id.' принят.':'Заказ №'.$id.' отклонён.');} flash('Статус обновлён'); redirect('/orders'); }
q("UPDATE shop_orders SET status='expired' WHERE status='waiting_pay' AND expires_at
'; layout('Заказы',$html);
}
function logsPage(): void { requireAuth(); $rows=allRows('SELECT * FROM telegram_updates ORDER BY id DESC LIMIT 100'); $html='