commit afa563d3214207ea9c1dbe6b5d32c100db0b4f6d Author: Alex Borisov <79996669747@ya.ru> Date: Sat Jan 27 21:30:58 2024 +0300 python diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fd04bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.env +/venv +/Eljur/__pycache__ +/.idea +/__pycache__ +/logfile.log +/data + +# Временно +/docker \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1b4da9f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "python.analysis.typeCheckingMode": "basic", + "cSpell.words": [ + "authorisation", + "domcontentloaded", + "fromdesc", + "fromlines", + "lgray", + "lxml", + "todesc", + "tolines", + "wrapcolumn" + ], + "cSpell.language": "en,ru" +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..62ec62e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.11-slim-bullseye + +WORKDIR /app/ + +COPY requirements.txt . + +RUN pip install -r requirements.txt --no-cache-dir + +RUN playwright install chromium + +RUN playwright install-deps + +COPY . . + +ENTRYPOINT ["python", "main.py"] + +CMD [ "start" ] \ No newline at end of file diff --git a/Eljur/auth.py b/Eljur/auth.py new file mode 100644 index 0000000..f875de2 --- /dev/null +++ b/Eljur/auth.py @@ -0,0 +1,93 @@ +from bs4 import BeautifulSoup +from requests import Session, post +import json +from Eljur.errors import _checkStatus, _checkSubdomain, _findData + + +class Authorization: + def login(self, subdomain, data): + """ + Подключение к пользователю eljur.ru. + + # :param subdomain: Поддомен eljur.ru // str + :param data: Дата, состоящая из {"username": "ваш логин", + "password": "ваш пароль"} // dict + + :return: Словарь с ошибкой или с положительным ответом: // dict + answer // dict + session // Session + subdomain // str + result // bool + """ + + subdomain = _checkSubdomain(subdomain) + if "error" in subdomain: + return subdomain + + session = Session() + url = f"https://{subdomain}.eljur.ru/ajaxauthorize" + err = session.post(url=url, data=data) + + checkStatus = _checkStatus(err, url) + if "error" in checkStatus: + return checkStatus + del checkStatus + + if not err.json()["result"]: + return {"error": {"error_code": -103, + "error_msg": err.json()['error'], + "full_error": err.json()}} + del err + + url = f"https://{subdomain}.eljur.ru/?show=home" + account = session.get(url=url) + checkStatus = _checkStatus(account, url) + if "error" in checkStatus: + return checkStatus + + soup = BeautifulSoup(account.text, 'lxml') + + sentryData = _findData(soup) + del soup + if not sentryData: + return {"error": {"error_code": -104, + "error_msg": "Данные о пользователю не найдены."}} + + return {"answer": json.loads(sentryData[17:-1]), + "session": session, + "subdomain": subdomain, + "result": True} + + def recover(self, subdomain, email): + """ + Восстановление пароль пользователю eljur.ru. через почту. + + Внимание! Для использования данные функции требуется привязать почту. + В ином случае восстановление происходит через Администратора или другого лица вашей школы. + + :param subdomain: Домен вашей школы. // str + :param email: Ваша почта, привязанная к аккаунту eljur // str + + :return: Словарь с ошибкой или с положительным ответом: // dict + answer // dict + result // bool + """ + + subdomain = _checkSubdomain(subdomain) + if "error" in subdomain: + return subdomain + + url = f"https://{subdomain}.eljur.ru/ajaxrecover" + answer = post(url=url, + data={"email": email}) + + checkStatus = _checkStatus(answer, url) + if "error" in checkStatus: + return checkStatus + + if not answer.json()["result"]: + return {"error": {"error_code": -105, + "error_msg": answer.json()['error'], + "full_error": answer.json()}} + return {"answer": "Сообщение успешно отправлено на почту.", + "result": True} diff --git a/Eljur/errors.py b/Eljur/errors.py new file mode 100644 index 0000000..2afeac0 --- /dev/null +++ b/Eljur/errors.py @@ -0,0 +1,86 @@ +import re +from bs4 import BeautifulSoup +from requests import Session + + +def _findData(soup): + for tag in soup.find_all("script"): + contents = tag.contents + for content in contents: + if "sentryData" in content: + return content + + +def _checkStatus(err, url): + if not err.status_code: + return {"error": {"error_code": -102, + "error_msg": f"Возникла ошибка при отправке запроса по ссылке {url}"}} + if err.status_code >= 400: + return {"error": {"error_code": -102, + "error_msg": f"Возникла ошибка {err.status_code} при отправке запроса по ссылке {url}"}} + else: + return {"answer": "Ok", + "result": True} + + +def _checkSubdomain(subdomain): + subdomain = re.search(r"[a-zA-Z0-9]+", subdomain) + if not subdomain: + return {"error": {"error_code": -101, + "error_msg": "Поддомен не найден"}} + else: + return subdomain[0] + + +def _checkInstance(obj, cls): + if not isinstance(obj, cls): + return {"error": {"error_code": -201, + "error_msg": f"Экземпляр не пренадлежит к классу. {type(obj)} - {type(cls)}"}} + else: + return {"answer": "Ok", + "result": True} + + +def _fullCheck(subdomain, session, url, data=None): + subdomain = _checkSubdomain(subdomain) + if "error" in subdomain: + return subdomain + + checkSession = _checkInstance(session, Session) + if "error" in checkSession: + return checkSession + del checkSession + + getInfo = session.post(url=url, data=data) + + checkStatus = _checkStatus(getInfo, url) + if "error" in checkStatus: + return checkStatus + del checkStatus + + soup = BeautifulSoup(getInfo.text, 'lxml') + del getInfo, url + + sentryData = _findData(soup) + if not sentryData: + return {"error": {"error_code": -104, + "error_msg": "Данные о пользователе не найдены."}} + del sentryData + + return soup + + +def _smallCheck(subdomain, session, args): + subdomain = _checkSubdomain(subdomain) + if "error" in subdomain: + return subdomain + + checkSession = _checkInstance(session, Session) + if "error" in checkSession: + return checkSession + del checkSession + + checkDict = _checkInstance(args, dict) + if "error" in checkDict: + return checkDict + del checkDict diff --git a/Eljur/journal.py b/Eljur/journal.py new file mode 100644 index 0000000..f3e0de9 --- /dev/null +++ b/Eljur/journal.py @@ -0,0 +1,68 @@ +from Eljur.errors import _fullCheck, _checkInstance + + +class Journal: + + def journal(self, subdomain, session, week=0): + """ + Получение страницы дневника с расписанием, оценками и другой информации. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + :param week: Нужная вам неделя (0 - нынешняя, -1 - предыдущая, 1 - следующая). По умолчанию 0 (нынешняя) // str + + :return: Словарь с ошибкой или с расписанием пользователя: // dict + answer // dict + result // bool + """ + checkWeek = _checkInstance(week, int) + if "error" in checkWeek: + return checkWeek + del checkWeek + + url = f"https://{subdomain}.eljur.ru/journal-app/week.{week * -1}" + + soup = _fullCheck(subdomain, session, url) + if "error" in soup: + return soup + + info = {} + for day in soup.find_all("div", class_="dnevnik-day"): + title = day.find("div", class_="dnevnik-day__title") + week, date = title.contents[0].strip().replace("\n", "").split(", ") + + if day.find("div", class_="page-empty"): + info.update([(week, {"date": date, "isEmpty": True, "comment": "Нет уроков", "lessons": {}})]) + continue + + if day.find("div", class_="dnevnik-day__holiday"): + info.update([(week, {"date": date, "isEmpty": True, "comment": "Выходной", "lessons": {}})]) + continue + + lessons = day.find_all("div", class_="dnevnik-lesson") + lessonsDict = {} + if lessons: + for lesson in lessons: + lessonNumber = lesson.find("div", class_="dnevnik-lesson__number dnevnik-lesson__number--time") + if lessonNumber: + lessonNumber = lessonNumber.contents[0].replace("\n", "").strip()[:-1] + + lessonTime = lesson.find("div", class_="dnevnik-lesson__time").contents[0].strip().replace("\n", "") + lessonName = lesson.find("span", class_="js-rt_licey-dnevnik-subject").contents[0] + + lessonHomeTask = lesson.find("div", class_="dnevnik-lesson__task") + if lessonHomeTask: + lessonHomeTask = lessonHomeTask.contents[2].replace("\n", "").strip() + + lessonMark = lesson.find("div", class_="dnevnik-mark") + if lessonMark: + lessonMark = lessonMark.contents[1].attrs["value"] + + lessonsDict.update([(lessonNumber, {"time": lessonTime, + "name": lessonName, + "hometask": lessonHomeTask, + "mark": lessonMark})]) + + info.update([(week, {"date": date, "isEmpty": False, "comment": "Выходной", "lessons": lessonsDict})]) + + return info diff --git a/Eljur/message.py b/Eljur/message.py new file mode 100644 index 0000000..34f9b5c --- /dev/null +++ b/Eljur/message.py @@ -0,0 +1,196 @@ +from Eljur.errors import _checkStatus, _checkSubdomain, _smallCheck + + +class Message: + + def schoolList(self, subdomain, session): + """ + Получение тех, кому можем отправить сообщение. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + + :return: Возвращает ошибку или массив из словарей возможных получателей сообщения. // list + """ + + pattern = {"0": "school", + "1": "null", + "2": "null", + "3": "null"} + typePattern = ["classruks", "administration", "specialists", "ext_5_teachers", "teachers", "parents", + "students"] + listAnswer = {} + + subdomain = _checkSubdomain(subdomain) + if "error" in subdomain: + return subdomain + + for typeOf in enumerate(typePattern): + if typeOf[0] == 4: + pattern["2"] = "1" + + pattern["1"] = typePattern[typeOf[0]] + + url = f"https://{subdomain}.eljur.ru/journal-messages-ajax-action?method=getRecipientList" + getPattern = session.post(url=url, data=pattern) + + checkStatus = _checkStatus(getPattern, url) + if "error" in checkStatus: + return checkStatus + del checkStatus + + listAnswer.update([(typeOf[1], getPattern.json())]) + + return [listAnswer, typePattern] + + def sendMessage(self, subdomain, session, args): + """ + Отправка сообщения по ID пользователя. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + + :param args: Словарь, состоящий из: + receivers: ID пользователя. Если несколько, то через ; // str + subject: Тема сообщение (заглавие) // str + message: Сообщение // str + + + :return: Словарь с ошибкой или bool ответ, в котором True - успешная отправка соообщения // dict или bool + """ + + check = _smallCheck(subdomain, session, args) + if not check: + return check + del check + + url = f"https://{subdomain}.eljur.ru/journal-messages-compose-action" + get_cookies = session.get(url, data={"_msg": "sent"}) + + checkStatus = _checkStatus(get_cookies, url) + if "error" in checkStatus: + return checkStatus + del checkStatus + + pattern = {"csrf": get_cookies.cookies.values()[0], + "submit": "Отправить", + "cancel": "Отмена"} + pattern.update(args) + + url = f"https://{subdomain}.eljur.ru/journal-messages-send-action/" + send = session.post(url, data=pattern) + + checkStatus = _checkStatus(send, url) + if "error" in checkStatus: + return checkStatus + del checkStatus + + return True + + def getMessages(self, subdomain, session, args): + """ + Получение сообщений пользователя. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + :param args: Словарь, состоящий из: + "0": inbox/sent (полученные/отправленные) // str + "1": Текст заглавия // str + "2": Сколько сообщений показать (default: 20 / limit: 44) // str + "3": С какого сообщение начало // str + "4": 0 или id пользователя. // str + "5": read/unread/trash (Прочитанные/Непрочитанные/Корзина) // str + "6": ID пользователя, чьи сообщения мы хотим получить. // str + "7": Дата (false, today, week, month, two_month, year) // str + + :return: Словарь с ошибкой или с сообщениями // dict + """ + + pattern = {"method": "getList", + "2": "20"} + + check = _smallCheck(subdomain, session, args) + if check: + return check + del check + + pattern.update(args) + + url = f"https://{subdomain}.eljur.ru/journal-messages-ajax-action/getmessages" + send = session.post(url, data=pattern) + + checkStatus = _checkStatus(send, url) + if "error" in checkStatus: + return checkStatus + del checkStatus + + return send.json() + + def deleteMessages(self, subdomain, session, args): + """ + Удаление сообщения у пользователя. + Внимание! Eljur удаляет сообщение ТОЛЬКО у пользователя (как если вы не выбрали "Также удалить у") + Получатель сможет прочитать сообщение даже после удаления у вас. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + :param args: Словарь, состоящий из: + 0: ID сообщения. Если несколько, то через ; без пробелов // str + 1: Полученные/Отправленные (inbox/sent) Корзина (trash) не удаляется // str + + + :return: Словарь с ошибкой или с ответом: // dict + result // bool + error // str + """ + + check = _smallCheck(subdomain, session, args) + if not check: + return check + del check + + pattern = {"method": "delete"} + pattern.update(args) + + url = f"https://{subdomain}.eljur.ru/journal-messages-ajax-action/" + delete = session.post(url, data=pattern) + + checkStatus = _checkStatus(delete, url) + if "error" in checkStatus: + return checkStatus + del checkStatus + + return delete.json() + + def recoverMessages(self, subdomain, session, args): + """ + Возвращает сообщение из Корзины. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + :param args: Словарь, состоящий из: + 0: ID сообщения. Если несколько, то через ; без пробелов // str + + :return: Словарь с ошибкой или с ответом: // dict + result // bool + error // str + """ + + check = _smallCheck(subdomain, session, args) + if not check: + return check + del check + + pattern = {"method": "restore", + "1": "inbox"} + pattern.update(args) + + url = f"https://{subdomain}.eljur.ru/journal-messages-ajax-action/" + recover = session.post(url, data=pattern) + + checkStatus = _checkStatus(recover, url) + if "error" in checkStatus: + return checkStatus + del checkStatus + + return recover.json() diff --git a/Eljur/portfolio.py b/Eljur/portfolio.py new file mode 100644 index 0000000..d3fc321 --- /dev/null +++ b/Eljur/portfolio.py @@ -0,0 +1,132 @@ +from Eljur.errors import _fullCheck + + +def _checkForID(lesson_id): + return lesson_id.has_attr("data-lesson_id") + + +def _pattern(att): + dictionary = {"Всего": att.contents[3].contents[0], + "По болезни": att.contents[5].contents[0], + "По ув. причине": att.contents[7].contents[0], + "По неув. причине": att.contents[9].contents[0]} + return dictionary + + +class Portfolio: + + def reportCard(self, subdomain, session, user_id, quarter="I"): + """ + Получение списка оценок. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + :param user_id: ID пользователя // str + :param quarter: Четверть (I, II, III, IV) // str + + :return: Словарь с ошибкой или ответом (отсутствие оценок или словарь с оценками) // dict + """ + + url = f"https://{subdomain}.eljur.ru/journal-student-grades-action/u.{user_id}/sp.{quarter}+четверть" + + soup = _fullCheck(subdomain, session, url) + if "error" in soup: + return soup + + answer = soup.find("div", class_="page-empty") + + if answer: + return {"answer": answer.contents[0], + "result": False} + + card = {} + subjects = soup.find_all("div", class_="text-overflow lhCell offset16") + + for subject in subjects: + scores = [] + for score in soup.find_all("div", class_=["cell blue", "cell"], attrs={"name": subject.contents[0]}): + if "mark_date" in score.attrs: + if score.attrs["id"] != "N": + scores.append({score.attrs["mark_date"], score.contents[1].contents[0]}) + card.update([(subject.contents[0], scores)]) + card.update(result=True) + + return card + + def attendance(self, subdomain, session, user_id, quarter="I"): + """ + Изменение подписи в новых сообщениях пользователя. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + :param user_id: ID пользователя // str + :param quarter: Четверть (I, II, III, IV) // str + + :return: Словарь с ошибкой или ответом в виде словаря с предметами и пропущенными уроками // dict + """ + url = f"https://{subdomain}.eljur.ru/journal-app/view.miss_report/u.{user_id}/sp.{quarter}+четверть" + + soup = _fullCheck(subdomain, session, url) + if "error" in soup: + return soup + + answer = soup.find("div", class_="page-empty") + + if answer: + return {"answer": answer.contents[0], + "result": False} + + card = {} + subjects = soup.find_all(_checkForID) + + for subject in subjects: + lessonInfo = _pattern(subject) + if not subject.contents[1].contents: + subject.contents[1].contents = ["Всего"] + card.update([(subject.contents[1].contents[0], lessonInfo)]) + + days = soup.find_all("tr", attrs={"xls": "hrow"}) + daysInfo = _pattern(days[1]) + + card.update([(days[1].contents[1].contents[0], daysInfo)]) + + return card + + def finalGrades(self, subdomain, session, user_id, data=None): + """ + Изменение подписи в новых сообщениях пользователя. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + :param user_id: ID пользователя // str + :param data: Учебный год (Например: 2020/2021) // str + + :return: Словарь с ошибкой или ответом (отсутствие оценок или словарь с оценками) // dict + """ + url = f"https://{subdomain}.eljur.ru/journal-student-resultmarks-action/u.{user_id}" + + soup = _fullCheck(subdomain, session, url, data) + if "error" in soup: + return soup + + answer = soup.find("div", class_="page-empty") + + if answer: + return {"answer": answer.contents[0], + "result": False} + + card = {} + subjects = soup.find_all("div", class_="text-overflow lhCell offset16") + + for subject in subjects: + scores = [] + for score in soup.find_all("div", class_="cell", attrs={"name": subject.contents[0]}): + if score.contents[0].attrs["class"][0] != "cell-data": + continue + if not score.contents[0].contents: + continue + scores.append(score.contents[0].contents[0]) + card.update([(subject.contents[0], scores)]) + card.update(result=True) + + return card diff --git a/Eljur/profile.py b/Eljur/profile.py new file mode 100644 index 0000000..3a31f17 --- /dev/null +++ b/Eljur/profile.py @@ -0,0 +1,202 @@ +from requests import Session +from Eljur.errors import _checkInstance, _checkStatus, _checkSubdomain, _fullCheck + + +class Profile: + + def getProfile(self, subdomain, session): + """ + Получение информации о пользователе. + Внимание. В данной функции специально не выводится СНИЛС, почта и мобильный телефон пользователя. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + + :return: Словарь с ошибкой или с информацией о пользователе: // dict + """ + + url = f"https://{subdomain}.eljur.ru/journal-user-preferences-action" + + soup = _fullCheck(subdomain, session, url) + if "error" in soup: + return soup + + label = None + info = {} + for tag in soup.find_all(["label", "span"], class_=["ej-form-label", "control-label"]): + if tag.contents[0] == "СНИЛС": + break + + if tag.name == "label": + label = tag.contents[0] + info.update([(label, None)]) + + if tag.name == "span": + info[label] = tag.contents[0] + + return info + + +class Security: + + def changePassword(self, subdomain, session, old_password, new_password): + """ + Изменение пароля в личном кабинете пользователя. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + :param old_password: Старый пароль. // str + :param new_password: Новый пароль, который пользователь желает использовать. // str + + :return: Словарь с ошибкой или bool ответ, в котором True - успешная смена пароля // dict или bool + """ + + subdomain = _checkSubdomain(subdomain) + if "error" in subdomain: + return subdomain + + checkSession = _checkInstance(session, Session) + if "error" in checkSession: + return checkSession + del checkSession + + url = f"https://{subdomain}.eljur.ru/journal-messages-compose-action" + getCookies = session.get(url=url, data={"_msg": "sent"}) + + checkStatus = _checkStatus(getCookies, url) + if "error" in checkStatus: + return checkStatus + del checkStatus + + data = {"csrf": getCookies.cookies.values()[0], + "old_password": old_password, + "new_password": new_password, + "verify": new_password, + "submit_button": "Сохранить"} + + url = f"https://{subdomain}.eljur.ru/journal-user-security-action/" + answer = session.post(url=url, data=data) + + checkStatus = _checkStatus(answer, url) + if "error" in checkStatus: + return checkStatus + del checkStatus + + if "Ваш пароль успешно изменен!" in answer.text: + return True + return False + + +class Settings: + + def changeSing(self, subdomain, session, text): + """ + Изменение подписи в новых сообщениях пользователя. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + :param text: Текст подписи // str + + :return: Словарь с ошибкой или bool ответ, в котором True - успешное изменение подписи. // dict или bool + """ + + subdomain = _checkSubdomain(subdomain) + if "error" in subdomain: + return subdomain + + checkSession = _checkInstance(session, Session) + if "error" in checkSession: + return checkSession + del checkSession + + url = f"https://{subdomain}.eljur.ru/journal-index-rpc-action" + data = {"method": "setPref", + "0": "msgsignature", + "1": text} + + changeSing = session.post(url=url, data=data) + + checkStatus = _checkStatus(changeSing, url) + if "error" in checkStatus: + return checkStatus + + if "result" not in checkStatus: + return False + else: + return checkStatus["result"] + + def switcher(self, subdomain, session, choose, switch): + """ + Переключение настраиваиваемых функций в настройках. + Доступно переключение следующих функций: + `Отмечать сообщение прочитанным при его открытии на электронной почте` // 0 или checkforwardedemail + `Отображать расписание обучающегося по умолчанию (вместо расписания класса)` // 1 или schedule_default_student + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + :param choose: Выбор переключаемой функции // int или str + :param switch: True/False // bool + + :return: Словарь с ошибкой или bool ответ, в котором True - успешное переключение. // dict или bool + """ + + numSwitch = [0, 1] + numStrSwitch = ["0", "1"] + strSwitch = ["checkforwardedemail", "schedule_default_student"] + switchers = {"checkforwardedemail": {"on": "on", + "off": "off"}, + "schedule_default_student": {"on": "yes", + "off": "no"}} + data = {"method": "setPref", + "0": None, + "1": None} + + subdomain = _checkSubdomain(subdomain) + if "error" in subdomain: + return subdomain + + checkSession = _checkInstance(session, Session) + if "error" in checkSession: + return checkSession + del checkSession + + checkInt = _checkInstance(choose, int) + if "error" in checkInt: + checkStr = _checkInstance(choose, str) + if "error" in checkStr: + return checkStr + else: + if choose not in strSwitch: + if choose not in numStrSwitch: + return {"error": {"error_code": -302, + "error_msg": f"Вашего выбора нет в предложенных. {choose}"}} + else: + data["0"] = strSwitch[int(choose)] + data["0"] = choose + else: + if choose not in numSwitch: + return {"error": {"error_code": -302, + "error_msg": f"Вашего выбора нет в предложенных. {choose}"}} + data["0"] = strSwitch[choose] + + checkBool = _checkInstance(switch, bool) + if "error" not in checkBool: + return checkBool + del checkBool, checkStr, checkInt + + if switch: + data["1"] = switchers[data["0"]]["on"] + else: + data["1"] = switchers[data["0"]]["off"] + + url = f"https://{subdomain}.eljur.ru/journal-index-rpc-action" + changeSing = session.post(url=url, data=data) + + checkStatus = _checkStatus(changeSing, url) + if "error" in checkStatus: + return checkStatus + + if "result" not in checkStatus: + return False + else: + return checkStatus["result"] diff --git a/Eljur/timetable.py b/Eljur/timetable.py new file mode 100644 index 0000000..b32c484 --- /dev/null +++ b/Eljur/timetable.py @@ -0,0 +1,29 @@ +class Timetable: + + def timetable(self, subdomain, session, week): + """ + Получение страницы расписания. + + :param week: Нужная вам неделя (even, odd, both) // str + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + + :return: Словарь с ошибкой или с расписанием пользователя: // dict + answer // dict + result // bool + """ + return + + def journal(self, subdomain, session, week=0): + """ + Получение страницы дневника с расписанием и оценками. + + :param subdomain: Поддомен eljur.ru // str + :param session: Активная сессия пользователя // Session + :param week: Нужная вам неделя (0, -1, 3 и.т.д). По умолчанию 0 (нынешняя) // str + + :return: Словарь с ошибкой или с расписанием пользователя: // dict + answer // dict + result // bool + """ + return diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..30fca6e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Alex Borisov + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ff3257 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Элжур бот + +Предназначен для получения объявлений в телеграме в виде скриншота и html файла, +а также присутствует возможность узнать какие объявления были удалены. + +- [Быстрая установка и запуск с помощью Docker](https://git.soaska.ru/sosiska/eljur/-/wikis/home) + +## Экспуатация проекта + +#### Скачивание проекта +```bash +git clone https://git.soaska.ru/sosiska/eljur +cd eljur +``` + +#### Создание .env файла +Используйте [sample.env](https://git.soaska.ru/sosiska/eljur/-/blob/main/sample.env?ref_type=heads), чтобы создать свой .env файл. +[env.example](https://git.soaska.ru/sosiska/eljur/-/blob/main/env.example?ref_type=heads) - пример, как может выглядеть файл .env + +``` +ELJUR_LOGIN=Vasya2005 +ELJUR_PASSWORD=password_example +ELJUR_DOMAIN=2007 + +TG_TOKEN=123045678:ABCD_Uj3dQwUpDrf6e2-iCmI34v2SEGdZz0 +TG_ID=1234567321 +TG_API_URL=https://api.telegram.org/ +``` + +- [Как получить данные телеграм](https://git.soaska.ru/sosiska/eljur/-/wikis/How-to-get-telegram-data) + +### Подготовка к запуску проекта + +1) Создайте виртуальное окружение в папке с проектом и активируйте его: + +`(Debian)` +```bash +python3 -m venv venv +source venv/bin/activate +``` + +2) Установите библиотеки: + +```bash +pip install -r requirements.txt +``` + +3) Установите движок chromium: + +```bash +playwright install chromium +playwright install-deps +``` + +4) Запустите проект: + +`(Debian)` +```bash +python3 main.py +``` diff --git a/env.example b/env.example new file mode 100644 index 0000000..0c4b1c9 --- /dev/null +++ b/env.example @@ -0,0 +1,7 @@ +ELJUR_LOGIN=Vasya2005 +ELJUR_PASSWORD=password_example +ELJUR_DOMAIN=2007 + +TG_TOKEN=123045678:ABCD_Uj3dQwUpDrf6e2-iCmI34v2SEGdZz0 +TG_ID=1234567321 +TG_API_URL=https://api.telegram.org/ diff --git a/env_init.py b/env_init.py new file mode 100644 index 0000000..7ad6f08 --- /dev/null +++ b/env_init.py @@ -0,0 +1,58 @@ +import os +from dotenv import load_dotenv +import logging +from typing import Any, Dict, Tuple +from requests import Session +import requests + + +load_dotenv() + +LOGIN = os.getenv("ELJUR_LOGIN") +PASSWORD = os.getenv("ELJUR_PASSWORD") +DOMAIN = os.getenv("ELJUR_DOMAIN") + +TG_TOKEN = os.getenv("TG_TOKEN") +TG_ID = os.getenv("TG_ID") +TG_API_URL = os.getenv("TG_API_URL") + + +logging.basicConfig(level=logging.DEBUG, filename="logfile.log",filemode="w") + +if LOGIN == None or PASSWORD == None or DOMAIN == None or TG_TOKEN == None or TG_ID == None or TG_API_URL == None: + logging.critical("Env load error!") + exit(1) + +TG_ID = int(TG_ID) + + +def send_photo(path): + url = f'{TG_API_URL}bot{TG_TOKEN}/sendPhoto' + + params = { + 'chat_id': TG_ID, + } + + with open(path, 'rb') as img_file: + response = requests.post(url, params=params, files={'photo': img_file}) + + if response.status_code == 200: + logging.debug('photo sent') + else: + logging.error(f'can`t send photo: {response.status_code}') + + +def send_document(path): + url = f'{TG_API_URL}bot{TG_TOKEN}/sendDocument' + + params = { + 'chat_id': TG_ID, + } + + with open(path, 'rb') as doc_file: + response = requests.post(url, params=params, files={'document': doc_file}) + + if response.status_code == 200: + logging.debug('doc sent') + else: + logging.error(f'can`t send document: {response.status_code}') diff --git a/main.py b/main.py new file mode 100644 index 0000000..1713400 --- /dev/null +++ b/main.py @@ -0,0 +1,125 @@ +from Eljur.auth import Authorization +from scraper import scrape +from env_init import LOGIN, PASSWORD, DOMAIN, TG_ID, send_photo, send_document +import logging +from time import sleep +from random import randint +from os import mkdir +from difflib import HtmlDiff +from bs4 import BeautifulSoup + + +def get_difference(old, new): + old = old.split("\n") + new = new.split("\n") + difference = HtmlDiff(wrapcolumn=81) + + try: + html_file = open("data/difference.html", "w") + html_file.truncate(0) + ans = difference.make_file(fromlines=old, tolines=new, fromdesc="Было", todesc="Стало") + html_file.write(ans) + html_file.close + logging.debug("data/difference.html updated") + except: + logging.critical(f"cant update data/difference.html: {Exception}") + + +def update_html(val=""): + try: + html_file = open("data/table.html", "w") + html_file.truncate(0) + html_file.write(val) + html_file.close + logging.debug("data/table.html updated") + except FileNotFoundError: + mkdir("data") + logging.info(f"cant update data/table.html: {Exception}") + logging.info("retry") + update_html(val=val) + except: + logging.critical(f"cant update data/table.html: {Exception}") + exit(1) + + +def get_html(): + val = "" + try: + with open('data/table.html') as html_file: + val = html_file.read() + logging.debug("data/table.html got") + except Exception as error: + logging.error(f"cant read data/table.html: {str(error)}") + return val + + +def verify_data(): + authorisation = Authorization() + + data = { + "username": LOGIN, + "password": PASSWORD, + } + subdomain = DOMAIN + + answer = authorisation.login(subdomain, data) + if "session" not in answer: + print(answer) + logging.critical("can`t log in") + logging.error(answer) + exit(1) + elif answer['answer']['user']['username'] != LOGIN: # type: ignore + print(answer) + logging.critical("data verified. Wrong LOGIN!") + logging.error(f"Your login is {answer['answer']['user']['username']}! Update env") # type: ignore + exit(255) + logging.info(f'logged in as {LOGIN}') + + + +def run(): + global new_table, old_table + new_table = scrape() + old_table = get_html() + old_last_block = "" + + if old_table != "": + old_last_block = old_table[old_table.find("""

Объявления

"""):old_table.find("""