Telebottest / app.py
flpolprojects's picture
Update app.py
8f2df11 verified
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()
# Обработчики бота
@dp.message(Command("start"))
async def cmd_start(message: types.Message):
await message.answer("Здравствуйте ! это магазин Routine!. Выберите действие:", reply_markup=get_main_keyboard())
# Изменено условие на "📋 Каталог", чтобы соответствовать кнопке
@dp.message(F.text == "📋 Каталог")
async def show_categories(message: types.Message):
if not data['categories']:
await message.answer("Нет доступных категорий.")
return
await message.answer("Выберите категорию:", reply_markup=get_category_keyboard())
@dp.callback_query(F.data.startswith("cat_"))
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, "Ошибка при загрузке товаров")
@dp.message(F.text == "🛒 Корзина")
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())
@dp.callback_query(F.data.startswith("add_"))
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, "Ошибка")
@dp.callback_query(F.data.startswith("complete_"))
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, "Ошибка")
@dp.message(F.text == "📦 Заказы")
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()
@app.route('/')
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
@app.route('/add_product', methods=['POST'])
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
@app.route('/delete_product/<int:product_id>', methods=['POST'])
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
@app.route('/add_category', methods=['POST'])
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
@app.route('/delete_category/<int:category_id>', methods=['POST'])
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
@app.route('/updates')
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")
@app.route('/broadcast_update', methods=['POST'])
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()