1
0
Fork 0
forked from Fuji/Fuji

Reapply "Merge branch 'main' of https://git.wpkg.ovh/Fuji/Fuji"

This reverts commit c58a24a4c6.
This commit is contained in:
Marioneq 2025-02-08 11:19:13 +01:00
parent c58a24a4c6
commit 330d4f694b
32 changed files with 0 additions and 430 deletions

View file

@ -1,152 +0,0 @@
from datetime import datetime
import requests
import json
from bs4 import BeautifulSoup
from impl.hebece.src.signer import *
from impl.hebece.src.utils import *
from impl.hebece.src.const import *
session = requests.Session()
certificate, fingerprint, private_key = generate_key_pair()
def getDebugInfo(data):
data = json.loads(data)
status = data.get("Status", {})
code = status.get("Code")
message = status.get("Message")
return code, message
def makeRequest(url):
digest, canonical_url, signature = get_signature_values(fingerprint, private_key, body=None, full_url=url, timestamp=datetime.now())
headers = makeHeader(signature, canonical_url)
response = requests.get(url, headers=headers)
content = response.text
dinfo = getDebugInfo(content)
return content, dinfo
def APILogin(login, password):
url = "https://eduvulcan.pl/"
response1 = session.get(url)
url = "https://eduvulcan.pl/logowanie"
response2 = session.get(url)
soup = BeautifulSoup(response2.text, 'html.parser')
token_input = soup.find('input', {'name': '__RequestVerificationToken'})
token = {"__RequestVerificationToken": token_input['value']}
cookies = {**response1.cookies.get_dict(), **response2.cookies.get_dict()}
cookies_str = "; ".join([f"{key}={value}" for key, value in cookies.items()])
cookies_str += f"; __RequestVerificationToken={token_input['value']}"
# Prometheus
url = "https://eduvulcan.pl/logowanie?ReturnUrl=%2fapi%2fap"
headers = makeLoginHeader(cookies_str)
data = {
"Alias": login,
"Password": password,
"captchaUser": "",
"__RequestVerificationToken": token_input['value'],
}
response = session.post(url, headers=headers, data=data)
content = response.text
cookie_jar = response.cookies.get_dict()
try:
soup = BeautifulSoup(content, "html.parser")
input_element = soup.find("input", {"id": "ap"})
value = input_element["value"]
parsed_json = json.loads(value)
tokens = parsed_json.get("Tokens", [])
token = " ".join(tokens)
return token
except TypeError:
pass
def JWTLogin(token, debug=False):
tenant = get_tenant_from_jwt(token)
url = f"https://lekcjaplus.vulcan.net.pl/{tenant}{JWT}"
RequestId = getRandomIdentifier()
SelfIdentifier = getRandomIdentifier()
Certificate = certificate
CertificateThumbprint = fingerprint
Tokens = token
digest, canonical_url, signature = get_signature_values(fingerprint, private_key, body=None, full_url=url, timestamp=datetime.now())
headers = makeHeader(signature, canonical_url)
timestamp = datetime.now()
date = getDate()
body = {
"AppName": "DzienniczekPlus 3.0",
"AppVersion": "24.11.07 (G)",
"NotificationToken": None,
"API": 1,
"RequestId": str(RequestId),
"Timestamp": getTimestamp(),
"TimestampFormatted": str(date),
"Envelope": {
"OS": DEVICE_OS,
"Certificate": Certificate,
"CertificateType": "X509",
"DeviceModel": DEVICE,
"SelfIdentifier": str(SelfIdentifier),
"CertificateThumbprint": CertificateThumbprint,
"Tokens": [Tokens]
}
}
body_json = json.dumps(body, indent=4)
response = session.post(url, headers=headers, data=body_json)
content = response.text
if debug:
dinfo = getDebugInfo(content)
return content, dinfo
return content
def HEBELogin(tenant, debug=False):
url = f"https://lekcjaplus.vulcan.net.pl/{tenant}{HEBE}?mode=2&lastSyncDate=1970-01-01%2001%3A00%3A00"
content, dinfo = makeRequest(url)
return content, dinfo
def getLuckyNumber(tenant, schoolid, pupilid, constituentid, debug=False):
timestamp = datetime.now()
date = timestamp.strftime("%Y-%m-%d")
url = f"https://lekcjaplus.vulcan.net.pl/{tenant}/{schoolid}{LUCKY}?pupilId={pupilid}&constituentId={constituentid}&day={date}"
content, dinfo = makeRequest(url)
return content, dinfo
def getGrades(tenant, schoolid, pupilid, unitid, periodid, debug=False):
url = f"https://lekcjaplus.vulcan.net.pl/{tenant}/{schoolid}/api/mobile/grade/byPupil?unitId={unitid}&pupilId={pupilid}&periodId={periodid}&lastSyncDate=1970-01-01%2001%3A00%3A00&lastId=-2147483648&pageSize=500"
content, dinfo = makeRequest(url)
return content, dinfo
def getTimetable(tenant, schoolid, pupilid, start_date, end_date, debug=False):
url = f"https://lekcjaplus.vulcan.net.pl/{tenant}/{schoolid}/api/mobile/schedule/withchanges/byPupil?pupilId={pupilid}&dateFrom={start_date}&dateTo={end_date}&lastId=-2147483648&pageSize=500&lastSyncDate=1970-01-01%2001%3A00%3A00"
content, dinfo = makeRequest(url)
return content, dinfo
def getExams(tenant, schoolid, pupilid, start_date, end_date, debug=False):
url = f"https://lekcjaplus.vulcan.net.pl/{tenant}/{schoolid}/api/mobile/exam/byPupil?pupilId={pupilid}&dateFrom={start_date}&dateTo={end_date}&lastId=-2147483648&pageSize=500&lastSyncDate=1970-01-01%2001%3A00%3A00"
content, dinfo = makeRequest(url)
return content, dinfo

View file

