Shveiauto commited on
Commit
38c4fa2
·
verified ·
1 Parent(s): 15288fa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +221 -252
app.py CHANGED
@@ -1,313 +1,282 @@
1
- import asyncio
2
- import logging
3
- import sqlite3
4
  import os
5
- import io
6
  import threading
7
- from pathlib import Path
8
-
9
  from aiogram import Bot, Dispatcher, types
 
10
  from aiogram.utils import executor
11
- from aiogram.types import InputFile, ParseMode
12
-
13
- from flask import Flask, request, render_template_string, redirect, url_for, flash
14
 
15
  # --- Configuration ---
16
- BOT_TOKEN = "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4"
17
  FLASK_HOST = "0.0.0.0"
18
  FLASK_PORT = 7860
19
- DB_NAME = "subscribers.db"
20
- UPLOAD_FOLDER = 'uploads' # For temporary file storage if needed, though we'll use streams
21
 
22
- # --- Logging ---
23
- logging.basicConfig(level=logging.INFO)
24
- logger = logging.getLogger(__name__)
 
 
 
25
 
26
- # --- Database Setup ---
27
- def init_db():
28
- conn = sqlite3.connect(DB_NAME)
29
- cursor = conn.cursor()
30
- cursor.execute('''
31
- CREATE TABLE IF NOT EXISTS users (
32
- user_id INTEGER PRIMARY KEY
33
- )
34
- ''')
35
- conn.commit()
36
- conn.close()
37
- logger.info("Database initialized.")
38
 
39
- def add_user(user_id: int):
40
- conn = sqlite3.connect(DB_NAME)
41
- cursor = conn.cursor()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  try:
43
- cursor.execute("INSERT INTO users (user_id) VALUES (?)", (user_id,))
44
- conn.commit()
45
- logger.info(f"User {user_id} added to database.")
46
- return True
47
- except sqlite3.IntegrityError:
48
- logger.info(f"User {user_id} already exists in database.")
49
- return False # Already exists
50
- finally:
51
- conn.close()
52
 
53
- def get_all_users():
54
- conn = sqlite3.connect(DB_NAME)
55
- cursor = conn.cursor()
56
- cursor.execute("SELECT user_id FROM users")
57
- users = [row[0] for row in cursor.fetchall()]
58
- conn.close()
59
- return users
60
 
61
- # --- Aiogram Bot Setup ---
62
- bot = Bot(token=BOT_TOKEN)
63
- dp = Dispatcher(bot)
64
 
65
- # --- Flask App Setup ---
66
- flask_app = Flask(__name__)
67
- flask_app.secret_key = os.urandom(24) # For flash messages
68
- if not os.path.exists(UPLOAD_FOLDER):
69
- os.makedirs(UPLOAD_FOLDER)
70
- flask_app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
 
 
 
 
 
71
 
