Compare commits

...

5 Commits

Author SHA1 Message Date
7495bd949b hotfix: log any update and use index for excels
All checks were successful
Build and Run VSTU Public TG bot / build_and_run (push) Successful in 27s
2026-04-13 23:10:54 +03:00
8bbb440bce fix: user_id -> chat_id for unsub:
All checks were successful
Build and Run VSTU Public TG bot / build_and_run (push) Successful in 6s
2026-04-05 22:43:04 +03:00
64c3f27386 fix: missing unwrap with .scalar_one_or_none
All checks were successful
Build and Run VSTU Public TG bot / build_and_run (push) Successful in 8s
2026-04-05 22:40:30 +03:00
8d84931fca add Dockerfile and requirements.txt
All checks were successful
Build and Run VSTU Public TG bot / build_and_run (push) Successful in 27s
2026-04-05 22:35:03 +03:00
7f890e927a add Dockerfile and requirements.txt 2026-04-05 22:34:47 +03:00
9 changed files with 109 additions and 24 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
.env .env
__pycache__

6
Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "-u", "main.py"]

Binary file not shown.

View File

@@ -4,11 +4,13 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DataManager: class DataManager:
def __init__(self, facultets_url, groups_url): def __init__(self, facultets_url, groups_url, parser_url):
self.facultets_url = facultets_url self.facultets_url = facultets_url
self.groups_url = groups_url self.groups_url = groups_url
self.parser_url = parser_url
self.facs = {} # {id: {name: ...}} self.facs = {} # {id: {name: ...}}
self.groups = {} # {id: {real_name: ..., facultet_tech: ..., ...}} self.groups = {} # {id: {real_name: ..., facultet_tech: ..., ...}}
self.parser = {}
async def refresh_cache(self, connector): async def refresh_cache(self, connector):
async with aiohttp.ClientSession(connector=connector) as session: async with aiohttp.ClientSession(connector=connector) as session:
@@ -17,6 +19,8 @@ class DataManager:
try: try:
self.facs = await fetch(self.facultets_url) self.facs = await fetch(self.facultets_url)
self.groups = (await fetch(self.groups_url))['groups'] self.groups = (await fetch(self.groups_url))['groups']
self.parser = (await fetch(self.parser_url))
logger.info(f"Loaded {len(self.facs)} facs and {len(self.groups)} groups") logger.info(f"Loaded {len(self.facs)} facs and {len(self.groups)} groups")
except Exception as e: except Exception as e:
logger.error(f"Cache refresh failed: {e}") logger.error(f"Cache refresh failed: {e}")
@@ -29,11 +33,12 @@ class DataManager:
def get_files_by_fac(self, fac_id): def get_files_by_fac(self, fac_id):
files = {} # {uniqpath*: display_name} files = {} # {uniqpath*: display_name}
for g in self.groups.values(): i = -1
if g.get('facultet_tech') == fac_id or g.get('facultet_recognized') == fac_id: for ex in self.parser['all_files']:
for ex in g.get('excels', []): i += 1
fn = ex['uniqpath'].replace("vstu.ru/rasp?dep=", "") if ex.get('facultet') == fac_id:
files[fn] = ex['display_filename'] files[i] = ex['display_filename']
return list(files.items()) return list(files.items())
def get_excel_by_uniqpath(self, uniqid): def get_excel_by_uniqpath(self, uniqid):

45
main.py
View File

