+ Recoded entire codebase

+ Added proper README.md
This commit is contained in:
b267a 2025-08-02 22:37:17 +02:00
parent 302999189d
commit d1c02e5b62
8 changed files with 585 additions and 252 deletions

374
kit.py
View file

@ -1,226 +1,232 @@
import requests
import urllib.parse
import os
import re
import html
import time
import datetime
import pytz
import os
import requests
import datetime
import urllib.parse
from dotenv import load_dotenv
def get_day():
dt = datetime.datetime.now(pytz.timezone('Europe/Berlin')) + datetime.timedelta(days=3)
date = dt.strftime('%Y-%m-%d')
return date
def login(username, password):
# === CONFIGURATION CONSTANTS === #
AUTH_BASE_URL = "https://auth.anny.eu"
ANNY_BASE_URL = "https://anny.eu"
BOOKING_API_BASE = "https://b.anny.eu/api/v1"
CHECKOUT_FORM_API = "https://b.anny.eu/api/ui/checkout-form"
RESOURCE_URL = f"{BOOKING_API_BASE}/resources/1-lehrbuchsammlung-eg-und-1-og/children"
SERVICE_ID = "449"
session = requests.Session()
DEFAULT_HEADERS = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0',
'accept': 'application/vnd.api+json',
'accept-encoding': 'plain'
}
session.headers = {
'accept': 'text/html, application/xhtml+xml',
'accept-encoding': 'plain',
'referer': 'https://auth.anny.eu/',
'origin': 'https://auth.anny.eu',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0',
}
r = session.get(
'https://auth.anny.eu/login/sso'
)
session.headers['X-XSRF-TOKEN'] = urllib.parse.unquote(r.cookies['XSRF-TOKEN'])
match = re.search(r'data-page="(.*?)"', r.text)
if match:
decoded = html.unescape(match.group(1))
version_match = re.search(r'"version"\s*:\s*"([a-f0-9]{32})"', decoded)
if version_match:
x_inertia_version = version_match.group(1)
else:
x_inertia_version = '66b32acea13402d3aef4488ccd239c93'
def get_future_datetime(days_ahead=3, hour="13:00:00"):
dt = datetime.datetime.now(pytz.timezone('Europe/Berlin')) + datetime.timedelta(days=days_ahead)
return dt.strftime(f"%Y-%m-%dT{hour}+02:00")
def extract_html_value(text, pattern):
match = re.search(pattern, text)
if not match:
raise ValueError(f"Pattern not found: {pattern}")
return html.unescape(match.group(1))
session.headers.update(
{
class AnnySession:
def __init__(self, username, password):
self.session = requests.Session()
self.username = username
self.password = password
self.cookies = None
def login(self):
try:
self._init_headers()
self._sso_login()
self._kit_auth()
self._consume_saml()
self.cookies = self.session.cookies
print("✅ Login successful")
return self.cookies
except Exception as e:
print(f"❌ Login failed: {e}")
return None
def _init_headers(self):
self.session.headers.update({
**DEFAULT_HEADERS,
'accept': 'text/html, application/xhtml+xml',
'referer': AUTH_BASE_URL + '/',
'origin': AUTH_BASE_URL
})
def _sso_login(self):
r1 = self.session.get(f"{AUTH_BASE_URL}/login/sso")
self.session.headers['X-XSRF-TOKEN'] = urllib.parse.unquote(r1.cookies['XSRF-TOKEN'])
page_data = extract_html_value(r1.text, r'data-page="(.*?)"')
version = re.search(r'"version"\s*:\s*"([a-f0-9]{32})"', page_data)
x_inertia_version = version.group(1) if version else '66b32acea13402d3aef4488ccd239c93'
self.session.headers.update({
'x-requested-with': 'XMLHttpRequest',
'x-inertia': 'true',
'x-inertia-version': x_inertia_version,
}
)
'x-inertia-version': x_inertia_version
})
r2 = session.post(
'https://auth.anny.eu/login/sso',
json={
'domain': 'kit.edu'
}, headers={
'referer': 'https://auth.anny.eu/login/sso',
}
)
r2 = self.session.post(f"{AUTH_BASE_URL}/login/sso", json={"domain": "kit.edu"})
redirect_url = r2.headers['x-inertia-location']
self.redirect_response = self.session.get(redirect_url)
redirect_url = r2.headers['x-inertia-location']
def _kit_auth(self):
self.session.headers.pop('x-requested-with', None)
self.session.headers.pop('x-inertia', None)
self.session.headers.pop('x-inertia-version', None)
r3 = session.get(
redirect_url,
allow_redirects=True
)
csrf_token = extract_html_value(self.redirect_response.text, r'name="csrf_token" value="([^"]+)"')
session.headers.pop('x-requested-with')
session.headers.pop('x-inertia')
session.headers.pop('x-inertia-version')
r4 = self.session.post(
'https://idp.scc.kit.edu/idp/profile/SAML2/Redirect/SSO?execution=e1s1',
data={
'csrf_token': csrf_token,
'j_username': self.username,
'j_password': self.password,
'_eventId_proceed': '',
'fudis_web_authn_assertion_input': '',
}
)
pattern = r'name="csrf_token" value="([^"]+)"'
csrf_token = re.search(pattern, r3.text).group(1)
if "/consume" not in html.unescape(r4.text):
raise Exception("KIT authentication failed")
r4 = session.post(
'https://idp.scc.kit.edu/idp/profile/SAML2/Redirect/SSO?execution=e1s1',
data={
'csrf_token': csrf_token,
'j_username': username,
'j_password': password,
'_eventId_proceed': '',
'fudis_web_authn_assertion_input': '',
}
)
self.saml_response_html = r4.text
response = html.unescape(r4.text)
def _consume_saml(self):
consume_url = extract_html_value(self.saml_response_html, r'form action="([^"]+)"')
relay_state = extract_html_value(self.saml_response_html, r'name="RelayState" value="([^"]+)"')
saml_response = extract_html_value(self.saml_response_html, r'name="SAMLResponse" value="([^"]+)"')
if '/consume' in response:
print("KIT-Login successful!")
else:
print("Failed to login, probably wrong credentials!")
return False
self.session.post(consume_url, data={
'RelayState': relay_state,
'SAMLResponse': saml_response
})
pattern = r'form action="([^"]+)"'
consume_url = re.search(pattern, response).group(1)
pattern = r'name="RelayState" value="([^"]+)"'
relayState = re.search(pattern, response).group(1)
pattern = r'name="SAMLResponse" value="([^"]+)"'
samlResponse = re.search(pattern, response).group(1)
r5 = session.post(
consume_url,
data={
'RelayState': relayState,
'SAMLResponse': samlResponse
}
)
r6 = session.get(
'https://anny.eu/en-us/login?target=/en-us/home?withoutIntent=true',
allow_redirects=True
)
self.session.get(f"{ANNY_BASE_URL}/en-us/login?target=/en-us/home?withoutIntent=true")
return session.cookies
def test_reservation():
load_dotenv('credentials.env', override=True)
username = os.getenv('USERNAME')
password = os.getenv('PASSWORD')
cookies = login(username, password)
TOKEN = cookies['anny_shop_jwt']
ses = requests.Session()
ses.cookies = cookies
ses.headers = {
'accept': 'application/vnd.api+json',
'accept-encoding': 'plain',
'authorization': 'Bearer ' + TOKEN,
class BookingClient:
def __init__(self, cookies):
self.session = requests.Session()
self.session.cookies = cookies
self.token = cookies.get('anny_shop_jwt')
self.session.headers.update({
**DEFAULT_HEADERS,
'authorization': f'Bearer {self.token}',
'content-type': 'application/vnd.api+json',
'origin': 'https://anny.eu',
'referer': 'https://anny.eu/',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0'
}
'origin': ANNY_BASE_URL,
'referer': ANNY_BASE_URL + '/'
})
start_date = get_day() + "T13:00:00+02:00"
end_date = get_day() + "T18:00:00+02:00"
pre = ses.get(
'https://b.anny.eu/api/v1/resources/1-lehrbuchsammlung-eg-und-1-og/children',
params={
def find_available_resource(self, start, end):
response = self.session.get(RESOURCE_URL, params={
'page[number]': 1,
'page[size]': 250,
'filter[available_from]': start_date,
'filter[available_to]': end_date,
'filter[available_from]': start,
'filter[available_to]': end,
'filter[availability_exact_match]': 1,
'filter[exclude_hidden]': 0,
'filter[exclude_child_resources]': 0,
'filter[availability_service_id]': 449,
'filter[availability_service_id]': SERVICE_ID,
'filter[include_unavailable]': 0,
'filter[pre_order_ids]': '',
'sort': 'name'
}
)
})
ressources = pre.json()['data']
resources = response.json().get('data', [])
return resources[0]['id'] if resources else None
if len(ressources) <= 0:
print("No slots available anymore!")
return False
ressource_id = ressources[0]['id']
r = ses.post(
'https://b.anny.eu/api/v1/order/bookings?include=customer,voucher,bookings.booking_add_ons.add_on.cover_image,bookings.sub_bookings.resource,bookings.sub_bookings.service,bookings.series_bookings,bookings.customer,bookings.service.custom_forms.custom_fields,bookings.cancellation_policy,bookings.resource.cover_image,bookings.resource.parent,bookings.resource.category,bookings.reminders,bookings.booking_series,sub_orders.bookings,sub_orders.organization.legal_documents&stateless=1',
json={
"resource_id": [
ressource_id
], "service_id": {
"449": 1
}, "start_date": start_date,
"end_date": end_date,
"description":"", "customer_note": "",
"add_ons_by_service": {
"449": [
[]
]
}, "sub_bookings_by_service": {},
"strategy": "multi-resource"
}, cookies=cookies
)
if not r.ok:
print("Slot is not available anymore!")
return False
oid = r.json()['data']['id']
oat = r.json()['data']['attributes']['access_token']
r2 = ses.get(
'https://b.anny.eu/api/ui/checkout-form?oid=' + oid + '&oat=' + oat + '&stateless=1'
)
customer = r2.json()['default']['customer']
r3 = ses.post(
'https://b.anny.eu/api/v1/order?include=customer,voucher,bookings.booking_add_ons.add_on,bookings.sub_bookings.resource,bookings.sub_bookings.service,bookings.service.custom_forms.custom_fields,bookings.cancellation_policy,bookings.resource.cover_image,bookings.resource.parent,bookings.reminders,bookings.customer,bookings.attendees,sub_orders.bookings,sub_orders.organization.legal_documents,last_payment.method&oid=' + oid + '&oat=' + oat + '&stateless=1',
json={
"customer": {
"given_name": customer['given_name'],
"family_name": customer['family_name'],
"email": customer['email']
}, "accept_terms": True,
"payment_method": "",
"success_url": "https://anny.eu/checkout/success?oids=" + oid + "&oats=" + oat,
"cancel_url": "https://anny.eu/checkout?step=checkout&childResource=3302",
"meta": {
"timezone":"Europe/Berlin"
def reserve(self, resource_id, start, end):
booking = self.session.post(
f"{BOOKING_API_BASE}/order/bookings?include=customer&stateless=1",
json={
"resource_id": [resource_id],
"service_id": {SERVICE_ID: 1},
"start_date": start,
"end_date": end,
"description": "",
"customer_note": "",
"add_ons_by_service": {SERVICE_ID: [[]]},
"sub_bookings_by_service": {},
"strategy": "multi-resource"
}
}
)
)
if r3.ok:
print("Reservation successful!")
return True
if not booking.ok:
print("❌ Slot already taken.")
return False
print("Reservation failed!")
return False
data = booking.json().get("data", {})
oid = data.get("id")
oat = data.get("attributes", {}).get("access_token")
test_reservation()
checkout = self.session.get(f"{CHECKOUT_FORM_API}?oid={oid}&oat={oat}&stateless=1")
customer = checkout.json().get("default", {}).get("customer", {})
final = self.session.post(
f"{BOOKING_API_BASE}/order?oid={oid}&oat={oat}&stateless=1",
json={
"customer": {
"given_name": customer.get("given_name"),
"family_name": customer.get("family_name"),
"email": customer.get("email")
},
"accept_terms": True,
"payment_method": "",
"success_url": f"{ANNY_BASE_URL}/checkout/success?oids={oid}&oats={oat}",
"cancel_url": f"{ANNY_BASE_URL}/checkout?step=checkout&childResource={resource_id}",
"meta": {"timezone": "Europe/Berlin"}
}
)
if final.ok:
print("✅ Reservation successful!")
return True
print("❌ Reservation failed.")
return False
def main():
load_dotenv('credentials.env', override=True)
username = os.getenv("USERNAME")
password = os.getenv("PASSWORD")
if not username or not password:
print("❌ Missing credentials in .env")
return
session = AnnySession(username, password)
cookies = session.login()
if not cookies:
return
booking = BookingClient(cookies)
start = get_future_datetime(hour="13:00:00")
end = get_future_datetime(hour="18:00:00")
resource_id = booking.find_available_resource(start, end)
if not resource_id:
print("⚠️ No available resources found.")
return
booking.reserve(resource_id, start, end)
if __name__ == "__main__":
main()