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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +229 -307
app.py CHANGED
@@ -1,391 +1,313 @@
1
  import asyncio
2
- import json
3
  import logging
 
4
  import os
 
5
  import threading
6
  from pathlib import Path
7
 
8
  from aiogram import Bot, Dispatcher, types
9
- from aiogram.filters import CommandStart
10
- from aiogram.enums import ParseMode
11
- from aiogram.types import FSInputFile
12
- from aiogram.utils.markdown import html_escape
13
- from aiogram.exceptions import TelegramAPIError
14
 
15
- from flask import Flask, request, render_template_string, redirect, url_for
16
- from werkzeug.utils import secure_filename
17
 
18
  # --- Configuration ---
19
  BOT_TOKEN = "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4"
20
  FLASK_HOST = "0.0.0.0"
21
  FLASK_PORT = 7860
22
-
23
- BASE_DIR = Path(__file__).resolve().parent
24
- SUBSCRIBED_USERS_FILE = BASE_DIR / "subscribed_users.json"
25
- UPLOAD_FOLDER = BASE_DIR / "uploads"
26
 
27
  # --- Logging ---
28
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
29
  logger = logging.getLogger(__name__)
30
 
31
- # --- Global Variables for Bot and Queue ---
32
- # These will be initialized in main_async
33
- bot_instance: Bot = None
34
- dp: Dispatcher = None
35
- message_queue: asyncio.Queue = None
36
- subscribed_users: set = set()
37
- bot_loop: asyncio.AbstractEventLoop = None # To run coroutines from Flask thread
38
-
39
- # --- User Persistence ---
40
- def load_subscribed_users():
41
- global subscribed_users
42
- if SUBSCRIBED_USERS_FILE.exists():
43
- try:
44
- with open(SUBSCRIBED_USERS_FILE, 'r') as f:
45
- user_ids = json.load(f)
46
- subscribed_users = set(user_ids)
47
- logger.info(f"Loaded {len(subscribed_users)} subscribed users.")
48
- except json.JSONDecodeError:
49
- logger.error(f"Error decoding JSON from {SUBSCRIBED_USERS_FILE}. Starting with empty list.")
50
- subscribed_users = set()
51
- except Exception as e:
52
- logger.error(f"Error loading subscribed users: {e}")
53
- subscribed_users = set()
54
- else:
55
- logger.info("Subscribed users file not found. Starting with empty list.")
56
- subscribed_users = set()
57
-
58
- def save_subscribed_users():
59
- try:
60
- with open(SUBSCRIBED_USERS_FILE, 'w') as f:
61
- json.dump(list(subscribed_users), f)
62
- logger.info(f"Saved {len(subscribed_users)} subscribed users.")
63
- except Exception as e:
64
- logger.error(f"Error saving subscribed users: {e}")
65
-
66
- # --- Aiogram Bot Logic ---
67
- async def send_notification_to_user(user_id: int, text: str, file_path: str = None, file_type: str = None):
68
- """Sends a notification to a single user, handling potential errors."""
69
- global bot_instance
70
  try:
71
- input_file = FSInputFile(file_path) if file_path else None
72
-
73
- if file_path and input_file:
74
- if file_type == 'photo':
75
- await bot_instance.send_photo(chat_id=user_id, photo=input_file, caption=text, parse_mode=ParseMode.HTML)
76
- elif file_type == 'video':
77
- await bot_instance.send_video(chat_id=user_id, video=input_file, caption=text, parse_mode=ParseMode.HTML)
78
- elif file_type == 'document':
79
- await bot_instance.send_document(chat_id=user_id, document=input_file, caption=text, parse_mode=ParseMode.HTML)
80
- elif file_type == 'audio':
81
- await bot_instance.send_audio(chat_id=user_id, audio=input_file, caption=text, parse_mode=ParseMode.HTML)
82
- else: # Default to sending text if file_type is unknown or file couldn't be processed
83
- await bot_instance.send_message(chat_id=user_id, text=text, parse_mode=ParseMode.HTML)
84
- if file_path: logger.warning(f"Unknown file type '{file_type}' for {file_path}, sent as text.")
85
- elif text:
86
- await bot_instance.send_message(chat_id=user_id, text=text, parse_mode=ParseMode.HTML)
87
-
88
- logger.info(f"Sent notification to {user_id}")
89
  return True
 
 
 
 
 
90
 