@@ -22,12 +22,22 @@ import keyboards as kb
import re import re
import fnmatch import fnmatch
from middleware import LoggingMiddleware
load_dotenv() load_dotenv()
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler()
]
)
# --- Настройки --- # --- Настройки ---
engine = create_async_engine(os.getenv("DATABASE_URL")) engine = create_async_engine(os.getenv("DATABASE_URL"))
async_session = async_sessionmaker(engine, expire_on_commit=False) async_session = async_sessionmaker(engine, expire_on_commit=False)
data_mgr = DataManager(os.getenv("FACULTETS_JSON_URL"), os.getenv("GROUPS_JSON_URL")) data_mgr = DataManager(os.getenv("FACULTETS_JSON_URL"), os.getenv("GROUPS_JSON_URL"), os.getenv("PARSER_JSON_URL"))
bot = None bot = None
dp = Dispatcher() dp = Dispatcher()
@@ -203,16 +213,17 @@ async def view_group(callback: types.CallbackQuery):
# Просмотр файла # Просмотр файла
@dp.callback_query(F.data.startswith("view_excel:")) @dp.callback_query(F.data.startswith("view_excel:"))
async def view_excel(callback: types.CallbackQuery): async def view_excel(callback: types.CallbackQuery):
pre_uniq_path = callback.data.split(":")[1] pre_index = callback.data.split(":")[1]
uniqpath = "vstu.ru/rasp?dep=" + pre_uniq_path
# Находим имя файла для красоты # Находим имя файла для красоты
h_name = "Excel файл" h_name = "Excel файл"
for g in data_mgr.groups.values(): try:
for ex in g.get('excels', []): ex = data_mgr.parser['all_files'][int(pre_index)]
if ex.get('uniqpath', "") == uniqpath: h_name = ex['display_filename']; break h_name = ex['display_filename']
except Exception as e:
print(f"safe?: {e}")
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(types.InlineKeyboardButton(text="🔔 Подписаться на файл", callback_data=f"suco:excel:{pre_uniq_path}")) builder.row(types.InlineKeyboardButton(text="🔔 Подписаться на файл", callback_data=f"suco:excel:{pre_index}"))
builder.row(types.InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_start")) builder.row(types.InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_start"))
await callback.message.edit_text(f"📄 Файл: *{h_name}*", reply_markup=builder.as_markup(), parse_mode="Markdown") await callback.message.edit_text(f"📄 Файл: *{h_name}*", reply_markup=builder.as_markup(), parse_mode="Markdown")
@@ -224,11 +235,13 @@ async def sub_confirm(callback: types.CallbackQuery):
sub_value = val sub_value = val
if mode == "excel": if mode == "excel":
pre_uniq_path = h_name pre_index = h_name
uniqpath = "vstu.ru/rasp?dep=" + pre_uniq_path try:
excel = data_mgr.get_excel_by_uniqpath(uniqpath) excel = data_mgr.parser['all_files'][int(pre_index)]
sub_value = excel['url'].split("/")[-1] sub_value = excel['url'].split("/")[-1]
h_name = sub_value h_name = sub_value
except Exception as e:
print(f"safe? e={e}")
async with async_session() as session: async with async_session() as session:
# Регистрация пользователя если нет # Регистрация пользователя если нет
@@ -270,18 +283,18 @@ async def my_subs(callback: types.CallbackQuery):
@dp.callback_query(F.data.startswith("unsub:")) @dp.callback_query(F.data.startswith("unsub:"))
async def unsub(callback: types.CallbackQuery): async def unsub(callback: types.CallbackQuery):
sid = int(callback.data.split(":")[1]) sid = int(callback.data.split(":")[1])
user_id = callback.from_user.id # ID того, кто нажал на кнопку chat_id = callback.message.chat.id
async with async_session() as session: async with async_session() as session:
# Добавляем условие chat_id == user_id
stmt = select(Subscription).where( stmt = select(Subscription).where(
Subscription.id == sid, Subscription.id == sid,
Subscription.chat_id == user_id, Subscription.chat_id == chat_id,
Subscription.deleted == False Subscription.deleted == False
) )
result = await session.execute(stmt) result = await session.execute(stmt)
result.deleted = True sub = result.scalar_one_or_none()
session.add(result) sub.deleted = True
session.add(sub)
await session.commit() await session.commit()
# Проверяем, было ли реально что-то удалено # Проверяем, было ли реально что-то удалено
@@ -337,6 +350,8 @@ async def main():
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
await data_mgr.refresh_cache(connector) await data_mgr.refresh_cache(connector)
dp.update.middleware(LoggingMiddleware()) # <-- добавьте эту строку
asyncio.create_task(start_rabbitmq_consumer()) asyncio.create_task(start_rabbitmq_consumer())
# Периодическое обновление индексов # Периодическое обновление индексов

51
middleware.py Normal file
View File

@@ -0,0 +1,51 @@
from aiogram.types import Update
from aiogram import BaseMiddleware
from typing import Callable, Dict, Any, Awaitable
import logging
import time
import json
class LoggingMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]],
event: Update,
data: Dict[str, Any]
) -> Any:
# Логируем входящий апдейт
update_id = event.update_id
update_type = "unknown"
if event.message:
update_type = "message"
chat_id = event.message.chat.id
text = event.message.text or "[non-text]"
log_msg = f"📥 [UPDATE {update_id}] type={update_type} chat={chat_id} text={text}"
elif event.callback_query:
update_type = "callback_query"
chat_id = event.callback_query.message.chat.id
data_str = event.callback_query.data or "[no data]"
log_msg = f"📥 [UPDATE {update_id}] type={update_type} chat={chat_id} data={data_str}"
elif event.my_chat_member:
update_type = "my_chat_member"
log_msg = f"📥 [UPDATE {update_id}] type={update_type}"
elif event.chat_member:
update_type = "chat_member"
log_msg = f"📥 [UPDATE {update_id}] type={update_type}"
else:
log_msg = f"📥 [UPDATE {update_id}] type={update_type} raw={event.json()}"
# Печатаем или используем logging
logging.info(log_msg)
# Если настроен logging: logging.info(log_msg)
# Передаём управление дальше (в хендлеры)
try:
result = await handler(event, data)
except Exception as e:
# Логируем ошибку, но не подавляем
logging.error(f"❌ [UPDATE {update_id}] handler error: {e}")
raise
else:
logging.info(f"✅ [UPDATE {update_id}] handled successfully")
return result

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
aio_pika==9.6.2
aiogram==3.27.0
aiohttp==3.11.16
aiohttp_socks==0.11.0
python-dotenv==1.2.2
SQLAlchemy==2.0.49
asyncpg