72
- # --- HTML Templates (Inline) ---
73
- ADMIN_PANEL_HTML = """
74
- <!DOCTYPE html>
 
 
 
75
  <html lang="ru">
76
  <head>
77
- <meta charset="UTF-8">
78
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
79
- <title>Админ Панель Уведомлений</title>
80
  <style>
81
- body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f4f4f4; color: #333; }
82
- .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 600px; margin: auto; }
83
- h1 { text-align: center; color: #333; }
84
- label { display: block; margin-bottom: 8px; font-weight: bold; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  textarea, input[type="file"] {
86
- width: calc(100% - 22px);
87
  padding: 10px;
88
- margin-bottom: 20px;
89
- border: 1px solid #ddd;
90
  border-radius: 4px;
91
- box-sizing: border-box;
 
 
 
 
92
  }
93
  button {
 
94
  background-color: #007bff;
95
  color: white;
96
- padding: 10px 20px;
97
  border: none;
98
  border-radius: 4px;
99
  cursor: pointer;
100
- font-size: 16px;
101
- display: block;
102
- width: 100%;
 
 
103
  }
104
- button:hover { background-color: #0056b3; }
105
- .flash-messages { list-style: none; padding: 0; margin-bottom: 20px; }
106
- .flash-messages li {
107
  padding: 10px;
108
- margin-bottom: 10px;
109
  border-radius: 4px;
 
 
 
110
  }
111
- .flash-success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
112
- .flash-error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
113
- #preview-container { margin-top: 15px; text-align: center; }
114
- #media-preview { max-width: 100%; max-height: 200px; border: 1px solid #ddd; margin-top: 5px; }
115
  </style>
116
  </head>
117
  <body>
118
  <div class="container">
119
- <h1>Отправить Уведомление</h1>
120
- {% with messages = get_flashed_messages(with_categories=true) %}
121
- {% if messages %}
122
- <ul class="flash-messages">
123
- {% for category, message in messages %}
124
- <li class="flash-{{ category }}">{{ message }}</li>
125
- {% endfor %}
126
- </ul>
127
- {% endif %}
128
  {% endwith %}
129
- <form method="POST" action="{{ url_for('send_notification_route') }}" enctype="multipart/form-data" onsubmit="return showLoader();">
130
- <div>
131
- <label for="message">Текст сообщения (HTML поддерживается):</label>
132
- <textarea name="message" id="message" rows="5" placeholder="Введите ваше сообщение..."></textarea>
133
- </div>
134
- <div>
135
- <label for="media">Медиафайл (фото, видео, документ):</label>
136
- <input type="file" name="media" id="media" onchange="previewFile()">
137
- </div>
138
- <div id="preview-container">
139
- <img id="media-preview" src="#" alt="Предпросмотр медиа" style="display:none;"/>
140
- <video id="video-preview" controls style="display:none; max-width: 100%; max-height: 200px;"></video>
141
- <p id="file-name-preview" style="display:none;"></p>
142
- </div>
143
- <button type="submit" id="submit-button">Отправить Всем</button>
144
- <div id="loader" style="display:none; text-align:center; margin-top:15px;">Отправка... Пожалуйста, подождите.</div>
145
- </form>
146
- </div>
147
- <script>
148
- function previewFile() {
149
- const preview = document.getElementById('media-preview');
150
- const videoPreview = document.getElementById('video-preview');
151
- const fileNamePreview = document.getElementById('file-name-preview');
152
- const file = document.getElementById('media').files[0];
153
- const reader = new FileReader();
154
 
155
- preview.style.display = 'none';
156
- videoPreview.style.display = 'none';
157
- fileNamePreview.style.display = 'none';
158
- videoPreview.src = ''; // Reset video src
159
 
160
- if (file) {
161
- reader.onloadend = function() {
162
- if (file.type.startsWith('image/')) {
163
- preview.src = reader.result;
164
- preview.style.display = 'block';
165
- } else if (file.type.startsWith('video/')) {
166
- videoPreview.src = reader.result;
167
- videoPreview.style.display = 'block';
168
- } else {
169
- fileNamePreview.textContent = "Выбран файл: " + file.name;
170
- fileNamePreview.style.display = 'block';
171
- }
172
- }
173
- reader.readAsDataURL(file);
174
- }
175
- }
176
- function showLoader() {
177
- document.getElementById('submit-button').style.display = 'none';
178
- document.getElementById('loader').style.display = 'block';
179
- return true; // Proceed with form submission
180
- }
181
- </script>
182
  </body>
183
  </html>
184
  """
185
 
186
- # --- Aiogram Handlers ---
187
- @dp.message_handler(commands=['start'])
188
- async def send_welcome(message: types.Message):
189
- user_id = message.from_user.id
190
- username = message.from_user.username or message.from_user.first_name
191
- if add_user(user_id):
192
- await message.reply("🎉 Вы успешно подписались на уведомления! 🎉")
193
- logger.info(f"User {username} (ID: {user_id}) subscribed.")
194
- else:
195
- await message.reply("Вы уже подписаны на уведомления. 👍")
196
- logger.info(f"User {username} (ID: {user_id}) tried to subscribe again.")
197
 
198
- # --- Flask Routes ---
199
- @flask_app.route('/', methods=['GET'])
200
- def admin_panel_route():
201
- return render_template_string(ADMIN_PANEL_HTML)
 
 
202
 
203
- @flask_app.route('/send_notification', methods=['POST'])
204
- def send_notification_route():
205
- message_text = request.form.get('message', '')
 
 
 
206
  media_file = request.files.get('media')
 
207
 
