Spaces:
Paused
Paused
import asyncio | |
import json | |
import os | |
import urllib.parse | |
from datetime import datetime | |
from aiogram import Bot, Dispatcher, types, F | |
from aiogram.filters import Command | |
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder | |
from flask import Flask, request, jsonify, render_template_string, redirect | |
import logging | |
import threading | |
from huggingface_hub import HfApi, hf_hub_download | |
from huggingface_hub.utils import RepositoryNotFoundError | |
from werkzeug.utils import secure_filename | |
# Настройка логирования | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
# Инициализация бота и Flask | |
BOT_TOKEN = '7595736142:AAHSU3WGItBkebIgjO293J2WjX5qWAne8Y8' | |
bot = Bot(token=BOT_TOKEN) | |
dp = Dispatcher() | |
app = Flask(__name__) | |
# Путь для хранения данных | |
DATA_FILE = 'data2.json' | |
# Настройки Hugging Face | |
REPO_ID = "flpolprojects/Clients" | |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN") | |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") | |
# Функции для работы с данными | |
def load_data(): | |
try: | |
download_db_from_hf() | |
with open(DATA_FILE, 'r', encoding='utf-8') as f: | |
loaded_data = json.load(f) | |
if not (isinstance(loaded_data, dict) and 'products' in loaded_data and 'orders' in loaded_data): | |
logger.error("Неверная структура JSON файла") | |
loaded_data = {'products': [], 'orders': []} | |
if "categories" not in loaded_data: | |
loaded_data["categories"] = [] | |
return loaded_data | |
except Exception as e: | |
logger.error(f"Ошибка при загрузке данных: {e}") | |
return {'products': [], 'orders': [], 'categories': []} | |
def save_data(data): | |
try: | |
with open(DATA_FILE, 'w', encoding='utf-8') as f: | |
json.dump(data, f, ensure_ascii=False, indent=4) | |
# upload_db_to_hf() убрано отсюда | |
except Exception as e: | |
logger.error(f"Ошибка при сохранении данных: {e}") | |
def upload_db_to_hf(): | |
try: | |
api = HfApi() | |
api.upload_file( | |
path_or_fileobj=DATA_FILE, | |
path_in_repo=DATA_FILE, | |
repo_id=REPO_ID, | |
repo_type="dataset", | |
token=HF_TOKEN_WRITE, | |
commit_message=f"Автоматическое резервное копирование {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" | |
) | |
logger.info("База загружена на Hugging Face") | |
except Exception as e: | |
logger.error(f"Ошибка при загрузке резервной копии: {e}") | |
def download_db_from_hf(): | |
try: | |
hf_hub_download( | |
repo_id=REPO_ID, | |
filename=DATA_FILE, | |
repo_type="dataset", | |
token=HF_TOKEN_READ, | |
local_dir=".", | |
local_dir_use_symlinks=False | |
) | |
logger.info("База скачана из Hugging Face") | |
except Exception as e: | |
logger.error(f"Ошибка при скачивании: {e}") | |
raise | |
# Периодическое копирование каждые 30 секунд | |
def start_periodic_backup(): | |
def backup_loop(): | |
upload_db_to_hf() | |
# Запускаем следующий вызов через 30 секунд | |
threading.Timer(30, backup_loop).start() | |
# Запускаем первый вызов | |
threading.Timer(30, backup_loop).start() | |
logger.info("Периодическое копирование каждые 30 секунд запущено") | |
# Загрузка данных | |
data = load_data() | |
# Формирование клавиатур | |
def get_main_keyboard(): | |
builder = ReplyKeyboardBuilder() | |
builder.button(text="📋 Каталог") | |
builder.button(text="🛒 Корзина") | |
builder.button(text="📦 Заказы") | |
builder.adjust(2) | |
return builder.as_markup(resize_keyboard=True) | |
def get_category_keyboard(): | |
builder = InlineKeyboardBuilder() | |
for category in data['categories']: | |
builder.button(text=category['name'], callback_data=f"cat_{category['id']}") | |
builder.adjust(2) | |
return builder.as_markup() | |
def get_product_keyboard(product_id): | |
builder = InlineKeyboardBuilder() | |
builder.button(text="Добавить в корзину", callback_data=f"add_{product_id}") | |
return builder.as_markup() | |
# Обработчики бота | |
async def cmd_start(message: types.Message): | |
await message.answer("Здравствуйте ! это магазин Routine!. Выберите действие:", reply_markup=get_main_keyboard()) | |
# Изменено условие на "📋 Каталог", чтобы соответствовать кнопке | |
async def show_categories(message: types.Message): | |
if not data['categories']: | |
await message.answer("Нет доступных категорий.") | |
return | |
await message.answer("Выберите категорию:", reply_markup=get_category_keyboard()) | |
async def show_products_in_category(callback_query: types.CallbackQuery): | |
try: | |
cat_id = int(callback_query.data.split('_')[1]) | |
products_in_cat = [p for p in data['products'] if p.get('category_id') == cat_id] | |
if not products_in_cat: | |
await bot.send_message(callback_query.from_user.id, "В этой категории нет товаров.") | |
await bot.answer_callback_query(callback_query.id) | |
return | |
async def send_product_batch(products_batch): | |
for product in products_batch: | |
photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{product['photo']}" if product.get('photo') else None | |
caption = f"🏷 {product['name']} - {product['price']} сом\nОписание: {product['description']}\n/id: {product['id']}" | |
try: | |
if photo_url: | |
await bot.send_photo(chat_id=callback_query.from_user.id, photo=photo_url, caption=caption, reply_markup=get_product_keyboard(product['id'])) | |
else: | |
await bot.send_message(callback_query.from_user.id, caption, reply_markup=get_product_keyboard(product['id'])) | |
except Exception as e: | |
logger.error(f"Ошибка при отправке: {e}") | |
await bot.send_message(callback_query.from_user.id, caption, reply_markup=get_product_keyboard(product['id'])) | |
batch_size = 5 | |
for i in range(0, len(products_in_cat), batch_size): | |
batch = products_in_cat[i:i + batch_size] | |
await send_product_batch(batch) | |
await asyncio.sleep(0.1) | |
await bot.answer_callback_query(callback_query.id) | |
except Exception as e: | |
logger.error(f"Ошибка в show_products_in_category: {e}") | |
await bot.answer_callback_query(callback_query.id, "Ошибка при загрузке товаров") | |
async def show_cart(message: types.Message): | |
user_id = message.from_user.id | |
cart = next((o for o in data['orders'] if o['user_id'] == user_id and not o.get('completed')), None) | |
if not cart or not cart['items']: | |
await message.answer("Ваша корзина пуста.") | |
return | |
total = 0 | |
response = "Ваша корзина:\n" | |
for item in cart['items']: | |
product = next(p for p in data['products'] if p['id'] == item['product_id']) | |
response += f"🏷 {product['name']} - {product['price']} сом x {item['quantity']}\n" | |
total += product['price'] * item['quantity'] | |
response += f"\nИтого: {total} сом" | |
builder = InlineKeyboardBuilder() | |
builder.button(text="Оформить заказ", callback_data=f"complete_{user_id}") | |
await message.answer(response, reply_markup=builder.as_markup()) | |
async def add_to_cart(callback_query: types.CallbackQuery): | |
try: | |
product_id = int(callback_query.data.split('_')[1]) | |
product = next((p for p in data['products'] if p['id'] == product_id), None) | |
if product: | |
user_id = callback_query.from_user.id | |
cart = next((o for o in data['orders'] if o['user_id'] == user_id and not o.get('completed')), None) | |
if not cart: | |
cart = {'user_id': user_id, 'items': [], 'date': datetime.now().isoformat()} | |
data['orders'].append(cart) | |
cart['items'].append({'product_id': product_id, 'quantity': 1}) | |
save_data(data) | |
await bot.answer_callback_query(callback_query.id, "Товар добавлен в корзину!") | |
except Exception as e: | |
logger.error(f"Ошибка при добавлении в корзину: {e}") | |
await bot.answer_callback_query(callback_query.id, "Ошибка") | |
async def complete_order(callback_query: types.CallbackQuery): | |
try: | |
user_id = int(callback_query.data.split('_')[1]) | |
cart = next((o for o in data['orders'] if o['user_id'] == user_id and not o.get('completed')), None) | |
if cart and cart['items']: | |
total = 0 | |
cart_text = "Привет, я хочу сделать заказ:\n" | |
for item in cart['items']: | |
product = next((p for p in data['products'] if p['id'] == item['product_id']), None) | |
if product: | |
cart_text += f"{product['name']} - {product['price']} сом x {item['quantity']}\n" | |
total += product['price'] * item['quantity'] | |
cart_text += f"\nИтого: {total} сом" | |
encoded_text = urllib.parse.quote(cart_text) | |
whatsapp_link = f"https://wa.me/996709513331?text={encoded_text}" | |
data['orders'].remove(cart) | |
save_data(data) | |
await bot.send_message(user_id, f"Оформите заказ через WhatsApp:\n{whatsapp_link}") | |
await bot.answer_callback_query(callback_query.id) | |
except Exception as e: | |
logger.error(f"Ошибка при оформлении заказа: {e}") | |
await bot.answer_callback_query(callback_query.id, "Ошибка") | |
async def show_orders(message: types.Message): | |
user_id = message.from_user.id | |
user_orders = [o for o in data['orders'] if o.get('completed')] | |
if not user_orders: | |
await message.answer("У вас нет оформленных заказов.") | |
return | |
for order in user_orders: | |
response = "Ваш заказ:\n" | |
total = 0 | |
for item in order['items']: | |
product = next(p for p in data['products'] if p['id'] == item['product_id']) | |
response += f"🏷 {product['name']} - {product['price']} сом x {item['quantity']}\n" | |
total += product['price'] * item['quantity'] | |
response += f"\nИтого: {total} сом\nДата: {order['date']}" | |
await message.answer(response) | |
# Админ-панель | |
admin_html = """ | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Админ-панель</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
margin: 10px; | |
background-color: #f0f0f0; | |
} | |
.container { | |
max-width: 1000px; | |
margin: 0 auto; | |
} | |
.section { | |
background-color: #fff; | |
padding: 10px; | |
margin-bottom: 15px; | |
border-radius: 5px; | |
} | |
h1, h2, h3 { | |
margin: 10px 0; | |
} | |
input, textarea, select { | |
width: 100%; | |
margin: 5px 0; | |
padding: 8px; | |
box-sizing: border-box; | |
} | |
button { | |
background-color: #4CAF50; | |
color: white; | |
padding: 8px 12px; | |
border: none; | |
border-radius: 5px; | |
cursor: pointer; | |
width: 100%; | |
margin: 5px 0; | |
} | |
button:hover { | |
background-color: #45a049; | |
} | |
img { | |
max-width: 100%; | |
height: auto; | |
max-height: 150px; | |
} | |
.item { | |
border: 1px solid #ccc; | |
padding: 10px; | |
margin: 5px 0; | |
word-wrap: break-word; | |
} | |
@media (max-width: 600px) { | |
.section { | |
padding: 8px; | |
} | |
button { | |
padding: 6px 10px; | |
} | |
h1 { | |
font-size: 1.5em; | |
} | |
h2 { | |
font-size: 1.2em; | |
} | |
h3 { | |
font-size: 1em; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Админ-панель</h1> | |
<div class="section"> | |
<h2>Управление категориями</h2> | |
<form id="addCategoryForm" method="POST" action="/add_category"> | |
<input type="text" name="name" placeholder="Название категории" required> | |
<button type="submit">Добавить категорию</button> | |
</form> | |
<h3>Существующие категории</h3> | |
{% if categories %} | |
{% for category in categories %} | |
<div class="item"> | |
{{ category.name }} (ID: {{ category.id }}) | |
<button onclick="deleteCategory({{ category.id }})">Удалить</button> | |
</div> | |
{% endfor %} | |
{% else %} | |
<p>Нет категорий.</p> | |
{% endif %} | |
</div> | |
<div class="section"> | |
<h2>Управление товарами</h2> | |
<form id="addProductForm" method="POST" enctype="multipart/form-data" action="/add_product"> | |
<input type="text" name="name" placeholder="Название" required> | |
<input type="number" name="price" placeholder="Цена" step="0.01" required> | |
<textarea name="description" placeholder="Описание" required></textarea> | |
<label>Категория:</label> | |
<select name="category_id" required> | |
<option value="">Выберите категорию</option> | |
{% for category in categories %} | |
<option value="{{ category.id }}">{{ category.name }}</option> | |
{% endfor %} | |
</select> | |
<input type="file" name="photo" accept="image/*"> | |
<button type="submit">Добавить товар</button> | |
</form> | |
<h3>Существующие товары</h3> | |
{% if products %} | |
{% for product in products %} | |
<div class="item"> | |
{{ product.name }} - {{ product.price }} сом<br> | |
{{ product.description }}<br> | |
{% if product.photo %} | |
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photo }}" alt="{{ product.name }}"> | |
{% endif %} | |
<button onclick="deleteProduct({{ product.id }})">Удалить</button> | |
</div> | |
{% endfor %} | |
{% else %} | |
<p>Нет товаров.</p> | |
{% endif %} | |
</div> | |
<div class="section"> | |
<h2>Заказы</h2> | |
{% if orders %} | |
{% for order in orders %} | |
<div class="item"> | |
Пользователь: {{ order.user_id }}<br> | |
Дата: {{ order.date }}<br> | |
Товары: | |
{% for item in order['items'] %} | |
{% for product in products %} | |
{% if product.id == item.product_id %} | |
{{ item.quantity }} x {{ product.name }}<br> | |
{% endif %} | |
{% endfor %} | |
{% endfor %} | |
</div> | |
{% endfor %} | |
{% else %} | |
<p>Нет заказов.</p> | |
{% endif %} | |
</div> | |
</div> | |
<script> | |
const eventSource = new EventSource('/updates'); | |
eventSource.onmessage = function(event) { | |
if (event.data === 'update') { | |
window.location.reload(); | |
} | |
}; | |
eventSource.onerror = function() { | |
console.log("Ошибка SSE, reconnecting..."); | |
}; | |
async function deleteProduct(productId) { | |
const response = await fetch(`/delete_product/${productId}`, { method: 'POST' }); | |
if (response.ok) broadcastUpdate(); | |
} | |
async function deleteCategory(categoryId) { | |
const response = await fetch(`/delete_category/${categoryId}`, { method: 'POST' }); | |
if (response.ok) broadcastUpdate(); | |
} | |
function broadcastUpdate() { | |
fetch('/broadcast_update', { method: 'POST' }); | |
} | |
</script> | |
</body> | |
</html> | |
""" | |
update_event = threading.Event() | |
def admin_panel(): | |
try: | |
return render_template_string(admin_html, products=data['products'], orders=data['orders'], categories=data['categories'], repo_id=REPO_ID) | |
except Exception as e: | |
logger.error(f"Ошибка в шаблоне: {e}") | |
return "Ошибка сервера", 500 | |
def add_product(): | |
try: | |
name = request.form['name'] | |
price = float(request.form['price']) | |
description = request.form['description'] | |
category_id = int(request.form['category_id']) | |
photo = request.files.get('photo') | |
product_id = max((p['id'] for p in data['products']), default=0) + 1 | |
photo_filename = None | |
if photo and photo.filename: | |
photo_filename = secure_filename(photo.filename) | |
temp_path = os.path.join(".", photo_filename) | |
photo.save(temp_path) | |
api = HfApi() | |
api.upload_file( | |
path_or_fileobj=temp_path, | |
path_in_repo=f"photos/{photo_filename}", | |
repo_id=REPO_ID, | |
repo_type="dataset", | |
token=HF_TOKEN_WRITE, | |
commit_message=f"Добавлено фото для товара {name}" | |
) | |
os.remove(temp_path) | |
data['products'].append({ | |
'id': product_id, | |
'name': name, | |
'price': price, | |
'description': description, | |
'category_id': category_id, | |
'photo': photo_filename | |
}) | |
save_data(data) | |
update_event.set() | |
return redirect("/") | |
except Exception as e: | |
logger.error(f"Ошибка при добавлении товара: {e}") | |
return jsonify({'status': 'error', 'message': str(e)}), 500 | |
def delete_product(product_id): | |
try: | |
data['products'] = [p for p in data['products'] if p['id'] != product_id] | |
save_data(data) | |
update_event.set() | |
return jsonify({'status': 'success'}) | |
except Exception as e: | |
logger.error(f"Ошибка при удалении товара: {e}") | |
return jsonify({'status': 'error', 'message': str(e)}), 500 | |
def add_category(): | |
try: | |
name = request.form['name'] | |
category_id = max((c['id'] for c in data['categories']), default=0) + 1 | |
data['categories'].append({'id': category_id, 'name': name}) | |
save_data(data) | |
update_event.set() | |
return redirect("/") | |
except Exception as e: | |
logger.error(f"Ошибка при добавлении категории: {e}") | |
return jsonify({'status': 'error', 'message': str(e)}), 500 | |
def delete_category(category_id): | |
try: | |
data['categories'] = [c for c in data['categories'] if c['id'] != category_id] | |
save_data(data) | |
update_event.set() | |
return jsonify({'status': 'success'}) | |
except Exception as e: | |
logger.error(f"Ошибка при удалении категории: {e}") | |
return jsonify({'status': 'error', 'message': str(e)}), 500 | |
def sse_updates(): | |
def stream(): | |
while True: | |
update_event.wait() | |
yield "data: update\n\n" | |
update_event.clear() | |
return app.response_class(stream(), mimetype="text/event-stream") | |
def broadcast_update(): | |
update_event.set() | |
return jsonify({'status': 'success'}) | |
# Запуск | |
async def on_startup(_): | |
logger.info("Бот запущен!") | |
def run_flask(): | |
app.run(host='0.0.0.0', port=7860, debug=True, use_reloader=False) | |
if __name__ == '__main__': | |
flask_thread = threading.Thread(target=run_flask, daemon=True) | |
flask_thread.start() | |
logger.info("Flask запущен") | |
# Запуск периодического копирования | |
start_periodic_backup() | |
try: | |
asyncio.run(dp.start_polling(bot, on_startup=on_startup)) | |
except KeyboardInterrupt: | |
logger.info("Остановка") | |
finally: | |
flask_thread.join() |