91
- except TelegramAPIError as e:
92
- logger.error(f"Telegram API Error sending to {user_id}: {e}")
93
- if "bot was blocked by the user" in str(e) or "user is deactivated" in str(e) or "chat not found" in str(e):
94
- logger.info(f"User {user_id} blocked the bot or is deactivated. Removing from subscribers.")
95
- if user_id in subscribed_users:
96
- subscribed_users.remove(user_id)
97
- save_subscribed_users()
98
- return False
99
- except Exception as e:
100
- logger.error(f"Unexpected error sending to {user_id}: {e}")
101
- return False
102
-
103
- async def process_notifications_from_queue():
104
- """Continuously processes messages from the queue and sends them."""
105
- global message_queue
106
- while True:
107
- try:
108
- notification_item = await message_queue.get()
109
- text_content = notification_item.get("text")
110
- file_path = notification_item.get("file_path")
111
- file_type = notification_item.get("file_type")
112
-
113
- logger.info(f"Processing notification: Text='{text_content[:50]}...', File='{file_path}'")
114
-
115
- # Create a copy of subscribed_users to iterate over, in case it's modified
116
- current_subscribers = list(subscribed_users)
117
-
118
- for user_id in current_subscribers:
119
- await send_notification_to_user(user_id, text_content, file_path, file_type)
120
- await asyncio.sleep(0.1) # Small delay to avoid hitting rate limits too hard
121
-
122
- if file_path and os.path.exists(file_path):
123
- try:
124
- os.remove(file_path)
125
- logger.info(f"Temporary file {file_path} deleted.")
126
- except Exception as e:
127
- logger.error(f"Error deleting temporary file {file_path}: {e}")
128
-
129
- message_queue.task_done()
130
- except asyncio.CancelledError:
131
- logger.info("Notification processor task cancelled.")
132
- break
133
- except Exception as e:
134
- logger.error(f"Error in notification processor: {e}")
135
- # Potentially re-queue or handle the error, for now just log
136
- if 'message_queue' in locals() and message_queue is not None: # Check if message_queue exists
137
- message_queue.task_done() # Ensure task_done is called even on error to prevent blocking
138
 
139
- @dp.message(CommandStart())
140
- async def handle_start(message: types.Message):
141
- user_id = message.from_user.id
142
- if user_id not in subscribed_users:
143
- subscribed_users.add(user_id)
144
- save_subscribed_users()
145
- logger.info(f"User {user_id} ({message.from_user.full_name}) subscribed.")
146
- await message.reply("🎉 вы подписались на уведомления!")
147
- else:
148
- logger.info(f"User {user_id} ({message.from_user.full_name}) is already subscribed.")
149
- await message.reply("Вы уже подписаны на уведомления. 👍")
150
 
151
- # --- Flask Admin Panel ---
152
  flask_app = Flask(__name__)
