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 асинхронных операций (важно для чистого завершения)