This commit is contained in:
2026-07-09 23:34:34 +03:00
commit 73c4fc37f7
33 changed files with 2505 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.13-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "bot.py"]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+193
View File
@@ -0,0 +1,193 @@
import httpx
from datetime import date
from cachetools import TTLCache
from config import (
API_URL,
REQUEST_TIMEOUT,
GROUPS_CACHE_TTL,
)
from database import (
get_cached_schedule,
save_schedule_cache,
)
def get_schedule_ttl(day: date) -> int:
"""
Возвращает TTL кэша в минутах.
"""
today = date.today()
if day == today:
# Сегодня — обновляем каждый час
return 60
if day > today:
# Будущие даты — раз в сутки
return 60 * 24
# Прошедшие даты — неделю
return 60 * 24 * 7
class ScheduleAPI:
def __init__(self):
self.client = httpx.AsyncClient(
timeout=REQUEST_TIMEOUT
)
self._groups_cache = TTLCache(
maxsize=1,
ttl=GROUPS_CACHE_TTL
)
async def close(self):
await self.client.aclose()
async def _get(
self,
endpoint: str,
params: dict | None = None
):
response = await self.client.get(
f"{API_URL}/{endpoint}",
params=params
)
response.raise_for_status()
return response.json()
async def get_groups(self):
if "groups" in self._groups_cache:
return self._groups_cache["groups"]
response = await self._get("Groups")
groups = (
response
.get("data", {})
.get("groups", [])
)
self._groups_cache["groups"] = groups
return groups
async def get_group_by_id(
self,
group_id: int
):
groups = await self.get_groups()
for group in groups:
if group["groupID"] == group_id:
return group
return None
async def search_group_by_name(
self,
text: str
):
groups = await self.get_groups()
text = text.upper().strip()
exact = [
g
for g in groups
if g["groupName"].upper() == text
]
if exact:
return exact
return [
g
for g in groups
if text in g["groupName"].upper()
]
async def get_schedule(
self,
group_id: int,
day: date | None = None
):
request_day = day or date.today()
schedule_date = request_day.isoformat()
ttl = get_schedule_ttl(request_day)
# Проверяем кэш
cached = await get_cached_schedule(
group_id,
schedule_date,
ttl
)
if cached is not None:
print(f"[CACHE] {group_id} {schedule_date}")
return cached
print(f"[API] {group_id} {schedule_date}")
params = {
"idGroup": group_id,
"sdate": schedule_date
}
response = await self._get(
"Rasp",
params
)
schedule = response.get(
"data",
{}
)
# Сохраняем в кэш
await save_schedule_cache(
group_id,
schedule_date,
schedule
)
return schedule
async def get_teachers(self):
response = await self._get("Prepods")
return (
response
.get("data", {})
.get("prepods", [])
)
async def get_auditories(self):
response = await self._get("Auds")
return (
response
.get("data", {})
.get("auds", [])
)
api = ScheduleAPI()
+95
View File
@@ -0,0 +1,95 @@
import asyncio
import logging
from schedule import refresh_cache
from aiogram import Bot, Dispatcher
from aiogram.enums import ParseMode
from aiogram.client.default import DefaultBotProperties
from aiogram.fsm.storage.memory import MemoryStorage
from config import BOT_TOKEN
from api import api
from handlers.support import router as support_router
from handlers.start import router as start_router
from handlers.group import router as group_router
from handlers.admin import router as admin_router
from logger import logger
from database import (
init_db,
clear_old_cache
)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s"
)
bot = Bot(
token=BOT_TOKEN,
default=DefaultBotProperties(
parse_mode=ParseMode.HTML
)
)
dp = Dispatcher(
storage=MemoryStorage()
)
dp.include_router(start_router)
dp.include_router(group_router)
dp.include_router(support_router)
dp.include_router(admin_router)
#стартует бот
async def on_startup():
await init_db()
deleted = await clear_old_cache()
logger.info("База данных инициализирована")
logger.info(
f"Удалено {deleted} записей кэша"
)
asyncio.create_task(
refresh_cache()
)
logger.info(
"Фоновое обновление кэша запущено"
)
async def on_shutdown():
await api.close()
await bot.session.close()
logger.info("Бот остановлен")
async def main():
await on_startup()
try:
logger.info("Бот запущен")
await dp.start_polling(bot)
finally:
await on_shutdown()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Остановка...")
+31
View File
@@ -0,0 +1,31 @@
import os
from dotenv import load_dotenv
load_dotenv()
ADMIN_IDS = [
int(x)
for x in os.getenv(
"ADMIN_IDS",
""
).split(",")
if x
]
load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN")
API_URL = os.getenv(
"API_URL",
"https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api"
)
REQUEST_TIMEOUT = 15
GROUPS_CACHE_TTL = 3600 # 1 час
if not BOT_TOKEN:
raise RuntimeError("BOT_TOKEN не найден в .env")
BIN
View File
Binary file not shown.
+501
View File
@@ -0,0 +1,501 @@
import aiosqlite
from datetime import datetime
import json
DB_NAME = "database/database.db"
async def init_db():
async with aiosqlite.connect(DB_NAME) as db:
# Пользователи
await db.execute("""
CREATE TABLE IF NOT EXISTS users(
user_id INTEGER PRIMARY KEY,
username TEXT,
first_name TEXT,
last_name TEXT,
phone TEXT,
email TEXT,
group_id INTEGER,
group_name TEXT,
subgroup TEXT,
created_at TEXT,
last_seen TEXT
)
""")
# Кэш расписания
await db.execute("""
CREATE TABLE IF NOT EXISTS schedule_cache(
id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL,
date TEXT NOT NULL,
data TEXT NOT NULL,
created_at TEXT NOT NULL,
UNIQUE(group_id, date)
)
""")
# Индекс
await db.execute("""
CREATE INDEX IF NOT EXISTS idx_schedule_cache
ON schedule_cache(group_id, date)
""")
# Поддержка
await db.execute("""
CREATE TABLE IF NOT EXISTS support(
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
message TEXT NOT NULL,
answer TEXT,
status TEXT DEFAULT 'open',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
await db.commit()
async def save_group(
user_id: int,
group_id: int,
group_name: str,
subgroup: str | None = None
):
async with aiosqlite.connect(DB_NAME) as db:
await db.execute(
"""
INSERT INTO users(
user_id,
group_id,
group_name,
subgroup
)
VALUES(?,?,?,?)
ON CONFLICT(user_id)
DO UPDATE SET
group_id = excluded.group_id,
group_name = excluded.group_name,
subgroup = excluded.subgroup
""",
(
user_id,
group_id,
group_name,
subgroup
)
)
await db.commit()
async def get_group(
user_id: int
):
async with aiosqlite.connect(DB_NAME) as db:
cursor = await db.execute(
"""
SELECT
group_id,
group_name,
subgroup
FROM users
WHERE user_id=?
""",
(
user_id,
)
)
row = await cursor.fetchone()
if row is None:
return None
return {
"group_id": row[0],
"group_name": row[1],
"subgroup": row[2]
}
async def delete_group(
user_id: int
):
async with aiosqlite.connect(DB_NAME) as db:
await db.execute(
"""
DELETE FROM users
WHERE user_id=?
""",
(
user_id,
)
)
await db.commit()
async def update_subgroup(
user_id: int,
subgroup: str
):
async with aiosqlite.connect(DB_NAME) as db:
await db.execute(
"""
UPDATE users
SET subgroup=?
WHERE user_id=?
""",
(
subgroup,
user_id
)
)
await db.commit()
async def create_ticket(
user_id: int,
message: str
):
async with aiosqlite.connect(DB_NAME) as db:
cursor = await db.execute(
"""
INSERT INTO support(
user_id,
message
)
VALUES(?,?)
""",
(
user_id,
message
)
)
await db.commit()
return cursor.lastrowid
#tickets
async def get_ticket(ticket_id: int):
async with aiosqlite.connect(DB_NAME) as db:
cursor = await db.execute(
"""
SELECT
id,
user_id,
message,
status
FROM support
WHERE id=?
""",
(ticket_id,)
)
row = await cursor.fetchone()
return row
async def get_tickets():
async with aiosqlite.connect(DB_NAME) as db:
cursor = await db.execute(
"""
SELECT
id,
user_id,
message,
status
FROM support
WHERE status='open'
ORDER BY id DESC
"""
)
rows = await cursor.fetchall()
return rows
async def close_ticket(ticket_id: int):
async with aiosqlite.connect(DB_NAME) as db:
await db.execute(
"""
UPDATE support
SET status='closed'
WHERE id=?
""",
(ticket_id,)
)
await db.commit()
async def update_user(
user_id: int,
username: str | None = None,
first_name: str | None = None,
last_name: str | None = None
):
now = datetime.now().isoformat()
async with aiosqlite.connect(DB_NAME) as db:
await db.execute(
"""
INSERT INTO users(
user_id,
username,
first_name,
last_name,
created_at,
last_seen
)
VALUES(?,?,?,?,?,?)
ON CONFLICT(user_id)
DO UPDATE SET
username=excluded.username,
first_name=excluded.first_name,
last_name=excluded.last_name,
last_seen=excluded.last_seen
""",
(
user_id,
username,
first_name,
last_name,
now,
now
)
)
await db.commit()
async def save_phone(
user_id: int,
phone: str
):
async with aiosqlite.connect(DB_NAME) as db:
await db.execute(
"""
UPDATE users
SET phone=?
WHERE user_id=?
""",
(
phone,
user_id
)
)
await db.commit()
async def get_cached_schedule(
group_id: int,
schedule_date: str,
ttl_minutes: int
):
async with aiosqlite.connect(DB_NAME) as db:
cursor = await db.execute(
"""
SELECT
data,
created_at
FROM schedule_cache
WHERE group_id=?
AND date=?
""",
(
group_id,
schedule_date
)
)
row = await cursor.fetchone()
if row is None:
return None
created = datetime.fromisoformat(
row[1]
)
age = (
datetime.now() - created
).total_seconds() / 60
# кэш устарел
if age > ttl_minutes:
return None
return json.loads(row[0])
async def save_schedule_cache(
group_id: int,
schedule_date: str,
data: dict
):
async with aiosqlite.connect(DB_NAME) as db:
await db.execute(
"""
INSERT INTO schedule_cache(
group_id,
date,
data,
created_at
)
VALUES(?,?,?,?)
ON CONFLICT(group_id, date)
DO UPDATE SET
data = excluded.data,
created_at = excluded.created_at
""",
(
group_id,
schedule_date,
json.dumps(
data,
ensure_ascii=False
),
datetime.now().isoformat()
)
)
await db.commit()
async def clear_old_cache(days: int = 30):
async with aiosqlite.connect(DB_NAME) as db:
cursor = await db.execute(
"""
DELETE FROM schedule_cache
WHERE datetime(created_at)
< datetime('now', ?)
""",
(f"-{days} days",)
)
await db.commit()
return cursor.rowcount
async def get_active_groups():
async with aiosqlite.connect(DB_NAME) as db:
cursor = await db.execute(
"""
SELECT DISTINCT group_id
FROM users
WHERE group_id IS NOT NULL
"""
)
rows = await cursor.fetchall()
return [row[0] for row in rows]
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
services:
schedule_bot:
build: .
container_name: schedule_bot
restart: unless-stopped
env_file:
- .env
volumes:
- ./database.db:/app/database.db
dns:
- 8.8.8.8
- 1.1.1.1
+76
View File
@@ -0,0 +1,76 @@
from collections import defaultdict
def format_schedule(data):
rasp = data.get("rasp", [])
if not rasp:
return "🎉 Сегодня занятий нет."
days = defaultdict(list)
for lesson in rasp:
days[
lesson["день_недели"]
].append(lesson)
text = ""
for day, lessons in days.items():
text += f"\n📅 {day}\n\n"
for item in sorted(
lessons,
key=lambda x: x["номерЗанятия"]
):
number = item.get(
"номерЗанятия",
"?"
)
start = item.get(
"начало",
""
)
end = item.get(
"конец",
""
)
subject = item.get(
"дисциплина",
"Без названия"
)
teacher = item.get(
"преподаватель",
"-"
)
room = item.get(
"аудитория",
"-"
)
text += (
f"{number}️⃣ "
f"{start}-{end}\n"
f"📘 {subject}\n"
f"👤 {teacher}\n"
f"🏫 {room}\n\n"
"────────────\n\n"
)
return text
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+168
View File
@@ -0,0 +1,168 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import (
Message,
CallbackQuery
)
from aiogram.fsm.context import FSMContext
from config import ADMIN_IDS
from database import (
get_tickets,
get_ticket,
close_ticket
)
from states import AdminState
router = Router()
@router.message(Command("tickets"))
async def tickets(message: Message):
if message.from_user.id not in ADMIN_IDS:
return
tickets = await get_tickets()
if not tickets:
await message.answer(
"📭 Новых обращений нет."
)
return
text = "📩 <b>Обращения:</b>\n\n"
for ticket in tickets:
text += (
f"🆔 <b>#{ticket[0]}</b>\n"
f"👤 Пользователь: <code>{ticket[1]}</code>\n"
f"📝 Сообщение:\n"
f"{ticket[2]}\n"
f"📌 Статус: {ticket[3]}\n"
"━━━━━━━━━━━━━━\n"
)
await message.answer(text)
@router.callback_query(
F.data.startswith("reply_ticket:")
)
async def reply_ticket(
callback: CallbackQuery,
state: FSMContext
):
ticket_id = int(
callback.data.split(":")[1]
)
await state.update_data(
ticket_id=ticket_id
)
await state.set_state(
AdminState.waiting_reply
)
await callback.message.answer(
"💬 Напишите ответ пользователю:"
)
await callback.answer()
@router.message(AdminState.waiting_reply)
async def send_admin_reply(
message: Message,
state: FSMContext,
bot
):
data = await state.get_data()
ticket_id = data.get(
"ticket_id"
)
if not ticket_id:
await message.answer(
"❌ Тикет не найден."
)
await state.clear()
return
ticket = await get_ticket(
ticket_id
)
if not ticket:
await message.answer(
"❌ Обращение не найдено."
)
await state.clear()
return
user_id = ticket[1]
try:
await bot.send_message(
user_id,
(
"📩 <b>Ответ поддержки</b>\n\n"
f"{message.text}"
)
)
await message.answer(
"✅ Ответ отправлен пользователю."
)
except Exception as e:
await message.answer(
f"❌ Не удалось отправить сообщение:\n{e}"
)
await state.clear()
@router.callback_query(
F.data.startswith("close_ticket:")
)
async def close_ticket_handler(
callback: CallbackQuery
):
ticket_id = int(
callback.data.split(":")[1]
)
await close_ticket(
ticket_id
)
await callback.message.edit_text(
callback.message.html_text
+ "\n\n✅ <b>Тикет закрыт</b>"
)
await callback.answer(
"Тикет закрыт"
)
+803
View File
@@ -0,0 +1,803 @@
from datetime import date, timedelta
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from states import GroupState
from api import api
from database import save_group, get_group, update_subgroup
import math
from keyboards import main_keyboard
from keyboards import subgroup_keyboard
from keyboards import (
groups_keyboard,
date_keyboard,
calendar_keyboard,
main_keyboard,
start_keyboard,
)
router = Router()
DAYS = (
"Пн",
"Вт",
"Ср",
"Чт",
"Пт",
"Сб",
"Вс",
)
def format_date(day: date) -> str:
return f"{day.strftime('%d.%m')} ({DAYS[day.weekday()]})"
def build_keyboard(group_id: int, current_day: date):
kb = InlineKeyboardBuilder()
kb.button(
text="⬅️",
callback_data=f"day:{group_id}:{(current_day - timedelta(days=1)).isoformat()}"
)
kb.button(
text=format_date(current_day),
callback_data="ignore"
)
kb.button(
text="➡️",
callback_data=f"day:{group_id}:{(current_day + timedelta(days=1)).isoformat()}"
)
kb.adjust(3)
return kb.as_markup()
async def build_groups_keyboard(page: int = 0):
groups = await api.get_groups()
groups.sort(
key=lambda g: g["groupName"]
)
PER_PAGE = 30
start = page * PER_PAGE
end = start + PER_PAGE
current = groups[start:end]
kb = InlineKeyboardBuilder()
for group in current:
kb.button(
text=group["groupName"],
callback_data=f"select:{group['groupID']}"
)
kb.adjust(3)
pages = math.ceil(len(groups) / PER_PAGE)
if page > 0:
kb.button(
text="◀️",
callback_data=f"groups_page:{page-1}"
)
kb.button(
text=f"{page+1}/{pages}",
callback_data="ignore"
)
if page < pages - 1:
kb.button(
text="▶️",
callback_data=f"groups_page:{page+1}"
)
kb.adjust(3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3)
return kb.as_markup()
def build_schedule(
schedule: dict,
group: dict,
selected_day: date
) -> str:
lessons = [
lesson
for lesson in schedule.get("rasp", [])
if lesson.get("дата", "")[:10] == selected_day.isoformat()
]
lessons.sort(
key=lambda lesson: (
lesson.get("номерЗанятия", 0),
lesson.get("начало", "")
)
)
days = (
"Понедельник",
"Вторник",
"Среда",
"Четверг",
"Пятница",
"Суббота",
"Воскресенье"
)
text = (
"╔════════════════════╗\n"
" 📅 <b>РАСПИСАНИЕ</b>\n"
"╚════════════════════╝\n\n"
f"🎓 <b>{group['groupName']}</b>\n"
f"📆 <b>{days[selected_day.weekday()]}, "
f"{selected_day.strftime('%d.%m.%Y')}</b>\n\n"
)
if not lessons:
return text + "🎉 На этот день занятий нет."
separator = "━━━━━━━━━━━━━━━━━━━━\n"
for lesson in lessons:
text += separator
number = lesson.get("номерЗанятия", "-")
text += (
f"🟧 <b>{number} занятие</b>\n"
f"🕐 <b>{lesson.get('начало', '?')}"
f"{lesson.get('конец', '?')}</b>\n\n"
)
text += (
f"📚 <b>{lesson.get('дисциплина', '-')}</b>\n"
)
teacher = lesson.get("преподаватель")
if teacher:
text += f"👨‍🏫 {teacher}\n"
aud = lesson.get("аудитория")
if aud:
text += f"🏫 Аудитория: {aud}\n"
subgroup = lesson.get("номерПодгруппы")
if subgroup:
text += f"👥 Подгруппа: {subgroup}\n"
text += "\n"
return text
async def send_schedule(
message: Message,
group: dict,
selected_day: date
):
schedule = await api.get_schedule(
group["groupID"],
selected_day
)
text = build_schedule(
schedule,
group,
selected_day
)
LIMIT = 4000
if len(text) <= LIMIT:
await message.answer(
text,
reply_markup=build_keyboard(
group["groupID"],
selected_day
)
)
return
parts = []
current = ""
for line in text.split("\n"):
if len(current) + len(line) + 1 > LIMIT:
parts.append(current)
current = ""
current += line + "\n"
if current:
parts.append(current)
for i, part in enumerate(parts):
await message.answer(
part,
reply_markup=(
build_keyboard(
group["groupID"],
selected_day
)
if i == len(parts) - 1
else None
)
)
@router.message(Command("find_group"))
@router.message(
F.text.in_(
{
"🔍 Выбрать группу",
"🔍 Сменить группу"
}
)
)
async def find_group(message: Message):
await message.answer(
"📚 <b>Выберите свою группу</b>",
reply_markup=await build_groups_keyboard(0)
)
@router.message(
GroupState.waiting_group
)
async def search_group(
message: Message,
state: FSMContext
):
query = message.text.strip()
groups = await api.search_group_by_name(query)
if not groups:
await message.answer(
"❌ Группа не найдена.\nПопробуйте ещё раз."
)
return
if len(groups) == 1:
group = groups[0]
await message.answer(
"✅ Группа сохранена.",
reply_markup=main_keyboard()
)
await state.clear()
await message.answer(
f"✅ Группа <b>{group['groupName']}</b> сохранена."
)
await message.answer(
"📅 Выберите дату:",
reply_markup=date_keyboard()
)
return
kb = InlineKeyboardBuilder()
for group in groups[:10]:
kb.button(
text=f"{group['groupName']} (курс {group['course']})",
callback_data=f"select:{group['groupID']}"
)
kb.adjust(1)
await message.answer(
"Выберите группу:",
reply_markup=kb.as_markup()
)
async def search_group(message: Message):
query = message.text.strip()
groups = await api.search_group_by_name(query)
if not groups:
await message.answer(
"❌ Группа не найдена."
)
return
if len(groups) == 1:
group = groups[0]
await save_group(
message.from_user.id,
group["groupID"],
group["groupName"]
)
await message.answer(
f"✅ Группа <b>{group['groupName']}</b> сохранена."
)
await send_schedule(
message,
group,
date.today()
)
return
kb = InlineKeyboardBuilder()
for group in groups[:10]:
kb.button(
text=f"{group['groupName']} (курс {group['course']})",
callback_data=f"select:{group['groupID']}"
)
kb.adjust(1)
await message.answer(
"Найдено несколько групп.\nВыберите нужную:",
reply_markup=kb.as_markup()
)
# ==========================
# ВЫБОР ГРУППЫ
# ==========================
@router.callback_query(
F.data.startswith("select:")
)
async def select_group(
callback: CallbackQuery,
state: FSMContext
):
group_id = int(
callback.data.split(":")[1]
)
group = await api.get_group_by_id(
group_id
)
if group is None:
await callback.answer(
"Группа не найдена.",
show_alert=True
)
return
await save_group(
callback.from_user.id,
group["groupID"],
group["groupName"]
)
await state.clear()
await callback.message.edit_text(
"🎓 <b>Группа сохранена!</b>\n\n"
"Теперь выберите свою подгруппу:",
reply_markup=subgroup_keyboard()
)
await callback.answer()
@router.callback_query(F.data.startswith("groups_page:"))
async def groups_page(callback: CallbackQuery):
page = int(callback.data.split(":")[1])
await callback.message.edit_reply_markup(
reply_markup=await build_groups_keyboard(page)
)
await callback.answer()
@router.message(
F.text.regexp(r"^\d{1,2}[./]\d{1,2}([./]\d{2,4})?$")
)
async def schedule_by_date(message: Message):
saved_group = await get_group(
message.from_user.id
)
if not saved_group:
await message.answer(
"❌ Сначала выберите группу."
)
return
text = message.text.strip().replace("/", ".")
try:
parts = text.split(".")
if len(parts) == 2:
selected_date = date(
year=date.today().year,
month=int(parts[1]),
day=int(parts[0]),
)
else:
year = int(parts[2])
if year < 100:
year += 2000
selected_date = date(
year=year,
month=int(parts[1]),
day=int(parts[0]),
)
except ValueError:
await message.answer(
"❌ Некорректная дата."
)
return
group = await api.get_group_by_id(
saved_group["group_id"]
)
if group is None:
await message.answer(
"❌ Группа не найдена."
)
return
await send_schedule(
message,
group,
selected_date
)
# ==========================
# ПУСТАЯ КНОПКА
# ==========================
@router.callback_query(F.data == "ignore")
async def ignore_callback(callback: CallbackQuery):
await callback.answer()
# ==========================
# ПЕРЕКЛЮЧЕНИЕ ДНЕЙ
# ==========================
@router.callback_query(F.data.startswith("day:"))
async def change_day(callback: CallbackQuery):
try:
_, group_id, day = callback.data.split(":")
selected_date = date.fromisoformat(day)
group = await api.get_group_by_id(
int(group_id)
)
if group is None:
await callback.answer(
"Группа не найдена.",
show_alert=True
)
return
schedule = await api.get_schedule(
group["groupID"],
selected_date
)
text = build_schedule(
schedule,
group,
selected_date
)
if len(text) > 4096:
await callback.answer(
"Расписание слишком большое. Используйте ввод даты.",
show_alert=True
)
return
await callback.message.edit_text(
text,
reply_markup=build_keyboard(
group["groupID"],
selected_date
)
)
await callback.answer()
except Exception as e:
await callback.answer(
str(e),
show_alert=True
)
@router.callback_query(F.data == "date:today")
async def date_today(callback: CallbackQuery):
saved = await get_group(callback.from_user.id)
if not saved:
await callback.answer("Сначала выберите группу")
return
group = await api.get_group_by_id(saved["group_id"])
schedule = await api.get_schedule(
group["groupID"],
date.today()
)
text = build_schedule(
schedule,
group,
date.today()
)
await callback.message.edit_text(
text,
reply_markup=build_keyboard(
group["groupID"],
date.today()
)
)
@router.callback_query(F.data == "date:tomorrow")
async def date_tomorrow(callback: CallbackQuery):
tomorrow = date.today() + timedelta(days=1)
saved_group = await get_group(callback.from_user.id)
if not saved_group:
await callback.answer("Сначала выберите группу.", show_alert=True)
return
group = await api.get_group_by_id(saved_group["group_id"])
await callback.message.edit_text(
build_schedule(
await api.get_schedule(group["groupID"], tomorrow),
group,
tomorrow
),
reply_markup=build_keyboard(group["groupID"], tomorrow)
)
await callback.answer()
@router.callback_query(F.data == "date:input")
async def input_date(callback: CallbackQuery):
await callback.message.answer(
"Введите дату в формате:\n\n"
"01.09.2026"
)
await callback.answer()
@router.callback_query(F.data == "date:calendar")
async def open_calendar(callback: CallbackQuery):
today = date.today()
await callback.message.edit_reply_markup(
reply_markup=calendar_keyboard(
today.year,
today.month
)
)
await callback.answer()
@router.callback_query(F.data.startswith("month:"))
async def change_month(callback: CallbackQuery):
_, year, month = callback.data.split(":")
await callback.message.edit_reply_markup(
reply_markup=calendar_keyboard(
int(year),
int(month)
)
)
await callback.answer()
@router.callback_query(F.data.startswith("calendar:"))
async def calendar_select(callback: CallbackQuery):
_, year, month, day = callback.data.split(":")
selected_day = date(
int(year),
int(month),
int(day)
)
saved_group = await get_group(
callback.from_user.id
)
if not saved_group:
await callback.answer(
"Сначала выберите группу.",
show_alert=True
)
return
group = await api.get_group_by_id(
saved_group["group_id"]
)
schedule = await api.get_schedule(
group["groupID"],
selected_day
)
text = build_schedule(
schedule,
group,
selected_day
)
await callback.message.edit_text(
text,
reply_markup=build_keyboard(
group["groupID"],
selected_day
)
)
await callback.answer()
@router.callback_query(F.data == "calendar_today")
async def calendar_today(callback: CallbackQuery):
today = date.today()
saved_group = await get_group(
callback.from_user.id
)
if not saved_group:
return
group = await api.get_group_by_id(
saved_group["group_id"]
)
schedule = await api.get_schedule(
group["groupID"],
today
)
text = build_schedule(
schedule,
group,
today
)
await callback.message.edit_text(
text,
reply_markup=build_keyboard(
group["groupID"],
today
)
)
await callback.answer()
@router.message(F.text == "📅 Расписание")
async def schedule_menu(message: Message):
await message.answer(
"📅 Выберите дату:",
reply_markup=date_keyboard()
)
@router.message(F.text == "🔄 Группа")
async def change_group(message: Message):
await message.answer(
"Введите номер группы:",
reply_markup=start_keyboard()
)
@router.message(F.text == "⚙️ Моя группа")
async def my_group(message: Message):
saved_group = await get_group(message.from_user.id)
if not saved_group:
await message.answer(
"❌ У вас ещё не выбрана группа.\n\nНажмите «🔍 Выбрать группу»."
)
return
await message.answer(
f"🎓 Ваша группа: <b>{saved_group['group_name']}</b>"
)
@router.callback_query(F.data.startswith("sub:"))
async def select_subgroup(callback: CallbackQuery):
subgroup = callback.data.split(":")[1]
await update_subgroup(
callback.from_user.id,
subgroup
)
await callback.answer(
"Подгруппа сохранена!"
)
await callback.message.edit_text(
f"✅ Подгруппа <b>{subgroup}</b> сохранена!"
)
await callback.message.answer(
"Выберите действие:",
reply_markup=main_keyboard()
)
+77
View File
@@ -0,0 +1,77 @@
from aiogram import Router, F
from aiogram.filters import CommandStart, Command
from aiogram.types import Message
from database import get_group
from keyboards import main_keyboard, start_keyboard
from database import (
get_group,
update_user
)
from database import save_phone
router = Router()
@router.message(CommandStart())
async def start(message: Message):
await update_user(
user_id=message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
group = await get_group(message.from_user.id)
if group:
text = (
f"👋 Добро пожаловать!\n\n"
f"Ваша группа:\n"
f"<b>{group['group_name']}</b>\n\n"
"Выберите действие ниже."
)
await message.answer(
text,
reply_markup=main_keyboard()
)
else:
text = (
"👋 Добро пожаловать!\n\n"
"Похоже, вы еще не выбрали группу.\n\n"
"Нажмите <b>🔍 Выбрать группу</b> "
)
await message.answer(
text,
reply_markup=start_keyboard()
)
@router.message(Command("help"))
async def help_command(message: Message):
await message.answer(
"📖 <b>Справка</b>\n\n"
"🔍 Выберите группу.\n"
"📅 Получайте расписание на любой день.\n"
"💾 После выбора группа сохраняется автоматически.\n"
)
@router.message(F.contact)
async def get_phone(
message: Message
):
await save_phone(
message.from_user.id,
message.contact.phone_number
)
await message.answer(
"✅ Телефон сохранён."
)
+82
View File
@@ -0,0 +1,82 @@
from aiogram import Router, F
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from states import SupportState
from database import create_ticket
from config import ADMIN_IDS
from aiogram import Bot
from keyboards import ticket_keyboard
router = Router()
# пользователь нажал кнопку поддержки
@router.message(F.text == "✉️ Поддержка")
async def support_start(
message: Message,
state: FSMContext
):
await state.set_state(
SupportState.waiting_message
)
await message.answer(
"✉️ Опишите проблему.\n\n"
"Напишите сообщение, и оно будет отправлено разработчику."
)
# пользователь написал проблему --> соснул хуйца, я не буду фиксить
@router.message(
SupportState.waiting_message
)
@router.message(
SupportState.waiting_message
)
async def send_ticket(
message: Message,
state: FSMContext,
bot: Bot
):
ticket_id = await create_ticket(
message.from_user.id,
message.text
)
# уведомляем админов
for admin_id in ADMIN_IDS:
try:
await bot.send_message(
admin_id,
(
"📩 <b>Новое обращение!</b>\n\n"
f"🆔 Тикет: #{ticket_id}\n"
f"👤 Пользователь: "
f"<code>{message.from_user.id}</code>\n\n"
f"📝 Сообщение:\n"
f"{message.text}"
),
reply_markup=ticket_keyboard(ticket_id)
)
except Exception as e:
print(
f"Ошибка отправки админу {admin_id}: {e}"
)
await state.clear()
await message.answer(
f"✅ Обращение #{ticket_id} отправлено.\n\n"
"Спасибо за обратную связь!"
)
+229
View File
@@ -0,0 +1,229 @@
from aiogram.types import (
ReplyKeyboardMarkup,
KeyboardButton,
InlineKeyboardMarkup,
InlineKeyboardButton,
)
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
import calendar
from datetime import date
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
def main_keyboard():
return ReplyKeyboardMarkup(
keyboard=[
[
KeyboardButton(text="📅 Расписание"),
KeyboardButton(text="🔍 Сменить группу")
],
[
KeyboardButton(text="✉️ Поддержка")
]
],
resize_keyboard=True
)
def start_keyboard():
return ReplyKeyboardMarkup(
keyboard=[
[
KeyboardButton(text="🔍 Выбрать группу")
]
],
resize_keyboard=True
)
def date_keyboard():
kb = InlineKeyboardBuilder()
kb.button(
text="📅 Сегодня",
callback_data="date:today"
)
kb.button(
text="🗓 Календарь",
callback_data="date:calendar"
)
kb.adjust(2)
return kb.as_markup()
def groups_keyboard(groups):
keyboard = []
for group in groups:
keyboard.append(
[
InlineKeyboardButton(
text=f"{group['groupName']} (курс {group['course']})",
callback_data=f"select_group:{group['groupID']}"
)
]
)
return InlineKeyboardMarkup(
inline_keyboard=keyboard
)
def calendar_keyboard(year: int, month: int):
kb = []
months = [
"",
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь",
]
kb.append([
InlineKeyboardButton(
text=f"{months[month]} {year}",
callback_data="ignore"
)
])
kb.append([
InlineKeyboardButton(text="Пн", callback_data="ignore"),
InlineKeyboardButton(text="Вт", callback_data="ignore"),
InlineKeyboardButton(text="Ср", callback_data="ignore"),
InlineKeyboardButton(text="Чт", callback_data="ignore"),
InlineKeyboardButton(text="Пт", callback_data="ignore"),
InlineKeyboardButton(text="Сб", callback_data="ignore"),
InlineKeyboardButton(text="Вс", callback_data="ignore"),
])
cal = calendar.monthcalendar(year, month)
for week in cal:
row = []
for day in week:
if day == 0:
row.append(
InlineKeyboardButton(
text=" ",
callback_data="ignore"
)
)
else:
row.append(
InlineKeyboardButton(
text=str(day),
callback_data=f"calendar:{year}:{month}:{day}"
)
)
kb.append(row)
prev_month = month - 1
prev_year = year
if prev_month == 0:
prev_month = 12
prev_year -= 1
next_month = month + 1
next_year = year
if next_month == 13:
next_month = 1
next_year += 1
kb.append([
InlineKeyboardButton(
text="⬅️",
callback_data=f"month:{prev_year}:{prev_month}"
),
InlineKeyboardButton(
text="🏠 Сегодня",
callback_data="calendar_today"
),
InlineKeyboardButton(
text="➡️",
callback_data=f"month:{next_year}:{next_month}"
),
])
return InlineKeyboardMarkup(
inline_keyboard=kb
)
def subgroup_keyboard():
kb = InlineKeyboardBuilder()
kb.button(text="Общая", callback_data="sub:0")
for i in range(1, 5):
kb.button(text=f"{i}А", callback_data=f"sub:{i}А")
kb.button(text=f"{i}Б", callback_data=f"sub:{i}Б")
kb.adjust(1, 2, 2, 2, 2)
return kb.as_markup()
def support_keyboard():
return ReplyKeyboardMarkup(
keyboard=[
[
KeyboardButton(
text="✉️ Поддержка"
)
]
],
resize_keyboard=True
)
def ticket_keyboard(ticket_id: int):
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="💬 Ответить",
callback_data=f"reply_ticket:{ticket_id}"
),
InlineKeyboardButton(
text="✅ Закрыть",
callback_data=f"close_ticket:{ticket_id}"
)
]
]
)
def phone_keyboard():
return ReplyKeyboardMarkup(
keyboard=[
[
KeyboardButton(
text="📱 Отправить телефон",
request_contact=True
)
]
],
resize_keyboard=True,
one_time_keyboard=True
)
+12
View File
@@ -0,0 +1,12 @@
import logging
logging.basicConfig(
filename="logs/bot.log",
level=logging.INFO,
format=
"%(asctime)s | %(message)s"
)
logger = logging.getLogger()
+128
View File
@@ -0,0 +1,128 @@
2026-07-09 22:53:52,570 | База данных инициализирована
2026-07-09 22:53:52,570 | Бот запущен
2026-07-09 22:53:52,571 | Start polling
2026-07-09 22:53:52,854 | Run polling for bot @ural_colleges_bot id=8809518082 - 'УЭК'
2026-07-09 22:53:55,054 | Update id=210321910 is handled. Duration 712 ms by bot id=8809518082
2026-07-09 22:54:00,942 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Groups "HTTP/1.1 200 OK"
2026-07-09 22:54:01,106 | Update id=210321911 is handled. Duration 1852 ms by bot id=8809518082
2026-07-09 22:54:03,001 | Update id=210321912 is handled. Duration 439 ms by bot id=8809518082
2026-07-09 22:54:04,262 | Update id=210321913 is handled. Duration 356 ms by bot id=8809518082
2026-07-09 22:54:06,227 | Update id=210321914 is handled. Duration 107 ms by bot id=8809518082
2026-07-09 22:54:07,597 | Update id=210321915 is handled. Duration 215 ms by bot id=8809518082
2026-07-09 22:54:09,584 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12679&sdate=2026-07-02 "HTTP/1.1 200 OK"
2026-07-09 22:54:09,812 | Update id=210321916 is handled. Duration 891 ms by bot id=8809518082
2026-07-09 22:54:11,908 | Update id=210321917 is handled. Duration 110 ms by bot id=8809518082
2026-07-09 22:54:13,406 | Update id=210321918 is handled. Duration 117 ms by bot id=8809518082
2026-07-09 22:54:15,273 | Update id=210321919 is handled. Duration 228 ms by bot id=8809518082
2026-07-09 22:54:16,785 | Update id=210321920 is handled. Duration 212 ms by bot id=8809518082
2026-07-09 22:54:17,569 | Update id=210321921 is handled. Duration 210 ms by bot id=8809518082
2026-07-09 22:54:18,463 | Update id=210321922 is handled. Duration 210 ms by bot id=8809518082
2026-07-09 22:54:19,173 | Update id=210321923 is handled. Duration 216 ms by bot id=8809518082
2026-07-09 22:54:23,103 | Update id=210321924 is handled. Duration 201 ms by bot id=8809518082
2026-07-09 22:54:24,779 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12679&sdate=2026-02-02 "HTTP/1.1 200 OK"
2026-07-09 22:54:24,983 | Update id=210321925 is handled. Duration 998 ms by bot id=8809518082
2026-07-09 22:54:33,062 | Update id=210321926 is handled. Duration 111 ms by bot id=8809518082
2026-07-09 22:55:26,983 | Received SIGINT signal
2026-07-09 22:55:26,984 | Polling stopped for bot @ural_colleges_bot id=8809518082 - 'УЭК'
2026-07-09 22:55:26,984 | Polling stopped
2026-07-09 22:55:27,235 | Бот остановлен
2026-07-09 23:03:50,570 | База данных инициализирована
2026-07-09 23:03:50,570 | Удалено 0 записей кэша
2026-07-09 23:03:50,570 | Фоновое обновление кэша запущено
2026-07-09 23:03:50,571 | Бот запущен
2026-07-09 23:03:50,571 | Start polling
2026-07-09 23:03:50,925 | Run polling for bot @ural_colleges_bot id=8809518082 - 'УЭК'
2026-07-09 23:03:56,560 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12679&sdate=2026-07-09 "HTTP/1.1 200 OK"
2026-07-09 23:03:56,785 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12679&sdate=2026-07-10 "HTTP/1.1 200 OK"
2026-07-09 23:03:59,421 | Update id=210321927 is not handled. Duration 2 ms by bot id=8809518082
2026-07-09 23:04:01,960 | Update id=210321928 is handled. Duration 578 ms by bot id=8809518082
2026-07-09 23:04:12,965 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Groups "HTTP/1.1 200 OK"
2026-07-09 23:04:13,099 | Update id=210321929 is handled. Duration 1131 ms by bot id=8809518082
2026-07-09 23:04:14,717 | Update id=210321930 is handled. Duration 244 ms by bot id=8809518082
2026-07-09 23:04:18,738 | Update id=210321931 is handled. Duration 342 ms by bot id=8809518082
2026-07-09 23:04:21,391 | Update id=210321932 is handled. Duration 125 ms by bot id=8809518082
2026-07-09 23:04:23,676 | Update id=210321933 is handled. Duration 211 ms by bot id=8809518082
2026-07-09 23:04:25,564 | Update id=210321934 is handled. Duration 251 ms by bot id=8809518082
2026-07-09 23:04:28,888 | Update id=210321935 is handled. Duration 218 ms by bot id=8809518082
2026-07-09 23:04:29,991 | Update id=210321936 is handled. Duration 309 ms by bot id=8809518082
2026-07-09 23:04:30,928 | Update id=210321937 is handled. Duration 262 ms by bot id=8809518082
2026-07-09 23:04:31,692 | Update id=210321938 is handled. Duration 269 ms by bot id=8809518082
2026-07-09 23:04:32,744 | Update id=210321939 is handled. Duration 304 ms by bot id=8809518082
2026-07-09 23:04:37,039 | Update id=210321940 is handled. Duration 291 ms by bot id=8809518082
2026-07-09 23:04:40,175 | Update id=210321941 is handled. Duration 400 ms by bot id=8809518082
2026-07-09 23:04:42,251 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12638&sdate=2025-11-07 "HTTP/1.1 200 OK"
2026-07-09 23:04:43,480 | Update id=210321942 is handled. Duration 2103 ms by bot id=8809518082
2026-07-09 23:09:58,630 | Received SIGINT signal
2026-07-09 23:09:58,631 | Polling stopped for bot @ural_colleges_bot id=8809518082 - 'УЭК'
2026-07-09 23:09:58,631 | Polling stopped
2026-07-09 23:09:58,882 | Бот остановлен
2026-07-09 23:11:18,096 | База данных инициализирована
2026-07-09 23:11:18,096 | Удалено 0 записей кэша
2026-07-09 23:11:18,096 | Фоновое обновление кэша запущено
2026-07-09 23:11:18,096 | Бот запущен
2026-07-09 23:11:18,096 | Start polling
2026-07-09 23:11:18,580 | Run polling for bot @ural_colleges_bot id=8809518082 - 'УЭК'
2026-07-09 23:11:19,151 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12638&sdate=2026-07-09 "HTTP/1.1 200 OK"
2026-07-09 23:11:19,341 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12638&sdate=2026-07-10 "HTTP/1.1 200 OK"
2026-07-09 23:11:19,899 | Update id=210321943 is handled. Duration 1197 ms by bot id=8809518082
2026-07-09 23:11:36,111 | Update id=210321944 is handled. Duration 298 ms by bot id=8809518082
2026-07-09 23:11:37,267 | Update id=210321945 is handled. Duration 93 ms by bot id=8809518082
2026-07-09 23:11:44,401 | Update id=210321946 is handled. Duration 144 ms by bot id=8809518082
2026-07-09 23:12:17,894 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Groups "HTTP/1.1 200 OK"
2026-07-09 23:12:18,487 | Update id=210321947 is handled. Duration 2905 ms by bot id=8809518082
2026-07-09 23:12:23,004 | Update id=210321948 is handled. Duration 254 ms by bot id=8809518082
2026-07-09 23:12:24,230 | Update id=210321949 is handled. Duration 240 ms by bot id=8809518082
2026-07-09 23:12:28,705 | Update id=210321950 is handled. Duration 210 ms by bot id=8809518082
2026-07-09 23:12:31,863 | Update id=210321951 is handled. Duration 347 ms by bot id=8809518082
2026-07-09 23:12:35,168 | Update id=210321952 is handled. Duration 116 ms by bot id=8809518082
2026-07-09 23:12:37,284 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-09 "HTTP/1.1 200 OK"
2026-07-09 23:12:37,410 | Update id=210321953 is handled. Duration 892 ms by bot id=8809518082
2026-07-09 23:12:39,908 | Update id=210321954 is handled. Duration 109 ms by bot id=8809518082
2026-07-09 23:12:40,110 | Update id=210321955 is handled. Duration 100 ms by bot id=8809518082
2026-07-09 23:12:41,239 | Update id=210321956 is handled. Duration 104 ms by bot id=8809518082
2026-07-09 23:12:42,118 | Update id=210321957 is handled. Duration 105 ms by bot id=8809518082
2026-07-09 23:12:42,834 | Update id=210321958 is handled. Duration 109 ms by bot id=8809518082
2026-07-09 23:12:43,753 | Update id=210321959 is handled. Duration 101 ms by bot id=8809518082
2026-07-09 23:12:44,576 | Update id=210321960 is handled. Duration 95 ms by bot id=8809518082
2026-07-09 23:12:46,112 | Update id=210321961 is handled. Duration 104 ms by bot id=8809518082
2026-07-09 23:12:50,923 | Update id=210321962 is handled. Duration 102 ms by bot id=8809518082
2026-07-09 23:12:56,554 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-08 "HTTP/1.1 200 OK"
2026-07-09 23:12:56,885 | Update id=210321963 is handled. Duration 1132 ms by bot id=8809518082
2026-07-09 23:12:58,757 | Update id=210321964 is handled. Duration 233 ms by bot id=8809518082
2026-07-09 23:12:59,798 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-07 "HTTP/1.1 200 OK"
2026-07-09 23:13:00,053 | Update id=210321965 is handled. Duration 426 ms by bot id=8809518082
2026-07-09 23:13:00,614 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-06 "HTTP/1.1 200 OK"
2026-07-09 23:13:00,844 | Update id=210321966 is handled. Duration 398 ms by bot id=8809518082
2026-07-09 23:13:01,290 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-05 "HTTP/1.1 200 OK"
2026-07-09 23:13:01,583 | Update id=210321967 is handled. Duration 464 ms by bot id=8809518082
2026-07-09 23:13:28,484 | Update id=210321968 is handled. Duration 440 ms by bot id=8809518082
2026-07-09 23:13:34,189 | Update id=210321969 is handled. Duration 348 ms by bot id=8809518082
2026-07-09 23:13:41,625 | Update id=210321970 is handled. Duration 215 ms by bot id=8809518082
2026-07-09 23:13:50,633 | Update id=210321971 is handled. Duration 234 ms by bot id=8809518082
2026-07-09 23:13:54,291 | Update id=210321972 is handled. Duration 658 ms by bot id=8809518082
2026-07-09 23:13:56,382 | Update id=210321973 is handled. Duration 224 ms by bot id=8809518082
2026-07-09 23:13:57,276 | Update id=210321974 is handled. Duration 95 ms by bot id=8809518082
2026-07-09 23:13:58,843 | Update id=210321975 is handled. Duration 217 ms by bot id=8809518082
2026-07-09 23:14:00,451 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-04 "HTTP/1.1 200 OK"
2026-07-09 23:14:00,677 | Update id=210321976 is handled. Duration 1037 ms by bot id=8809518082
2026-07-09 23:14:02,721 | Update id=210321977 is handled. Duration 111 ms by bot id=8809518082
2026-07-09 23:14:04,556 | Update id=210321978 is handled. Duration 206 ms by bot id=8809518082
2026-07-09 23:14:16,271 | Update id=210321979 is handled. Duration 352 ms by bot id=8809518082
2026-07-09 23:14:20,890 | Update id=210321980 is handled. Duration 209 ms by bot id=8809518082
2026-07-09 23:14:25,022 | Update id=210321981 is handled. Duration 87 ms by bot id=8809518082
2026-07-09 23:14:26,404 | Update id=210321982 is handled. Duration 319 ms by bot id=8809518082
2026-07-09 23:14:32,718 | Update id=210321983 is not handled. Duration 2 ms by bot id=8809518082
2026-07-09 23:14:41,187 | Received SIGINT signal
2026-07-09 23:14:41,188 | Polling stopped for bot @ural_colleges_bot id=8809518082 - 'УЭК'
2026-07-09 23:14:41,189 | Polling stopped
2026-07-09 23:14:41,441 | Бот остановлен
2026-07-09 23:27:37,784 | База данных инициализирована
2026-07-09 23:27:37,784 | Удалено 0 записей кэша
2026-07-09 23:27:37,784 | Фоновое обновление кэша запущено
2026-07-09 23:27:37,784 | Бот запущен
2026-07-09 23:27:37,784 | Start polling
2026-07-09 23:27:38,246 | Run polling for bot @ural_colleges_bot id=8809518082 - 'УЭК'
2026-07-09 23:27:49,524 | Received SIGINT signal
2026-07-09 23:27:49,525 | Polling stopped for bot @ural_colleges_bot id=8809518082 - 'УЭК'
2026-07-09 23:27:49,525 | Polling stopped
2026-07-09 23:27:49,779 | Бот остановлен
+27
View File
@@ -0,0 +1,27 @@
aiofiles==24.1.0
aiogram==3.13.1
aiohappyeyeballs==2.7.1
aiohttp==3.10.11
aiosignal==1.4.0
aiosqlite==0.20.0
annotated-types==0.7.0
anyio==4.14.1
attrs==26.1.0
cachetools==5.5.0
certifi==2026.6.17
frozenlist==1.8.0
h11==0.16.0
httpcore==1.0.9
httpx==0.27.2
idna==3.18
magic-filter==1.0.12
multidict==6.7.1
propcache==0.5.2
pydantic==2.9.2
pydantic_core==2.23.4
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
six==1.17.0
sniffio==1.3.1
typing_extensions==4.16.0
yarl==1.24.2
+41
View File
@@ -0,0 +1,41 @@
import asyncio
from datetime import date, timedelta
from api import api
from database import get_active_groups
async def refresh_cache():
while True:
groups = await get_active_groups()
if groups:
print(f"[CACHE] Обновляем {len(groups)} групп")
for group_id in groups:
try:
# сегодня
await api.get_schedule(
group_id,
date.today()
)
# завтра
await api.get_schedule(
group_id,
date.today() + timedelta(days=1)
)
except Exception as e:
print(
f"[CACHE] Ошибка {group_id}: {e}"
)
await asyncio.sleep(1800)
+13
View File
@@ -0,0 +1,13 @@
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.state import StatesGroup, State
class SupportState(StatesGroup):
waiting_message = State()
class GroupState(StatesGroup):
waiting_group = State()
class AdminState(StatesGroup):
waiting_reply = State()