Добавлено: загрузка расписания
This commit is contained in:
parent
c1a06a834b
commit
28cd1673ac
40
bot.py
40
bot.py
|
@ -55,6 +55,19 @@ class Bot:
|
||||||
except telegram.error.BadRequest:
|
except telegram.error.BadRequest:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def edit(self, query: telegram.CallbackQuery, text=None, markup=None):
|
||||||
|
if isinstance(text, str):
|
||||||
|
try:
|
||||||
|
query.edit_message_text(text)
|
||||||
|
except telegram.error.BadRequest:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if isinstance(markup, telegram.ReplyKeyboardMarkup):
|
||||||
|
try:
|
||||||
|
query.edit_message_reply_markup(text)
|
||||||
|
except telegram.error.BadRequest:
|
||||||
|
pass
|
||||||
|
|
||||||
def checkMessages(self):
|
def checkMessages(self):
|
||||||
"""Проверка и обработка входящих сообщений"""
|
"""Проверка и обработка входящих сообщений"""
|
||||||
|
|
||||||
|
@ -69,13 +82,7 @@ class Bot:
|
||||||
tgId = query.from_user.id
|
tgId = query.from_user.id
|
||||||
|
|
||||||
if 'conf' in tag:
|
if 'conf' in tag:
|
||||||
success = self.shedule.uploadShedule(query, tag[5:], loc)
|
self.confirmGroup(query, tag)
|
||||||
self.answer(query)
|
|
||||||
if success:
|
|
||||||
# Код с загрузкой расписания
|
|
||||||
self.tg_db.changeTag(tgId, 'ready')
|
|
||||||
else:
|
|
||||||
self.tg_db.changeTag(tgId, 'add')
|
|
||||||
|
|
||||||
if update.message:
|
if update.message:
|
||||||
query = update.message
|
query = update.message
|
||||||
|
@ -86,10 +93,10 @@ class Bot:
|
||||||
if tag == 'not_started':
|
if tag == 'not_started':
|
||||||
self.start(query)
|
self.start(query)
|
||||||
|
|
||||||
if tag == 'add':
|
elif tag == 'add':
|
||||||
self.addGroup(l9Id, query)
|
self.addGroup(l9Id, query)
|
||||||
|
|
||||||
if query.text == 'Отмена':
|
elif query.text == 'Отмена':
|
||||||
# TODO: прописать отмену при отсутствующих группах
|
# TODO: прописать отмену при отсутствующих группах
|
||||||
self.tg_db.changeTag(tgId, 'ready')
|
self.tg_db.changeTag(tgId, 'ready')
|
||||||
self.tg.sendMessage(
|
self.tg.sendMessage(
|
||||||
|
@ -170,6 +177,21 @@ class Bot:
|
||||||
reply_markup=Keyboard.cancel(),
|
reply_markup=Keyboard.cancel(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def confirmGroup(self, query: telegram.CallbackQuery, tag: str):
|
||||||
|
"""Процесс подтверждения группы и загрузка расписания"""
|
||||||
|
tgId = query.from_user.id
|
||||||
|
self.answer(query)
|
||||||
|
if query.data == 'yes':
|
||||||
|
self.edit(query, loc['group']['loading'])
|
||||||
|
self.shedule.loadShedule(
|
||||||
|
tag[5:], query.message.date, config['first_week']
|
||||||
|
)
|
||||||
|
self.edit(query, loc['group']['loaded'], Keyboard.menu())
|
||||||
|
self.tg_db.changeTag(tgId, 'ready')
|
||||||
|
else:
|
||||||
|
self.edit(query, loc['group']['nogroup'], Keyboard.cancel())
|
||||||
|
self.tg_db.changeTag(tgId, 'add')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
initLogger()
|
initLogger()
|
||||||
|
|
|
@ -3,9 +3,13 @@ from bs4 import BeautifulSoup
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
import datetime
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
logger = logging.getLogger('bot')
|
logger = logging.getLogger('bot')
|
||||||
|
|
||||||
def findInRasp(req) -> dict:
|
|
||||||
|
def findInRasp(req: str):
|
||||||
"""Поиск группы (преподавателя) в расписании"""
|
"""Поиск группы (преподавателя) в расписании"""
|
||||||
logger.debug(f'Find {req}')
|
logger.debug(f'Find {req}')
|
||||||
|
|
||||||
|
@ -22,7 +26,7 @@ def findInRasp(req) -> dict:
|
||||||
|
|
||||||
rasp.headers['Accept'] = 'application/json'
|
rasp.headers['Accept'] = 'application/json'
|
||||||
rasp.headers['X-CSRF-TOKEN'] = csrf_token
|
rasp.headers['X-CSRF-TOKEN'] = csrf_token
|
||||||
result = rasp.post("https://ssau.ru/rasp/search", data = {'text':req})
|
result = rasp.post("https://ssau.ru/rasp/search", data={'text': req})
|
||||||
if result.status_code == 200:
|
if result.status_code == 200:
|
||||||
num = literal_eval(result.text)
|
num = literal_eval(result.text)
|
||||||
else:
|
else:
|
||||||
|
@ -32,3 +36,163 @@ def findInRasp(req) -> dict:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return num[0]
|
return num[0]
|
||||||
|
|
||||||
|
|
||||||
|
def connect(groupId: str, week: int, reconnects=0) -> BeautifulSoup:
|
||||||
|
"""Подключение к сайту с расписанием"""
|
||||||
|
logger.debug(
|
||||||
|
f'Connecting to sasau, groupId = {groupId}, week N {week}, attempt {reconnects}'
|
||||||
|
)
|
||||||
|
rasp = requests.Session()
|
||||||
|
rasp.headers['User-Agent'] = 'Mozilla/5.0'
|
||||||
|
site = rasp.get(
|
||||||
|
f'https://ssau.ru/rasp?groupId={groupId}&selectedWeek={week}'
|
||||||
|
)
|
||||||
|
if site.status_code == 200:
|
||||||
|
contents = site.text.replace("\n", " ")
|
||||||
|
soup = BeautifulSoup(contents, 'html.parser')
|
||||||
|
return soup
|
||||||
|
elif reconnects < 5:
|
||||||
|
time.sleep(2)
|
||||||
|
return connect(groupId, week, reconnects + 1)
|
||||||
|
else:
|
||||||
|
raise 'Connection to sasau failed!'
|
||||||
|
|
||||||
|
|
||||||
|
def getGroupInfo(groupId: str) -> dict:
|
||||||
|
"""Получение информации о группе (ID, полный номер, название направления)"""
|
||||||
|
logger.debug(f'Getting group {groupId} information')
|
||||||
|
soup = connect(groupId, 1)
|
||||||
|
|
||||||
|
group_spec_soup = soup.find(
|
||||||
|
"div", {"class": "body-text info-block__description"}
|
||||||
|
)
|
||||||
|
group_spec = group_spec_soup.find("div").contents[0].text[1:]
|
||||||
|
|
||||||
|
group_name_soup = soup.find("h2", {"class": "h2-text info-block__title"})
|
||||||
|
group_name = group_name_soup.text[1:5]
|
||||||
|
|
||||||
|
info = {
|
||||||
|
'groupId': groupId,
|
||||||
|
'groupName': group_name,
|
||||||
|
'specName': group_spec,
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
lesson_types = ('lect', 'lab', 'pract', 'other')
|
||||||
|
teacher_columns = ('surname', 'name', 'midname', 'teacherId')
|
||||||
|
|
||||||
|
|
||||||
|
def parseWeek(groupId: str, week: int, teachers=[]):
|
||||||
|
|
||||||
|
soup = connect(groupId, week)
|
||||||
|
|
||||||
|
dates_soup = soup.find_all("div", {"class": "schedule__head-date"})
|
||||||
|
dates = []
|
||||||
|
for date in dates_soup:
|
||||||
|
date = datetime.datetime.strptime(
|
||||||
|
date.contents[0].text, ' %d.%m.%Y'
|
||||||
|
).date()
|
||||||
|
dates.append(date)
|
||||||
|
|
||||||
|
blocks = soup.find("div", {"class": "schedule__items"})
|
||||||
|
|
||||||
|
blocks = [
|
||||||
|
item
|
||||||
|
for item in blocks
|
||||||
|
if "schedule__head" not in item.attrs["class"]
|
||||||
|
]
|
||||||
|
|
||||||
|
numInDay = 0
|
||||||
|
weekday = 0
|
||||||
|
times = []
|
||||||
|
shedule = []
|
||||||
|
week = []
|
||||||
|
for block in blocks:
|
||||||
|
if block.attrs['class'] == ['schedule__time']:
|
||||||
|
begin = datetime.datetime.strptime(
|
||||||
|
block.contents[0].text, ' %H:%M '
|
||||||
|
).time()
|
||||||
|
end = datetime.datetime.strptime(
|
||||||
|
block.contents[1].text, ' %H:%M '
|
||||||
|
).time()
|
||||||
|
times.append((begin, end))
|
||||||
|
numInDay += 1
|
||||||
|
weekday = 0
|
||||||
|
|
||||||
|
if numInDay != 1:
|
||||||
|
week = []
|
||||||
|
else:
|
||||||
|
begin_dt = datetime.datetime.combine(dates[weekday], begin)
|
||||||
|
end_dt = datetime.datetime.combine(dates[weekday], end)
|
||||||
|
|
||||||
|
sub_pairs = block.find_all("div", {"class": "schedule__lesson"})
|
||||||
|
|
||||||
|
pair = []
|
||||||
|
for sub_pair in sub_pairs:
|
||||||
|
if sub_pair != []:
|
||||||
|
name = sub_pair.select_one('div.schedule__discipline')
|
||||||
|
lesson_type = lesson_types[
|
||||||
|
int(name['class'][-1][-1]) - 1
|
||||||
|
]
|
||||||
|
name = name.text
|
||||||
|
|
||||||
|
place = sub_pair.select_one('div.schedule__place').text
|
||||||
|
place = place if "on" not in place.lower() else "ONLINE"
|
||||||
|
place = place if place != "" else None
|
||||||
|
|
||||||
|
teacher = sub_pair.select_one('.schedule__teacher a')
|
||||||
|
teacherId = (
|
||||||
|
teacher['href'][14:] if teacher != None else None
|
||||||
|
)
|
||||||
|
if teacher != None:
|
||||||
|
if teacherId not in [
|
||||||
|
str(i['teacherId']) for i in teachers
|
||||||
|
]:
|
||||||
|
teacher_name = teacher.text[:-4]
|
||||||
|
t_info = findInRasp(teacher_name)['text'].split()
|
||||||
|
t_info.append(teacherId)
|
||||||
|
teachers.append(
|
||||||
|
dict(zip(teacher_columns, t_info))
|
||||||
|
)
|
||||||
|
|
||||||
|
groups = sub_pair.select_one('div.schedule__groups').text
|
||||||
|
groups = "\n" + groups if 'групп' in groups else ""
|
||||||
|
|
||||||
|
comment = sub_pair.select_one(
|
||||||
|
'div.schedule__comment'
|
||||||
|
).text
|
||||||
|
comment = comment if comment != "" else None
|
||||||
|
|
||||||
|
full_name = f'{name}{groups}'
|
||||||
|
|
||||||
|
lesson = {
|
||||||
|
'numInDay': numInDay,
|
||||||
|
'numInShedule': numInDay,
|
||||||
|
'type': lesson_type,
|
||||||
|
'name': full_name,
|
||||||
|
'groupId': groupId,
|
||||||
|
'begin': begin_dt,
|
||||||
|
'end': end_dt,
|
||||||
|
'teacherId': teacherId,
|
||||||
|
'place': place,
|
||||||
|
'addInfo': comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
shedule.append(lesson)
|
||||||
|
|
||||||
|
weekday += 1
|
||||||
|
|
||||||
|
shedule = sorted(shedule, key=lambda d: d['begin'])
|
||||||
|
new_shedule = []
|
||||||
|
|
||||||
|
# Correct numInDay
|
||||||
|
for date, day in groupby(shedule, key=lambda d: d['begin'].date()):
|
||||||
|
day = list(day)
|
||||||
|
first_num = day[0]['numInDay'] - 1
|
||||||
|
for l in day:
|
||||||
|
l['numInDay'] -= first_num
|
||||||
|
new_shedule.append(l)
|
||||||
|
return new_shedule, teachers
|
||||||
|
|
|
@ -2,6 +2,7 @@ from .l9 import L9_DB
|
||||||
from .a_ssau_parser import *
|
from .a_ssau_parser import *
|
||||||
import telegram
|
import telegram
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
class Shedule_DB:
|
class Shedule_DB:
|
||||||
|
@ -61,14 +62,22 @@ class Shedule_DB:
|
||||||
else:
|
else:
|
||||||
return 'Empty'
|
return 'Empty'
|
||||||
|
|
||||||
def uploadShedule(
|
def loadShedule(self, groupId, date, first_week):
|
||||||
self, query: telegram.CallbackQuery, groupId: str, loc: ConfigParser
|
week = date.isocalendar()[1] - first_week
|
||||||
):
|
|
||||||
if query.data == 'yes':
|
|
||||||
query.edit_message_text(loc['group']['loading'])
|
|
||||||
|
|
||||||
else:
|
self.db.execute(
|
||||||
query.edit_message_text(loc['group']['nogroup'])
|
f'DELETE FROM `lessons` WHERE WEEK(`begin`, 1) = {date.isocalendar()[1]} AND groupId = {groupId};'
|
||||||
query.edit_message_reply_markup(Keyboard.cancel())
|
)
|
||||||
|
|
||||||
return query.data == 'yes'
|
t_info = self.db.get('teachers', None, teacher_columns)
|
||||||
|
t_info = [dict(zip(teacher_columns, i)) for i in t_info]
|
||||||
|
lessons, teachers = parseWeek(groupId, week, t_info)
|
||||||
|
|
||||||
|
g = getGroupInfo(groupId)
|
||||||
|
self.db.insert('groups', g)
|
||||||
|
|
||||||
|
for t in teachers:
|
||||||
|
self.l9lk.db.insert('teachers', t)
|
||||||
|
|
||||||
|
for l in lessons:
|
||||||
|
self.l9lk.db.insert('lessons', l)
|
||||||
|
|
|
@ -43,3 +43,76 @@ CREATE TABLE IF NOT EXISTS `groups_users` (
|
||||||
CONSTRAINT `gr_gu` FOREIGN KEY (`groupId`) REFERENCES `groups` (`groupId`) ON DELETE CASCADE ON UPDATE CASCADE,
|
CONSTRAINT `gr_gu` FOREIGN KEY (`groupId`) REFERENCES `groups` (`groupId`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
CONSTRAINT `l9_gu` FOREIGN KEY (`l9Id`) REFERENCES `users` (`l9Id`) ON DELETE CASCADE ON UPDATE CASCADE
|
CONSTRAINT `l9_gu` FOREIGN KEY (`l9Id`) REFERENCES `users` (`l9Id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Преподаватели
|
||||||
|
CREATE TABLE IF NOT EXISTS `teachers` (
|
||||||
|
`teacherId` bigint NOT NULL,
|
||||||
|
-- Идентификатор преподавателя, соответствует ID на сайте
|
||||||
|
|
||||||
|
`surname` varchar(45) DEFAULT 'Brzęczyszczykiewicz',
|
||||||
|
`name` varchar(45) DEFAULT 'Grzegorz',
|
||||||
|
`midname` varchar(45) DEFAULT 'Chrząszczyżewoszywicz',
|
||||||
|
-- ФИО преподавателя
|
||||||
|
|
||||||
|
PRIMARY KEY (`teacherId`)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Занятия
|
||||||
|
CREATE TABLE IF NOT EXISTS `lessons` (
|
||||||
|
`lessonId` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
-- (service) Идентификатор занятия, устанавливается автоматически
|
||||||
|
|
||||||
|
`addedBy` varchar(4) DEFAULT 'ssau',
|
||||||
|
-- Источник информации о занятии:
|
||||||
|
-- 'ssau' - сайт Университета
|
||||||
|
-- 'lk' - Личный кабинет сайта Университета
|
||||||
|
-- '`l9Id`' - добавлено пользователем
|
||||||
|
|
||||||
|
`cancelled` bigint DEFAULT '0',
|
||||||
|
-- Отметка, является ли занятие отменённым
|
||||||
|
-- '0' - занятие НЕ отменено
|
||||||
|
-- '`l9Id`' - занятие отменено пользователем
|
||||||
|
|
||||||
|
`migrated` bigint DEFAULT '0',
|
||||||
|
-- Отметка, является ли занятие перенесённым
|
||||||
|
-- '0' - занятие НЕ перенесено
|
||||||
|
-- '`lessonId`' - занятие перенесено на другое время
|
||||||
|
|
||||||
|
`numInDay` int DEFAULT '1',
|
||||||
|
-- Порядковый номер занятия в текущем дне
|
||||||
|
|
||||||
|
`numInShedule` int DEFAULT '1',
|
||||||
|
-- Порядковый номер занятия относительно расписания на неделю
|
||||||
|
|
||||||
|
`type` char(5) DEFAULT 'other',
|
||||||
|
-- Тип занятия:
|
||||||
|
-- 'lect' - лекция
|
||||||
|
-- 'pract' - практика (семинар)
|
||||||
|
-- 'lab' - лабораторная работа
|
||||||
|
-- 'other' - прочие
|
||||||
|
|
||||||
|
`name` text,
|
||||||
|
-- Название занятия
|
||||||
|
|
||||||
|
`groupId` bigint NOT NULL,
|
||||||
|
-- ID учебной группы
|
||||||
|
|
||||||
|
`begin` datetime NOT NULL,
|
||||||
|
`end` datetime NOT NULL,
|
||||||
|
-- Начало и конец занятия
|
||||||
|
|
||||||
|
`teacherId` bigint DEFAULT NULL,
|
||||||
|
-- (опционально) ID преподавателя
|
||||||
|
|
||||||
|
`place` text,
|
||||||
|
-- (опционально) Учебная аудитория
|
||||||
|
|
||||||
|
`addInfo` text,
|
||||||
|
-- (опционально) Дополнительная информация
|
||||||
|
|
||||||
|
PRIMARY KEY (`lessonId`),
|
||||||
|
KEY `gr_l_idx` (`groupId`),
|
||||||
|
KEY `t_l_idx` (`teacherId`),
|
||||||
|
CONSTRAINT `group_l` FOREIGN KEY (`groupId`) REFERENCES `groups` (`groupId`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `teach_l` FOREIGN KEY (`teacherId`) REFERENCES `teachers` (`teacherId`) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
|
@ -26,4 +26,6 @@ empty=К сожалению, такой группы нет ни в моей б
|
||||||
|
|
||||||
loading=Загружаю расписание...
|
loading=Загружаю расписание...
|
||||||
|
|
||||||
|
loaded=Расписание загружено!
|
||||||
|
|
||||||
nogroup=Возможно, ты написал не ту группу, попробуй снова
|
nogroup=Возможно, ты написал не ту группу, попробуй снова
|
Reference in New Issue