@ -1,52 +0,0 @@
from impl.hebece.src.utils import *
# Endpoints
JWT = "/api/mobile/register/jwt"
HEBE = "/api/mobile/register/hebe"
LUCKY = "/api/mobile/school/lucky"
GRADES = "/api/mobile/grade/byPupil"
TIMETABLE = "/api/mobile/schedule/withchanges/byPupil"
EXAMS = "/api/mobile/exam/byPupil"
# Header
DEVICE = "SM-G935F"
DEVICE_OS = "Android"
APPVERSION = "24.11.07 (G)"
def makeHeader(signature, canonical_url):
return {
"accept-encoding": "gzip",
"content-type": "application/json",
"host": "lekcjaplus.vulcan.net.pl",
"signature": signature,
"user-agent": "Dart/3.3 (dart:io)",
"vapi": "1",
"vcanonicalurl": canonical_url,
"vdate": getDate(),
"vos": DEVICE_OS,
"vversioncode": "640",
}
def makeLoginHeader(cookies):
return {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-US,en;q=0.9",
"Cache-Control": "max-age=0",
"Connection": "keep-alive",
"Content-Type": "application/x-www-form-urlencoded",
"Cookie": cookies,
"Host": "eduvulcan.pl",
"Origin": "https://eduvulcan.pl",
"Referer": "https://eduvulcan.pl/logowanie?ReturnUrl=%2fapi%2fap",
"sec-ch-ua": "\"Chromium\";v=\"130\", \"Android WebView\";v=\"130\", \"Not?A_Brand\";v=\"99\"",
"sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": "\"Android\"",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Linux; Android 13; SM-G935F Build/TQ3A.230901.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.107 Mobile Safari/537.36",
"X-Requested-With": "pl.edu.vulcan.hebe.ce",
}

View file

@ -1,37 +0,0 @@
import requests
import json
import uuid
import hashlib
import sqlite3
import os
import base64
from impl.hebece.src.signer import *
from impl.hebece.src.utils import *
from impl.hebece.src.api import *
from datetime import datetime, timedelta
from bs4 import BeautifulSoup
def getUserInfo(tenant):
content, dinfo = HEBELogin(tenant)
data = json.loads(content)
envelope = data.get("Envelope", [])[0]
pupil = envelope.get("Pupil", {})
unit = envelope.get("Unit", {})
links = envelope.get("Links", {})
ConstituentUnit = envelope.get("ConstituentUnit", {})
periods = envelope.get("Periods", [])
Name = pupil.get("FirstName", {})
SecondName = pupil.get("SecondName", {})
Surname = pupil.get("Surname", {})
Class = envelope.get("ClassDisplay", {})
PupilID = pupil.get("Id", {})
SchoolID = links.get("Symbol", {})
ConstituentID = ConstituentUnit.get("Id", {})
UnitID = unit.get("Id", {})
PeriodID = next((period.get('Id') for period in periods if period.get('Current')), None)
return Name, SecondName, Surname, Class, PupilID, SchoolID, ConstituentID, UnitID, PeriodID

View file

@ -1,54 +0,0 @@
import base64
import json
import uuid
from datetime import datetime, timedelta
def encodebase64(data):
return base64.b64encode(data.encode("utf-8")).decode("utf-8")
def decodebase64(data):
return base64.b64decode(data.encode("utf-8")).decode("utf-8")
def get_tenant_from_jwt(token):
try:
# Split the JWT into parts
header, payload, signature = token.split('.')
# Decode the payload from Base64
# Add padding
payload += '=' * (-len(payload) % 4)
decoded_payload = base64.urlsafe_b64decode(payload).decode('utf-8')
# Parse the payload as JSON
payload_json = json.loads(decoded_payload)
# Return the tenant
return payload_json.get('tenant')
except (ValueError, json.JSONDecodeError, KeyError) as e:
print(f"Error decoding JWT: {e}")
return None
def getRandomIdentifier():
ruuid = str(uuid.uuid4())
return ruuid
def get_current_week():
# Get today's date
today = datetime.today()
# Calculate the start of the week (Monday)
start_of_week = today - timedelta(days=today.weekday())
# Calculate the end of the week (Sunday)
end_of_week = start_of_week + timedelta(days=6)
# Return the dates as formatted strings
return start_of_week.strftime('%Y-%m-%d'), end_of_week.strftime('%Y-%m-%d')
def getDate():
return datetime.now().strftime("%a, %d %b %Y %H:%M:%S GMT")
def getTimestamp():
now = datetime.now()
Timestamp = now.timestamp()
return Timestamp

173
src/sdk/src/README.md Normal file
View file

@ -0,0 +1,173 @@
# Fuji SDK
## Jak używać
Obecnie działa jedynie prometeusz (eduvulcan).
### Techniczny wstęp
Trzeba skorzystać z interfejsu np. `PrometheusInterface`. Dla każdego interfejsu podajemy dwa parametry:
- `auth_context` - są w nim dane logowania, sesje, certyfikaty (ogółem dane, które pozwalają dostać się do dziennika, **pamiętaj, by trzymać je w bezpiecznym miejscu**),
- `student_context` - są w nim dane ucznia, pozwalające na wykonywanie zapytań do API, których używają interfejsy. (nie jest on konieczny, gdy logujesz się albo pobierasz uczniów)
Typy tych contextów są różne dla każdego dziennika. Jak uzyskać te contexty, dowiesz się niżej.
### Logowanie
#### Prometeusz (eduvulcan)
Musimy stworzyć instancję interfejsu `PrometheusInterface`, na początku musi on zawierać jedynie `auth_context`, który musi zawierać jedynie dane logowania do dziennika.
**UWAGA: Nie loguj się do dziennika od nowa za każdym razem, o tym jak zapisać zalogowane konto niżej.**
```py
from sdk.src.interfaces.prometheus.context import (
PrometheusAuthContext,
PrometheusWebCredentials,
)
from sdk.src.interfaces.prometheus.interface import PrometheusInterface
# Tworzymy instancję prometeuszowego interfejsu
interface = PrometheusInterface(
# Auth context
auth_context=PrometheusAuthContext(
# Dane logowania
prometheus_web_credentials=PrometheusWebCredentials(
username="<nazwa użytkownika>", password="<hasło użytkownika>"
)
),
student_context=None,
)
```
Gdy stworzyliśmy już instancję interfejsu, trzeba się zalogować, jest to bardzo proste. ~~Szkoda tylko, że w środku interfejsu już nie.~~
```py
interface.login()
```
Reszta część będzie wspólna dla wszystkich dzienników.
### Zapisywanie zalogowanego konta
Po zalogowaniu trzeba by było zapisać te dane, bo logowanie za każdym razem od nowa jest czasochłonne.
```py
auth_context = interface.get_auth_context()
```
Później, jak będziesz tworzyć instancję interfejsu, to w parametrze `auth_context` daj właśnie to.
**Pamiętaj, by trzymać to w bezpiecznym miejscu!**
### Pobieranie i wybieranie uczniów
Jak jesteśmy zalogowani, to możemy pobrać uczniów, jest to bardzo proste. **UWAGA! Nie rób tego za każdym razem, tylko zapisz sobie gdzieś te dane.**
```py
students = interface.get_students()
```
Jak dokładnie wygląda to co zwraca `get_students()` możesz sprawdzić w kodzie, to co teraz istotne to to, że każdy uczeń ma w sobie pole `context`. Jak możesz się domyślać jest to pole, które będziemy dawać interfejsowi. Możemy to zrobić albo przy tworzeniu instancji interfejsu:
```py
interface = PrometheusInterface(
auth_context=...,
student_context=<tu_to_dajesz>,
)
```
albo, gdy mamy już stworzoną instancję interfejsu:
```py
interface.select_student(<tu_to_dajesz>)
```
### Co dalej?
No możesz pobrać na przykład oceny, możesz to zrobić (gdy jesteś zalogowany i masz wybranego ucznia) tak:
```py
grades = interface.get_grades(<numer semestru>)
```
## Jak dodawać funkcjonalności:
1. Zrób model(e) do danych, nie dawaj zbyt dużo danych, bo pamiętaj, że ma być to uniwersalny model dla wielu dzienników.
2. Zrób funkcje w odpowiednich api do pobierania tych danych.
3. Zrób mappery w modelach dla odpowiednich api.
4. Zrób funkcję we wszystkich interfejsach do pobierania tych danych, przy obsłudze api.
### Przykład dla sprawdzianów (tylko dla Prometeusza)
1 i 3. `sdk/src/models/exam.py`
```py
class ExamType(Enum):
TEST = 0
SHORT_TEST = 1
CLASSWORK = 2
OTHER = 3
@staticmethod
def from_hebe_type_name(type_name: str):
match type_name:
case "Sprawdzian":
return ExamType.TEST
case "Kartkówka":
return ExamType.SHORT_TEST
case "Praca klasowa":
return ExamType.CLASSWORK
case _:
return ExamType.OTHER
@dataclass
class Exam:
deadline: date
subject: str
type: ExamType
description: str
creator: str
created_at: datetime
@staticmethod
def from_hebe_dict(data: dict):
return Exam(
deadline=datetime.fromtimestamp(data["Deadline"]["Timestamp"] / 1000),
subject=data["Subject"]["Name"],
type=ExamType.from_hebe_type_name(data["Type"]),
description=data["Content"],
creator=data["Creator"]["DisplayName"],
created_at=datetime.fromtimestamp(data["DateCreated"]["Timestamp"] / 1000),
)
```
2. `sdk/src/apis/hebe/client.py`
```py
def get_exams(self, student_id: int, from_: date, to: date):
envelope = self._send_request(
"GET",
ENDPOINT_EXAM_BYPUPIL,
params={
"pupilId": student_id,
"dateFrom": from_,
"dateTo": to,
},
)
return list(map(Exam.from_hebe_dict, envelope))
```
4.
4.1. prometheus interface
```py
def get_exams(self, from_: date, to: date):
self._check_is_auth_context_full()
self._check_is_student_selected()
return self._hebe_client.get_exams(self._student_context.student_id, from_, to)
```
4.2. core interface
```py
def get_exams(from_: date, to: date) -> list[Exam]:
pass
```

