First working version
Signed-off-by: root <root@candilib>
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.token
|
269
bot.py
Executable file
@ -0,0 +1,269 @@
|
||||
#!/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} !')
|
||||
|
||||
|
||||
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))
|
||||
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)
|
||||
|
||||
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)
|
||||
send_mail(json.dumps(dates, indent=2), centre.nom)
|
||||
centre.dates = dates
|
||||
|
||||
PREFERRED_CENTRES = ["MASSY", "RUNGIS", "ETAMPES", "SAINT CLOUD", "SAINT PRIEST", "LA TRONCHE"]
|
||||
|
||||
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):
|
||||
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)
|
BIN
captcha_images/airplane.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
captcha_images/airplane.ppm
Normal file
1
captcha_images/airplane.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
51f0e08ae25ece91a945ed011aac2232539e5e1719a133a8abb62f8f8923a31e95bfee329ccbffa074e18f4a508563a2679cd933a2457287d063f5883c5cc990
|
BIN
captcha_images/airplane@2x.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
captcha_images/balloons.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
captcha_images/balloons.ppm
Normal file
1
captcha_images/balloons.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
08761348254993f5de1b5f440d44a04a112ae5deb650f85de566cb8dd7e5d5414bb5d0a1c1abf0bef2202dfe76185b2a4fc1ba3ce4e468e8abedc3ba7b9c9cda
|
BIN
captcha_images/balloons@2x.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
captcha_images/camera.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
captcha_images/camera.ppm
Normal file
1
captcha_images/camera.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
d2fc721f7e1e6a3ff54168e62a391715342e731661ee03adec0a189ba2f5da82c16db7156d9d12cc1269cc5b7d7a221ce088087758698b6b43257cab3811ff6e
|
BIN
captcha_images/camera@2x.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
captcha_images/car.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
captcha_images/car.ppm
Normal file
1
captcha_images/car.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
dae190d2e56deccfee2930d5f6248e51506d11014e141e728f24f01a823f2560fc22d09cf03a99eec9593c0cf48d609da5c81ea0faa7532d83f36a32689c9c0c
|
BIN
captcha_images/car@2x.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
captcha_images/cat.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
captcha_images/cat.ppm
Normal file
1
captcha_images/cat.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
53d1fc65eee71d67455148d401afd46c2653cf06586da5ce7045d0bdfedd88fad2b1b40864fd5b56fb6f0fc7665c2f0ed7ca02c2bfe6442acb1904b9266ba61a
|
BIN
captcha_images/cat@2x.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
captcha_images/chair.png
Normal file
After Width: | Height: | Size: 936 B |
BIN
captcha_images/chair.ppm
Normal file
1
captcha_images/chair.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
2f7fd3e2119875ff97a155bdac5364a7bd896ce0376931b91bea0e4ea3be6288f8fee5df6ace851aeffa10204873bb5980a1961c7708dc7fdf1b99975b51b98c
|
BIN
captcha_images/chair@2x.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
captcha_images/clip.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
captcha_images/clip.ppm
Normal file
1
captcha_images/clip.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
5d4d262cd0b0874711abd478dcd6524ea99844fd3698fb8c6a054ed429d831c717d93ca8c8b18de679f54674dd54bb115ef2e5e85d364378820beaeae25a6802
|
BIN
captcha_images/clip@2x.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
captcha_images/clock.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
captcha_images/clock.ppm
Normal file
1
captcha_images/clock.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
9143b35b6e0ff73899643e965d2dd2b76a7eb1c489b9423b6cdcef2b4522f64b3ebeaa8ae2ba700db9599e41826dc8f32412b43e240bc3146c5bc5b3cc68546a
|
BIN
captcha_images/clock@2x.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
captcha_images/cloud.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
captcha_images/cloud.ppm
Normal file
1
captcha_images/cloud.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
1a9bc03d2298a2cfc61e82d91b4b36701cb1c36b645a7447bf1b60a8f87bf77a37e0c4fb98d77061cf9322dae56ab9b67829fce6e01c78dc089ad1dfb3bf6559
|
BIN
captcha_images/cloud@2x.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
captcha_images/computer.png
Normal file
After Width: | Height: | Size: 774 B |
BIN
captcha_images/computer.ppm
Normal file
1
captcha_images/computer.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
a4a71334299297e9d2fe7191226a89fd1a94ef1ad159acf7cfda0df6d994c2e1d181cab8ed489697e22a88946f94651fc5f81d0f404e72d85cfed0ec9c3f42bd
|
BIN
captcha_images/computer@2x.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
captcha_images/envelope.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
captcha_images/envelope.ppm
Normal file
1
captcha_images/envelope.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
97b81403a5b78a21733edf049b77b7382451d721237b0e17f7718811d61e72543cd35a773c974c50d5591d024db7f61f846aa46023e58ad510237457721dad4c
|
BIN
captcha_images/envelope@2x.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
captcha_images/eye.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
captcha_images/eye.ppm
Normal file
1
captcha_images/eye.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
4cd357aebbe717aff7b8f3d5c57452c7e8900fcbfb4e87971eb4465754d0154bf1f20437541f2bf16e5c148f6160151e23f7b59a2a7a35cb73a1acb338cd4b73
|
BIN
captcha_images/eye@2x.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
captcha_images/flag.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
captcha_images/flag.ppm
Normal file
1
captcha_images/flag.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
a2e58d7529ceaef1ebc931f8f3dc729d89aaae7992350aa472adfcadd6c770ce2b755ab0badb7c292045f2036409e4f94d43c9e845cbb763916f392b94ef5efd
|
BIN
captcha_images/flag@2x.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
captcha_images/folder.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
captcha_images/folder.ppm
Normal file
1
captcha_images/folder.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
794236454c973d2d9926ce9fbeb8825b9360afd33c656111924f8a9f687bad88c5e01343dccd6a37591cd1ac5c2d021ed4eff329192b310aa5fb7eaa5015d75a
|
BIN
captcha_images/folder@2x.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
captcha_images/foot.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
captcha_images/foot.ppm
Normal file
1
captcha_images/foot.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
424a1b17e9004b80cef2f7058c1348d6f9dcfb910f0672f9099bd56e9c613080a71d026083f575605fe3d15ab53d233ad53cba157d3c9b0424e59d0acf759411
|
BIN
captcha_images/foot@2x.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
captcha_images/graph.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
captcha_images/graph.ppm
Normal file
1
captcha_images/graph.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
714d17158fc7fa575404ff69ea72de3de59e2243954bed4bc88390bcddd40bc4a9238136e4ea970d3c08578379054d2733774b232401f8890b264e4ad46dd761
|
BIN
captcha_images/graph@2x.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
captcha_images/house.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
captcha_images/house.ppm
Normal file
1
captcha_images/house.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
a53e4c43f24af98a20a12f123900857add9f11bd33c68df2dcc0161992ba6f9bac0e269cc74884fe3684679353c456b8879ec1dd14543aea2bf903ae41079d45
|
BIN
captcha_images/house@2x.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
captcha_images/key.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
captcha_images/key.ppm
Normal file
1
captcha_images/key.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
8358a6dd745c26a43355ceac8ffeaf6569ba27debdc6a83a1eed6e0b7106c81d1554f6dfec5e538098aad435707ba4244beeeab5a45935b552ac27a015fd005b
|
BIN
captcha_images/key@2x.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
captcha_images/leaf.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
captcha_images/leaf.ppm
Normal file
1
captcha_images/leaf.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
db28135111aad2bccea0c1d25c147bb70d4aff9ddcb338c1ec960f092bf8b8f11a86528a6add74b34d49b1a94bda2f112f1316933a190c85c3dbe5229534517e
|
BIN
captcha_images/leaf@2x.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
captcha_images/light-bulb.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
captcha_images/light-bulb.ppm
Normal file
1
captcha_images/light-bulb.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
3ee888ecc26ea19ffa9f6312afcaac0a7900002f26db76e02eee5237a352c3db48d2341e8aba3c5cb332230da0b091b8e4168aff5b951d62e6333a3ed9314741
|
BIN
captcha_images/light-bulb@2x.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
captcha_images/lock.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
captcha_images/lock.ppm
Normal file
1
captcha_images/lock.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
0a59e6f0c2f2448f13c98785b51b0d2565592b06f789ca7c2187c2e728968e8c621a347d268cebdb441ce07e4993c65646ff033bd3babefb6ff8f2bcdf01838d
|
BIN
captcha_images/lock@2x.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
captcha_images/magnifying-glass.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
captcha_images/magnifying-glass.ppm
Normal file
1
captcha_images/magnifying-glass.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
b423be22daef5b0c3d251c42f1bc94b1c8668f7800418381ee8d794fe005ef7e954867856609bc420dcc6285078c5d7605d0ef738dcc553c9da3dfe9e82c8bda
|
BIN
captcha_images/magnifying-glass@2x.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
captcha_images/man.png
Normal file
After Width: | Height: | Size: 966 B |
BIN
captcha_images/man.ppm
Normal file
1
captcha_images/man.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
3f7c883cf5e168b8533cc522c958b13870f19a68920f99177ddc647d117b7771d91907c2a6a33a8271f9995794866cda4456caebd193d6d32096ded500941f63
|
BIN
captcha_images/man@2x.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
captcha_images/music-note.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
captcha_images/music-note.ppm
Normal file
1
captcha_images/music-note.ppm.sum
Normal file
@ -0,0 +1 @@
|
||||
e6e4f3ce5530e8514dc6b8eb09fb7bfef85504d5dfbdf70f79afb6642b1502f51675150e04148435064b6f62843a1ca2f2d48b76aa4eda429d6a5c9a161744b8
|
BIN
captcha_images/music-note@2x.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
captcha_images/pants.png
Normal file
After Width: | Height: | Size: 1.1 KiB |