Fri Jun 23 16:28:43 CEST 2023

This commit is contained in:
mnjx
2023-06-23 20:18:17 +02:00
parent a820c87277
commit 8168ff3cd4
7 changed files with 324 additions and 120 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
SHELL=/bin/bash
install:
python3 -m pip install -r requirements.txt --break-system-packages
python3 -m pip install -r requirements.txt
./.config/mkdirs.sh
+3 -3
View File
@@ -39,8 +39,8 @@ SSH: `git clone git@github.com:mnjx/solen.git`
# Usage
Upon startup, the user os prompted to enter their ŠkolaOnLine credentials.
If the login proccess is successfull, the preconfigured functions in `/bin/solen`
Upon startup, the user is prompted to enter their ŠkolaOnLine credentials.
If the login proccess is successful, the preconfigured functions in `/bin/solen`
will execute. By default, the functions are: showing the schedule for this week,
showing the schedule for today and tomorrow and showing the grades. You can edit
that file if you want to change what is being displayed. Alternatively, you can
@@ -57,4 +57,4 @@ Here they are:
| -p, --password | provide the password for login |
| -l, --local | only show the local data without attempting to connect to the internet |
| -d, --delete-local-files | deletes all locally stored user data, including schedules, grades and login credentials |
| -b, --backup PASSPHRASE DESTINATION | securly backs up all local user data into a .tar.gz archive |
| -b, --backup PASSPHRASE DESTINATION | backs up all local user data into a .tar.gz archive |
+76
View File
@@ -0,0 +1,76 @@
"""
Cryptography related stuff - mostly
encrypting and decrypting user data.
"""
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.fernet import Fernet
import base64
import os
def get_key(password: str) -> bytes:
"""
Derives a bytes key
from a given password.
Useful when you want
to generate the same
key over and over again.
E.g. to encrypt/decrypt
locally stored user data.
"""
password = password.encode()
salt = b'?\x0f\xa1\x97otT\xe48\xc9O\xb4\x1f\xef\xf5\xea'
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256,
length=32,
salt=salt,
iterations=100000,
backend=default_backend()
)
key = base64.urlsafe_b64encode(kdf.derive(password))
return key
def encrypt_and_write_to_file(password: str, strdata: str, file_path: str) -> bool:
"""
Encrypts the given string data
and stores them in a file.
"""
try:
key = get_key(password)
f = Fernet(key)
encrypted = f.encrypt(strdata.encode())
with open(file_path, "wb") as ef:
ef.write(encrypted)
return 0
except:
return 1
def read_from_file_and_decrypt(password: str, file_path: str) -> str:
"""
Reads contents of an encrypted file
and decrypts them.
"""
try:
key = get_key(password)
f = Fernet(key)
with open(file_path, "rb") as ef:
decrypted = f.decrypt(ef.read())
return decrypted.decode()
except:
return ""
+113 -76
View File
@@ -1,39 +1,38 @@
"""
Functions for grades related stuff.
Like 1, 2, 3, 4 and 5, in what subject,
what is the average of a subject, ...
"""
from solenlib import networking as nw
from solenlib import crypto as cr
from cryptography.fernet import Fernet
from bs4 import BeautifulSoup
import os
not_login = 0
def fetch_locally() -> list:
def fetch_locally(password, username) -> list:
"""
Helper function for exctracting
data from local files.
"""
global not_login
if not_login == 1:
not_login = 0
#print("Could not sign in (you may have entered invalid login credentials)")
#print("Fetching grades locally\n")
grades = grades_from_file()
grades = grades_from_file(password, username)
return grades
def get_grades(password, url: str) -> list:
"""
Extracts the grades from the HTML soup
and returns them in a list of dictionaries.
"""
"""
# deprecated
def get_avg(grades: list) -> str:
if len(grades) < 1:
return ""
sum_ = 0
for i in grades:
sum_ += int(i)
avg = round(sum_ / len(grades), 2)
avg = str(avg)
if len(avg) == 3:
avg += "0"
return avg
"""
def get_grades(url: str):
try:
html = nw.get_html_from(url)
@@ -51,15 +50,21 @@ def get_grades(url: str):
subject = rows[i].find("td").text
temp_dict = {}
temp_dict["SUBJECT"] = subject
cell = rows[i].find_all("span", class_="VHVysl")
cells = rows[i].find_all("span", class_="VHVysl")
temp = []
for j in range(len(cell)):
temp.append(cell[j].text)
temp_dict["GRADES"] = temp
for j in range(len(cells)):
if cells[j].parent.has_attr("class"):
if "VHUzaverka" not in cells[j].parent["class"]:
temp.append(cells[j].text)
else:
temp.append(cells[j].text)
temp_dict["FINAL"] = rows[i].find("td", class_="VHUzaverka CCATooltip").text.replace(" ", "")
avg = rows[i].find("td", class_="VHCelkPrumer").text
avg = rows[i].find("td", class_="VHCelkPrumer").text.replace(",", ".")
temp_dict["AVG"] = avg
temp_dict["GRADES"] = temp
grades.append(temp_dict)
return grades
@@ -67,18 +72,23 @@ def get_grades(url: str):
global not_login
not_login = 1
grades = fetch_locally()
grades = fetch_locally(password, username)
return grades
def show_grades(local=False):
def show_grades(password, username, local=False):
"""
Shows the grades in the terminal
in a pretty way.
"""
if local == False:
if nw.is_connected() == True:
grades = get_grades("/SOL/App/Hodnoceni/KZH001_HodnVypisStud.aspx")
grades_to_file(grades)
grades = get_grades(password, "/SOL/App/Hodnoceni/KZH001_HodnVypisStud.aspx")
grades_to_file(password, username, grades)
else:
grades = grades_from_file()
grades = grades_from_file(password, username)
else:
grades = grades_from_file()
grades = grades_from_file(password, username)
whitespace = ""
max_len_of_subject = 0
@@ -98,73 +108,100 @@ def show_grades(local=False):
print("N/A\n")
import os
if os.path.exists(f"{os.environ['HOME']}/.local/lib/solendata/grades.csv"):
os.remove(f"{os.environ['HOME']}/.local/lib/solendata/grades.csv")
if os.path.exists(f"{os.environ['HOME']}/.local/lib/solendata/{nw.get_user_hash(username)}-encrypted_grades"):
os.remove(f"{os.environ['HOME']}/.local/lib/solendata/{nw.get_user_hash(username)}-encrypted_grades")
return
for i in range(len(grades)):
if grades[i]["GRADES"] != []:
whitespace = " " * (max_len_of_subject - len(grades[i]["SUBJECT"]))
print(grades[i]["SUBJECT"] + whitespace + "|", end=" ")
print(grades[i]["FINAL"], "|", end=" ")
print(grades[i]["AVG"], "|", end=" ")
print("".join([grades[i]["GRADES"][j]+" " for j in range(len(grades[i]["GRADES"]))]), end=" ")
print("")
print("".join([grades[i]["GRADES"][j]+" " for j in range(len(grades[i]["GRADES"]))]))
else:
if grades[i]["FINAL"] is not None:
whitespace = " " * (max_len_of_subject - len(grades[i]["SUBJECT"]))
print(grades[i]["SUBJECT"] + whitespace + "|", end=" ")
print(grades[i]["FINAL"], "|")
def grades_to_file(password, username, grades=None):
"""
Writes the grades to a local file
for later use and extraction.
"""
def grades_to_file(grades=None): # !!! make sure the format of the grades list is saved correctly !!!
import csv
if grades is None:
try:
grades = get_grades("/SOL/App/Hodnoceni/KZH001_HodnVypisStud.aspx")
grades = get_grades(password, username, "/SOL/App/Hodnoceni/KZH001_HodnVypisStud.aspx")
except Exception:
print("An error occured, you probably can't access the internet right now")
return
data = []
lstrdata = []
with open(f"{os.environ['HOME']}/.local/lib/solendata/grades.csv", mode="w", newline="") as f:
for i in range(len(grades)):
temp = {}
temp["SUBJECT"] = grades[i]["SUBJECT"]
temp["AVG"] = grades[i]["AVG"]
temp_grades = []
for j in range(len(grades[i]["GRADES"])):
temp_grades.append(grades[i]["GRADES"][j])
temp["GRADES"] = temp_grades
data.append(temp)
for i in range(len(grades)):
temp = {}
temp["SUBJECT"] = grades[i]["SUBJECT"]
temp["FINAL"] = grades[i]["FINAL"]
temp["AVG"] = grades[i]["AVG"]
temp_grades = []
for j in range(len(grades[i]["GRADES"])):
temp_grades.append(grades[i]["GRADES"][j])
temp["GRADES"] = temp_grades
data.append(temp)
writer = csv.writer(f)
for row in data:
_row = []
_row.append(row["SUBJECT"])
_row.append(row["AVG"])
for i in row["GRADES"]:
_row.append(i)
writer.writerow(_row)
for row in data:
lstrdata.append(row["SUBJECT"])
lstrdata.append(",")
lstrdata.append(row["FINAL"])
lstrdata.append(",")
lstrdata.append(row["AVG"])
lstrdata.append(",")
for i in row["GRADES"]:
lstrdata.append(i)
lstrdata.append(",")
del lstrdata[-1]
lstrdata.append("\n")
def grades_from_file() -> list:
strdata = "".join(lstrdata)
cr.encrypt_and_write_to_file(password, strdata, f"{os.environ['HOME']}/.local/lib/solendata/{nw.get_user_hash(username)}-encrypted_grades")
def grades_from_file(password, username) -> list:
"""
Extracts the grades from the local
file and returns them in a better
format.
"""
import csv
grades = []
from io import StringIO
try:
with open(f"{os.environ['HOME']}/.local/lib/solendata/grades.csv", mode="r") as f:
reader = csv.reader(f)
rows = []
for row in reader:
rows.append(row)
strdata = cr.read_from_file_and_decrypt(password, f"{os.environ['HOME']}/.local/lib/solendata/{nw.get_user_hash(username)}-encrypted_grades")
except:
print("Could not read the file")
return
f = StringIO(strdata)
reader = csv.reader(f, delimiter=",")
grades = []
rows = []
for row in reader:
rows.append(row)
for row in range(len(rows)):
temp = {}
for row in range(len(rows)):
temp = {}
temp["SUBJECT"] = rows[row][0]
temp["AVG"] = rows[row][1]
temp_lst = []
for i in range(2, len(rows[row])):
temp_lst.append(rows[row][i])
temp["GRADES"] = temp_lst
temp["SUBJECT"] = rows[row][0]
temp["FINAL"] = rows[row][1]
temp["AVG"] = rows[row][2]
temp_lst = []
for i in range(3, len(rows[row])):
temp_lst.append(rows[row][i])
temp["GRADES"] = temp_lst
grades.append(temp)
except FileNotFoundError:
print("No grades.csv file found\n")
grades.append(temp)
return grades
+42 -5
View File
@@ -1,3 +1,7 @@
"""
Functions for network related stuff.
"""
import requests
import hashlib
from bs4 import BeautifulSoup
@@ -8,6 +12,11 @@ F_not_anymore = 0
session = requests.Session()
def _read_shadow() -> str:
"""
Reads the shadow file and returns it in
a suitable format for other functions.
"""
result = ""
with open(f"{os.environ['HOME']}/.local/lib/solendata/shadow", "r") as f:
for row in f:
@@ -16,6 +25,11 @@ def _read_shadow() -> str:
return result
def _save_creds(user: str, pwd: str):
"""
Saves credentials into the shadow file
by appending them.
"""
creds = _get_creds_hash(user, pwd)
try:
@@ -26,6 +40,11 @@ def _save_creds(user: str, pwd: str):
print("No shadow file found")
def _eval_creds(creds: str) -> bool:
"""
Evaluates the credentials for if
they are in the shadow file or not.
"""
shadow = _read_shadow()
if creds in shadow:
@@ -34,13 +53,26 @@ def _eval_creds(creds: str) -> bool:
return False
def _get_creds_hash(user: str, pwd: str) -> str:
"""
Creates a sha512 hash from the username
and password.
"""
user_hash = hashlib.sha512(bytes(user, 'utf-8')).hexdigest()
pwd_hash = hashlib.sha512(bytes(pwd, 'utf-8')).hexdigest()
creds = user_hash+":"+pwd_hash
return creds
def get_user_hash(user: str) -> str:
return str(hashlib.sha512(bytes(user, 'utf-8')).hexdigest())
def login(user: str, pwd: str) -> bool:
"""
Tries to send a request to the SOL
servers in order to log in.
"""
if is_connected():
url: str = "https://aplikace.skolaonline.cz/SOL/Prihlaseni.aspx"
data = {
@@ -61,21 +93,21 @@ def login(user: str, pwd: str) -> bool:
post_login = session.post(url, data=data)
if post_login.status_code != 200:
#print("Could not sign in")
return False
else:
#print("Sign in successful")
return True
else:
#print("Could not sign in")
#print("Fetching data from local files")
if _eval_creds(_get_creds_hash(user, pwd)):
return True
else:
return False
def is_connected() -> bool:
"""
Checks whether there is internet
access present.
"""
import socket
try:
@@ -88,6 +120,11 @@ def is_connected() -> bool:
return False
def get_html_from(params=None) -> str:
"""
Fetches the HTML markup of
a specified subsite of SOL.
"""
base = "https://aplikace.skolaonline.cz"
if params is not None:
+47 -5
View File
@@ -1,3 +1,8 @@
"""
Functions related to today's
and tomorrow's schedule.
"""
from solenlib import networking as nw
from bs4 import BeautifulSoup
import os
@@ -5,16 +10,25 @@ import os
not_login = 0
def fetch_locally() -> list:
"""
Helper function for exctracting
data from a local file.
"""
global not_login
if not_login == 1:
not_login = 0
#print("Could not sign in (you may have entered invalid login credentials)")
#print("Fetching the schedule for this week locally\n")
schedule = schedule_from_file()
return schedule
def get_schedule(url: str):
def get_schedule(url: str) -> list:
"""
Filters & extracts the schedule from
the HTML soup.
"""
try:
html = nw.get_html_from(url)
@@ -120,10 +134,16 @@ def get_schedule(url: str):
global not_login
not_login = 1
schedule = fetch_locally()
return schedule
return fetch_locally()
def schedule_from_file_to_list():
"""
Extracts the schedule from
a local file and returns
it in a list, which is better
for the get_whitespace() function.
"""
schedule = []
schedule_ = []
@@ -161,6 +181,13 @@ def schedule_from_file_to_list():
return schedule
def get_whitespace(key: str, idx=0) -> int:
"""
Helper function that calculates
how much whitespaces are needed
in order to display the table grid
correctly.
"""
schedule = schedule_from_file_to_list()
max_len = 0
@@ -178,6 +205,11 @@ def get_whitespace(key: str, idx=0) -> int:
return max_len
def show_schedule(local=False):
"""
Shows the schedule on the
terminal in a pretty way.
"""
if local == False:
# if there is internet access, load stuff from the web
# if not, load the schedule.csv file
@@ -228,6 +260,12 @@ def show_schedule(local=False):
print("")
def schedule_to_file(schedule=None):
"""
Writes the schedule to
a local file for later
use.
"""
import csv
if schedule is None:
try:
@@ -269,6 +307,10 @@ def schedule_to_file(schedule=None):
writer.writerow(row)
def schedule_from_file() -> list:
"""
"""
import csv
schedule = []
+42 -30
View File
@@ -7,7 +7,8 @@ from solenlib import schedule_tdy_tmr as st
import sys
import getpass
import argparse
import datetime
import os
from datetime import datetime
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog="ŠOLEn", description="ŠOLEn, an enhanced version of the ŠkolaOnLine online school system")
@@ -20,9 +21,7 @@ if __name__ == "__main__":
parser.add_argument("-l", "--local", action="store_true", help="Only fetch local files, don't connect to the internet")
parser.add_argument("-d", "--delete-local-files", action="store_true", help="Delete all local user data, including schedules, grades and user credentials")
parser.add_argument("-b", "--backup", action="store_true", help="Securely back up all user data and store them in a .tar.gz archive in the destination directory")
parser.add_argument("-D", "--backup-directory", action="store", help="Backup destination directory")
parser.add_argument("-P", "--backup-password", action="store", help="Backup password to encrypt the archive with")
parser.add_argument("-b", "--backup", action="store_true", help="Back up all user data and store them in a .tar.gz archive in the destination directory")
parser.add_argument("-R", "--backup-restore", action="store", nargs=1, metavar="PATH", help="Specify the path to a .tar.gz archive that was earlier created with ŠOLEn to restore backed up user data")
args = parser.parse_args()
@@ -30,37 +29,51 @@ if __name__ == "__main__":
if args.delete_local_files:
try:
print("Removing all schedules and grades ...", end=" ")
os.system(f"rm -rf {os.environ['HOME']}/.local/lib/solendata/*.csv")
os.system(f"rm -rf {os.environ['HOME']}/.local/lib/solendata/*")
print("done")
print("Removing all user credentials ...", end=" ")
os.system(f"rm -rf {os.environ['HOME']}/.local/lib/solendata/shadow")
print("done")
print("All user data removed successfully")
sys.exit(0)
except:
print("Could not remove user data")
sys.exit(1)
elif args.backup:
try:
if not args.backup_directory or not args.backup_password:
print("-D, --backup-directory BACKUP-DIRECTORY and -P, --backup-password BACKUP-PASSWORD are required")
sys.exit(1)
else:
nowtime = str(datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
try:
print("Creating the backup archive ...", end=" ")
os.system(f"tar cf solen-backup-{nowtime} {os.path.abspath(args.backup-directory)}")
print("done")
print("Backup archive created successfully")
sys.exit(0)
except:
print("The 'tar' utility is not installed")
sys.exit(1)
nowtime = str(datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
try:
print("Creating the backup archive ...", end=" ")
os.system(f"tar cf solen-backup-{nowtime}.tar {os.getcwd()}")
print("done")
print("Compressing the archive ...", end=" ")
os.system(f"gzip {os.getcwd()}/solen-backup-{nowtime}.tar")
print("done")
print("Backup archive created successfully")
except:
print("The 'tar' utility or the 'gzip' utility is not installed")
except:
print("Could not backup user data")
sys.exit(1)
elif args.backup_restore:
try:
backup_path = args.backup_restore
print("Preparing ...", end=" ")
os.system(f"mkdir {os.getcwd()}/.solentmp")
print("done")
print("Uncompressing the archive ...", end=" ")
os.system(f"gunzip {backup_path}")
print("done")
print("Unpacking the archive ...", end=" ")
os.system(f"tar xzjf {backup_path} {os.getcwd()}/.solentmp")
print("done")
print("Moving the files ...", end=" ")
os.system(f"mv {os.getcwd()}/.solentmp/* {os.environ['HOME']}/.local/lib/solendata")
print("done")
print("Cleaning up ...", end=" ")
os.system(f"rm -rf {os.getcwd()}/.solentmp")
print("done")
print("Backup restored successfully")
except:
print("Could not restore the backup")
else:
if args.username:
user = str(args.username)
@@ -76,7 +89,7 @@ if __name__ == "__main__":
pwd = str(input("Zadejte heslo: "))
if not nw.login(user, pwd):
print("Login unsuccessfull")
print("Login unsuccessful")
sys.exit(1)
else:
try:
@@ -92,20 +105,19 @@ if __name__ == "__main__":
st.show_schedule()
if args.grades:
if args.local:
gr.show_grades(local=True)
gr.show_grades(pwd, user, local=True)
else:
gr.show_grades()
gr.show_grades(pwd, user)
if not args.grades and not args.today_tomorrow and not args.week:
if args.local:
sw.show_schedule(local=True)
st.show_schedule(local=True)
gr.show_grades(local=True)
gr.show_grades(pwd, user, local=True)
else:
sw.show_schedule()
st.show_schedule()
gr.show_grades()
sys.exit(0)
gr.show_grades(pwd, user)
except Exception as e:
raise e
print("Could not execute")
sys.exit(1)