View file

@ -0,0 +1,15 @@
from dataclasses import dataclass
@dataclass
class FsLsQuery:
wa: str
wtrealm: str
wctx: str
@dataclass
class FsLsResponse:
wa: str
wresult: str
wctx: str

View file

@ -0,0 +1,12 @@
from bs4 import BeautifulSoup
from sdk.src.apis.common.models import FsLsResponse
def parse_fs_ls_response_form(html: str):
soup = BeautifulSoup(html, "html.parser")
return FsLsResponse(
wa=soup.select_one('input[name="wa"]')["value"],
wresult=soup.select_one('input[name="wresult"]')["value"],
wctx=soup.select_one('input[name="wctx"]')["value"],
)

View file

@ -0,0 +1,56 @@
import dataclasses
import requests
from sdk.src.apis.common.models import FsLsResponse, FsLsQuery
from sdk.src.apis.common.utils import parse_fs_ls_response_form
from sdk.src.apis.efeb.constants import (
ENDPOINT_LOGIN_FS_LS,
ENDPOINT_MESSAGES_APP,
ENDPOINT_STUDENT_APP,
BASE_LOGIN,
BASE_MESSAGES,
BASE_MESSAGES_CE,
BASE_STUDENT,
BASE_STUDENT_CE,
)
from sdk.src.apis.efeb.utils import parse_app_html
class EfebClient:
def __init__(self, cookies: dict, symbol: str, is_ce: bool):
self._session = requests.Session()
self._session.cookies.update(cookies)
self._symbol = symbol
self._is_ce = is_ce
def get_cookies(self):
return self._session.cookies.get_dict()
def login_fs_ls(
self, query: FsLsQuery, prometheus_response: FsLsResponse | None = None
):
response = self._session.request(
method="POST" if prometheus_response else "GET",
url=f"{BASE_LOGIN}/{self._symbol}/{ENDPOINT_LOGIN_FS_LS}",
data=(
dataclasses.asdict(prometheus_response) if prometheus_response else None
),
params=dataclasses.asdict(query),
)
return parse_fs_ls_response_form(response.text)
def student_app(self, login_response: FsLsResponse | None = None):
response = self._session.request(
method="POST" if login_response else "GET",
url=f"{BASE_STUDENT_CE if self._is_ce else BASE_STUDENT}/{self._symbol}/{ENDPOINT_STUDENT_APP}",
data=dataclasses.asdict(login_response) if login_response else None,
)
return parse_app_html(response.text)
def messages_app(self, login_response: FsLsResponse | None = None):
response = self._session.request(
method="POST" if login_response else "GET",
url=f"{BASE_MESSAGES_CE if self._is_ce else BASE_MESSAGES}/{self._symbol}/{ENDPOINT_MESSAGES_APP}",
data=dataclasses.asdict(login_response) if login_response else None,
)
return parse_app_html(response.text)

View file

@ -0,0 +1,9 @@
BASE_LOGIN = "https://dziennik-logowanie.vulcan.net.pl"
BASE_STUDENT_CE = "https://uczen.eduvulcan.pl"
BASE_STUDENT = "https://dziennik-uczen.vulcan.net.pl"
BASE_MESSAGES_CE = "https://wiadomosci.eduvulcan.pl"
BASE_MESSAGES = "https://dziennik-wiadomosci.vulcan.net.pl"
ENDPOINT_LOGIN_FS_LS = "fs/ls"
ENDPOINT_STUDENT_APP = "App"
ENDPOINT_MESSAGES_APP = "App"

View file

@ -0,0 +1,17 @@
from bs4 import BeautifulSoup
import re
def parse_fs_ls_form(html: str):
soup = BeautifulSoup(html, "html.parser")
return {
"wa": soup.select_one('input[name="wa"]')["value"],
"wresult": soup.select_one('input[name="wresult"]')["value"],
"wctx": soup.select_one('input[name="wctx"]')["value"],
}
def parse_app_html(html: str):
return re.search("appGuid: '(.*?)'", html).group(1), re.search(
"antiForgeryToken: '(.*?)'", html
).group(1)

