-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
289 lines (259 loc) · 14.4 KB
/
main.py
File metadata and controls
289 lines (259 loc) · 14.4 KB
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
283
284
285
286
287
288
289
# /home/telegram_gemini_bot/main.py
import asyncio
import signal
import sys
import google.generativeai as genai
from loguru import logger
from aiogram import Bot, Dispatcher, F
from aiogram.enums import ParseMode
from aiogram.types import BotCommand, BotCommandScopeDefault, BotCommandScopeChat
from aiogram.exceptions import (
TelegramBadRequest,
TelegramNetworkError,
TelegramForbiddenError,
TelegramUnauthorizedError, # Добавлен импорт
)
# --- Configuration Loading and Logging Setup ---
try:
from config import settings
# Пытаемся настроить логгер из utils, если не получится - используем базовый
if not getattr(settings, 'LOGURU_CONFIGURED', False):
try:
from utils.logger import setup_logging
setup_logging()
settings.LOGURU_CONFIGURED = True
logger.info("Logger configured from utils.logger.")
except ImportError:
logger.warning("utils.logger not found, using basic loguru config.")
logger.remove() # Удаляем стандартный обработчик, чтобы избежать дублирования
logger.add(sys.stderr, level="INFO")
log_file = getattr(settings, 'LOG_FILE', 'logs/bot.log') # Берем путь из настроек, если есть
logger.add(log_file, rotation="10 MB", retention="7 days", level="DEBUG", encoding="utf-8")
settings.LOGURU_CONFIGURED = True
except Exception as log_e:
logger.error(f"Failed to setup logger from utils.logger: {log_e}")
logger.add(sys.stderr, level="INFO") # Fallback
settings.LOGURU_CONFIGURED = True
except ImportError as e:
logger.critical(f"Failed to import settings or logger: {e}. Ensure config/settings.py and utils/logger.py exist and are correct.")
sys.exit(1)
except Exception as e:
logger.critical(f"Unexpected error during initial import/setup: {e}")
sys.exit(1)
# Импортируем остальные компоненты после настройки логгера
try:
from services import database, gemini # Импортируем сервисы
from bot.handlers import router as main_router
from bot.middleware import AuthMiddleware
except ImportError as e:
logger.critical(f"Failed to import core components (services, handlers, middleware): {e}")
sys.exit(1)
# --- Function to Set Bot Commands ---
async def set_bot_commands(bot: Bot):
"""Устанавливает команды меню для бота."""
commands_for_users = [
BotCommand(command="start", description="🚀 Старт / Помощь"),
BotCommand(command="weather", description="🌦️ Погода (напр. /weather Минск)"),
BotCommand(command="mood", description="🎭 Сменить стиль общения"),
BotCommand(command="toggle_speak", description="🔊 Вкл/Выкл озвучку"),
]
admin_commands = commands_for_users + [
BotCommand(command="admin", description="🛠️ Админ-панель"),
BotCommand(command="status", description="📊 Статус сервиса"),
BotCommand(command="restart", description="🔄 Перезапустить бота"),
]
try:
await bot.set_my_commands(commands=commands_for_users, scope=BotCommandScopeDefault())
logger.info("Default bot commands set successfully.")
admin_ids = getattr(settings, 'AUTHORIZED_USERS', [])
if admin_ids and isinstance(admin_ids, (list, tuple)):
successful_admins = 0
for admin_id in admin_ids:
try:
# Проверяем, что ID является числом
admin_id_int = int(admin_id)
await bot.set_my_commands(commands=admin_commands, scope=BotCommandScopeChat(chat_id=admin_id_int))
successful_admins += 1
except ValueError:
logger.error(f"Invalid admin ID found in AUTHORIZED_USERS: {admin_id}. Skipping.")
except TelegramForbiddenError:
logger.warning(f"Bot might be blocked by admin {admin_id}, cannot set commands for them.")
except TelegramBadRequest as e:
# Может возникнуть, если чат с админом не был начат
logger.warning(f"Could not set commands for admin {admin_id} (maybe chat not started?): {e}")
except Exception as admin_cmd_err:
logger.error(f"Failed to set admin commands for user {admin_id}: {admin_cmd_err}")
if successful_admins > 0:
logger.info(f"Admin commands set successfully for {successful_admins} admin(s).")
if successful_admins < len(admin_ids):
logger.warning(f"Could not set admin commands for {len(admin_ids) - successful_admins} admin(s). See previous logs.")
elif not admin_ids:
logger.info("No admin IDs found in settings.AUTHORIZED_USERS. Skipping admin-specific commands.")
else:
logger.error("settings.AUTHORIZED_USERS is defined but is not a list or tuple. Cannot set admin commands.")
except TelegramNetworkError as e:
logger.error(f"Network error setting bot commands: {e}. Continuing without setting commands.")
except TelegramUnauthorizedError:
logger.error("Invalid token when trying to set bot commands.") # Эта ошибка может быть критичной
except Exception as e:
logger.error(f"Failed to set bot commands: {e}")
# --- Main Application Logic ---
async def main():
logger.info("Starting bot application...")
# --- Configuration Loading ---
logger.info("Using loaded configuration from settings.")
# Проверим наличие критичных переменных
if not settings.TELEGRAM_BOT_TOKEN:
logger.critical("TELEGRAM_BOT_TOKEN not found in settings. Exiting.")
sys.exit(1)
# GEMINI_API_KEY необязателен для запуска, но его отсутствие будет залогировано ниже
# --- Google Generative AI Configuration ---
try:
# Используем правильное имя переменной из .env/settings.py
if api_key := getattr(settings, 'GOOGLE_API_KEY', None): # Используем правильное имя из предыдущего лога
genai.configure(api_key=api_key)
logger.info("Google Generative AI configured successfully.")
# Можно добавить тестовый вызов, если нужно убедиться в работоспособности ключа
# list(genai.list_models()) # Например
else:
logger.warning("GOOGLE_API_KEY not found in settings. Google AI features may not work.")
except Exception as e:
logger.error(f"Failed to configure or verify Google Generative AI: {e}")
# Не выходим, бот может работать и без Gemini (если логика это позволяет)
# --- Database Initialization ---
logger.info("Initializing database...")
try:
await database.init_db()
logger.info("Database initialized successfully.")
except Exception as e:
logger.critical(f"Failed to initialize database: {e}")
logger.exception(e)
sys.exit(1) # База данных критична
# --- Bot and Dispatcher Initialization ---
logger.info("Initializing bot...")
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
try:
user = await bot.get_me()
logger.info(f"Bot instance created and token verified for bot ID {user.id} (@{user.username})")
except TelegramUnauthorizedError:
logger.critical("Invalid Telegram Bot Token. Please check your .env file.")
sys.exit(1)
except TelegramNetworkError as e:
logger.error(f"Network error during bot initialization (get_me): {e}. Check connection.")
# Если не удалось проверить токен из-за сети, можно продолжить, но с риском
except Exception as e:
logger.critical(f"Failed to initialize bot: {e}")
logger.exception(e)
sys.exit(1)
# --- Set Bot Commands ---
await set_bot_commands(bot) # Вызываем установку команд
# --- Dispatcher Setup ---
dp = Dispatcher()
logger.info("Dispatcher instance created.")
# --- Middleware ---
# Проверяем, определены ли пользователи, прежде чем включать middleware
if hasattr(settings, 'AUTHORIZED_USERS') and settings.AUTHORIZED_USERS:
dp.update.outer_middleware(AuthMiddleware())
logger.info("Authorization middleware registered.")
else:
logger.warning("AUTHORIZED_USERS not defined or empty in settings. AuthMiddleware is disabled.")
# --- Routers ---
dp.include_router(main_router)
logger.info("Main router included.")
# --- Start Polling ---
logger.info("Starting polling...")
session_closed_cleanly = False # Флаг для finally
try:
await bot.delete_webhook(drop_pending_updates=True)
logger.info("Webhook deleted (if existed).")
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
except (KeyboardInterrupt, SystemExit):
logger.warning("Bot stopped by user (Ctrl+C or SystemExit).")
except TelegramUnauthorizedError:
logger.critical("Bot token became invalid during polling.")
except TelegramNetworkError as e:
logger.critical(f"Critical network error during polling: {e}")
except Exception as e:
logger.critical(f"Critical error during polling: {e}")
logger.exception(e)
finally:
logger.warning("Bot polling stopped.")
# Корректное закрытие сессии бота
try:
# Проверяем, есть ли сессия и не закрыта ли она уже
if bot.session and hasattr(bot.session, 'closed') and not await bot.session.closed():
await bot.session.close()
logger.info("Bot session closed.")
session_closed_cleanly = True
elif bot.session and not hasattr(bot.session, 'closed'): # Для старых версий aiogram/aiohttp
await bot.session.close()
logger.info("Bot session closed (assumed mechanism).")
session_closed_cleanly = True
elif not bot.session:
logger.warning("Bot session object does not exist.")
else:
logger.info("Bot session was already closed.")
session_closed_cleanly = True # Считаем, что закрыта
except Exception as close_err:
logger.error(f"Error closing bot session: {close_err}")
logger.info(f"Bot shutdown {'complete' if session_closed_cleanly else 'finished with potential issues'}.")
# --- Graceful Shutdown Handling ---
async def shutdown(sig: signal.Signals, loop: asyncio.AbstractEventLoop, bot: Bot):
"""Gracefully shutdown the bot on signal."""
logger.warning(f"Received exit signal {sig.name}... Shutting down.")
# Завершаем опрос (если он еще работает)
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
logger.info(f"Cancelling {len(tasks)} outstanding tasks...")
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
logger.info("Tasks cancelled.")
# Закрываем сессию бота
if bot.session and hasattr(bot.session, 'closed') and not await bot.session.closed():
await bot.session.close()
logger.info("Bot session closed during shutdown.")
elif bot.session and not hasattr(bot.session, 'closed'):
await bot.session.close()
logger.info("Bot session closed during shutdown (assumed mechanism).")
loop.stop() # Останавливаем цикл событий
# --- Entry Point ---
if __name__ == "__main__":
if sys.version_info < (3, 10):
print("ERROR: Bot requires Python 3.10 or higher.", file=sys.stderr)
sys.exit(1)
# --- Настройка цикла событий и обработчиков сигналов ---
# Используем try..except для совместимости с Windows, где add_signal_handler нет
try:
loop = asyncio.get_event_loop()
# Создаем временный объект бота ТОЛЬКО для передачи в shutdown
temp_bot_for_shutdown = Bot(token=settings.TELEGRAM_BOT_TOKEN)
signals_to_handle = (signal.SIGINT, signal.SIGTERM) # SIGINT (Ctrl+C), SIGTERM
for s in signals_to_handle:
loop.add_signal_handler(
s, lambda s=s: asyncio.create_task(shutdown(s, loop, temp_bot_for_shutdown))
)
logger.info(f"Registered signal handlers for {', '.join(s.name for s in signals_to_handle)}")
except NotImplementedError: # Для Windows
logger.warning("Signal handlers are not fully supported on this platform (Windows).")
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
except Exception as e:
logger.error(f"Error setting up event loop or signal handlers: {e}")
sys.exit(1)
# --- Запуск основного цикла ---
try:
logger.info("Starting event loop...")
loop.run_until_complete(main())
except asyncio.CancelledError:
logger.warning("Main task cancelled during shutdown.")
except Exception as e:
# Логируем критические ошибки, которые могли произойти до старта polling или после его остановки
logger.critical(f"Unhandled exception in event loop: {e}")
logger.exception(e)
finally:
logger.info("Closing event loop.")
# Убедимся, что все задачи завершены перед закрытием цикла
# loop.run_until_complete(loop.shutdown_asyncgens()) # Для более новых Python
loop.close()
logger.info("Event loop closed. Exiting application.")
sys.exit(0) # Явный выход с кодом 0