import os import asyncio import threading import uuid from flask import Flask, request, render_template_string, redirect, url_for from aiogram import Bot, Dispatcher, types from aiogram.types import InputFile from aiogram.utils import executor from aiogram.contrib.fsm_storage.memory import MemoryStorage # --- Configuration --- BOT_TOKEN = "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4" # Ваш токен бота FLASK_HOST = "0.0.0.0" FLASK_PORT = 7860 # --- Shared State --- # Используем set для хранения уникальных chat_id subscribed_users = set() # Временная директория для загружаемых медиафайлов TEMP_UPLOAD_FOLDER = 'temp_uploads' os.makedirs(TEMP_UPLOAD_FOLDER, exist_ok=True) # --- Aiogram Bot --- # Инициализируем объекты бота и диспетчера # Важно: создаем loop явно для использования в другом потоке loop = asyncio.get_event_loop() bot = Bot(token=BOT_TOKEN) storage = MemoryStorage() dp = Dispatcher(bot, storage=storage, loop=loop) @dp.message_handler(commands=['start']) async def process_start_command(message: types.Message): """ Handler для команды /start. Добавляет пользователя в список подписанных и отправляет приветственное сообщение. """ chat_id = message.chat.id if chat_id not in subscribed_users: subscribed_users.add(chat_id) print(f"User {chat_id} subscribed.") await message.reply("Вы подписались на уведомления!") else: await message.reply("Вы уже подписаны на уведомления!") # Опционально: сообщение для уже подписанных async def send_notification_to_user(chat_id, text, media_path=None): """ Асинхронная функция для отправки уведомления одному пользователю. """ try: if media_path and os.path.exists(media_path): # Отправляем медиа с текстом в качестве подписи with open(media_path, 'rb') as photo: await bot.send_photo(chat_id, photo=InputFile(photo), caption=text) elif text: # Отправляем только текст await bot.send_message(chat_id, text=text) else: print(f"Skipping sending to {chat_id}: No text or media provided.") except Exception as e: # Обрабатываем ошибки отправки (например, пользователь заблокировал бота) print(f"Failed to send message to {chat_id}: {e}") # Опционально: удалить пользователя из subscribed_users, если ошибка указывает на блокировку # if "bot was blocked by the user" in str(e): # subscribed_users.discard(chat_id) async def send_notification_to_all(text, media_path=None): """ Асинхронная функция для отправки уведомления всем подписанным пользователям. """ print(f"Attempting to send notification to {len(subscribed_users)} users.") # Создаем копию set, чтобы избежать проблем при изменении set во время итерации users_to_notify = list(subscribed_users) for chat_id in users_to_notify: await send_notification_to_user(chat_id, text, media_path) # Небольшая задержка, чтобы избежать превышения лимитов Telegram API await asyncio.sleep(0.05) # 50ms delay per user # --- Flask Admin Panel --- app = Flask(__name__) # HTML для админ панели ADMIN_HTML = """ Админ панель бота

Отправить уведомление

{% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}

Подписано пользователей: {{ user_count }}

""" from flask import flash # Импортируем flash для сообщений @app.route('/admin') def admin_panel(): """ Отображает страницу админ панели. """ return render_template_string(ADMIN_HTML, user_count=len(subscribed_users)) @app.route('/send_notification', methods=['POST']) def send_notification(): """ Обрабатывает отправку уведомления из админ панели. """ message_text = request.form.get('message', '').strip() media_file = request.files.get('media') media_path = None # Проверяем, загружен ли файл if media_file and media_file.filename != '': # Создаем уникальное имя файла и сохраняем его временно filename = str(uuid.uuid4()) + os.path.splitext(media_file.filename)[1] media_path = os.path.join(TEMP_UPLOAD_FOLDER, filename) try: media_file.save(media_path) print(f"Saved media file to {media_path}") except Exception as e: print(f"Error saving file: {e}") flash("Ошибка при сохранении медиафайла.", "error") return redirect(url_for('admin_panel')) # Проверяем, есть ли текст или медиа if not message_text and not media_path: flash("Пожалуйста, введите текст или прикрепите медиафайл.", "warning") if media_path and os.path.exists(media_path): os.remove(media_path) # Удалить временный файл, если он был создан return redirect(url_for('admin_panel')) # Вызываем асинхронную функцию отправки уведомлений в контексте asyncio loop # run_coroutine_threadsafe отправляет корутину в loop, который выполняется в другом потоке try: future = asyncio.run_coroutine_threadsafe( send_notification_to_all(message_text, media_path), loop # Используем loop бота ) # Ждем завершения выполнения корутины (опционально, можно запустить в фоне) # .result() делает этот Flask запрос блокирующим до отправки всем пользователям future.result() flash(f"Уведомление отправлено {len(subscribed_users)} пользователям.", "success") print("Notification sent successfully.") except Exception as e: print(f"Error sending notification: {e}") flash(f"Произошла ошибка при отправке: {e}", "error") finally: # Удаляем временный файл после отправки if media_path and os.path.exists(media_path): try: os.remove(media_path) print(f"Cleaned up temporary file {media_path}") except Exception as e: print(f"Error cleaning up file {media_path}: {e}") # Опционально: удалить пустую временную директорию # if not os.listdir(TEMP_UPLOAD_FOLDER): # os.rmdir(TEMP_UPLOAD_FOLDER) return redirect(url_for('admin_panel')) # Перенаправляем обратно на админ панель # --- Running Both --- def start_bot_polling(): """ Функция для запуска поллинга бота в отдельном потоке. """ print("Starting bot polling...") # Устанавливаем loop для этого потока, хотя aiogram 2.x уже делает это # asyncio.set_event_loop(loop) # Возможно, не нужно с aiogram 2.x+ и explicit loop # executor.start_polling блокирует выполнение # Мы запускаем его в отдельном потоке, чтобы основной поток мог запустить Flask loop.run_until_complete(dp.start_polling()) if __name__ == '__main__': # Flask требует SECRET_KEY для flash сообщений app.secret_key = 'super secret key for flask notifications' # Замените на реальный секретный ключ # Создаем и запускаем поток для бота bot_thread = threading.Thread(target=start_bot_polling, daemon=True) # daemon=True позволяет приложению завершиться, даже если поток бота еще работает bot_thread.start() # Запускаем Flask веб-сервер в основном потоке print(f"Starting Flask admin panel on http://{FLASK_HOST}:{FLASK_PORT}/admin") # use_reloader=False очень важен, чтобы избежать запуска Flask в двух процессах, # что привело бы к двойному запуску бота и проблемам с потоками/логами. app.run(host=FLASK_HOST, port=FLASK_PORT, use_reloader=False) # Остановка бота при завершении Flask (опционально, т.к. daemon=True) # print("Stopping bot...") # loop.call_soon_threadsafe(dp.stop_polling) # bot_thread.join(timeout=5) # Ждем завершения потока бота # print("Bot stopped.") # loop.close() # Закрываем loop асинхронных операций (важно для чистого завершения)