View file

@ -0,0 +1,2 @@
from sdk.src.apis.hebe.certificate import Certificate as HebeCertificate
from sdk.src.apis.hebe.client import HebeClient

View file

@ -0,0 +1,15 @@
from dataclasses import dataclass
from sdk.src.apis.hebe.signer import generate_key_pair
@dataclass
class Certificate:
certificate: str
fingerprint: str
private_key: str
type: str
@staticmethod
def generate():
certificate, fingerprint, private_key = generate_key_pair()
return Certificate(certificate, fingerprint, private_key, "X509")

View file

@ -0,0 +1,209 @@
from datetime import date, datetime
import json
import uuid
import requests
from sdk.src.apis.hebe.certificate import Certificate
from sdk.src.apis.hebe.constants import (
API_VERSION,
APP_HEBE_NAME,
APP_HEBECE_NAME,
APP_VERSION,
APP_VERSION_CODE,
DEVICE_NAME,
DEVICE_OS,
ENDPOINT_EXAM_BYPUPIL,
ENDPOINT_GRADE_BYPUPIL,
ENDPOINT_HEARTBEAT,
ENDPOINT_NOTE_BYPUPIL,
ENDPOINT_REGISTER_HEBE,
ENDPOINT_REGISTER_JWT,
ENDPOINT_REGISTER_TOKEN,
ENDPOINT_SCHOOL_LUCKY,
USER_AGENT,
)
from sdk.src.apis.hebe.exceptions import (
ExpiredTokenException,
HebeClientException,
InvalidPINException,
InvalidRequestEnvelopeStructure,
InvalidRequestHeadersStructure,
NoPermissionsException,
NoUnitSymbolException,
NotFoundEntityException,
UnauthorizedCertificateException,
UsedTokenException,
)
from sdk.src.apis.hebe.student import HebeStudent
from sdk.src.apis.hebe.signer import get_signature_values
from sdk.src.models.exam import Exam
from sdk.src.models.grade import Grade
from sdk.src.models.note import Note
class HebeClient:
def __init__(
self, certificate: Certificate, rest_url: str | None = None, is_ce: bool = True
):
self._session = requests.Session()
self._certificate = certificate
self._rest_url = rest_url
self._is_ce = is_ce
def set_rest_url(self, new_value: str):
self._rest_url = new_value
def _send_request(self, method: str, endpoint: str, envelope: any = None, **kwargs):
if not self._rest_url:
raise Exception("No rest_url!")
date = datetime.now()
url = f"{self._rest_url}/{endpoint}"
body = json.dumps(self._build_body(date, envelope)) if envelope else None
headers = self._build_headers(date, url, body)
response = self._session.request(
method, url, data=body, headers=headers, **kwargs
)
data = response.json()
self._check_response_status_code(data["Status"]["Code"])
return data["Envelope"]
def _build_headers(self, date: datetime, url: str, body: any):
digest, canonical_url, signature = get_signature_values(
self._certificate.fingerprint,
self._certificate.private_key,
body,
url,
date,
)
headers = {
"signature": signature,
"vcanonicalurl": canonical_url,
"vos": DEVICE_OS,
"vdate": date.strftime("%a, %d %b %Y %H:%M:%S GMT"),
"vapi": API_VERSION,
"vversioncode": APP_VERSION_CODE,
"user-agent": USER_AGENT,
}
if digest:
headers["digest"] = digest
headers["content-type"] = "application/json"
return headers
def _build_body(self, date: datetime, envelope: any):
return {
"AppName": APP_HEBECE_NAME if self._is_ce else APP_HEBE_NAME,
"AppVersion": APP_VERSION,
"NotificationToken": "",
"API": int(API_VERSION),
"RequestId": str(uuid.uuid4()),
"Timestamp": int(date.timestamp()),
"TimestampFormatted": date.strftime("%Y-%m-%d %H:%M:%S"),
"Envelope": envelope,
}
@staticmethod
def _check_response_status_code(status_code: int):
if status_code == 0:
return
if status_code == 100:
raise NoPermissionsException()
if status_code == 101:
raise InvalidRequestEnvelopeStructure()
if status_code == 102:
raise InvalidRequestHeadersStructure()
if status_code == 104:
raise NoUnitSymbolException()
if status_code == 108:
raise UnauthorizedCertificateException()
if status_code == 200:
raise NotFoundEntityException()
if status_code == 201:
raise UsedTokenException()
if status_code == 203:
raise InvalidPINException()
if status_code == 204:
raise ExpiredTokenException()
raise HebeClientException(status_code)
def register_jwt(self, tokens: list[str]):
envelope = {
"OS": DEVICE_OS,
"Certificate": self._certificate.certificate,
"CertificateType": self._certificate.type,
"DeviceModel": DEVICE_NAME,
"SelfIdentifier": str(uuid.uuid4()),
"CertificateThumbprint": self._certificate.fingerprint,
"Tokens": tokens,
}
return self._send_request("POST", ENDPOINT_REGISTER_JWT, envelope)
def register_token(self, token: str, pin: str):
# For hebe interface
envelope = {
"OS": DEVICE_OS,
"Certificate": self._certificate.certificate,
"CertificateType": self._certificate.type,
"DeviceModel": DEVICE_NAME,
"SelfIdentifier": str(uuid.uuid4()),
"CertificateThumbprint": self._certificate.fingerprint,
"SecurityToken": token,
"PIN": pin,
}
return self._send_request("POST", ENDPOINT_REGISTER_TOKEN, envelope)
def get_students(self, mode: int = 2):
envelope = self._send_request(
"GET", ENDPOINT_REGISTER_HEBE, params={"mode": mode}
)
return map(HebeStudent.from_dict, envelope)
def heartbeat(self):
self._send_request("GET", ENDPOINT_HEARTBEAT)
def get_lucky_number(self, student_id: int, constituent_id: int, day: date):
envelope = self._send_request(
"GET",
ENDPOINT_SCHOOL_LUCKY,
params={
"pupilId": student_id,
"constituentId": constituent_id,
"day": day.strftime("%Y-%m-%d"),
},
)
return envelope["Number"]
def get_grades(self, student_id: int, unit_id: int, period_id: int):
envelope = self._send_request(
"GET",
ENDPOINT_GRADE_BYPUPIL,
params={
"pupilId": student_id,
"unitId": unit_id,
"periodId": period_id,
},
)
return list(map(Grade.from_hebe_dict, envelope))
def get_exams(self, student_id: int, from_: date, to: date):
envelope = self._send_request(
"GET",
ENDPOINT_EXAM_BYPUPIL,
params={
"pupilId": student_id,
"dateFrom": from_,
"dateTo": to,
},
)
return list(map(Exam.from_hebe_dict, envelope))
def get_notes(self, student_id: int):
envelope = self._send_request(
"GET", ENDPOINT_NOTE_BYPUPIL, params={"pupilId": student_id}
)
return list(map(Note.from_hebe_dict, envelope))

