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)