Spaces:
Runtime error
Runtime error
| import os | |
| import sqlite3 | |
| import telebot | |
| from flask import Flask, request, render_template_string, flash, redirect, url_for | |
| from werkzeug.middleware.proxy_fix import ProxyFix | |
| # ================= НАСТРОЙКИ ================= | |
| # ВАЖНО: токен теперь берётся из переменной окружения (Settings -> Variables and secrets -> BOT_TOKEN) | |
| # Никогда не храните токен прямо в коде, особенно в публичном Space! | |
| TOKEN = os.environ.get('BOT_TOKEN') | |
| if not TOKEN: | |
| raise RuntimeError( | |
| "Не найден BOT_TOKEN. Добавьте его в Settings -> Variables and secrets вашего Space " | |
| "(и не забудьте отозвать старый токен через @BotFather, если он раньше был в коде)." | |
| ) | |
| HOST = '0.0.0.0' | |
| PORT = 7860 | |
| # Если в Space включено Persistent Storage, оно монтируется в /data. | |
| # Если папка есть - используем её (данные переживут перезапуск контейнера), | |
| # иначе - как раньше, локальную папку (данные будут теряться при "засыпании" Space). | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| PERSIST_DIR = '/data' | |
| DB_DIR = PERSIST_DIR if os.path.isdir(PERSIST_DIR) and os.access(PERSIST_DIR, os.W_OK) else BASE_DIR | |
| DB_FILE = os.path.join(DB_DIR, 'subscribers.db') | |
| bot = telebot.TeleBot(TOKEN) | |
| app = Flask(__name__) | |
| app.secret_key = os.environ.get('FLASK_SECRET', 'super_secret_key_for_flask') | |
| # Настройка прокси для корректной работы Webhook внутри Hugging Face | |
| app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) | |
| # ================= БАЗА ДАННЫХ ================= | |
| def init_db(): | |
| with sqlite3.connect(DB_FILE, timeout=10) as conn: | |
| cursor = conn.cursor() | |
| cursor.execute('CREATE TABLE IF NOT EXISTS subscribers (chat_id INTEGER PRIMARY KEY)') | |
| conn.commit() | |
| def add_subscriber(chat_id): | |
| try: | |
| with sqlite3.connect(DB_FILE, timeout=10) as conn: | |
| cursor = conn.cursor() | |
| cursor.execute('INSERT OR IGNORE INTO subscribers (chat_id) VALUES (?)', (chat_id,)) | |
| conn.commit() | |
| except Exception as e: | |
| print(f"Ошибка БД (добавление): {e}") | |
| def get_all_subscribers(): | |
| try: | |
| with sqlite3.connect(DB_FILE, timeout=10) as conn: | |
| cursor = conn.cursor() | |
| cursor.execute('SELECT chat_id FROM subscribers') | |
| return [row[0] for row in cursor.fetchall()] | |
| except Exception: | |
| return [] | |
| def remove_subscriber(chat_id): | |
| try: | |
| with sqlite3.connect(DB_FILE, timeout=10) as conn: | |
| cursor = conn.cursor() | |
| cursor.execute('DELETE FROM subscribers WHERE chat_id = ?', (chat_id,)) | |
| conn.commit() | |
| except Exception as e: | |
| print(f"Ошибка БД (удаление): {e}") | |
| # ================= БОТ (HANDLERS) ================= | |
| def handle_start(message): | |
| chat_id = message.chat.id | |
| first_name = message.from_user.first_name or "Пользователь" | |
| add_subscriber(chat_id) | |
| text = (f"Привет, {first_name}! 👋\n\n" | |
| f"Вы подписались на рассылку и будете получать все обновления.\n\n" | |
| f"Чтобы отписаться, напишите /stop.") | |
| bot.reply_to(message, text) | |
| def handle_stop(message): | |
| chat_id = message.chat.id | |
| remove_subscriber(chat_id) | |
| bot.reply_to(message, "Вы успешно отписались от рассылки. 🔇") | |
| # ================= FLASK UI & WEBHOOK ================= | |
| def webhook(): | |
| if request.headers.get('content-type') == 'application/json': | |
| json_string = request.get_data().decode('utf-8') | |
| update = telebot.types.Update.de_json(json_string) | |
| bot.process_new_updates([update]) | |
| return '', 200 | |
| return 'Forbidden', 403 | |
| HTML_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Панель рассылки</title> | |
| <style> | |
| body { font-family: sans-serif; margin: 40px; background-color: #f4f4f9; color: #333;} | |
| .container { max-width: 600px; margin: auto; background: white; padding: 25px; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); } | |
| h2 { text-align: center; color: #2c3e50; margin-bottom: 5px; } | |
| .subtitle { text-align: center; color: #7f8c8d; font-size: 13px; margin-bottom: 20px; } | |
| .form-group { margin-bottom: 20px; } | |
| label { font-weight: bold; display: block; margin-bottom: 8px; color: #34495e;} | |
| textarea, input[type="file"] { width: 100%; padding: 12px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; font-family: inherit;} | |
| textarea { resize: vertical; height: 150px; font-size: 14px;} | |
| button { width: 100%; padding: 12px; background-color: #3498db; color: white; border: none; border-radius: 6px; font-size: 16px; font-weight: bold; cursor: pointer; transition: background 0.3s;} | |
| button:hover { background-color: #2980b9; } | |
| .alert { padding: 15px; margin-bottom: 20px; border-radius: 6px; } | |
| .alert-success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } | |
| .alert-danger { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } | |
| .stats { text-align: center; margin-bottom: 20px; font-size: 15px; color: #7f8c8d; background: #ecf0f1; padding: 15px; border-radius: 6px;} | |
| .stats strong { color: #2c3e50; font-size: 18px;} | |
| .status-badge { display: inline-block; padding: 3px 8px; background: #2ecc71; color: white; border-radius: 4px; font-size: 12px; font-weight: bold; margin-top: 5px;} | |
| .warn-badge { display: inline-block; padding: 3px 8px; background: #e67e22; color: white; border-radius: 4px; font-size: 12px; font-weight: bold; margin-top: 5px;} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h2>Панель рассылки Telegram</h2> | |
| <div class="subtitle">Работает через стабильный Webhook ⚡</div> | |
| <div class="stats"> | |
| Всего пользователей в базе: <strong>{{ subs_count }}</strong><br> | |
| {% if persistent %} | |
| <span class="status-badge">Хранилище: постоянное (/data)</span> | |
| {% else %} | |
| <span class="warn-badge">Хранилище: временное (сбросится при перезапуске)</span> | |
| {% endif %} | |
| </div> | |
| {% with messages = get_flashed_messages(with_categories=true) %} | |
| {% if messages %} | |
| {% for category, message in messages %} | |
| <div class="alert alert-{{ category }}">{{ message }}</div> | |
| {% endfor %} | |
| {% endif %} | |
| {% endwith %} | |
| <form method="POST" enctype="multipart/form-data"> | |
| <div class="form-group"> | |
| <label>Прикрепить фотографию (необязательно):</label> | |
| <input type="file" name="photo" accept="image/*"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Текст сообщения:</label> | |
| <textarea name="text" placeholder="Введите текст рассылки..."></textarea> | |
| </div> | |
| <button type="submit">Отправить рассылку</button> | |
| </form> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| def index(): | |
| subs = get_all_subscribers() | |
| if request.method == 'POST': | |
| text = request.form.get('text', '').strip() | |
| photo = request.files.get('photo') | |
| photo_data = None | |
| if photo and photo.filename: | |
| photo_data = photo.read() | |
| if not text and not photo_data: | |
| flash('Пожалуйста, введите текст сообщения или выберите фотографию!', 'danger') | |
| return redirect(url_for('index')) | |
| if not subs: | |
| flash('В базе пока нет подписчиков для рассылки.', 'danger') | |
| return redirect(url_for('index')) | |
| success_count = 0 | |
| file_id = None | |
| for chat_id in subs: | |
| try: | |
| if photo_data: | |
| if file_id is None: | |
| msg = bot.send_photo(chat_id, photo=photo_data, caption=text) | |
| file_id = msg.photo[-1].file_id | |
| else: | |
| bot.send_photo(chat_id, photo=file_id, caption=text) | |
| else: | |
| bot.send_message(chat_id, text) | |
| success_count += 1 | |
| except telebot.apihelper.ApiTelegramException as e: | |
| desc = (e.description or "").lower() | |
| if "blocked" in desc or "not found" in desc or "deactivated" in desc: | |
| remove_subscriber(chat_id) | |
| except Exception as e: | |
| print(f"Ошибка отправки {chat_id}: {e}") | |
| flash(f'Рассылка успешно завершена! Доставлено: {success_count} из {len(subs)}.', 'success') | |
| return redirect(url_for('index')) | |
| return render_template_string( | |
| HTML_TEMPLATE, | |
| subs_count=len(subs), | |
| persistent=(DB_DIR == PERSIST_DIR), | |
| ) | |
| # ================= НАСТРОЙКА WEBHOOK ПРИ СТАРТЕ ================= | |
| def setup_webhook(): | |
| space_host = os.environ.get('SPACE_HOST') | |
| if space_host: | |
| webhook_url = f"https://{space_host}/webhook" | |
| else: | |
| # Фолбэк, если SPACE_HOST не задан (например, локальный запуск) — | |
| # в этом случае webhook нужно ставить вручную. | |
| print("⚠️ SPACE_HOST не найден. Задайте webhook вручную или проверьте окружение.") | |
| return | |
| try: | |
| info = bot.get_webhook_info() | |
| if info.url != webhook_url: | |
| bot.remove_webhook() | |
| bot.set_webhook(url=webhook_url) | |
| print(f"✅ Webhook установлен на {webhook_url}") | |
| else: | |
| print(f"ℹ️ Webhook уже установлен корректно: {webhook_url}") | |
| except Exception as e: | |
| print(f"❌ Ошибка установки Webhook: {e}") | |
| # ================= ЗАПУСК ================= | |
| if __name__ == '__main__': | |
| init_db() | |
| print(f"📦 База данных: {DB_FILE} (persistent={DB_DIR == PERSIST_DIR})") | |
| setup_webhook() | |
| # threaded=True — чтобы вебхук-запросы Telegram не блокировались во время рассылки | |
| app.run(host=HOST, port=PORT, threaded=True) |