diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..73f69e09 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/aws.xml b/.idea/aws.xml new file mode 100644 index 00000000..b63b642c --- /dev/null +++ b/.idea/aws.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/.idea/doctoshotgun.iml b/.idea/doctoshotgun.iml new file mode 100644 index 00000000..4f2c9af6 --- /dev/null +++ b/.idea/doctoshotgun.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..86561143 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..4278ec1e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/doctoshotgun.py b/doctoshotgun.py index 8e9096ef..1e4e0a45 100755 --- a/doctoshotgun.py +++ b/doctoshotgun.py @@ -35,6 +35,7 @@ try: from playsound import playsound as _playsound, PlaysoundException + def playsound(*args): try: return _playsound(*args) @@ -107,7 +108,7 @@ def get_next_page(self): # JavaScript: # var t = (e = r()(e)).data("u") # , n = atob(t.replace(/\s/g, '').split('').reverse().join('')); - + import base64 href = base64.urlsafe_b64decode(''.join(span.attrib['data-u'].split())[::-1]).decode() query = dict(parse.parse_qsl(parse.urlsplit(href).query)) @@ -121,9 +122,10 @@ def get_next_page(self): if 'page' in query: return int(query['page']) - + return None + class CenterResultPage(JsonPage): pass @@ -162,8 +164,8 @@ def get_agenda_ids(self, motive_id, practice_id=None): agenda_ids = [] for a in self.doc['data']['agendas']: if motive_id in a['visit_motive_ids'] and \ - not a['booking_disabled'] and \ - (not practice_id or a['practice_id'] == practice_id): + not a['booking_disabled'] and \ + (not practice_id or a['practice_id'] == practice_id): agenda_ids.append(str(a['id'])) return agenda_ids @@ -250,7 +252,8 @@ def __init__(self, *args, **kwargs): self.session.headers['sec-fetch-dest'] = 'document' self.session.headers['sec-fetch-mode'] = 'navigate' self.session.headers['sec-fetch-site'] = 'same-origin' - self.session.headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36' + self.session.headers[ + 'User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36' self.patient = None @@ -259,8 +262,9 @@ def do_login(self, code): self.open(self.BASEURL + '/sessions/new') except ServerError as e: if e.response.status_code in [503] \ - and 'text/html' in e.response.headers['Content-Type'] \ - and ('cloudflare' in e.response.text or 'Checking your browser before accessing' in e .response.text): + and 'text/html' in e.response.headers['Content-Type'] \ + and ( + 'cloudflare' in e.response.text or 'Checking your browser before accessing' in e.response.text): log('Request blocked by CloudFlare', color='red') if e.response.status_code in [520]: log('Cloudflare is unable to connect to Doctolib server. Please retry later.', color='red') @@ -279,7 +283,9 @@ def do_login(self, code): print("Requesting 2fa code...") if not code: if not sys.__stdin__.isatty(): - log("Auth Code input required, but no interactive terminal available. Please provide it via command line argument '--code'.", color='red') + log( + "Auth Code input required, but no interactive terminal available. Please provide it via command line argument '--code'.", + color='red') return False self.send_auth_code.go( json={'two_factor_auth_method': 'email'}, method="POST") @@ -299,12 +305,12 @@ def find_centers(self, where, motives=None, page=1): for city in where: try: self.centers.go(where=city, params={ - 'ref_visit_motive_ids[]': motives, 'page': page}) + 'ref_visit_motive_ids[]': motives, 'page': page}) except ServerError as e: if e.response.status_code in [503]: if 'text/html' in e.response.headers['Content-Type'] \ - and ('cloudflare' in e.response.text or - 'Checking your browser before accessing' in e .response.text): + and ('cloudflare' in e.response.text or + 'Checking your browser before accessing' in e.response.text): log('Request blocked by CloudFlare', color='red') return if e.response.status_code in [520]: @@ -359,7 +365,8 @@ def try_to_book(self, center, vaccine_list, start_date, end_date, only_second, o motives_id = dict() for vaccine in vaccine_list: motives_id[vaccine] = self.page.find_motive( - r'.*({})'.format(vaccine), singleShot=(vaccine == self.vaccine_motives[self.KEY_JANSSEN] or only_second or only_third)) + r'.*({})'.format(vaccine), + singleShot=(vaccine == self.vaccine_motives[self.KEY_JANSSEN] or only_second or only_third)) motives_id = dict((k, v) for k, v in motives_id.items() if v is not None) @@ -379,12 +386,14 @@ def try_to_book(self, center, vaccine_list, start_date, end_date, only_second, o # do not filter to give a chance agenda_ids = center_page.get_agenda_ids(motive_id) - if self.try_to_book_place(profile_id, motive_id, practice_id, agenda_ids, vac_name.lower(), start_date, end_date, only_second, only_third, dry_run): + if self.try_to_book_place(profile_id, motive_id, practice_id, agenda_ids, vac_name.lower(), start_date, + end_date, only_second, only_third, dry_run): return True return False - def try_to_book_place(self, profile_id, motive_id, practice_id, agenda_ids, vac_name, start_date, end_date, only_second, only_third, dry_run=False): + def try_to_book_place(self, profile_id, motive_id, practice_id, agenda_ids, vac_name, start_date, end_date, + only_second, only_third, dry_run=False): date = start_date.strftime('%Y-%m-%d') while date is not None: self.availabilities.go( @@ -435,9 +444,9 @@ def try_to_book_place(self, profile_id, motive_id, practice_id, agenda_ids, vac_ log(' ├╴ Best slot found: %s', parse_date( slot_date_first).strftime('%c')) - appointment = {'profile_id': profile_id, + appointment = {'profile_id': profile_id, 'source_action': 'profile', - 'start_date': slot_date_first, + 'start_date': slot_date_first, 'visit_motive_ids': str(motive_id), } @@ -548,6 +557,33 @@ def try_to_book_place(self, profile_id, motive_id, practice_id, agenda_ids, vac_ return self.page.doc['confirmed'] +class DoctolibFR(Doctolib): + BASEURL = 'https://www.doctolib.fr' + KEY_PFIZER = '6970' + KEY_PFIZER_SECOND = '6971' + KEY_PFIZER_THIRD = None + KEY_MODERNA = '7005' + KEY_MODERNA_SECOND = '7004' + KEY_MODERNA_THIRD = None + KEY_JANSSEN = '7945' + KEY_ASTRAZENECA = '7107' + KEY_ASTRAZENECA_SECOND = '7108' + vaccine_motives = { + KEY_PFIZER: 'Pfizer', + KEY_PFIZER_SECOND: '2de.*Pfizer', + KEY_PFIZER_THIRD: '3e.*Pfizer', + KEY_MODERNA: 'Moderna', + KEY_MODERNA_SECOND: '2de.*Moderna', + KEY_MODERNA_THIRD: '3e.*Moderna', + KEY_JANSSEN: 'Janssen', + KEY_ASTRAZENECA: 'AstraZeneca', + KEY_ASTRAZENECA_SECOND: '2de.*AstraZeneca', + } + + centers = URL(r'/vaccination-covid-19/(?P\w+)', CentersPage) + center = URL(r'/centre-de-sante/.*', CenterPage) + + class DoctolibDE(Doctolib): BASEURL = 'https://www.doctolib.de' KEY_PFIZER = '6768' @@ -574,33 +610,6 @@ class DoctolibDE(Doctolib): center = URL(r'/praxis/.*', CenterPage) -class DoctolibFR(Doctolib): - BASEURL = 'https://www.doctolib.fr' - KEY_PFIZER = '6970' - KEY_PFIZER_SECOND = '6971' - KEY_PFIZER_THIRD = '8192' - KEY_MODERNA = '7005' - KEY_MODERNA_SECOND = '7004' - KEY_MODERNA_THIRD = '8193' - KEY_JANSSEN = '7945' - KEY_ASTRAZENECA = '7107' - KEY_ASTRAZENECA_SECOND = '7108' - vaccine_motives = { - KEY_PFIZER: 'Pfizer', - KEY_PFIZER_SECOND: '2de.*Pfizer', - KEY_PFIZER_THIRD: '3e.*Pfizer', - KEY_MODERNA: 'Moderna', - KEY_MODERNA_SECOND: '2de.*Moderna', - KEY_MODERNA_THIRD: '3e.*Moderna', - KEY_JANSSEN: 'Janssen', - KEY_ASTRAZENECA: 'AstraZeneca', - KEY_ASTRAZENECA_SECOND: '2de.*AstraZeneca', - } - - centers = URL(r'/vaccination-covid-19/(?P\w+)', CentersPage) - center = URL(r'/centre-de-sante/.*', CenterPage) - - class Application: @classmethod def create_default_logger(cls): @@ -688,7 +697,8 @@ def main(self, cli_args=None): patients = docto.get_patients() if len(patients) == 0: - print("It seems that you don't have any Patient registered in your Doctolib account. Please fill your Patient data on Doctolib Website.") + print( + "It seems that you don't have any Patient registered in your Doctolib account. Please fill your Patient data on Doctolib Website.") return 1 if args.patient >= 0 and args.patient < len(patients): docto.patient = patients[args.patient] @@ -830,7 +840,8 @@ def main(self, cli_args=None): log('Center %(name_with_title)s (%(city)s):' % center) - if docto.try_to_book(center, vaccine_list, start_date, end_date, args.only_second, args.only_third, args.dry_run): + if docto.try_to_book(center, vaccine_list, start_date, end_date, args.only_second, args.only_third, + args.dry_run): log('') log('💉 %s Congratulations.' % colored('Booked!', 'green', attrs=('bold',))) @@ -839,7 +850,8 @@ def main(self, cli_args=None): sleep(SLEEP_INTERVAL_AFTER_CENTER) log('') - log('No free slots found at selected centers. Trying another round in %s sec...', SLEEP_INTERVAL_AFTER_RUN) + log('No free slots found at selected centers. Trying another round in %s sec...', + SLEEP_INTERVAL_AFTER_RUN) sleep(SLEEP_INTERVAL_AFTER_RUN) except CityNotFound as e: print('\n%s: City %s not found. Make sure you selected a city from the available countries.' % (