208
- if not message_text and not media_file:
209
- flash("Сообщение не может быть пустым, если не прикреплен медиафайл.", "error")
210
- return redirect(url_for('admin_panel_route'))
211
-
212
- users = get_all_users()
213
- if not users:
214
- flash("Нет подписчиков для отправки уведомлений.", "error")
215
- return redirect(url_for('admin_panel_route'))
216
-
217
- sent_count = 0
218
- error_count = 0
 
 
 
 
 
 
 
 
219
 
220
- # We need to run async bot functions from this synchronous Flask thread
221
- # Get the bot's event loop
222
- loop = bot.loop # or dp.loop
 
 
 
 
 
 
 
 
 
223
 
224
- for user_id in users:
225
- try:
226
- if media_file:
227
- # Reset stream position for each send
228
- media_file.seek(0)
229
- file_content = io.BytesIO(media_file.read())
230
- file_content.name = media_file.filename # Important for aiogram to determine type
231
-
232
- input_file = InputFile(file_content, filename=media_file.filename)
233
-
234
- content_type = media_file.content_type
235
- logger.info(f"Attempting to send media with content type: {content_type} to {user_id}")
236
 
237
- coro = None
238
- if 'image' in content_type:
239
- coro = bot.send_photo(user_id, input_file, caption=message_text if message_text else None, parse_mode=ParseMode.HTML)
240
- elif 'video' in content_type:
241
- coro = bot.send_video(user_id, input_file, caption=message_text if message_text else None, parse_mode=ParseMode.HTML)
242
- elif 'audio' in content_type: # Added audio support
243
- coro = bot.send_audio(user_id, input_file, caption=message_text if message_text else None, parse_mode=ParseMode.HTML)
244
- else: # Default to document
245
- coro = bot.send_document(user_id, input_file, caption=message_text if message_text else None, parse_mode=ParseMode.HTML)
246
-
247
- if coro:
248
- # Run the coroutine in the bot's event loop
249
- future = asyncio.run_coroutine_threadsafe(coro, loop)
250
- future.result(timeout=30) # Wait for completion with timeout
251
- else: # If only text was provided with media, but media type was not sendable this way
252
- if message_text:
253
- coro_text = bot.send_message(user_id, message_text, parse_mode=ParseMode.HTML)
254
- future = asyncio.run_coroutine_threadsafe(coro_text, loop)
255
- future.result(timeout=10)
256
 
257
- elif message_text: # Only text
258
- coro = bot.send_message(user_id, message_text, parse_mode=ParseMode.HTML)
259
- future = asyncio.run_coroutine_threadsafe(coro, loop)
260
- future.result(timeout=10) # Wait for completion with timeout
261
-
262
- sent_count += 1
263
- logger.info(f"Notification sent to {user_id}")
264
-
265
- except Exception as e:
266
- error_count += 1
267
- logger.error(f"Failed to send notification to {user_id}: {e}")
268
- # Common errors: bot blocked by user, chat not found, timeout
269
- if "bot was blocked by the user" in str(e).lower():
270
- logger.warning(f"User {user_id} blocked the bot. Consider removing them.")
271
- # You might want to remove users who have blocked the bot from the DB here
272
-
273
- if media_file: # Close the original file stream if it was opened
274
- media_file.close()
275
 
276
- flash(f"Уведомление отправлено {sent_count} пользователям. Ошибок: {error_count}.", "success" if error_count == 0 else "error")
277
- return redirect(url_for('admin_panel_route'))
278
 
279
 
280
- # --- Functions to run Flask and Aiogram ---
281
- def run_flask():
282
- # Use '0.0.0.0' to make it accessible externally, False for debug in "production"
283
- flask_app.run(host=FLASK_HOST, port=FLASK_PORT, debug=False, use_reloader=False)
284
 
285
- async def main_bot_loop():
286
- # Start polling
287
- logger.info("Starting bot polling...")
288
- # No need to pass bot, dp will use the one it was initialized with
289
- await dp.start_polling()
 
 
 
 
 
290
 
291
 
292
  if __name__ == '__main__':
293
- init_db() # Initialize database on startup
 
294
 
295
- # Run Flask in a separate thread
296
- # daemon=True ensures the thread exits when the main program exits
297
- flask_thread = threading.Thread(target=run_flask, daemon=True)
298
- flask_thread.start()
299
- logger.info(f"Flask admin panel running on http://{FLASK_HOST}:{FLASK_PORT}")
300
 
