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)