View file

@ -0,0 +1,22 @@
DEVICE_OS = "Android"
DEVICE_NAME = "SM-G935F"
APP_HEBE_NAME = "DzienniczekPlus 2.0"
APP_HEBECE_NAME = "DzienniczekPlus 3.0"
APP_VERSION = "24.11.07 (G)"
APP_VERSION_CODE = "640"
USER_AGENT = "Dart/3.3 (dart:io)"
API_VERSION = "1"
# Endpoints
ENDPOINT_REGISTER_JWT = "mobile/register/jwt"
ENDPOINT_REGISTER_TOKEN = "mobile/register/new"
ENDPOINT_REGISTER_HEBE = "mobile/register/hebe"
ENDPOINT_HEARTBEAT = "mobile/heartbeat"
ENDPOINT_SCHOOL_LUCKY = "mobile/school/lucky"
ENDPOINT_GRADE_BYPUPIL = "mobile/grade/byPupil"
ENDPOINT_SCHEDULE_WITHCHANGES_BYPUPIL = "mobile/schedule/withchanges/byPupil"
ENDPOINT_EXAM_BYPUPIL = "mobile/exam/byPupil"
ENDPOINT_NOTE_BYPUPIL = "mobile/note/byPupil"

View file

@ -0,0 +1,42 @@
class HebeClientException(Exception):
pass
class NotFoundEndpointException(HebeClientException):
pass
class NoUnitSymbolException(HebeClientException):
pass
class NoPermissionsException(HebeClientException):
pass
class InvalidRequestEnvelopeStructure(HebeClientException):
pass
class InvalidRequestHeadersStructure(HebeClientException):
pass
class UnauthorizedCertificateException(HebeClientException):
pass
class NotFoundEntityException(HebeClientException):
pass
class UsedTokenException(HebeClientException):
pass
class InvalidPINException(HebeClientException):
pass
class ExpiredTokenException(HebeClientException):
pass

View file

@ -1,4 +1,5 @@
import cryptography
import hashlib
import json
import re
import urllib
import base64
@ -17,6 +18,7 @@ def get_encoded_path(full_url):
)
return urllib.parse.quote(path[1], safe="").lower()
def get_digest(body):
if not body:
return None
@ -25,6 +27,7 @@ def get_digest(body):
m.update(bytes(body, "utf-8"))
return base64.b64encode(m.digest()).decode("utf-8")
def get_headers_list(body, digest, canonical_url, timestamp):
sign_data = [
["vCanonicalUrl", canonical_url],
@ -37,28 +40,26 @@ def get_headers_list(body, digest, canonical_url, timestamp):
"".join(item[1] for item in sign_data if item),
)
def get_signature(data, private_key):
# Convert data to a string representatio
data_str = (
json.dumps(data)
if isinstance(data, (dict, list))
else str(data)
)
data_str = json.dumps(data) if isinstance(data, (dict, list)) else str(data)
# Decode the base64 private key and load it
private_key_bytes = base64.b64decode(private_key)
pkcs8_key = load_der_private_key(private_key_bytes, password=None, backend=default_backend())
pkcs8_key = load_der_private_key(
private_key_bytes, password=None, backend=default_backend()
)
# Sign the data
signature = pkcs8_key.sign(
bytes(data_str, "utf-8"),
padding.PKCS1v15(),
hashes.SHA256()
bytes(data_str, "utf-8"), padding.PKCS1v15(), hashes.SHA256()
)
# Encode the signature in base64 and return
return base64.b64encode(signature).decode("utf-8")
def get_signature_values(fingerprint, private_key, body, full_url, timestamp):
canonical_url = get_encoded_path(full_url)
digest = get_digest(body)
@ -73,9 +74,11 @@ def get_signature_values(fingerprint, private_key, body, full_url, timestamp):
),
)
def pem_getraw(pem):
return pem.decode("utf-8").replace("\n", "").split("-----")[2]
def generate_key_pair():
pkcs8 = crypto.PKey()
pkcs8.generate_key(crypto.TYPE_RSA, 2048)

View file

@ -0,0 +1,58 @@
from dataclasses import dataclass
from datetime import date
@dataclass
class HebePeriod:
id: int
number: int
current: bool
from_: date
to: date
@staticmethod
def from_dict(data: dict):
return HebePeriod(
id=data["Id"],
number=data["Number"],
current=data["Current"],
from_=date.fromtimestamp(data["Start"]["Timestamp"] / 1000),
to=date.fromtimestamp(data["End"]["Timestamp"] / 1000),
)
@dataclass
class HebeStudent:
id: int
full_name: str
unit_id: int
constituent_id: int
capabilities: list[str]
register_id: int
register_student_number: int | None
class_name: str
is_parent: bool
messagebox_key: str
messagebox_name: str
periods: list[HebePeriod]
rest_url: str
symbol: str
@staticmethod
def from_dict(data: dict):
return HebeStudent(
id=data["Pupil"]["Id"],
full_name=data["Pupil"]["FirstName"] + " " + data["Pupil"]["Surname"],
unit_id=data["Unit"]["Id"],
constituent_id=data["ConstituentUnit"]["Id"],
capabilities=data["Capabilities"],
register_id=data["Journal"]["Id"],
register_student_number=data["Journal"]["PupilNumber"],
class_name=data["ClassDisplay"],
is_parent=bool(data["CaretakerId"]),
messagebox_key=data["MessageBox"]["GlobalKey"],
messagebox_name=data["MessageBox"]["Name"],
periods=list(map(HebePeriod.from_dict, data["Periods"])),
rest_url=data["Unit"]["RestURL"],
symbol=data["TopLevelPartition"],
)

View file

