Bot / app.py
Kgshop's picture
Update app.py
466c4dc verified
Raw
History Blame Contribute Delete
11.2 kB
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 = """
<!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>
"""
@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)