274 lines
8.2 KiB
Python
Executable File
274 lines
8.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
from PIL import Image
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
import hashlib
|
|
import json
|
|
from random import choice
|
|
import requests
|
|
import smtplib
|
|
|
|
|
|
API_PREFIX = "https://beta.interieur.gouv.fr/candilib/api/v2/"
|
|
# API_PREFIX = "https://candilib.ynerant.fr/candilib/api/v2/"
|
|
# API_PREFIX = "http://localhost/candilib/api/v2/"
|
|
|
|
CAPTCHA_IMAGES = {
|
|
"L'avion": "airplane",
|
|
"Les ballons": "balloons",
|
|
"L'appareil photo": "camera",
|
|
"La Voiture": "car",
|
|
"Le chat": "cat",
|
|
"La chaise": "chair",
|
|
"Le trombone": "clip",
|
|
"L'horloge": "clock",
|
|
"Le nuage": "cloud",
|
|
"L'ordinateur": "computer",
|
|
"L'enveloppe": "envelope",
|
|
"L'oeil": "eye",
|
|
"Le drapeau": "flag",
|
|
"Le dossier": "folder",
|
|
"Le pied": "foot",
|
|
"Le graphique": "graph",
|
|
"La maison": "house",
|
|
"La clef": "key",
|
|
"La feuille": "leaf",
|
|
"L'ampoule": "light-bulb",
|
|
"Le cadenas": "lock",
|
|
"La loupe": "magnifying-glass",
|
|
"L'homme": "man",
|
|
"La note de musique": "music-note",
|
|
"Le pantalon": "pants",
|
|
"Le crayon": "pencil",
|
|
"L'imprimante": "printer",
|
|
"Le robot": "robot",
|
|
"Les ciseaux": "scissors",
|
|
"Les lunettes de soleil": "sunglasses",
|
|
"L'étiquette": "tag",
|
|
"L'arbre": "tree",
|
|
"Le camion": "truck",
|
|
"Le T-Shirt": "t-shirt",
|
|
"Le parapluie": "umbrella",
|
|
"La femme": "woman",
|
|
"La planète": "world"
|
|
}
|
|
|
|
|
|
def calculate_checksums():
|
|
for name in CAPTCHA_IMAGES.values():
|
|
img = Image.open(f'captcha_images/{name}.png')
|
|
img.save(f'captcha_images/{name}.ppm')
|
|
with open(f'captcha_images/{name}.ppm', 'rb') as f:
|
|
with open(f'captcha_images/{name}.ppm.sum', 'w') as f_sum:
|
|
f_sum.write(hashlib.sha512(f.read()).hexdigest())
|
|
|
|
|
|
@dataclass
|
|
class Candidat:
|
|
codeNeph: str = ''
|
|
homeDepartement: str = ''
|
|
departement: str = '75'
|
|
email: str = ''
|
|
nomNaissance: str = ''
|
|
prenom: str = ''
|
|
portable: str = ''
|
|
adresse: str = ''
|
|
visibilityHour: str = '12H50'
|
|
dateETG: str = ''
|
|
isInRecentlyDept: bool = False
|
|
|
|
|
|
@dataclass
|
|
class Places:
|
|
_id: str = ''
|
|
centre: "Centre" = None
|
|
date: str = ''
|
|
lastDateToCancel: str = ''
|
|
canBookFrom: str = ''
|
|
timeOutToRetry: int = 0
|
|
dayToForbidCancel: int = 7
|
|
visibilityHour: str = '12H50'
|
|
|
|
|
|
@dataclass(order=True)
|
|
class Departement:
|
|
geoDepartement: str = ''
|
|
centres: list["Centre"] = None
|
|
count: int = None
|
|
|
|
|
|
@dataclass
|
|
class Centre:
|
|
_id: str
|
|
count: int = 0
|
|
longitude: float = 0.0
|
|
latitude: float = 0.0
|
|
nom: str = ""
|
|
departement: Departement = None
|
|
|
|
|
|
def api(path: str, token: str, user_id: str, app: str = 'candidat', **kwargs) -> dict:
|
|
return requests.get(
|
|
API_PREFIX + app + '/' + path,
|
|
data=kwargs,
|
|
headers={
|
|
'Authorization': f'Bearer {token}',
|
|
'Content-Type': 'application/json',
|
|
'X-USER-ID': user_id,
|
|
},
|
|
).json()
|
|
|
|
|
|
def send_mail(content: str, subject: str) -> None:
|
|
print('\n')
|
|
print(subject)
|
|
print(len(subject) * '-')
|
|
print()
|
|
return print(content)
|
|
smtp = smtplib.SMTP('localhost', 25)
|
|
content = f"""From: Ÿnérant <ynerant@crans.org>
|
|
To: Ÿnérant <ynerant+candilib@crans.org>
|
|
Subject: {subject}
|
|
|
|
""" + content
|
|
content = content.encode('UTF-8')
|
|
smtp.sendmail('ynerant@crans.org', ['ynerant+candilib@crans.org'], content)
|
|
|
|
|
|
def main(token: str) -> None:
|
|
response = requests.get(API_PREFIX + 'auth/candidat/verify-token?token=' + token)
|
|
try:
|
|
assert response.json()['auth']
|
|
except (AssertionError, KeyError):
|
|
raise ValueError(f"Une erreur est survenue lors de la connexion : {response.content.decode('utf-8')}")
|
|
user_id = response.headers['X-USER-ID']
|
|
|
|
me = Candidat(**api('me', token, user_id)['candidat'])
|
|
print(f'Salut {me.prenom} {me.nomNaissance} !')
|
|
print(f"Tu as accès aux places à {me.visibilityHour}.")
|
|
|
|
|
|
departements = [Departement(**dpt) for dpt in api('departements', token, user_id)['geoDepartementsInfos']]
|
|
departements.sort()
|
|
|
|
for dpt in departements:
|
|
centres = api(f'centres?departement={dpt.geoDepartement}&end=2021-07-31T23:59:59.999', token, user_id)
|
|
dpt.centres = []
|
|
dpt.count = 0
|
|
for centre in centres:
|
|
centre = Centre(
|
|
_id=centre['centre']['_id'],
|
|
nom=centre['centre']['nom'],
|
|
count=centre['count'],
|
|
longitude=centre['centre']['geoloc']['coordinates'][0],
|
|
latitude=centre['centre']['geoloc']['coordinates'][1],
|
|
departement=dpt,
|
|
)
|
|
dpt.centres.append(centre)
|
|
dpt.count += centre.count
|
|
|
|
places = Places(**api('places', token, user_id))
|
|
if places.centre:
|
|
for dpt in departements:
|
|
for centre in dpt.centres:
|
|
if centre._id == places.centre['_id']:
|
|
places.centre = centre
|
|
break
|
|
else:
|
|
continue
|
|
break
|
|
|
|
if places.date:
|
|
print(f"Vous avez déjà une date d'examen, le {places.date}.")
|
|
exit(1)
|
|
|
|
print("\n")
|
|
for dpt in departements:
|
|
print(dpt.geoDepartement, dpt.count, '(' + ', '.join(centre.nom for centre in dpt.centres) + ')')
|
|
|
|
for dpt in departements:
|
|
for centre in dpt.centres:
|
|
dates = api(f"places?begin=2021-04-26T00:00:00.000&end=2021-07-31T23:59:59.999+02:00&geoDepartement={dpt.geoDepartement}&nomCentre={centre.nom}", token, user_id)
|
|
if dates:
|
|
send_mail(json.dumps(dates, indent=2), centre.nom)
|
|
centre.dates = dates
|
|
|
|
PREFERRED_CENTRES = ["MASSY", "ANTONY", "RUNGIS", "MONTGERON", "CLAMART", "SAINT CLOUD", "EVRY",
|
|
"VILLABE", "ETAMPES", "VELIZY VILLACOUBLAY", "MAISONS ALFORT", "TRAPPES"]
|
|
|
|
for name in PREFERRED_CENTRES:
|
|
for dpt in departements:
|
|
for centre in dpt.centres:
|
|
if centre.nom == name:
|
|
break
|
|
else:
|
|
continue
|
|
break
|
|
for date in centre.dates:
|
|
if name == "SAINT PRIEST" and ("T07:" in date or "T08:" in date or "T09:" in date or "T10:" in date):
|
|
continue
|
|
break
|
|
else:
|
|
continue
|
|
break
|
|
else:
|
|
print("Aucune date intéressante")
|
|
return
|
|
|
|
print("Centre :", centre.nom)
|
|
print("Date :", date)
|
|
# Resolve captcha
|
|
captcha_info = api('verifyzone/start', token, user_id)
|
|
print(captcha_info)
|
|
captcha_info = captcha_info['captcha']
|
|
field = captcha_info['imageFieldName']
|
|
image_name = captcha_info['imageName']
|
|
image_file = CAPTCHA_IMAGES[image_name]
|
|
captcha_images = captcha_info['values']
|
|
|
|
with open(f'captcha_images/{image_file}.ppm.sum') as f:
|
|
valid_checksum = f.read().strip()
|
|
|
|
for i in range(5):
|
|
response = requests.get(
|
|
f'{API_PREFIX}candidat/verifyzone/image/{i}',
|
|
headers={
|
|
'Accept': 'application/json',
|
|
'Authorization': f'Bearer {token}',
|
|
'X-USER-ID': user_id,
|
|
},
|
|
)
|
|
with open(f'/tmp/image_{i}.png', 'wb') as f:
|
|
f.write(response.content)
|
|
img = Image.open(f'/tmp/image_{i}.png')
|
|
img.save(f'/tmp/image_{i}.ppm')
|
|
with open(f'/tmp/image_{i}.ppm', 'rb') as f:
|
|
checksum = hashlib.sha512(f.read()).hexdigest()
|
|
if checksum == valid_checksum:
|
|
captcha_result = captcha_images[i]
|
|
|
|
print(requests.patch(
|
|
API_PREFIX + 'candidat/places',
|
|
headers={
|
|
'Content-Type': 'application/json',
|
|
'Authorization': f'Bearer {token}',
|
|
'X-USER-ID': user_id,
|
|
},
|
|
json={
|
|
'geoDepartement': centre.departement.geoDepartement,
|
|
'nomCentre': centre.nom,
|
|
'date': date,
|
|
'hasDualControlCar': True,
|
|
'isAccompanied': True,
|
|
'isModification': False,
|
|
field: captcha_result,
|
|
},
|
|
).content)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
with open('/var/local/candibot/.token') as f:
|
|
token = f.read().strip()
|
|
main(token)
|