@ -0,0 +1,109 @@
import dataclasses
import json
from bs4 import BeautifulSoup
import requests
from sdk.src.apis.common.models import FsLsQuery
from sdk.src.apis.common.utils import parse_fs_ls_response_form
from sdk.src.apis.prometheus_web.constants import (
ENDPOINT_ACCOUNT_QUERY_USER_INFO,
ENDPOINT_API_AP,
ENDPOINT_FS_LS,
ENDPOINT_LOGIN,
HEADERS,
PROMETHEUS_WEB_BASE,
)
from sdk.src.apis.prometheus_web.exceptions import (
NoLoggedInException,
InvalidCredentialsException,
)
class PrometheusWebClient:
def __init__(self, cookies: dict[str, str] | None = None):
self._session = requests.Session()
if cookies:
self._session.cookies.update(cookies)
def is_logged(self):
if not self._session.cookies.get_dict():
return False
home_response = self._session.get(PROMETHEUS_WEB_BASE)
soup = BeautifulSoup(home_response.text, "html.parser")
return bool(soup.select_one("div.user-avatar"))
def get_login_data(self):
login_page = self._session.get(
f"{PROMETHEUS_WEB_BASE}/logowanie",
headers=HEADERS,
).text
return self._parse_login_page(login_page)
def _parse_login_page(self, html: str):
soup = BeautifulSoup(html, "html.parser")
request_verification_token = soup.select_one(
'input[name="__RequestVerificationToken"]'
)["value"]
captcha_text = soup.select_one('label[for="captchaUser"]').text
captcha_images = [img["src"] for img in soup.select(".v-captcha-image")]
return request_verification_token, captcha_text, captcha_images
def query_user_info(self, username: str, request_verification_token: str):
response = self._session.post(
f"{PROMETHEUS_WEB_BASE}/{ENDPOINT_ACCOUNT_QUERY_USER_INFO}",
{
"alias": username,
"__RequestVerificationToken": request_verification_token,
},
)
data = response.json()["data"]
return data["ExtraMessage"], data["ShowCaptcha"]
def login(
self,
username: str,
password: str,
reqest_verification_token: str,
captcha: str | None,
):
login_result = self._session.post(
f"{PROMETHEUS_WEB_BASE}/{ENDPOINT_LOGIN}",
data={
"Alias": username,
"Password": password,
"captchaUser": captcha,
"__RequestVerificationToken": reqest_verification_token,
},
headers=HEADERS,
)
if "Zła nazwa użytkownika lub hasło." in login_result.text:
raise InvalidCredentialsException()
def get_mobile_data(self):
response = self._session.get(
f"{PROMETHEUS_WEB_BASE}/{ENDPOINT_API_AP}", headers=HEADERS
)
if "Logowanie" in response.text:
raise NoLoggedInException()
soup = BeautifulSoup(response.text, "html.parser")
return json.loads(soup.select_one("input#ap")["value"])
def fs_ls(self, query: FsLsQuery):
response = self._session.get(
f"{PROMETHEUS_WEB_BASE}/{ENDPOINT_FS_LS}",
params=dataclasses.asdict(query),
)
if "Logowanie" in response.text:
raise NoLoggedInException()
return parse_fs_ls_response_form(response.text)
def get_cookies(self):
return self._session.cookies.get_dict()

View file

@ -0,0 +1,11 @@
PROMETHEUS_WEB_BASE = "https://eduvulcan.pl"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Linux; Android 13; SM-G935F Build/TQ3A.230901.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.107 Mobile Safari/537.36",
"X-Requested-With": "pl.edu.vulcan.hebe.ce",
}
ENDPOINT_LOGIN = "logowanie"
ENDPOINT_API_AP = "api/ap"
ENDPOINT_ACCOUNT_QUERY_USER_INFO = "Account/QueryUserInfo"
ENDPOINT_FS_LS = "fs/ls"

View file

@ -0,0 +1,10 @@
class PrometheusWebException(Exception):
pass
class InvalidCredentialsException(PrometheusWebException):
pass
class NoLoggedInException(PrometheusWebException):
pass

View file

@ -0,0 +1,24 @@
from datetime import date
from sdk.src.models.exam import Exam
from sdk.src.models.grade import Grade
from sdk.src.models.student import Student
class CoreInterface:
def select_student(self, context) -> None:
pass
def login(self) -> None:
pass
def get_students(self) -> list[Student]:
pass
def get_lucky_number(self) -> int:
pass
def get_grades(period_number: int) -> list[Grade]:
pass
def get_exams(from_: date, to: date) -> list[Exam]:
pass

View file

@ -0,0 +1,47 @@
from dataclasses import dataclass
from sdk.src.apis.hebe import HebeCertificate
from sdk.src.apis.hebe.student import HebeStudent
from sdk.src.interfaces.prometheus.utils import get_context_periods_from_hebe_periods
@dataclass
class PrometheusStudentContext:
student_id: int
unit_id: int
constituent_id: int
periods: dict[int, int]
register_id: int
hebe_url: str
symbol: str
messagebox_key: str
messagebox_name: str
@staticmethod
def create(hebe_student: HebeStudent):
return PrometheusStudentContext(
student_id=hebe_student.id,
unit_id=hebe_student.unit_id,
constituent_id=hebe_student.constituent_id,
periods=get_context_periods_from_hebe_periods(hebe_student.periods),
register_id=hebe_student.register_id,
hebe_url=hebe_student.rest_url,
symbol=hebe_student.symbol,
messagebox_key=hebe_student.messagebox_key,
messagebox_name=hebe_student.messagebox_name,
)
@dataclass
class PrometheusWebCredentials:
username: str
password: str
@dataclass
class PrometheusAuthContext:
prometheus_web_credentials: PrometheusWebCredentials
symbols: list[str] | None = None
hebe_certificate: HebeCertificate | None = None
prometheus_web_cookies: dict[str, str] | None = None
efeb_web_cookies: dict[str, str | None] | None = None

View file

@ -0,0 +1,10 @@
class PrometheusInterfaceException(Exception):
pass
class NoLoggedInException(PrometheusInterfaceException):
pass
class NoStudentSelectedException(PrometheusInterfaceException):
pass

View file