153
- flask_app.config['UPLOAD_FOLDER'] = str(UPLOAD_FOLDER) # Flask expects string path
154
- flask_app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50 MB limit for uploads
155
-
156
- # Ensure UPLOAD_FOLDER exists
157
- UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
158
 
 
159
  ADMIN_PANEL_HTML = """
160
  <!DOCTYPE html>
161
  <html lang="ru">
162
  <head>
163
  <meta charset="UTF-8">
164
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
165
- <title>Admin Panel - Telegram Notifier</title>
166
  <style>
167
- body { font-family: sans-serif; margin: 20px; background-color: #f4f4f9; color: #333; }
168
- .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
169
- h1 { color: #5a5a5a; text-align: center; }
170
  label { display: block; margin-bottom: 8px; font-weight: bold; }
171
  textarea, input[type="file"] {
172
- width: calc(100% - 22px); /* Adjust for padding and border */
173
  padding: 10px;
174
  margin-bottom: 20px;
175
  border: 1px solid #ddd;
176
  border-radius: 4px;
177
- box-sizing: border-box; /* Important for width calculation */
178
  }
179
- textarea { min-height: 100px; resize: vertical; }
180
  button {
181
- background-color: #007bff; color: white; padding: 12px 20px;
182
- border: none; border-radius: 4px; cursor: pointer; font-size: 16px;
183
- transition: background-color 0.3s ease;
 
 
 
 
 
 
184
  }
185
  button:hover { background-color: #0056b3; }
186
- .status {
187
- margin-top: 20px; padding: 10px; border-radius: 4px;
188
- text-align: center;
189
- }
190
- .status.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
191
- .status.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
192
- .loader {
193
- border: 5px solid #f3f3f3; /* Light grey */
194
- border-top: 5px solid #3498db; /* Blue */
195
- border-radius: 50%;
196
- width: 30px;
197
- height: 30px;
198
- animation: spin 1s linear infinite;
199
- margin: 10px auto;
200
- display: none; /* Hidden by default */
201
- }
202
- @keyframes spin {
203
- 0% { transform: rotate(0deg); }
204
- 100% { transform: rotate(360deg); }
205
  }
 
 
 
 
206
  </style>
207
  </head>
208
  <body>
209
  <div class="container">
210
- <h1>Отправить уведомление</h1>
211
- <form id="notificationForm" enctype="multipart/form-data">
 
 
 
 
 
 
 
 
 
212
  <div>
213
- <label for="message_text">Текст сообщения (HTML поддерживается):</label>
214
- <textarea id="message_text" name="message_text" rows="5" required></textarea>
215
  </div>
216
  <div>
217
- <label for="media_file">Прикрепить медиафайл (фото, видео, документ, аудио):</label>
218
- <input type="file" id="media_file" name="media_file" accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.txt,.zip">
219
  </div>
220
- <button type="submit">Отправить всем</button>
 
 
 
 
 
 
221
  </form>
222
- <div class="loader" id="loader"></div>
223
- <div id="statusMessage" class="status" style="display:none;"></div>
224
  </div>
225
-
226
  <script>
227
- document.getElementById('notificationForm').addEventListener('submit', async function(event) {
228
- event.preventDefault();
229
- const form = event.target;
230
- const formData = new FormData(form);
231
- const statusDiv = document.getElementById('statusMessage');
232
- const loader = document.getElementById('loader');
233
-
234
- statusDiv.style.display = 'none';
235
- statusDiv.className = 'status'; // Reset class
236
- loader.style.display = 'block'; // Show loader
237
-
238
- try {
239
- const response = await fetch('/send_notification', {
240
- method: 'POST',
241
- body: formData
242
- });
243
- const result = await response.json();
244
-
245
- if (response.ok) {
246
- statusDiv.textContent = result.message || 'Уведомление успешно отправлено в очередь.';
247
- statusDiv.classList.add('success');
248
- form.reset(); // Clear the form
249
- } else {
250
- statusDiv.textContent = result.error || 'Ошибка при отправке уведомления.';
251
- statusDiv.classList.add('error');
252
  }
253
- } catch (error) {
254
- console.error('Fetch error:', error);
255
- statusDiv.textContent = 'Сетевая ошибка или ошибка сервера. Подробности в консоли.';
256
- statusDiv.classList.add('error');
257
- } finally {
258
- loader.style.display = 'none'; // Hide loader
259
- statusDiv.style.display = 'block';
260
  }
261
- });
 
 
 
 
 
262
  </script>
263
  </body>
264
  </html>
265
  """
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  @flask_app.route('/', methods=['GET'])
268
- def admin_panel():
269
  return render_template_string(ADMIN_PANEL_HTML)
270
 
271
- def get_file_type(filename: str) -> str:
272
- """Determines file type based on extension."""
273
- ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
274
- if ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']:
275
- return 'photo'
276
- elif ext in ['mp4', 'mov', 'avi', 'mkv', 'webm']:
277
- return 'video'
278
- elif ext in ['mp3', 'ogg', 'wav', 'flac', 'm4a']:
279
- return 'audio'
280
- elif ext in ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'zip', 'rar']:
281
- return 'document'
282
- return None # Unknown or unsupported for specific sending
283
-
284
  @flask_app.route('/send_notification', methods=['POST'])
285
- def handle_send_notification():
286
- global message_queue, bot_loop
287
- if not message_queue or not bot_loop:
288
- logger.error("Bot components (queue or loop) not initialized for Flask.")
289
- return {"error": "Серверная ошибка: компоненты бота не инициализированы."}, 500
290
 
291
- message_text_html = request.form.get('message_text', '')
292
- if not message_text_html and 'media_file' not in request.files:
293
- return {"error": "Требуется текст сообщения или медиафайл."}, 400
294
 
295
- # HTML escape text if you don't trust the admin input for Telegram HTML parsing
296
- # For this example, we assume admin can use basic HTML like <b>, <i>, <a>
297
- # text_content = html_escape(message_text) # if you want to be super safe and send as plain text
298
- text_content = message_text_html # Use as is, assuming admin knows HTML for Telegram
299
 