301
- # Run Aiogram in the main thread's asyncio loop
302
- # executor.start_polling(dp, skip_updates=True) # This is a blocking call
303
- # For more control or if mixing with other asyncio tasks:
304
- loop = asyncio.get_event_loop()
305
- try:
306
- loop.run_until_complete(main_bot_loop())
307
- except KeyboardInterrupt:
308
- logger.info("Bot polling stopped by user.")
309
- finally:
310
- logger.info("Shutting down...")
311
- # You might want to close bot session here if needed, though start_polling handles it
312
- # await bot.session.close() # if you are managing session explicitly
313
- loop.close()
 
 
 
 
1
  import os
2
+ import asyncio
3
  import threading
4
+ import uuid
5
+ from flask import Flask, request, render_template_string, redirect, url_for
6
  from aiogram import Bot, Dispatcher, types
7
+ from aiogram.types import InputFile
8
  from aiogram.utils import executor
9
+ from aiogram.contrib.fsm_storage.memory import MemoryStorage
 
 
10
 
11
  # --- Configuration ---
12
+ BOT_TOKEN = "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4" # Ваш токен бота
13
  FLASK_HOST = "0.0.0.0"
14
  FLASK_PORT = 7860
 
 
15
 
16
+ # --- Shared State ---
17
+ # Используем set для хранения уникальных chat_id
18
+ subscribed_users = set()
19
+ # Временная директория для загружаемых медиафайлов
20
+ TEMP_UPLOAD_FOLDER = 'temp_uploads'
21
+ os.makedirs(TEMP_UPLOAD_FOLDER, exist_ok=True)
22
 
23
+ # --- Aiogram Bot ---
24
+ # Инициализируем объекты бота и диспетчера
25
+ # Важно: создаем loop явно для использования в другом потоке
26
+ loop = asyncio.get_event_loop()
27
+ bot = Bot(token=BOT_TOKEN)
28
+ storage = MemoryStorage()
29
+ dp = Dispatcher(bot, storage=storage, loop=loop)
 
 
 
 
 
30
 
31
+ @dp.message_handler(commands=['start'])
32
+ async def process_start_command(message: types.Message):
33
+ """
34
+ Handler для команды /start.
35
+ Добавляет пользователя в список подписанных и отправляет приветственное сообщение.
36
+ """
37
+ chat_id = message.chat.id
38
+ if chat_id not in subscribed_users:
39
+ subscribed_users.add(chat_id)
40
+ print(f"User {chat_id} subscribed.")
41
+ await message.reply("Вы подписались на уведомления!")
42
+ else:
43
+ await message.reply("Вы уже подписаны на уведомления!") # Опционально: сообщение для уже подписанных
44
+
45
+ async def send_notification_to_user(chat_id, text, media_path=None):
46
+ """
47
+ Асинхронная функция для отправки уведомления одному пользователю.
48
+ """
49
  try:
50
+ if media_path and os.path.exists(media_path):
51
+ # Отправляем медиа с текстом в качестве подписи
52
+ with open(media_path, 'rb') as photo:
53
+ await bot.send_photo(chat_id, photo=InputFile(photo), caption=text)
54
+ elif text:
55
+ # Отправляем только текст
56
+ await bot.send_message(chat_id, text=text)
57
+ else:
58
+ print(f"Skipping sending to {chat_id}: No text or media provided.")
59
 
60
+ except Exception as e:
61
+ # Обрабатываем ошибки отправки (например, пользователь заблокировал бота)
62
+ print(f"Failed to send message to {chat_id}: {e}")
63
+ # Опционально: удалить пользователя из subscribed_users, если ошибка указывает на блокировку
64
+ # if "bot was blocked by the user" in str(e):
65
+ # subscribed_users.discard(chat_id)
 
66
 
 
 
 
67
 
68
+ async def send_notification_to_all(text, media_path=None):
69
+ """
70
+ Асинхронная функция для отправ��и уведомления всем подписанным пользователям.
71
+ """
72
+ print(f"Attempting to send notification to {len(subscribed_users)} users.")
73
+ # Создаем копию set, чтобы избежать проблем при изменении set во время итерации
74
+ users_to_notify = list(subscribed_users)
75
+ for chat_id in users_to_notify:
76
+ await send_notification_to_user(chat_id, text, media_path)
77
+ # Небольшая задержка, чтобы избежать превышения лимитов Telegram API
78
+ await asyncio.sleep(0.05) # 50ms delay per user
79
 
