🎉 First working version
Signed-off-by: ynerant <ynerant@zamokv5.crans.org>
This commit is contained in:
commit
5e6834c749
|
@ -0,0 +1 @@
|
||||||
|
__pycache__
|
|
@ -0,0 +1,137 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import date
|
||||||
|
from math import acos, cos, pi, sin
|
||||||
|
import requests
|
||||||
|
from threading import Thread
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from irc import IRCClient
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Location:
|
||||||
|
longitude: float = 0.0
|
||||||
|
latitude: float = 0.0
|
||||||
|
city: str = ""
|
||||||
|
|
||||||
|
def distance(self, other: "Location") -> float:
|
||||||
|
earth_radius = 6378
|
||||||
|
|
||||||
|
phi_a, phi_b = self.latitude * pi / 180, other.latitude * pi / 180
|
||||||
|
lambda_a, lambda_b = self.longitude * pi / 180, other.longitude * pi / 180
|
||||||
|
unit_dist = acos(sin(phi_a) * sin(phi_b) \
|
||||||
|
+ cos(phi_a) * cos(phi_b) * cos(lambda_b - lambda_a))
|
||||||
|
|
||||||
|
return earth_radius * unit_dist
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CentreMetadata:
|
||||||
|
address: str = ""
|
||||||
|
phone_number: str = ""
|
||||||
|
business_hours: dict = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Centre:
|
||||||
|
departement: str = ""
|
||||||
|
nom: str = ""
|
||||||
|
url: str = ""
|
||||||
|
location: Location = None
|
||||||
|
metadata: CentreMetadata = None
|
||||||
|
prochain_rdv: str = ""
|
||||||
|
plateforme: str = "Doctolib"
|
||||||
|
type: str = "vaccination-center"
|
||||||
|
appointment_count: int = 0
|
||||||
|
internal_id: str = ""
|
||||||
|
vaccine_type: list[str] = None
|
||||||
|
appointment_by_phone_only: bool = False
|
||||||
|
erreur: any = None
|
||||||
|
last_scan_with_availabilities: str = ""
|
||||||
|
appointment_schedules: list[dict] = None
|
||||||
|
gid: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def check_dpt(dpt_number: int, position: Location, radius: int = 20):
|
||||||
|
"""
|
||||||
|
Recherche de rendez-vous disponibles pour les majeurs non-prioritaires
|
||||||
|
dans le département indiqué.
|
||||||
|
Renvoie une liste de couples (centre, nombre de doses dispo).
|
||||||
|
"""
|
||||||
|
res = requests.get(f'https://vitemadose.gitlab.io/vitemadose/{dpt_number}.json').json()
|
||||||
|
|
||||||
|
last_update = res['last_updated']
|
||||||
|
centres_dispo = res['centres_disponibles']
|
||||||
|
centres_indispo = res['centres_indisponibles']
|
||||||
|
print(len(centres_dispo), "centres disponibles sur", len(centres_indispo), "dans le", dpt_number)
|
||||||
|
|
||||||
|
places = []
|
||||||
|
|
||||||
|
for centre in centres_dispo:
|
||||||
|
centre = Centre(**centre)
|
||||||
|
centre.location = Location(**centre.location)
|
||||||
|
centre.metadata = CentreMetadata(**centre.metadata)
|
||||||
|
|
||||||
|
if centre.location.distance(position) > radius:
|
||||||
|
# Centre trop loin
|
||||||
|
continue
|
||||||
|
|
||||||
|
for schedule in centre.appointment_schedules:
|
||||||
|
if schedule['name'] == 'chronodose':
|
||||||
|
if schedule['total']:
|
||||||
|
# Places dispo en chronodose
|
||||||
|
places.append((centre, schedule['total']))
|
||||||
|
return places
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
gif = Location(latitude=48.7090418, longitude=2.1648068, city="Gif-sur-Yvette")
|
||||||
|
lyon = Location(latitude=45.7579502, longitude=4.8001017, city="Lyon")
|
||||||
|
chambéry = Location(latitude=45.5822142, longitude=5.8713341, city="Chambéry")
|
||||||
|
nantes = Location(latitude=47.2382007, longitude=-1.6300954, city="Nantes")
|
||||||
|
marseille = Location(latitude=43.2803692, longitude=5.3104571, city="Marseille")
|
||||||
|
|
||||||
|
irc_client = IRCClient('irc.crans.org', 'chronodose')
|
||||||
|
Thread(target=irc_client.start).start()
|
||||||
|
sleep(10)
|
||||||
|
irc_client.join('#chronodose')
|
||||||
|
irc_client.privmsg('#chronodose', 'coucou')
|
||||||
|
|
||||||
|
already_indicated = []
|
||||||
|
|
||||||
|
def msg(*mesg: str) -> None:
|
||||||
|
# Afficher un message dans la console et sur IRC
|
||||||
|
print(*mesg)
|
||||||
|
irc_client.privmsg('#chronodose', ' '.join(str(a) for a in mesg))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
places = {}
|
||||||
|
for dpt, ville in [(91, gif), (92, gif), (94, gif), (78, gif), (69, lyon),
|
||||||
|
(73, chambéry), (44, nantes), (13, marseille)]:
|
||||||
|
places[dpt] = check_dpt(dpt, ville)
|
||||||
|
|
||||||
|
for dpt, places in places.items():
|
||||||
|
if not places:
|
||||||
|
print("Pas de dose disponible dans le", dpt)
|
||||||
|
continue
|
||||||
|
print(sum(place[1] for place in places), "doses disponibles dans le", dpt)
|
||||||
|
for centre, count in places:
|
||||||
|
if (centre.internal_id, date.today()) in already_indicated:
|
||||||
|
# Message déjà envoyé, on spam pas
|
||||||
|
continue
|
||||||
|
already_indicated.append((centre.internal_id, date.today()))
|
||||||
|
|
||||||
|
msg(count, "doses dans le centre de", centre.nom)
|
||||||
|
msg("Type de vaccin :", ", ".join(centre.vaccine_type))
|
||||||
|
msg(centre.metadata.address, centre.metadata.phone_number)
|
||||||
|
msg("Réserver sur", centre.url)
|
||||||
|
msg(" ")
|
||||||
|
|
||||||
|
# 5 minutes
|
||||||
|
sleep(300)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,20 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class Code(Enum):
|
||||||
|
CAP = 'CAP'
|
||||||
|
INVITE = 'INVITE'
|
||||||
|
JOIN = 'JOIN'
|
||||||
|
NICK = 'NICK'
|
||||||
|
NOTICE = 'NOTICE'
|
||||||
|
PASS = 'PASS'
|
||||||
|
PING = 'PING'
|
||||||
|
PONG = 'PONG'
|
||||||
|
PRIVMSG = 'PRIVMSG'
|
||||||
|
USER = 'USER'
|
||||||
|
WHOIS = 'WHOIS'
|
||||||
|
RPL_WELCOME = 1
|
||||||
|
RPL_WHOISREGNICK = 307
|
||||||
|
RPL_ENDOFWHOIS = 318
|
||||||
|
RPL_WHOISACCOUNT = 330
|
||||||
|
ERR_NICKNAMEINUSE = 433
|
||||||
|
ERR_ALREADYREGISTRED = 462
|
|
@ -0,0 +1,237 @@
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from codes import Code
|
||||||
|
|
||||||
|
|
||||||
|
class IRCClient:
|
||||||
|
def __init__(self, server, nickname, port=None, tls=False, username=None, realname=None, encoding='utf-8', errors='ignore', capabilities=None):
|
||||||
|
self.server = server
|
||||||
|
self.nickname = nickname
|
||||||
|
if not self.nickname:
|
||||||
|
raise ValueError("Nick must not be empty")
|
||||||
|
if port is None:
|
||||||
|
if tls:
|
||||||
|
self.port = 6697
|
||||||
|
else:
|
||||||
|
self.port = 6667
|
||||||
|
else:
|
||||||
|
self.port = port
|
||||||
|
self.tls = tls
|
||||||
|
if not username:
|
||||||
|
self.username = self.nickname
|
||||||
|
else:
|
||||||
|
self.username = username
|
||||||
|
if not realname:
|
||||||
|
self.realname = self.username
|
||||||
|
else:
|
||||||
|
self.realname = realname
|
||||||
|
self.encoding = encoding
|
||||||
|
self.errors = errors
|
||||||
|
if capabilities is None:
|
||||||
|
capabilities = []
|
||||||
|
self.capabilities = capabilities
|
||||||
|
self.enabled_capabilities = []
|
||||||
|
self.socket_mutex = threading.Lock()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.socket = socket.create_connection((self.server, self.port))
|
||||||
|
if self.tls:
|
||||||
|
self.raw_socket = self.socket
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
self.socket = context.wrap_socket(self.raw_socket, server_hostname=self.server)
|
||||||
|
if self.capabilities:
|
||||||
|
self.cap_ls(True)
|
||||||
|
self.nick(self.nickname)
|
||||||
|
self.user(self.username, self.realname)
|
||||||
|
threads = []
|
||||||
|
while True:
|
||||||
|
data = b''
|
||||||
|
while not data or data[-1] != ord('\n'):
|
||||||
|
data += self.socket.recv(4096)
|
||||||
|
data = data.decode(self.encoding, self.errors).split('\r\n')
|
||||||
|
for command in data:
|
||||||
|
if command:
|
||||||
|
thread = threading.Thread(target=self.process_command, args=(command,))
|
||||||
|
thread.start()
|
||||||
|
threads.append(thread)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_command_params(params):
|
||||||
|
result = []
|
||||||
|
last_param = 0
|
||||||
|
for i, char in enumerate(params):
|
||||||
|
if char == ' ':
|
||||||
|
if params[i+1] == ':':
|
||||||
|
result.append(params[last_param:i])
|
||||||
|
result.append(params[i+2:])
|
||||||
|
last_param = len(params)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
result.append(params[last_param:i])
|
||||||
|
last_param = i+1
|
||||||
|
elif i == 0 and char == ':':
|
||||||
|
result.append(params[i+1:])
|
||||||
|
last_param = len(params)
|
||||||
|
break
|
||||||
|
if last_param != len(params):
|
||||||
|
result.append(params[last_param:])
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_tags(tags):
|
||||||
|
if tags[0] != '@':
|
||||||
|
raise ValueError("Invalid tags")
|
||||||
|
result = {}
|
||||||
|
tags = tags[1:].split(';')
|
||||||
|
for tag in tags:
|
||||||
|
if '=' in tag:
|
||||||
|
tag = tag.split('=', 1)
|
||||||
|
result[tag[0]] = tag[1]
|
||||||
|
else:
|
||||||
|
result[tag] = None
|
||||||
|
return result
|
||||||
|
|
||||||
|
def process_command(self, command):
|
||||||
|
match = re.match(r'^(?P<tags>@(?:(?:\+?(?:[0-9A-Za-z.-]+/)?[0-9A-Za-z-]+)(?:=[^\x00\r\n; ]+)?)?(?:;(?:\+?(?:[0-9A-Za-z.-]+/)?[0-9A-Za-z-]+)(?:=[^\x00\r\n; ]+)?)*)? *(?::(?P<target>[^ ]*))? *(?P<command>[a-zA-Z]+|[0-9]{3}) *(?:(?P<params>.*?))?$', command)
|
||||||
|
if match is None:
|
||||||
|
print('Unknown command:', command, file=sys.stderr)
|
||||||
|
return
|
||||||
|
tags = match.group('tags')
|
||||||
|
if tags is not None:
|
||||||
|
tags = self.parse_tags(tags)
|
||||||
|
else:
|
||||||
|
tags = {}
|
||||||
|
target = match.group('target')
|
||||||
|
command = match.group('command')
|
||||||
|
params = self.parse_command_params(match.group('params'))
|
||||||
|
print('<=', tags, target, command, params)
|
||||||
|
if command.isnumeric():
|
||||||
|
command = int(command)
|
||||||
|
try:
|
||||||
|
command = Code(command)
|
||||||
|
except ValueError:
|
||||||
|
self.on_command(command, *params, target=target, tags=tags)
|
||||||
|
if command == Code.CAP:
|
||||||
|
if params[1] == 'ACK':
|
||||||
|
self.on_cap_ack(params[2], target=target, tags=tags)
|
||||||
|
elif params[1] == 'LS':
|
||||||
|
self.on_cap_ls(params[2], target=target, tags=tags)
|
||||||
|
elif command == Code.INVITE:
|
||||||
|
self.on_invite(*params, target=target, tags=tags)
|
||||||
|
elif command == Code.JOIN:
|
||||||
|
self.on_join(*params, target=target, tags=tags)
|
||||||
|
elif command == Code.PING:
|
||||||
|
self.on_ping(*params, target=target, tags=tags)
|
||||||
|
elif command == Code.PRIVMSG:
|
||||||
|
self.on_privmsg(*params, target=target, tags=tags)
|
||||||
|
elif command == Code.RPL_ENDOFWHOIS:
|
||||||
|
self.on_endofwhois(*params, target=target, tags=tags)
|
||||||
|
elif command == Code.RPL_WELCOME:
|
||||||
|
self.on_welcome(*params, target=target, tags=tags)
|
||||||
|
elif command == Code.RPL_WHOISACCOUNT:
|
||||||
|
self.on_whoisaccount(*params, target=target, tags=tags)
|
||||||
|
elif command == Code.RPL_WHOISREGNICK:
|
||||||
|
self.on_whoisregnick(*params, target=target, tags=tags)
|
||||||
|
|
||||||
|
def cap_end(self):
|
||||||
|
self.socket_mutex.acquire()
|
||||||
|
self.socket.sendall(f'{Code.CAP.value} END\r\n'.encode(self.encoding))
|
||||||
|
self.socket_mutex.release()
|
||||||
|
|
||||||
|
def cap_ls(self, _302=False):
|
||||||
|
self.socket_mutex.acquire()
|
||||||
|
if self.capabilities:
|
||||||
|
if _302:
|
||||||
|
self.socket.sendall(f'{Code.CAP.value} LS 302\r\n'.encode(self.encoding))
|
||||||
|
else:
|
||||||
|
self.socket.sendall(f'{Code.CAP.value} LS\r\n'.encode(self.encoding))
|
||||||
|
self.socket_mutex.release()
|
||||||
|
|
||||||
|
def cap_req(self, capabilities=None):
|
||||||
|
self.socket_mutex.acquire()
|
||||||
|
if capabilities is None:
|
||||||
|
capabilities = self.capabilities
|
||||||
|
capabilities = ' '.join(capabilities)
|
||||||
|
self.socket.sendall(f'{Code.CAP.value} REQ :{capabilities}\r\n'.encode(self.encoding))
|
||||||
|
self.socket_mutex.release()
|
||||||
|
|
||||||
|
def invite(self, nickname, channel):
|
||||||
|
self.socket_mutex.acquire()
|
||||||
|
self.socket.sendall(f'{Code.INVITE.value} {nickname} {channel}\r\n'.encode(self.encoding))
|
||||||
|
self.socket_mutex.release()
|
||||||
|
|
||||||
|
def join(self, channel):
|
||||||
|
self.socket_mutex.acquire()
|
||||||
|
self.socket.sendall(f'{Code.JOIN.value} {channel}\r\n'.encode(self.encoding))
|
||||||
|
self.socket_mutex.release()
|
||||||
|
|
||||||
|
def nick(self, nickname):
|
||||||
|
self.socket_mutex.acquire()
|
||||||
|
self.socket.sendall(f'{Code.NICK.value} {nickname}\r\n'.encode(self.encoding))
|
||||||
|
self.socket_mutex.release()
|
||||||
|
|
||||||
|
def pong(self, data):
|
||||||
|
self.socket_mutex.acquire()
|
||||||
|
self.socket.sendall(f'{Code.PONG.value} :{data}\r\n'.encode(self.encoding))
|
||||||
|
self.socket_mutex.release()
|
||||||
|
|
||||||
|
def privmsg(self, channel, message):
|
||||||
|
self.socket_mutex.acquire()
|
||||||
|
messages = message.split('\n')
|
||||||
|
for message in messages:
|
||||||
|
self.socket.sendall(f'{Code.PRIVMSG.value} {channel} :{message}\r\n'.encode(self.encoding))
|
||||||
|
self.socket_mutex.release()
|
||||||
|
|
||||||
|
def user(self, username, realname, mode=0):
|
||||||
|
self.socket_mutex.acquire()
|
||||||
|
self.socket.sendall(f'{Code.USER.value} {username} {mode} * :{realname}\r\n'.encode(self.encoding))
|
||||||
|
self.socket_mutex.release()
|
||||||
|
|
||||||
|
def whois(self, *masks, target=None):
|
||||||
|
self.socket_mutex.acquire()
|
||||||
|
masks = ' '.join(masks)
|
||||||
|
if target is None:
|
||||||
|
self.socket.sendall(f'{Code.WHOIS.value} {masks}\r\n'.encode(self.encoding))
|
||||||
|
else:
|
||||||
|
self.socket.sendall(f'{Code.WHOIS.value} {target} {masks}\r\n'.encode(self.encoding))
|
||||||
|
self.socket_mutex.release()
|
||||||
|
|
||||||
|
def on_cap_ack(self, capabilities, target, tags):
|
||||||
|
self.enabled_capabilities = capabilities.split()
|
||||||
|
self.cap_end()
|
||||||
|
|
||||||
|
def on_cap_ls(self, capabilities, target, tags):
|
||||||
|
capabilities = capabilities.split()
|
||||||
|
self.enabled_capabilities = [capability for capability in self.capabilities if capability in capabilities]
|
||||||
|
self.cap_req(self.enabled_capabilities)
|
||||||
|
|
||||||
|
def on_endofwhois(self, nickname, whoisnickname, info, target, tags):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_invite(self, nickname, channel, target, tags):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_join(self, channel, accountname='*', realname=None, target=None, tags=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_ping(self, data, target, tags):
|
||||||
|
self.pong(data)
|
||||||
|
|
||||||
|
def on_privmsg(self, channel, message, target, tags):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_welcome(self, nickname, reply, target, tags):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_whoisaccount(self, nickname, whoisnickname, account, info, target, tags):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_whoisregnick(self, nickname, whoisnickname, reply, target, tags):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_command(code, *params, target=None, tags=None):
|
||||||
|
pass
|
Loading…
Reference in New Issue