300
- file_path = None
301
- file_type = None
302
-
303
- uploaded_file = request.files.get('media_file')
304
- if uploaded_file and uploaded_file.filename != '':
305
- filename = secure_filename(uploaded_file.filename)
306
- file_path = str(UPLOAD_FOLDER / filename) # Store as string
 
307
  try:
308
- uploaded_file.save(file_path)
309
- file_type = get_file_type(filename)
310
- if not file_type:
311
- logger.warning(f"Uploaded file {filename} has an unknown type for specific sending, will be sent as generic document or text only.")
312
- # If file_type is crucial, you might want to return an error or default to 'document'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  except Exception as e:
314
- logger.error(f"Error saving uploaded file {filename}: {e}")
315
- return {"error": f"Ошибка сохранения файла: {e}"}, 500
 
 
 
 
316
 
317
- notification_item = {
318
- "text": text_content,
319
- "file_path": file_path,
320
- "file_type": file_type
321
- }
322
 
323
- try:
324
- # Put the item into the asyncio queue from this synchronous thread
325
- future = asyncio.run_coroutine_threadsafe(message_queue.put(notification_item), bot_loop)
326
- future.result(timeout=5) # Wait for the put operation to complete, with a timeout
327
- logger.info(f"Notification enqueued: Text='{text_content[:30]}...', File='{file_path}'")
328
- return {"message": "Уведомление добавлено в очередь на отправку."}, 200
329
- except asyncio.TimeoutError:
330
- logger.error("Timeout putting notification onto the queue.")
331
- if file_path and os.path.exists(file_path): # Cleanup if queueing failed
332
- os.remove(file_path)
333
- return {"error": "Не удалось добавить уведомление в очередь (тайм-аут)."}, 500
334
- except Exception as e:
335
- logger.error(f"Error putting notification onto the queue: {e}")
336
- if file_path and os.path.exists(file_path): # Cleanup if queueing failed
337
- os.remove(file_path)
338
- return {"error": f"Ошибка добавления в очередь: {e}"}, 500
339
 
340
- # --- Main Execution ---
341
- async def main_async():
342
- global bot_instance, dp, message_queue, bot_loop
343
-
344
- bot_instance = Bot(token=BOT_TOKEN)
345
- # dp is already initialized globally if you stick to that pattern,
346
- # otherwise initialize here: dp = Dispatcher()
347
- message_queue = asyncio.Queue()
348
- bot_loop = asyncio.get_running_loop() # Get the current event loop
349
-
350
- load_subscribed_users() # Load users before starting any tasks
351
-
352
- # Start the background task for processing notifications
353
- asyncio.create_task(process_notifications_from_queue())
354
-
355
- logger.info("Starting Telegram bot polling...")
356
- try:
357
- await dp.start_polling(bot_instance, allowed_updates=dp.resolve_used_update_types())
358
- except Exception as e:
359
- logger.critical(f"Fatal error in bot polling: {e}")
360
- finally:
361
- await bot_instance.session.close()
362
- logger.info("Bot polling stopped and session closed.")
363
 
 
364
  def run_flask():
365
- logger.info(f"Starting Flask server on {FLASK_HOST}:{FLASK_PORT}")
366
- # Use a production-ready WSGI server like gunicorn or waitress in a real setup
367
- # For simplicity, Flask's built-in server is used here.
368
- # Setting debug=False is generally recommended for anything beyond local dev.
369
  flask_app.run(host=FLASK_HOST, port=FLASK_PORT, debug=False, use_reloader=False)
370
 
371
- if __name__ == '__main__':
372
- # Initialize Dispatcher here if not done globally already
373
- # This pattern is common when dp needs to be accessible globally for route decorators
374
- dp = Dispatcher()
375
- # Register handlers after dp is initialized
376
- dp.message.register(handle_start, CommandStart())
377
 
 
 
378
 
379
- # Start Flask in a separate thread
 
380
  flask_thread = threading.Thread(target=run_flask, daemon=True)
381
  flask_thread.start()
 
382
 
383
- # Start Aiogram bot
 
 
 
384
  try:
385
- asyncio.run(main_async())
386
  except KeyboardInterrupt:
387
- logger.info("Application shutting down (KeyboardInterrupt)...")
388
- except Exception as e:
389
- logger.critical(f"Application crashed: {e}", exc_info=True)
390
  finally:
391
- logger.info("Application shutdown complete.")
 
 
 
 
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()