80
+ # --- Flask Admin Panel ---
81
+ app = Flask(__name__)
82
+
83
+ # HTML для админ панели
84
+ ADMIN_HTML = """
85
+ <!doctype html>
86
  <html lang="ru">
87
  <head>
88
+ <meta charset="utf-8">
89
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
90
+ <title>Админ панель бота</title>
91
  <style>
92
+ body {
93
+ font-family: sans-serif;
94
+ line-height: 1.6;
95
+ margin: 20px;
96
+ background-color: #f8f8f8;
97
+ color: #333;
98
+ }
99
+ .container {
100
+ max-width: 600px;
101
+ margin: 0 auto;
102
+ background-color: #fff;
103
+ padding: 20px;
104
+ border-radius: 8px;
105
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
106
+ }
107
+ h1 {
108
+ text-align: center;
109
+ color: #0056b3;
110
+ }
111
+ form {
112
+ display: flex;
113
+ flex-direction: column;
114
+ }
115
+ label {
116
+ margin-bottom: 5px;
117
+ font-weight: bold;
118
+ }
119
  textarea, input[type="file"] {
120
+ margin-bottom: 15px;
121
  padding: 10px;
122
+ border: 1px solid #ccc;
 
123
  border-radius: 4px;
124
+ font-size: 1rem;
125
+ }
126
+ textarea {
127
+ resize: vertical;
128
+ min-height: 100px;
129
  }
130
  button {
131
+ padding: 10px 15px;
132
  background-color: #007bff;
133
  color: white;
 
134
  border: none;
135
  border-radius: 4px;
136
  cursor: pointer;
137
+ font-size: 1rem;
138
+ transition: background-color 0.3s ease;
139
+ }
140
+ button:hover {
141
+ background-color: #0056b3;
142
  }
143
+ .flash-message {
 
 
144
  padding: 10px;
145
+ margin-bottom: 15px;
146
  border-radius: 4px;
147
+ background-color: #d4edda;
148
+ color: #155724;
149
+ border: 1px solid #c3e6cb;
150
  }
 
 
 
 
151
  </style>
152
  </head>
153
  <body>
154
  <div class="container">
155
+ <h1>Отправить уведомление</h1>
156
+ {% with messages = get_flashed_messages() %}
157
+ {% if messages %}
158
+ {% for message in messages %}
159
+ <div class="flash-message">{{ message }}</div>
160
+ {% endfor %}
161
+ {% endif %}
 
 
162
  {% endwith %}
163
+ <p>Подписано пользователей: {{ user_count }}</p>
164
+ <form action="{{ url_for('send_notification') }}" method="post" enctype="multipart/form-data">
165
+ <label for="message">Текст уведомления:</label>
166
+ <textarea id="message" name="message" placeholder="Введите текст уведомления"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
+ <label for="media">Медиафайл (фото):</label>
169
+ <input type="file" id="media" name="media" accept="image/*">
 
 
170
 
171
+ <button type="submit">Отправить всем</button>
172
+ </form>
173
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  </body>
175
  </html>
176
  """
177
 
178
+ from flask import flash # Импортируем flash для сообщений
 
 
 
 
 
 
 
 
 
 
179
 
180
+ @app.route('/admin')
181
+ def admin_panel():
182
+ """
183
+ Отображает страницу админ панели.
184
+ """
185
+ return render_template_string(ADMIN_HTML, user_count=len(subscribed_users))
186
 
187
+ @app.route('/send_notification', methods=['POST'])
188
+ def send_notification():
189
+ """
190
+ Обрабатывает отправку уведомления из админ панели.
191
+ """
192
+ message_text = request.form.get('message', '').strip()
193
  media_file = request.files.get('media')
194
+ media_path = None
195
 
