initial
This commit is contained in:
+14
@@ -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.
@@ -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()
|
||||||
@@ -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("Остановка...")
|
||||||
|
|
||||||
@@ -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")
|
||||||
Binary file not shown.
+501
@@ -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.
@@ -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
|
||||||
@@ -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
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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(
|
||||||
|
"Тикет закрыт"
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
|
)
|
||||||
@@ -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(
|
||||||
|
"✅ Телефон сохранён."
|
||||||
|
)
|
||||||
@@ -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
@@ -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
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
filename="logs/bot.log",
|
||||||
|
level=logging.INFO,
|
||||||
|
format=
|
||||||
|
"%(asctime)s | %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
+128
@@ -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 | Бот остановлен
|
||||||
@@ -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
@@ -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)
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user