python for channel

This commit is contained in:
Alex Borisov 2024-01-27 21:34:28 +03:00
commit eefb87d715
19 changed files with 1368 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/.env
/venv
/Eljur/__pycache__
/.idea
/__pycache__
/logfile.log
/data

15
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
"python.analysis.typeCheckingMode": "basic",
"cSpell.words": [
"authorisation",
"domcontentloaded",
"fromdesc",
"fromlines",
"lgray",
"lxml",
"todesc",
"tolines",
"wrapcolumn"
],
"cSpell.language": "en,ru"
}

15
Dockerfile Normal file
View File

@ -0,0 +1,15 @@
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 . .
CMD ["python", "main.py"]

93
Eljur/auth.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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, чтобы создать свой .env файл.
env.example - пример, как может выглядеть файл .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
View 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
View 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}')

119
main.py Normal file
View File

@ -0,0 +1,119 @@
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:
logging.error(f"cant read data/table.html: {Exception}")
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():
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 = new_table_soup.prettify()
old_table_soup = BeautifulSoup(old_table, "lxml")
old_table = old_table_soup.prettify()
get_difference(old_table, new_table)
if old_last_block != new_last_block:
send_photo("data/screenshot.png")
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
View File

@ -0,0 +1,5 @@
bs4
requests
lxml
playwright
python-dotenv

BIN
sad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

7
sample.env Normal file
View 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'

68
scraper.py Normal file
View File

@ -0,0 +1,68 @@
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
def scrape():
# Use sync version of Playwright
with sync_playwright() as p:
try:
# Launch the browser
browser = p.chromium.launch()
logging.debug("browser opened")
# Open a new browser page
page = browser.new_page()
# Create a URI for our test file
# Open our test file in the opened page
page.goto(f"https://{DOMAIN}.eljur.ru/authorize?return_uri=%2Fjournal-board-action")
# 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()
page.wait_for_url(f"https://{DOMAIN}.eljur.ru/journal-board-action?user={LOGIN}&domain={DOMAIN}")
# 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")
# Close browser
browser.close()
logging.debug("browser closed")
# 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:
logging.error(f"Error while request: {Exception}")
return " "