Skip to content

Commit

Permalink
Merge branch 'release/v.0.01.0-beta'
Browse files Browse the repository at this point in the history
  • Loading branch information
RoRo160 committed Sep 9, 2021
2 parents 20b50a7 + d232e0c commit 52f1239
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
__pycache__
test
test.py
97 changes: 89 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,101 @@
# IServ.py <img src="https://iserv.de/downloads/logo/IServ_Logo_klein_RGB_clean.svg" alt="" height="25" align="right">
# IServ.py <img src="https://iserv.de/downloads/logo/IServ_Logo_klein_RGB_clean.svg" alt="" height="35" align="right">

`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
background.

> 🔴 **WARNING:** <br/>
> 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
- Get all tasks

### Coming soon:

- Summarized representation plan only with entries from your class
- Get all tasks
- Read all your emails
- Notifications

> **WARNING:** <br/>
> This module does **NOT** use an official api! <br/>
> Extensive usage might lead to problems with your IServ account. <br/>
> <br/>
> **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 `v.0.01.0-beta`
>
> [My GitHub](https://github.com/RoRo160)
203 changes: 203 additions & 0 deletions iserv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import datetime
import requests
import bs4
from bs4 import *


DAYS = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday"
]


class LoginError(Exception):
pass


class IServ:
paths = {
"login": "/iserv/app/login",
"logout": "/iserv/app/logout",
"plan": "/iserv/plan/show/raw/",
"tasks": "/iserv/exercise.csv"
}
messages = {
"login_failed": "Anmeldung fehlgeschlagen!"
}

def __init__(self, domain):
self.domain = domain
self._csrf_token = None
self._s = requests.Session()

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
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
# TODO check in header
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):
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]

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 days == [""]:
pass
elif 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

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
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
requests~=2.25.1
beautifulsoup4~=4.9.3

0 comments on commit 52f1239

Please sign in to comment.