From 2f71cce24ddd47cf9dcb5b00739bb6dac08ae0c6 Mon Sep 17 00:00:00 2001 From: RoRo160 <79665729+RoRo160@users.noreply.github.com> Date: Sun, 5 Sep 2021 20:05:58 +0200 Subject: [PATCH 1/8] .gitignore + requirements.txt updated --- .gitignore | 1 + requirements.txt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index deddec1..0fbe773 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea +__pycache__ test test.py diff --git a/requirements.txt b/requirements.txt index e69de29..57a6f7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests~=2.25.1 +beautifulsoup4~=4.9.3 From 12fbfb241ae30b95b380f893ad9c7cf4a679a036 Mon Sep 17 00:00:00 2001 From: RoRo160 <79665729+RoRo160@users.noreply.github.com> Date: Mon, 6 Sep 2021 21:48:50 +0200 Subject: [PATCH 2/8] IServ class: - handles login, logout and cookies, --- iserv.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 iserv.py diff --git a/iserv.py b/iserv.py new file mode 100644 index 0000000..0fcb883 --- /dev/null +++ b/iserv.py @@ -0,0 +1,64 @@ +import requests +from bs4 import * + + +class LoginError(Exception): + pass + + +class IServ: + paths = { + "login": "/iserv/app/login", + "logout": "/iserv/app/logout" + } + messages = { + "login_failed": "Anmeldung fehlgeschlagen!" + } + + def __init__(self, domain): + self.domain = domain + self._csrf_token = None + self._s = requests.Session() + + def login(self, user, pw): + # send post request with session object + r = self._s.post( + url=self.domain + IServ.paths['login'], # login path + data=f"_password={pw}&_username={user}", # pw ad user in body of request + headers={ + "Content-Type": "application/x-www-form-urlencoded" # tell server kind of form, necessary + } + ) + + # check if login was successful + if IServ.messages["login_failed"] in r.text: + raise LoginError("Login failed") + + # find and store csrf token, needed on logout + self._csrf_token = self._find_csrf(r.text) + + # return True if login was successful + return True + + def logout(self): + r = self._s.get( + # login path + url=self.domain + IServ.paths['logout'], + # add csrf token to query + params={"_csrf": self._csrf_token} + ) + + @staticmethod + def _find_csrf(doc: str): + s = BeautifulSoup(doc, "html.parser") + tag = s.find('body')\ + .find_all('div')[0]\ + .find_all('div')[0]\ + .find_all('ul')[0]\ + .find_all('li')[1]\ + .find('div')\ + .find('ul')\ + .find_all('li')[4]\ + .find('a') + + return tag.attrs["href"].split('=')[-1] From e96966dc227c6851a38cc499a0683b236a43ed8a Mon Sep 17 00:00:00 2001 From: RoRo160 <79665729+RoRo160@users.noreply.github.com> Date: Tue, 7 Sep 2021 21:44:51 +0200 Subject: [PATCH 3/8] .plan_changes() method: - get plan changes - filter by day, week and courses other small changes --- iserv.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/iserv.py b/iserv.py index 0fcb883..52130dc 100644 --- a/iserv.py +++ b/iserv.py @@ -1,7 +1,18 @@ +import datetime import requests +import bs4 from bs4 import * +DAYS = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday" +] + + class LoginError(Exception): pass @@ -9,7 +20,8 @@ class LoginError(Exception): class IServ: paths = { "login": "/iserv/app/login", - "logout": "/iserv/app/logout" + "logout": "/iserv/app/logout", + "plan": "/iserv/plan/show/raw/" } messages = { "login_failed": "Anmeldung fehlgeschlagen!" @@ -31,6 +43,7 @@ def login(self, user, pw): ) # check if login was successful + # TODO check in header if IServ.messages["login_failed"] in r.text: raise LoginError("Login failed") @@ -41,7 +54,7 @@ def login(self, user, pw): return True def logout(self): - r = self._s.get( + self._s.get( # login path url=self.domain + IServ.paths['logout'], # add csrf token to query @@ -62,3 +75,81 @@ def _find_csrf(doc: str): .find('a') return tag.attrs["href"].split('=')[-1] + + def plan_changes( + self, + courses=None, + days=None, + week: int = datetime.date.today().isocalendar()[1], + plan_name="0_Vertretungen (Schüler)" + ): + # default args + if days is None: + days = [""] + if type(days) == str: + days = [days] + days = [d.lower() for d in days] + + if courses is None: + courses = [""] + if type(courses) == str: + courses = [courses] + courses = [c.lower() for c in courses] + + # TODO on sunday next week + # get doc + r = self._s.get( + url=self.domain + self.paths["plan"] + plan_name + "/" + str(week) + "/w/w00000.htm" + ) + # validate return + if not r.headers["content-disposition"] == "inline; filename=w00000.htm": + raise Exception("invalid return") + + # create soup + s = BeautifulSoup(r.text, "html.parser") # , from_encoding="utf-8") + + # get list of needed tables in doc + tables = s.find_all("table", class_="subst") + + entries = {} + + for i in range(len(tables)): + # get days + day_temp = DAYS[i] + # check day + if day_temp not in days: + # skip this table + continue + entries[day_temp] = [] + + # iterate through all rows of table and store data + for row in tables[i]: + if type(row) != bs4.element.NavigableString: + # get all values of columns + clm = [i.string for i in row.find_all("td")] + try: + add = False + for course in courses: + add_temp = True + for letter in course: + if letter not in clm[0].lower(): + add_temp = False + if add_temp: + add = True + if add: + details = { + "courses": clm[0], + "hour": clm[1], + "subject": clm[2], + "teacher": clm[3], + "room": clm[4], + "comments": clm[5], + "org_subject": clm[6], + "org_teacher": clm[7], + "type": clm[8], + } + # print(details) + entries[day_temp].append(details) + except IndexError: + pass + return entries From 90d7a0f88294aa8c770785fce347c3651a041e0b Mon Sep 17 00:00:00 2001 From: RoRo160 <79665729+RoRo160@users.noreply.github.com> Date: Wed, 8 Sep 2021 18:19:06 +0200 Subject: [PATCH 4/8] README.md update --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f2f59f..f5b319f 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,11 @@ background. ### Features: +- Login/Logout with your IServ account +- plan changes, by week, day, filtered by course + ### Coming soon: -- Summarized representation plan only with entries from your class - Get all tasks - Read all your emails - Notifications From 5de35fbf817c2f56a8b8ca3aabc1764444017511 Mon Sep 17 00:00:00 2001 From: RoRo160 <79665729+RoRo160@users.noreply.github.com> Date: Wed, 8 Sep 2021 20:21:26 +0200 Subject: [PATCH 5/8] Get tasks from IServ: - returns list containing every task with details as a dictionary - filter by status and sort (for now only options supported by server, local filtering, searching and sorting will be added later) --- iserv.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/iserv.py b/iserv.py index 52130dc..b1e983c 100644 --- a/iserv.py +++ b/iserv.py @@ -21,7 +21,8 @@ class IServ: paths = { "login": "/iserv/app/login", "logout": "/iserv/app/logout", - "plan": "/iserv/plan/show/raw/" + "plan": "/iserv/plan/show/raw/", + "tasks": "/iserv/exercise.csv" } messages = { "login_failed": "Anmeldung fehlgeschlagen!" @@ -153,3 +154,47 @@ def plan_changes( except IndexError: pass return entries + + def tasks( + self, + status: str = "current", + sort_by: str = "enddate", + sort_dir: str = "DESC" + ): + # TODO add option to filter tasks + + r = self._s.get( + url=self.domain + IServ.paths["tasks"], + params={ + "filter[status]": status, + "sort[by]": sort_by, + "sort[dir]": sort_dir + } + ) + + tasks = [] + + tasks_ = r.text.splitlines() + tasks_.pop(0) + + # TODO use csv.reader to solve problems with quoted values and separators between those + # tasks_ = csv.reader(r.text, quotechar='"') + + for t in tasks_: + task = t.split(";") + for i in range(len(task)): + if task[i].lower() == "ja": + task[i] = True + elif task[i].lower() == "nein": + task[i] = False + + tasks.append({ + "task": task[0], + "startdate": task[1], + "enddate": task[2], + "tags": task[3], + "done": task[4], + "review": task[5], + }) + + return tasks From d1870a0a325b238c107bf643eca071f72fd9c9de Mon Sep 17 00:00:00 2001 From: RoRo160 <79665729+RoRo160@users.noreply.github.com> Date: Thu, 9 Sep 2021 16:17:07 +0200 Subject: [PATCH 6/8] README.md updated. Explanations added. Other fixes. --- README.md | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f5b319f..1f67bc9 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,101 @@ -# IServ.py +# IServ.py + +`Made by RoRo160` This module provides an easy way to communicate with your IServ account. I reverse-engineered parts of the internal IServ api and recreated some http requests your browser would do in the background. +> 🔴 **WARNING:**
+> This module does **NOT** use an official api! +> +> Extensive usage might lead to problems with your IServ account. +> +> **USE AT YOUR OWN RISK!!!** + ### Features: - Login/Logout with your IServ account -- plan changes, by week, day, filtered by course +- Plan changes, by week, day, filtered by course +- Get all tasks ### Coming soon: -- Get all tasks - Read all your emails - Notifications -> **WARNING:**
-> This module does **NOT** use an official api!
-> Extensive usage might lead to problems with your IServ account.
->
-> **USE AT YOUR OWN RISK!!!** +## Usage: + +> ⚠ **Note:** +> To run you might need to install some dependencies. +> +> Do that by running the following command: +> ````shell +> pip install -r requirements.txt +> ```` + +### Login/Logout: +````python +from iserv import * + +# create IServ object, link your IServ server +# (please add "https://" before domain name and don't add a "/" at the end!) +iserv = IServ("https://your.iserv.example") + +# login to your account +iserv.login("your.username", "password") + +# >>> do what ever you want here <<< + +# do NOT forget to logout +iserv.logout() +```` + +> #### 💡 **Advise:** Time delay between requests +> Many requests in a short time period may seem suspicious to the server. +> +> To prevent any issues with your account add a **random time delay** between requests. +> +> You can do this as shown here: +> +> ````python +> import time +> import random +> +> # first request +> +> # random break between two requests +> time.sleep(random.uniform(0.5, 10.0)) +> +> # second request +> ```` +> + + +### How to use all the features? + +#### Plan changes: +````python +changes = iserv.plan_changes( + courses=["7c", "10b"], + days=["monday", "thursday"] +) + +# do something with plan changes dictionary +```` +#### Tasks: +````python +tasks = iserv.tasks( + status="current", + sort_by="enddate", + sort_dir="DESC" +) + +# do something with task list +```` + +# + +> By RoRo160 +> +> [My GitHub](https://github.com/RoRo160) From 16ba8e791797bda538149f0265b97bc6e60ad56b Mon Sep 17 00:00:00 2001 From: RoRo160 <79665729+RoRo160@users.noreply.github.com> Date: Thu, 9 Sep 2021 16:22:55 +0200 Subject: [PATCH 7/8] Small changes: Type hint, corrected day filter algorithm in IServ.plan_changes() --- iserv.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/iserv.py b/iserv.py index b1e983c..dc70cac 100644 --- a/iserv.py +++ b/iserv.py @@ -33,7 +33,7 @@ def __init__(self, domain): self._csrf_token = None self._s = requests.Session() - def login(self, user, pw): + def login(self, user: str, pw: str): # send post request with session object r = self._s.post( url=self.domain + IServ.paths['login'], # login path @@ -118,9 +118,12 @@ def plan_changes( # get days day_temp = DAYS[i] # check day - if day_temp not in days: + if days == [""]: + pass + elif day_temp not in days: # skip this table continue + entries[day_temp] = [] # iterate through all rows of table and store data From d232e0ca88897530fb5a02c11e1e326e2939af97 Mon Sep 17 00:00:00 2001 From: RoRo160 <79665729+RoRo160@users.noreply.github.com> Date: Thu, 9 Sep 2021 16:28:19 +0200 Subject: [PATCH 8/8] Added version to README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1f67bc9..775a9a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # IServ.py -`Made by RoRo160` +`Made by RoRo160` `v.0.01.0-beta` This module provides an easy way to communicate with your IServ account. I reverse-engineered parts of the internal IServ api and recreated some http requests your browser would do in the @@ -96,6 +96,6 @@ tasks = iserv.tasks( # -> By RoRo160 +> By RoRo160 `v.0.01.0-beta` > > [My GitHub](https://github.com/RoRo160)