196
+ # Проверяем, загружен ли файл
197
+ if media_file and media_file.filename != '':
198
+ # Создаем уникальное имя файла и сохраняем его временно
199
+ filename = str(uuid.uuid4()) + os.path.splitext(media_file.filename)[1]
200
+ media_path = os.path.join(TEMP_UPLOAD_FOLDER, filename)
201
+ try:
202
+ media_file.save(media_path)
203
+ print(f"Saved media file to {media_path}")
204
+ except Exception as e:
205
+ print(f"Error saving file: {e}")
206
+ flash("Ошибка при сохранении медиафайла.", "error")
207
+ return redirect(url_for('admin_panel'))
208
+
209
+ # Проверяем, есть ли текст или медиа
210
+ if not message_text and not media_path:
211
+ flash("Пожалуйста, введите текст или прикрепите медиафайл.", "warning")
212
+ if media_path and os.path.exists(media_path):
213
+ os.remove(media_path) # Удалить временный файл, если он был создан
214
+ return redirect(url_for('admin_panel'))
215
 
216
+ # Вызываем асинхронную функцию отправки уведомлений в контексте asyncio loop
217
+ # run_coroutine_threadsafe отправляет корутину в loop, который выполняется в другом потоке
218
+ try:
219
+ future = asyncio.run_coroutine_threadsafe(
220
+ send_notification_to_all(message_text, media_path),
221
+ loop # Используем loop бота
222
+ )
223
+ # Ждем завершения выполнения корутины (опционально, можно запустить в фоне)
224
+ # .result() делает этот Flask запрос блокирующим до отправки всем пользователям
225
+ future.result()
226
+ flash(f"Уведомление отправлено {len(subscribed_users)} пользователям.", "success")
227
+ print("Notification sent successfully.")
228
 
229
+ except Exception as e:
230
+ print(f"Error sending notification: {e}")
231
+ flash(f"Произошла ошибка при отправке: {e}", "error")
 
 
 
 
 
 
 
 
 
232
 
233
+ finally:
234
+ # Удаляем временный файл после отправки
235
+ if media_path and os.path.exists(media_path):
236
+ try:
237
+ os.remove(media_path)
238
+ print(f"Cleaned up temporary file {media_path}")
239
+ except Exception as e:
240
+ print(f"Error cleaning up file {media_path}: {e}")
241
+ # Опционально: удалить пустую временную директорию
242
+ # if not os.listdir(TEMP_UPLOAD_FOLDER):
243
+ # os.rmdir(TEMP_UPLOAD_FOLDER)
 
 
 
 
 
 
 
 
244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
+ return redirect(url_for('admin_panel')) # Перенаправляем обратно на админ панель
 
247
 
248
 
249
+ # --- Running Both ---
 
 
 
250
 
251
+ def start_bot_polling():
252
+ """
253
+ Функция для запуска поллинга бота в отдельном потоке.
254
+ """
255
+ print("Starting bot polling...")
256
+ # Устанавливаем loop для этого потока, хотя aiogram 2.x уже делает это
257
+ # asyncio.set_event_loop(loop) # Возможно, не нужно с aiogram 2.x+ и explicit loop
258
+ # executor.start_polling блокирует выполнение
259
+ # Мы запускаем его в отдельном потоке, чтобы основной поток мог запустить Flask
260
+ loop.run_until_complete(dp.start_polling())
261
 
262
 
263
  if __name__ == '__main__':
264
+ # Flask требует SECRET_KEY для flash сообщений
265
+ app.secret_key = 'super secret key for flask notifications' # Замените на реальный секретный ключ
266
 
267
+ # Создаем и запускаем поток для бота
268
+ bot_thread = threading.Thread(target=start_bot_polling, daemon=True) # daemon=True позволяет приложению завершиться, даже если поток бота еще работает
269
+ bot_thread.start()
 
 
270
 
271
+ # Запускаем Flask веб-сервер в основном потоке
272
+ print(f"Starting Flask admin panel on http://{FLASK_HOST}:{FLASK_PORT}/admin")
273
+ # use_reloader=False очень важен, чтобы избежать запуска Flask в двух процессах,
274
+ # что привело бы к двойному запуску бота и проблемам с потоками/логами.
275
+ app.run(host=FLASK_HOST, port=FLASK_PORT, use_reloader=False)
276
+
277
+ # Остановка бота при завершении Flask (опционально, т.к. daemon=True)
278
+ # print("Stopping bot...")
279
+ # loop.call_soon_threadsafe(dp.stop_polling)
280
+ # bot_thread.join(timeout=5) # Ждем завершения потока бота
281
+ # print("Bot stopped.")
282
+ # loop.close() # Закрываем loop асинхронных операций (важно для чистого завер��ения)