@ -0,0 +1,265 @@
from datetime import date, datetime
from sdk.src.apis.common.models import FsLsQuery
from sdk.src.apis.efeb.client import EfebClient
from sdk.src.apis.hebe import HebeClient, HebeCertificate
from sdk.src.apis.prometheus_web.client import PrometheusWebClient
from sdk.src.apis.prometheus_web.exceptions import (
InvalidCredentialsException,
NoLoggedInException as PrometheusWebNoLoggedInException,
)
from sdk.src.interfaces.core.interface import CoreInterface
from sdk.src.interfaces.prometheus.context import (
PrometheusStudentContext,
PrometheusAuthContext,
)
from sdk.src.interfaces.prometheus.exceptions import (
NoLoggedInException,
NoStudentSelectedException,
)
from sdk.src.interfaces.prometheus.utils import (
flat_map,
get_hebe_url,
parse_jwt_payload,
)
from sdk.src.models.student import Student
class PrometheusInterface(CoreInterface):
def __init__(
self,
auth_context: PrometheusAuthContext,
student_context: PrometheusStudentContext | None = None,
):
self._auth_context = auth_context
self._student_context = student_context
self._prometheus_web_client = PrometheusWebClient(
self._auth_context.prometheus_web_cookies
)
self._hebe_client = (
HebeClient(
certificate=self._auth_context.hebe_certificate,
rest_url=(
self._student_context.hebe_url if self._student_context else None
),
is_ce=True,
)
if self._auth_context.hebe_certificate
else None
)
self._efeb_clients = {}
if self._auth_context.symbols:
for symbol in self._auth_context.symbols:
if (
self._auth_context.efeb_web_cookies
and self._auth_context.efeb_web_cookies.get(symbol)
):
self._efeb_clients[symbol] = EfebClient(
cookies=self._auth_context.efeb_web_cookies[symbol],
symbol=symbol,
is_ce=True,
)
self._efeb_student_vars = {}
self._efeb_messages_vars = {}
def select_student(self, context: PrometheusStudentContext):
self._student_context = context
self._hebe_client.set_rest_url(context.hebe_url)
self._hebe_client.heartbeat()
def get_auth_context(self):
return self._auth_context
def login(self, captcha: str | None = None):
if not self._prometheus_web_client.is_logged():
result = self._login_prometheus(captcha)
if result:
return result
self._efeb_clients = {}
self._auth_context.efeb_web_cookies = {}
self._efeb_student_vars = {}
self._efeb_messages_vars = {}
self._auth_context.prometheus_web_cookies = (
self._prometheus_web_client.get_cookies()
)
# TODO: Expired hebe certificate
if not self._auth_context.hebe_certificate:
self._login_hebe()
self._login_efeb()
def _login_prometheus(self, captcha: str | None = None):
print("Logging for prometheus...")
request_verification_token, captcha_text, captcha_images = (
self._prometheus_web_client.get_login_data()
)
_, show_captcha = self._prometheus_web_client.query_user_info(
self._auth_context.prometheus_web_credentials.username,
request_verification_token,
)
if show_captcha:
return {
"status": "captcha",
"captcha_text": captcha_text,
"captcha_images": captcha_images,
}
try:
self._prometheus_web_client.login(
self._auth_context.prometheus_web_credentials.username,
self._auth_context.prometheus_web_credentials.password,
request_verification_token,
captcha,
)
except InvalidCredentialsException:
_, show_captcha = self._prometheus_web_client.query_user_info(
self._auth_context.prometheus_web_credentials.username,
request_verification_token,
)
return {
"status": "invalid_credentials",
"captcha_text": captcha_text,
"captcha_images": captcha_images,
"show_captcha": show_captcha,
}
def _login_hebe(self):
print("Logging for hebe...")
self._auth_context.hebe_certificate = HebeCertificate.generate()
self._hebe_client = HebeClient(
certificate=self._auth_context.hebe_certificate, rest_url=None, is_ce=True
)
try:
mobile_data = self._prometheus_web_client.get_mobile_data()
except PrometheusWebNoLoggedInException:
self._auth_context.prometheus_web_cookies = None
raise NoLoggedInException()
symbols: dict[str, list[any]] = {}
for token in mobile_data["Tokens"]:
payload = parse_jwt_payload(token)
if not symbols.get(payload["tenant"]):
symbols[payload["tenant"]] = [token]
else:
symbols[payload["tenant"]].append(token)
for symbol in symbols:
self._hebe_client.set_rest_url(get_hebe_url(symbol))
self._hebe_client.register_jwt(symbols[symbol])
self._auth_context.symbols = list(symbols.keys())
def _login_efeb(self):
if not self._auth_context.efeb_web_cookies:
self._auth_context.efeb_web_cookies = dict(
[(symbol, None) for symbol in self._auth_context.symbols]
)
for symbol in self._auth_context.efeb_web_cookies:
if self._auth_context.efeb_web_cookies[symbol]:
continue
print(f"Logging for efeb at {symbol}")
try:
student_prometheus_response = self._prometheus_web_client.fs_ls(
FsLsQuery(
wa="wsignin1.0",
wtrealm=f"https://dziennik-logowanie.vulcan.net.pl/{symbol}/Fs/Ls?wa=wsignin1.0&wtrealm=https://uczen.eduvulcan.pl/{symbol}/App&wctx=auth=studentEV&nslo=1",
wctx="nslo=1",
)
)
except PrometheusWebNoLoggedInException:
self._auth_context.prometheus_web_cookies = None
raise NoLoggedInException()
self._efeb_clients[symbol] = EfebClient(
cookies=self._auth_context.prometheus_web_cookies,
symbol=symbol,
is_ce=True,
)
student_login_response = self._efeb_clients[symbol].login_fs_ls(
query=FsLsQuery(
wa="wsignin1.0",
wtrealm=f"https://uczen.eduvulcan.pl/{symbol}/App",
wctx="auth=studentEV&nslo=1",
),
prometheus_response=student_prometheus_response,
)
self._efeb_student_vars[symbol] = self._efeb_clients[symbol].student_app(
login_response=student_login_response
)
messages_login_response = self._efeb_clients[symbol].login_fs_ls(
query=FsLsQuery(
wa="wsignin1.0",
wtrealm=f"https://wiadomosci.eduvulcan.pl/{symbol}/App",
wctx="auth=studentEV&nslo=1",
),
)
self._efeb_messages_vars[symbol] = self._efeb_clients[symbol].messages_app(
login_response=messages_login_response
)
self._auth_context.efeb_web_cookies[symbol] = self._efeb_clients[
symbol
].get_cookies()
def _check_is_auth_context_full(self):
if (
not self._auth_context.hebe_certificate
or not self._auth_context.prometheus_web_cookies
or not self._auth_context.symbols
):
raise NoLoggedInException()
def _check_is_student_selected(self):
if not self._student_context:
raise NoStudentSelectedException()
def get_students(self):
self._check_is_auth_context_full()
return flat_map(
[*map(self._get_students_in_symbol, self._auth_context.symbols)]
)
def _get_students_in_symbol(self, symbol: str):
self._hebe_client.set_rest_url(get_hebe_url(symbol))
hebe_students = self._hebe_client.get_students()
return [
Student.from_hebe_student(
hebe_student, PrometheusStudentContext.create(hebe_student)
)
for hebe_student in hebe_students
]
def get_lucky_number(self):
self._check_is_auth_context_full()
self._check_is_student_selected()
return self._hebe_client.get_lucky_number(
self._student_context.student_id,
self._student_context.constituent_id,
datetime.now(),
)
def get_grades(self, period_number: int):
self._check_is_auth_context_full()
self._check_is_student_selected()
return self._hebe_client.get_grades(
self._student_context.student_id,
self._student_context.unit_id,
self._student_context.periods[period_number],
)
def get_exams(self, from_: date, to: date):
self._check_is_auth_context_full()
self._check_is_student_selected()
return self._hebe_client.get_exams(self._student_context.student_id, from_, to)
def get_notes(self):
self._check_is_auth_context_full()
self._check_is_student_selected()
return self._hebe_client.get_notes(self._student_context.student_id)

