File size: 12,533 Bytes
746f53a 38c4fa2 f7c081c 38c4fa2 746f53a 38c4fa2 15288fa 38c4fa2 5d14946 746f53a 38c4fa2 746f53a f7c081c 38c4fa2 746f53a 38c4fa2 15288fa 38c4fa2 8bf5835 38c4fa2 746f53a 38c4fa2 746f53a 5be4d06 38c4fa2 4e9cca7 38c4fa2 746f53a f7c081c 38c4fa2 f7c081c 38c4fa2 746f53a 38c4fa2 746f53a 38c4fa2 746f53a 38c4fa2 89507b1 746f53a 38c4fa2 15288fa 38c4fa2 5be4d06 38c4fa2 15288fa 38c4fa2 15288fa 38c4fa2 2c2afb1 f7c081c 746f53a 38c4fa2 15288fa 38c4fa2 15288fa 38c4fa2 15288fa 38c4fa2 04fdde2 746f53a e62320e 38c4fa2 15288fa 38c4fa2 746f53a 38c4fa2 15288fa 38c4fa2 746f53a 38c4fa2 15288fa 38c4fa2 15288fa 38c4fa2 15288fa 38c4fa2 15288fa e62320e 38c4fa2 2c9d170 746f53a 38c4fa2 f7c081c 38c4fa2 15288fa f7c081c 15288fa 38c4fa2 f7c081c 38c4fa2 746f53a 38c4fa2 |
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 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
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 = """
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Админ панель бота</title>
<style>
body {
font-family: sans-serif;
line-height: 1.6;
margin: 20px;
background-color: #f8f8f8;
color: #333;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #0056b3;
}
form {
display: flex;
flex-direction: column;
}
label {
margin-bottom: 5px;
font-weight: bold;
}
textarea, input[type="file"] {
margin-bottom: 15px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
textarea {
resize: vertical;
min-height: 100px;
}
button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #0056b3;
}
.flash-message {
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
</style>
</head>
<body>
<div class="container">
<h1>Отправить уведомление</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="flash-message">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<p>Подписано пользователей: {{ user_count }}</p>
<form action="{{ url_for('send_notification') }}" method="post" enctype="multipart/form-data">
<label for="message">Текст уведомления:</label>
<textarea id="message" name="message" placeholder="Введите текст уведомления"></textarea>
<label for="media">Медиафайл (фото):</label>
<input type="file" id="media" name="media" accept="image/*">
<button type="submit">Отправить всем</button>
</form>
</div>
</body>
</html>
"""
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 асинхронных операций (важно для чистого завершения) |