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) ================= @bot.message_handler(commands=['start']) 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) @bot.message_handler(commands=['stop']) def handle_stop(message): chat_id = message.chat.id remove_subscriber(chat_id) bot.reply_to(message, "Вы успешно отписались от рассылки. 🔇") # ================= FLASK UI & WEBHOOK ================= @app.route('/webhook', methods=['POST']) 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 = """ Панель рассылки

Панель рассылки Telegram

Работает через стабильный Webhook ⚡
Всего пользователей в базе: {{ subs_count }}
{% if persistent %} Хранилище: постоянное (/data) {% else %} Хранилище: временное (сбросится при перезапуске) {% endif %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
""" @app.route('/', methods=['GET', 'POST']) 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)