View file

@ -0,0 +1,36 @@
import base64
import json
from sdk.src.apis.hebe.student import HebePeriod
def parse_jwt_payload(token: str):
# Split the JWT into parts
_, payload, _ = token.split(".")
# Decode the payload from Base64
# Add padding
payload += "=" * (-len(payload) % 4)
decoded_payload = base64.urlsafe_b64decode(payload).decode("utf-8")
# Parse the payload as JSON
payload_json = json.loads(decoded_payload)
return payload_json
def get_hebe_url(symbol: str, unit_symbol: str | None = None):
return f"https://lekcjaplus.vulcan.net.pl/{symbol}{'/' + unit_symbol if unit_symbol else ''}/api"
def get_context_periods_from_hebe_periods(hebe_periods: list[HebePeriod]):
periods = {}
for period in hebe_periods:
periods[period.number] = period.id
return periods
def flat_map(list: list[list]):
new = []
for sublist in list:
[new.append(item) for item in sublist]
return new

View file

@ -0,0 +1,43 @@
from dataclasses import dataclass
from datetime import date, datetime
from enum import Enum
class ExamType(Enum):
TEST = 0
SHORT_TEST = 1
CLASSWORK = 2
OTHER = 3
@staticmethod
def from_hebe_type_name(type_name: str):
match type_name:
case "Sprawdzian":
return ExamType.TEST
case "Kartkówka":
return ExamType.SHORT_TEST
case "Praca klasowa":
return ExamType.CLASSWORK
case _:
return ExamType.OTHER
@dataclass
class Exam:
deadline: date
subject: str
type: ExamType
description: str
creator: str
created_at: datetime
@staticmethod
def from_hebe_dict(data: dict):
return Exam(
deadline=datetime.fromtimestamp(data["Deadline"]["Timestamp"] / 1000),
subject=data["Subject"]["Name"],
type=ExamType.from_hebe_type_name(data["Type"]),
description=data["Content"],
creator=data["Creator"]["DisplayName"],
created_at=datetime.fromtimestamp(data["DateCreated"]["Timestamp"] / 1000),
)

View file

@ -0,0 +1,29 @@
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Grade:
value: str
is_point: bool
point_numerator: int
point_denominator: int
weight: float
name: str
created_at: datetime
subject: str
creator: str
@staticmethod
def from_hebe_dict(data: dict[str, any]):
return Grade(
value=data["Content"],
is_point=data["Numerator"] != None,
point_numerator=data["Numerator"],
point_denominator=data["Denominator"],
weight=data["Column"]["Weight"],
name=data["Column"]["Name"] or data["Column"]["Code"],
created_at=datetime.fromtimestamp(data["DateCreated"]["Timestamp"] / 1000),
subject=data["Column"]["Subject"]["Name"],
creator=data["Creator"]["DisplayName"],
)

View file

@ -0,0 +1,21 @@
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Note:
name: str | None
content: str
points: str | None
creator: str
created_at: datetime
@staticmethod
def from_hebe_dict(data: dict):
return Note(
name=data["Category"]["Name"] if data["Category"] else None,
content=data["Content"],
points=str(data["Points"]) if data["Points"] else None,
creator=data["Creator"]["DisplayName"],
created_at=datetime.fromtimestamp(data["DateValid"]["Timestamp"] / 1000),
)

View file

@ -0,0 +1,48 @@
from dataclasses import dataclass
from datetime import date
from typing import Generic, TypeVar
from sdk.src.apis.hebe.student import HebePeriod, HebeStudent
@dataclass
class Period:
id: int
number: int
current: bool
from_: date
to: date
@staticmethod
def from_hebe_period(period: HebePeriod):
return Period(
id=period.id,
number=period.number,
current=period.current,
from_=period.from_,
to=period.to,
)
T = TypeVar("T")
@dataclass
class Student(Generic[T]):
full_name: str
is_parent: bool
class_name: str
register_number: int | None
periods: list[Period]
context: T
@staticmethod
def from_hebe_student(student: HebeStudent, context: T):
return Student(
full_name=student.full_name,
is_parent=student.is_parent,
class_name=student.class_name,
register_number=student.register_student_number,
periods=list(map(Period.from_hebe_period, student.periods)),
context=context,
)

View file

@ -1,37 +0,0 @@
from impl.hebece.src.api import *
from impl.hebece.src.parser import *
from impl.hebece.src.utils import *
from impl.hebece.src.signer import *
if __name__ == '__main__':
today = datetime.today().strftime('%d-%m-%y')
start_date, end_date = get_current_week()
token = APILogin(login = input("login: "),password = input("password: "))
if not token:
print("You entered wrong login, password or VULCAN asked for captcha. Verify your login and password and try to log into eduVULCAN from your browser.")
input("Press Enter to exit...")
exit()
tenant = get_tenant_from_jwt(token)
content, dinfoJWT = JWTLogin(token, debug=True)
content, dinfoHEBE = HEBELogin(tenant, debug=True)
Name, SecondName, Surname, Class, PupilID, SchoolID, ConstituentID, UnitID, PeriodID = getUserInfo(tenant)
content, dinfoLUCK = getLuckyNumber(tenant=tenant, schoolid=SchoolID, pupilid=PupilID, constituentid=ConstituentID, debug=True)
content, dinfoGRADE = getGrades(tenant=tenant, schoolid=SchoolID, pupilid=PupilID, unitid=UnitID, periodid=PeriodID, debug=True)
content, dinfoTIME = getTimetable(tenant=tenant, schoolid=SchoolID, pupilid=PupilID, start_date=start_date, end_date=end_date, debug=True)
content, dinfoEXAM = getExams(tenant=tenant, schoolid=SchoolID, pupilid=PupilID, start_date=start_date, end_date=end_date, debug=True)
print(f"\nJWT Status: {dinfoJWT[0]} {dinfoJWT[1]}")
print(f"HEBE Status: {dinfoHEBE[0]} {dinfoHEBE[1]}")
print(f"Lucky Number Status: {dinfoLUCK[0]} {dinfoLUCK[1]}")
print(f"Grades Status: {dinfoGRADE[0]} {dinfoGRADE[1]}")
print(f"Timetable Status: {dinfoTIME[0]} {dinfoTIME[1]}")
print(f"Exams Status: {dinfoEXAM[0]} {dinfoEXAM[1]}")