Files
Espace-citoyen/citoyen.py
2025-08-27 14:52:25 +02:00

279 lines
9.9 KiB
Python
Executable File

#!/usr/bin/env python3
import configparser
import argparse
import requests
from sys import exit
import logging
import coloredlogs
import json
import re
from io import StringIO
import getpass
import os
from bs4 import BeautifulSoup
from zoneinfo import ZoneInfo
from datetime import datetime
from math import floor
from ics import Calendar, Event
def getKey(dictionnary, key):
logger = logging.getLogger(__name__)
try:
return dictionnary[key]
except:
logger.error('Missing key: %s' % key)
exit(-1)
def authenticate(baseURL, city, login, password):
logger = logging.getLogger(__name__)
url = baseURL + '/%s/espace-citoyens' % city
logger.info('Retrieve base site')
html = requests.get(url, allow_redirects=False)
if html.status_code != 200:
logger.error('Impossible to retrieve Web site: %d' % html.status_code)
exit(-1)
cookies = html.cookies
found = False
p = re.compile('^.*<form action="(?P<url>[^"]+)".*method="post".*$')
content = StringIO(html.content.decode('utf8'))
for line in content.readlines():
m = p.match(line)
if m != None:
found = True
logonURL = baseURL+m.group('url')
break
if not found:
logger.error('Impossible to retrieve logon URL')
exit(-1)
logger.debug('Found logon: %s' % (logonURL))
found = False
p = re.compile('^.*name="__RequestVerificationToken" type="hidden" value="(?P<token>[^"]+)".*$')
content = StringIO(html.content.decode('utf8'))
for line in content.readlines():
m = p.match(line)
if m != None:
found = True
token = m.group('token')
break
if not found:
logger.error('Impossible to retrieve verification token')
exit(-1)
logger.debug('Found token: %s' % (token))
payload = { 'username':login, 'password':password, '__RequestVerificationToken':token }
auth = requests.post(logonURL, data=payload, cookies=cookies, allow_redirects=False)
if auth.status_code != 302:
logger.error('Impossible to login: %d' % html.status_code)
exit(-1)
else:
logger.info('Authentication successful')
mainpage = baseURL+auth.headers['Location']
return mainpage, cookies
def getReservationsKind(baseURL, url, cookies):
logger = logging.getLogger(__name__)
# Retrieve main page
html = requests.get(url, cookies=cookies, allow_redirects=False)
if html.status_code != 200:
logger.error('Impossible to retrieve main page: %d' % html.status_code)
exit(-1)
soup = BeautifulSoup(html.text, 'html.parser')
resas = soup.find_all('table', id='tblDalleDetail_Reservation')
if len(resas) != 1:
logger.error('Too many kind of reservations')
exit(-1)
resas = resas[0]
resas = resas.find_all('tr')
resaTypes = {}
for resa in resas:
resaType = resa.find('td', id=re.compile('ReservationActivite')).get_text()
url = resa.find('a').get('href')
resaTypes[resaType] = baseURL+url
return resaTypes
# Dump reservation into a calendar file
def dumpReservation(resaType, baseURL, city, url, cookies, output):
logger = logging.getLogger(__name__)
html = requests.get(url, cookies=cookies, allow_redirects=False)
if html.status_code != 200:
logger.info('Impossible to retrieve reservation page: %d' % html.status_code)
exit(-1)
variables = ['idPer', 'idIns', 'idLie', 'idClg']
values = {}
for var in variables:
found = False
content = StringIO(html.content.decode('utf8'))
p = re.compile('^.*var %s = (?P<value>[0-9]+).*$' % var)
for line in content.readlines():
m = p.match(line)
if m != None:
found = True
value = int(m.group('value'))
values[var] = value
break
if not found:
logger.error('Impossible to find value for variable: %s' % var)
exit(-1)
else:
logger.debug('Found value for var %s: %d' % (var, value))
# This URL should be retrieved more automatically ...
calendar = requests.get(baseURL+'/%s/espace-citoyens/DemandeEnfance/NouvelleDemandeReservationGetCalendrier' % city, params=values, cookies=cookies)
if calendar.status_code != 200:
logger.info('Impossible to retrieve calendar: %d' % html.status_code)
exit(-1)
calendar = json.load(StringIO(calendar.content.decode('utf8')))
weeks = getKey(calendar, 'listeSemainesAffichees')
typeResas = getKey(calendar, 'listeUnitesInscr')
dictResa = {}
for typeResa in typeResas:
idResa = getKey(typeResa, 'idUnite')
codeResa = getKey(typeResa, 'codeUnite')
descResa = getKey(typeResa, 'libUnite')
dictResa[idResa] = (descResa, codeResa)
cal = Calendar()
for week in weeks:
numSemaine = int(getKey(week, 'numSemaine'))
days = getKey(week, 'listeJoursAffiches')
for day in days:
resas = getKey(day, 'listeUnitesJour')
date = int(getKey(day, 'idJour'))
year = int(date/10000)
month = int((date - year*10000) / 100)
day = date - year*10000 - month*100
for resa in resas:
checked = getKey(resa, 'nbConsoBase') != None
if checked:
typeResa = getKey(resa, 'idUnite')
e = Event()
e.name = dictResa[typeResa][0]
code = dictResa[typeResa][1]
if 'Matin' in code:
begin = datetime(year,month,day,7,30,0, tzinfo=ZoneInfo("Europe/Paris"))
end = datetime(year,month,day,12,0,0, tzinfo=ZoneInfo("Europe/Paris"))
elif 'AM' in code:
begin = datetime(year,month,day,13,30,0, tzinfo=ZoneInfo("Europe/Paris"))
end = datetime(year,month,day,18,0,0, tzinfo=ZoneInfo("Europe/Paris"))
elif 'Repas' in code:
begin = datetime(year,month,day,12,00,0, tzinfo=ZoneInfo("Europe/Paris"))
end = datetime(year,month,day,13,30,0, tzinfo=ZoneInfo("Europe/Paris"))
else:
logger.warning('Impossible to determine the type of reservation: %s' % code)
begin = datetime(year,month,day)
end = datetime(year,month,day)
e.begin = begin
e.end = end
cal.events.add(e)
with open(output, 'w') as f:
f.writelines(cal.serialize_iter())
def main():
logger = logging.getLogger(__name__)
coloredlogs.install()
parser = argparse.ArgumentParser()
parser.add_argument("-l", "--login", dest='login', type=str, required=False, help="Login")
parser.add_argument("-C", "--city", dest='city', type=str, required=False, help="City.")
parser.add_argument("-c", "--config", dest='configFileName', required=False, default=None, help="Configuration file.")
parser.add_argument("-p", "--password", dest='password', nargs='?', required=False, default=None, help="Password.")
parser.add_argument("-d", "--debug", dest='debug', action='store_true', required=False, help="Activate debug.")
subparsers = parser.add_subparsers(title='subcommands', dest='command', help='subcommand help')
parserauth = subparsers.add_parser('A', help='Test authentication.')
parserlist = subparsers.add_parser('L', help='List all possible reservations types.')
parserdump = subparsers.add_parser('D', help='Dump all reservation of some kind')
parserdump.add_argument("-i", "--index", dest='index', type=int, required=True, help="Index of reservations to dump.")
parserdump.add_argument("-o", "--output", dest='calendar', required=True, default='cal.ics', help="Output calendar file.")
args = parser.parse_args()
logger.info("Initial arguments: %s" % args)
if args.debug:
logger.info('Setting logging to debug mode')
coloredlogs.set_level(level=logging.DEBUG)
if args.configFileName != None:
try:
configFile = open(args.configFileName, 'r')
except Exception as e:
logger.info('Impossible to open configuration file. Error: %s' % e)
exit(-1)
config = configparser.ConfigParser()
config.read_file(configFile)
sections = config.sections()
if 'Login' in sections:
options = config.options('Login')
if 'login' in options:
args.login = config.get('Login','login')
if 'password' in options:
args.password = config.get('Login','password')
logger.info("Final arguments: %s" % args)
if args.city == None:
logger.info('City must be provided.')
parser.print_help()
exit(-1)
if args.login == None:
logger.info('Login must be provided.')
parser.print_help()
exit(-1)
if args.password == None:
args.password = getpass.getpass()
baseURL = 'https://www.espace-citoyens.net'
# Authentication
mainpage, cookies = authenticate(baseURL, args.city, args.login, args.password)
if (args.command == None) or (args.command == 'A'):
exit(0)
# Switch between commands
logger.info('Retrieve reservation kinds')
resaTypes = getReservationsKind(baseURL, mainpage, cookies)
if (args.command == 'L'):
resaNum = 1
for resaType in resaTypes.keys():
print("%d - %s" % (resaNum, resaType))
resaNum+=1
elif (args.command == 'D'):
resaType = list(resaTypes)[args.index-1]
url = resaTypes[resaType]
dumpReservation(resaType, baseURL, args.city, url, cookies, args.calendar)
if __name__ == "__main__":
main()