Spaces:
Runtime error
Runtime error
File size: 11,229 Bytes
db93287 496b502 52b1acc 496b502 466c4dc 496b502 254cd33 466c4dc 254cd33 466c4dc 496b502 e22e9c9 466c4dc e22e9c9 52b1acc 496b502 52b1acc 496b502 e22e9c9 496b502 52b1acc 496b502 e22e9c9 496b502 254cd33 52b1acc 254cd33 496b502 254cd33 52b1acc 254cd33 e22e9c9 496b502 52b1acc 496b502 254cd33 466c4dc 496b502 466c4dc 254cd33 496b502 52b1acc 496b502 db93287 496b502 e22e9c9 496b502 52b1acc 496b502 52b1acc 496b502 52b1acc 466c4dc 496b502 db93287 496b502 52b1acc 466c4dc 52b1acc 466c4dc 52b1acc 496b502 e22e9c9 496b502 e22e9c9 496b502 db93287 8b0f940 db93287 496b502 8b0f940 496b502 466c4dc 496b502 466c4dc 496b502 8b0f940 496b502 466c4dc 496b502 466c4dc 496b502 466c4dc 496b502 8b0f940 496b502 466c4dc 496b502 db93287 496b502 466c4dc 496b502 e22e9c9 496b502 254cd33 496b502 466c4dc 496b502 254cd33 496b502 466c4dc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | 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) |