python
This commit is contained in:
commit
afa563d321
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/.env
|
||||||
|
/venv
|
||||||
|
/Eljur/__pycache__
|
||||||
|
/.idea
|
||||||
|
/__pycache__
|
||||||
|
/logfile.log
|
||||||
|
/data
|
||||||
|
|
||||||
|
# Временно
|
||||||
|
/docker
|
15
.vscode/settings.json
vendored
Normal file
15
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"python.analysis.typeCheckingMode": "basic",
|
||||||
|
"cSpell.words": [
|
||||||
|
"authorisation",
|
||||||
|
"domcontentloaded",
|
||||||
|
"fromdesc",
|
||||||
|
"fromlines",
|
||||||
|
"lgray",
|
||||||
|
"lxml",
|
||||||
|
"todesc",
|
||||||
|
"tolines",
|
||||||
|
"wrapcolumn"
|
||||||
|
],
|
||||||
|
"cSpell.language": "en,ru"
|
||||||
|
}
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -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" ]
|
93
Eljur/auth.py
Normal file
93
Eljur/auth.py
Normal file
@ -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}
|
86
Eljur/errors.py
Normal file
86
Eljur/errors.py
Normal file
@ -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
|
68
Eljur/journal.py
Normal file
68
Eljur/journal.py
Normal file
@ -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
|
196
Eljur/message.py
Normal file
196
Eljur/message.py
Normal file
@ -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()
|
132
Eljur/portfolio.py
Normal file
132
Eljur/portfolio.py
Normal file
@ -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
|
202
Eljur/profile.py
Normal file
202
Eljur/profile.py
Normal file
@ -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"]
|
29
Eljur/timetable.py
Normal file
29
Eljur/timetable.py
Normal file
@ -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
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -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.
|
60
README.md
Normal file
60
README.md
Normal file
@ -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
|
||||||
|
```
|
7
env.example
Normal file
7
env.example
Normal file
@ -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/
|
58
env_init.py
Normal file
58
env_init.py
Normal file
@ -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}')
|
125
main.py
Normal file
125
main.py
Normal file
@ -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("""<h1 class="page-title">Объявления</h1>"""):old_table.find("""<div class="board-item__footer">""")]
|
||||||
|
|
||||||
|
if new_table != " ":
|
||||||
|
# new_table = new_table[new_table.find("""<h1 class="page-title">Объявления</h1>"""):new_table.find("""<div class="board-item__footer">""")]
|
||||||
|
new_table = new_table[new_table.find("""<h1 class="page-title">Объявления</h1>"""):new_table.find("""<span class="control-label lgray">""")]
|
||||||
|
|
||||||
|
|
||||||
|
if old_table != new_table and new_table != " ":
|
||||||
|
new_last_block = new_table[new_table.find("""<h1 class="page-title">Объявления</h1>"""):new_table.find("""<div class="board-item__footer">""")]
|
||||||
|
|
||||||
|
update_html(new_table)
|
||||||
|
new_table_soup = BeautifulSoup(new_table, "lxml")
|
||||||
|
new_table_pretty = new_table_soup.prettify()
|
||||||
|
old_table_soup = BeautifulSoup(old_table, "lxml")
|
||||||
|
old_table_pretty = old_table_soup.prettify()
|
||||||
|
get_difference(old_table_pretty, new_table_pretty)
|
||||||
|
|
||||||
|
|
||||||
|
if old_last_block != new_last_block:
|
||||||
|
send_photo("data/screenshot.png")
|
||||||
|
else:
|
||||||
|
send_photo("sad.png")
|
||||||
|
|
||||||
|
send_document("data/difference.html")
|
||||||
|
send_document("data/table.html")
|
||||||
|
|
||||||
|
return
|
||||||
|
logging.debug("same result. nothing to do")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
verify_data()
|
||||||
|
logging.info("log in success")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
run()
|
||||||
|
interval = randint(300, 500)
|
||||||
|
logging.debug(f"sleep: {interval}")
|
||||||
|
sleep(interval)
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
bs4
|
||||||
|
requests
|
||||||
|
lxml
|
||||||
|
playwright
|
||||||
|
python-dotenv
|
7
sample.env
Normal file
7
sample.env
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
ELJUR_LOGIN='your login'
|
||||||
|
ELJUR_PASSWORD='your pass'
|
||||||
|
ELJUR_DOMAIN='your school number'
|
||||||
|
|
||||||
|
TG_TOKEN='telegram bot token'
|
||||||
|
TG_ID='your id in telegram'
|
||||||
|
TG_API_URL='your telegram API server url'
|
70
scraper.py
Normal file
70
scraper.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
from env_init import LOGIN, PASSWORD, DOMAIN
|
||||||
|
import logging
|
||||||
|
from playwright._impl import _api_types
|
||||||
|
from os import mkdir
|
||||||
|
|
||||||
|
|
||||||
|
# Use sync version of Playwright
|
||||||
|
p = sync_playwright().start()
|
||||||
|
# Launch the browser
|
||||||
|
browser = p.chromium.launch()
|
||||||
|
logging.debug("browser opened")
|
||||||
|
|
||||||
|
# Open a new browser page
|
||||||
|
page = browser.new_page()
|
||||||
|
|
||||||
|
|
||||||
|
def scrape():
|
||||||
|
global page
|
||||||
|
try:
|
||||||
|
# Open our test file in the opened page
|
||||||
|
page.goto(f"https://{DOMAIN}.eljur.ru/authorize?return_uri=%2Fjournal-board-action")
|
||||||
|
try:
|
||||||
|
# Log In
|
||||||
|
login_field = page.locator('[type="text"]')
|
||||||
|
password_field = page.locator('[type="password"]')
|
||||||
|
|
||||||
|
login_field.fill(value=LOGIN) # type: ignore
|
||||||
|
password_field.fill(value=PASSWORD) # type: ignore
|
||||||
|
|
||||||
|
submit_button = page.locator('[type="submit"]')
|
||||||
|
submit_button.click()
|
||||||
|
except Exception as error:
|
||||||
|
logging.debug(f"Error while request: {str(error)}")
|
||||||
|
page.goto(f"https://{DOMAIN}.eljur.ru/journal-board-action?user={LOGIN}&domain={DOMAIN}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
page.wait_for_url(f"https://{DOMAIN}.eljur.ru/journal-board-action?user={LOGIN}&domain={DOMAIN}")
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
if page.url != f"https://{DOMAIN}.eljur.ru/journal-board-action?user={LOGIN}&domain={DOMAIN}" and page.url != f"https://{DOMAIN}.eljur.ru/journal-board-action":
|
||||||
|
page.goto(f"https://{DOMAIN}.eljur.ru/journal-board-action")
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
|
||||||
|
page_content = page.content()
|
||||||
|
|
||||||
|
new_content = page.locator('[class="board-item active"]').first
|
||||||
|
try:
|
||||||
|
new_content.screenshot(path = "data/screenshot.png")
|
||||||
|
except FileNotFoundError:
|
||||||
|
mkdir("data")
|
||||||
|
logging.info("can`t take screenshot")
|
||||||
|
logging.info("retry")
|
||||||
|
new_content.screenshot(path = "data/screenshot.png")
|
||||||
|
except:
|
||||||
|
logging.error("can`t take screenshot")
|
||||||
|
|
||||||
|
# Process extracted content with BeautifulSoup
|
||||||
|
soup = BeautifulSoup(page_content, features="lxml")
|
||||||
|
|
||||||
|
logging.debug("content extracted")
|
||||||
|
# return '\n'.join(el.strip() for el in str(soup.get_text).split('\n') if el.strip())
|
||||||
|
return str(soup.get_text)
|
||||||
|
|
||||||
|
except _api_types.TimeoutError:
|
||||||
|
logging.error("connection timed out")
|
||||||
|
return " "
|
||||||
|
except Exception as error:
|
||||||
|
logging.error(f"Error while request: {str(error)}")
|
||||||
|
return " "
|
Loading…
Reference in New Issue
Block a user