diff --git a/bot.py b/bot.py index 3e344d9..c4a5f96 100644 --- a/bot.py +++ b/bot.py @@ -55,6 +55,19 @@ class Bot: except telegram.error.BadRequest: 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): """Проверка и обработка входящих сообщений""" @@ -69,13 +82,7 @@ class Bot: tgId = query.from_user.id if 'conf' in tag: - success = self.shedule.uploadShedule(query, tag[5:], loc) - self.answer(query) - if success: - # Код с загрузкой расписания - self.tg_db.changeTag(tgId, 'ready') - else: - self.tg_db.changeTag(tgId, 'add') + self.confirmGroup(query, tag) if update.message: query = update.message @@ -86,10 +93,10 @@ class Bot: if tag == 'not_started': self.start(query) - if tag == 'add': + elif tag == 'add': self.addGroup(l9Id, query) - if query.text == 'Отмена': + elif query.text == 'Отмена': # TODO: прописать отмену при отсутствующих группах self.tg_db.changeTag(tgId, 'ready') self.tg.sendMessage( @@ -170,6 +177,21 @@ class Bot: 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__": initLogger() diff --git a/database/a_ssau_parser.py b/database/a_ssau_parser.py index f17b26f..df5b67b 100644 --- a/database/a_ssau_parser.py +++ b/database/a_ssau_parser.py @@ -3,32 +3,196 @@ from bs4 import BeautifulSoup from ast import literal_eval import time import logging +import datetime +from itertools import groupby + logger = logging.getLogger('bot') -def findInRasp(req) -> dict: - """Поиск группы (преподавателя) в расписании""" - logger.debug(f'Find {req}') - - rasp = requests.Session() - rasp.headers['User-Agent'] = 'Mozilla/5.0' - hed = rasp.get("https://ssau.ru/rasp/") - if hed.status_code == 200: - soup = BeautifulSoup(hed.text, 'lxml') - csrf_token = soup.select_one('meta[name="csrf-token"]')['content'] - else: - return 'Error' - - time.sleep(1) - - rasp.headers['Accept'] = 'application/json' - rasp.headers['X-CSRF-TOKEN'] = csrf_token - result = rasp.post("https://ssau.ru/rasp/search", data = {'text':req}) - if result.status_code == 200: - num = literal_eval(result.text) - else: - return 'Error' - if len(num) == 0: - return None - else: - return num[0] \ No newline at end of file +def findInRasp(req: str): + """Поиск группы (преподавателя) в расписании""" + logger.debug(f'Find {req}') + + rasp = requests.Session() + rasp.headers['User-Agent'] = 'Mozilla/5.0' + hed = rasp.get("https://ssau.ru/rasp/") + if hed.status_code == 200: + soup = BeautifulSoup(hed.text, 'lxml') + csrf_token = soup.select_one('meta[name="csrf-token"]')['content'] + else: + return 'Error' + + time.sleep(1) + + rasp.headers['Accept'] = 'application/json' + rasp.headers['X-CSRF-TOKEN'] = csrf_token + result = rasp.post("https://ssau.ru/rasp/search", data={'text': req}) + if result.status_code == 200: + num = literal_eval(result.text) + else: + return 'Error' + + if len(num) == 0: + return None + else: + 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 diff --git a/database/shedule.py b/database/shedule.py index 3981d81..924e3b6 100644 --- a/database/shedule.py +++ b/database/shedule.py @@ -2,6 +2,7 @@ from .l9 import L9_DB from .a_ssau_parser import * import telegram from configparser import ConfigParser +import datetime class Shedule_DB: @@ -61,14 +62,22 @@ class Shedule_DB: else: return 'Empty' - def uploadShedule( - self, query: telegram.CallbackQuery, groupId: str, loc: ConfigParser - ): - if query.data == 'yes': - query.edit_message_text(loc['group']['loading']) + def loadShedule(self, groupId, date, first_week): + week = date.isocalendar()[1] - first_week - else: - query.edit_message_text(loc['group']['nogroup']) - query.edit_message_reply_markup(Keyboard.cancel()) + self.db.execute( + f'DELETE FROM `lessons` WHERE WEEK(`begin`, 1) = {date.isocalendar()[1]} AND groupId = {groupId};' + ) - 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) diff --git a/database/shedule.sql b/database/shedule.sql index 5a85297..70afc7b 100644 --- a/database/shedule.sql +++ b/database/shedule.sql @@ -42,4 +42,77 @@ CREATE TABLE IF NOT EXISTS `groups_users` ( KEY `gid_idx` (`groupId`), 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 + ); + +-- Преподаватели +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 ); \ No newline at end of file diff --git a/locale/ru.ini b/locale/ru.ini index dfb3b85..ec6ab8b 100644 --- a/locale/ru.ini +++ b/locale/ru.ini @@ -26,4 +26,6 @@ empty=К сожалению, такой группы нет ни в моей б loading=Загружаю расписание... +loaded=Расписание загружено! + nogroup=Возможно, ты написал не ту группу, попробуй снова \ No newline at end of file