├── requirements.txt ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── src ├── captcha.py ├── covid-vaccine-slot-booking.py └── utils.py ├── LICENSE ├── .gitignore └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | tabulate 3 | inputimeout 4 | svglib==1.0.1 5 | reportlab==3.5.59 6 | pyinstaller 7 | pysimplegui 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://www.buymeacoffee.com/pallupz'] 13 | -------------------------------------------------------------------------------- /src/captcha.py: -------------------------------------------------------------------------------- 1 | from svglib.svglib import svg2rlg 2 | from reportlab.graphics import renderPM 3 | import PySimpleGUI as sg 4 | import re 5 | 6 | 7 | def captcha_builder(resp): 8 | with open('captcha.svg', 'w') as f: 9 | f.write(re.sub('()', '', resp['captcha'])) 10 | 11 | drawing = svg2rlg('captcha.svg') 12 | renderPM.drawToFile(drawing, "captcha.png", fmt="PNG") 13 | 14 | layout = [[sg.Image('captcha.png')], 15 | [sg.Text("Enter Captcha Below")], 16 | [sg.Input(key='inp')], 17 | [sg.Button('Submit', bind_return_key=True)]] 18 | 19 | window = sg.Window('Enter Captcha', layout, finalize=True) 20 | window.TKroot.focus_force() # focus on window 21 | window.Element('inp').SetFocus() # focus on field 22 | window.BringToFront() #To bring the captcha window infront of all active windows 23 | event, values = window.read() 24 | window.close() 25 | return values['inp'] 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 pallupz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI Windows EXE Build 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events 8 | push: 9 | paths: 10 | - '**.py' 11 | 12 | pull_request: 13 | paths: 14 | - '**.py' 15 | 16 | 17 | # Allows you to run this workflow manually from the Actions tab 18 | workflow_dispatch: 19 | 20 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 21 | jobs: 22 | # This workflow contains a single job called "build" 23 | build: 24 | # The type of runner that the job will run on 25 | runs-on: windows-latest 26 | 27 | # Steps represent a sequence of tasks that will be executed as part of the job 28 | steps: 29 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 30 | - uses: actions/checkout@v2 31 | - uses: fregante/setup-git-user@v1 32 | 33 | # Set branch name 34 | - name: Set branch name 35 | run: | 36 | $branchName = $Env:GITHUB_REF -replace "refs/heads/", "" 37 | echo $branchName 38 | 39 | # Install all required dependencies and pyinstaller to package code 40 | - name: Install dependencies 41 | run: | 42 | pip install -r requirements.txt 43 | pip install pyinstaller 44 | 45 | # Package script and move to root folder 46 | - name: Package Script to EXE 47 | run: | 48 | rm covid-vaccine-slot-booking.exe -ErrorAction Ignore 49 | cd src 50 | pyinstaller --onefile covid-vaccine-slot-booking.py 51 | cd .. 52 | move src\dist\covid-vaccine-slot-booking.exe . 53 | 54 | # Commit and push EXE file to branch 55 | - name: Push EXE to branch 56 | run: | 57 | git add covid-vaccine-slot-booking.exe 58 | git commit -m "rebuilt via GitHub Actions" 59 | git push 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .idea/ 11 | slot-booking/ 12 | src/build 13 | src/dist 14 | src/covid-vaccine-slot-booking.spec 15 | src/vaccine-booking-details.json 16 | src/captcha.png 17 | src/captcha.svg 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | pip-wheel-metadata/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COVID-19 Vaccination Slot Booking Script 2 | ## Update: 3 | ### **First up, a big thank you to everyone for supporting this project and building upon it! I'm glad we were able to help a lot of people through this. However, recently we have been attracting some unwanted attention - which is getting harder to handle. So unfortunately there won't be any additional commits to this project. This has been put on indefinite hold.** 4 | 5 | 6 | 7 | ### Important: 8 | - This is a proof of concept project. I do NOT endorse or condone, in any shape or form, automating any monitoring/booking tasks. **Use at your own risk.** 9 | - This CANNOT book slots automatically. It doesn't skip any of the steps that a normal user would have to take on the official portal. You will still have to enter the OTP and Captcha. 10 | - Do NOT use unless all the beneficiaries selected are supposed to get the same vaccine and dose. 11 | - There is no option to register new mobile or add beneficiaries. This can be used only after beneficiary has been added through the official app/site. 12 | - This goes without saying but, once you get your shot, please do help out any underprivileged people around you who may not have a laptop or the know-how. For instance any sort of domestic help, or the staff in your local grocery store, or literally the thousands of people who don't have the knowledge or luxury we do. 13 | - API Details (read the first paragraph at least): https://apisetu.gov.in/public/marketplace/api/cowin/cowin-public-v2 14 | - BMC Link: https://www.buymeacoffee.com/pallupz 15 | - All donations, as they materialize, will be split equally between COVID Kerala CMDRF and a centre for cerebral palsied children with multiple handicaps. 16 | - Discord ID for DMs: pallupz#5726 17 | - And finally, I know, code quality is awful. May be some day I will rewrite the whole thing from scratch. 18 | 19 | ### Noteworthy Forks 20 | - https://github.com/bombardier-gif/covid-vaccine-booking : I haven't tried this personally but, it looks like a promising, bit more automated solution that would require some more setting up. 21 | - https://github.com/vishalv26/covid-vaccine-booking : Haven't personally tried this either. It uses portable python and simplifies running the code by avoiding the need for separate Python installation. 22 | 23 | ### Usage: 24 | 25 | EXE file that was being built via ```pyinstaller``` on GitHub Actions does not work anymore but the **Python 3.7** code still does. If you don't already have Python and do not know how to set it up, instructions are at the bottom. It's not complicated at all and takes literally 5 minutes. Please do that and come back here. 26 | 27 | Download this code as zip, and extract it to some folder like ```C:\temp\covid-vaccine-booking```. Going by this structure, the py files should be in ```C:\temp\covid-vaccine-booking\src```. 28 | 29 | Open command prompt and run ```cd C:\temp\covid-vaccine-booking``` 30 | 31 | Install all the dependencies with the below. This is a one-time activity (for anyone not familiar with Python) 32 | ``` 33 | pip install -r requirements.txt 34 | ``` 35 | 36 | If you're on Linux or MacOS, install the SoX ([Sound eXchange](http://sox.sourceforge.net/ "Sound eXchange")) before running the Python script. To install, run: 37 | 38 | Ubuntu: 39 | ``` 40 | sudo apt install sox 41 | ``` 42 | MacOS: 43 | ``` 44 | brew install sox 45 | ``` 46 | 47 | Finally, run the script file as shown below: 48 | ``` 49 | python src\covid-vaccine-slot-booking.py 50 | ``` 51 | 52 | If you already have a bearer token, you can also use: 53 | ``` 54 | python src\covid-vaccine-slot-booking.py --token=YOUR-TOKEN-HERE 55 | ``` 56 | 57 | If you'd prefer to pass the mobile number while running the script instead of typing in later, you can also use: 58 | ``` 59 | python src\covid-vaccine-slot-booking.py --mobile=YOUR-MOBILE-NUMBER 60 | ``` 61 | 62 | ### Python 3.7.3 Installation in Windows 63 | - Check if Python is already installed by opening command prompt and running ```python --version```. 64 | - If the above command returns ```Python ``` you're probably good - provided version number is above 3.6 65 | - If Python's not installed, command would say something like: ```'python' is not recognized as an internal or external command, operable program or batch file.``` 66 | - If so, download the installer from: https://www.python.org/ftp/python/3.7.3/python-3.7.3-amd64.exe 67 | - Run that. In the first screen of installer, there will be an option at the bottom to "Add Python 3.7 to Path". Make sure to select it. 68 | - Open command prompt and run ```python --version```. If everything went well it should say ```Python 3.7.3``` 69 | - You're all set! 70 | 71 | -------------------------------------------------------------------------------- /src/covid-vaccine-slot-booking.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import copy 4 | from types import SimpleNamespace 5 | import requests, sys, argparse, os, datetime 6 | from utils import generate_token_OTP, check_and_book, beep, BENEFICIARIES_URL, WARNING_BEEP_DURATION, \ 7 | display_info_dict, save_user_info, collect_user_details, get_saved_user_info, confirm_and_proceed 8 | 9 | 10 | def main(): 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument('--token', help='Pass token directly') 13 | parser.add_argument('--mobile', help='Pass mobile directly') 14 | args = parser.parse_args() 15 | 16 | filename = 'vaccine-booking-details.json' 17 | mobile = None 18 | 19 | print('Running Script') 20 | beep(500, 150) 21 | 22 | try: 23 | base_request_header = { 24 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36', 25 | 'origin': 'https://selfregistration.cowin.gov.in/', 26 | 'referer': 'https://selfregistration.cowin.gov.in/' 27 | } 28 | 29 | if args.token: 30 | token = args.token 31 | else: 32 | mobile = '' 33 | if args.mobile: 34 | mobile = args.mobile 35 | else: 36 | mobile = input("Enter the registered mobile number: ") 37 | token = generate_token_OTP(mobile, base_request_header) 38 | 39 | request_header = copy.deepcopy(base_request_header) 40 | request_header["Authorization"] = f"Bearer {token}" 41 | 42 | if os.path.exists(filename): 43 | print("\n=================================== Note ===================================\n") 44 | print(f"Info from perhaps a previous run already exists in {filename} in this directory.") 45 | print(f"IMPORTANT: If this is your first time running this version of the application, DO NOT USE THE FILE!") 46 | try_file = input("Would you like to see the details and confirm to proceed? (y/n Default y): ") 47 | try_file = try_file if try_file else 'y' 48 | 49 | if try_file == 'y': 50 | collected_details = get_saved_user_info(filename) 51 | print("\n================================= Info =================================\n") 52 | display_info_dict(collected_details) 53 | 54 | file_acceptable = input("\nProceed with above info? (y/n Default n): ") 55 | file_acceptable = file_acceptable if file_acceptable else 'n' 56 | 57 | if file_acceptable != 'y': 58 | collected_details = collect_user_details(request_header) 59 | save_user_info(filename, collected_details) 60 | 61 | else: 62 | collected_details = collect_user_details(request_header) 63 | save_user_info(filename, collected_details) 64 | 65 | else: 66 | collected_details = collect_user_details(request_header) 67 | save_user_info(filename, collected_details) 68 | confirm_and_proceed(collected_details) 69 | 70 | info = SimpleNamespace(**collected_details) 71 | 72 | token_valid = True 73 | while token_valid: 74 | request_header = copy.deepcopy(base_request_header) 75 | request_header["Authorization"] = f"Bearer {token}" 76 | 77 | # call function to check and book slots 78 | token_valid = check_and_book(request_header, info.beneficiary_dtls, info.location_dtls, info.search_option, 79 | min_slots=info.minimum_slots, 80 | ref_freq=info.refresh_freq, 81 | auto_book=info.auto_book, 82 | start_date=info.start_date, 83 | vaccine_type=info.vaccine_type, 84 | fee_type=info.fee_type) 85 | 86 | # check if token is still valid 87 | beneficiaries_list = requests.get(BENEFICIARIES_URL, headers=request_header) 88 | if beneficiaries_list.status_code == 200: 89 | token_valid = True 90 | 91 | else: 92 | # if token invalid, regenerate OTP and new token 93 | beep(WARNING_BEEP_DURATION[0], WARNING_BEEP_DURATION[1]) 94 | print('Token is INVALID.') 95 | token_valid = False 96 | 97 | tryOTP = input('Try for a new Token? (y/n Default y): ') 98 | if tryOTP.lower() == 'y' or not tryOTP: 99 | if not mobile: 100 | mobile = input("Enter the registered mobile number: ") 101 | token = generate_token_OTP(mobile, base_request_header) 102 | token_valid = True 103 | else: 104 | print("Exiting") 105 | os.system("pause") 106 | 107 | except Exception as e: 108 | print(str(e)) 109 | print('Exiting Script') 110 | os.system("pause") 111 | 112 | 113 | if __name__ == '__main__': 114 | main() 115 | 116 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from hashlib import sha256 3 | from inputimeout import inputimeout, TimeoutOccurred 4 | import tabulate, copy, time, datetime, requests, sys, os, random 5 | from captcha import captcha_builder 6 | 7 | BOOKING_URL = "https://cdn-api.co-vin.in/api/v2/appointment/schedule" 8 | BENEFICIARIES_URL = "https://cdn-api.co-vin.in/api/v2/appointment/beneficiaries" 9 | CALENDAR_URL_DISTRICT = "https://cdn-api.co-vin.in/api/v2/appointment/sessions/calendarByDistrict?district_id={0}&date={1}" 10 | CALENDAR_URL_PINCODE = "https://cdn-api.co-vin.in/api/v2/appointment/sessions/calendarByPin?pincode={0}&date={1}" 11 | CAPTCHA_URL = "https://cdn-api.co-vin.in/api/v2/auth/getRecaptcha" 12 | OTP_PUBLIC_URL = 'https://cdn-api.co-vin.in/api/v2/auth/public/generateOTP' 13 | OTP_PRO_URL = 'https://cdn-api.co-vin.in/api/v2/auth/generateMobileOTP' 14 | 15 | WARNING_BEEP_DURATION = (1000, 2000) 16 | 17 | 18 | try: 19 | import winsound 20 | 21 | except ImportError: 22 | import os 23 | 24 | def beep(freq, duration): 25 | # apt install sox/brew install sox 26 | os.system( 27 | f"play -n synth {duration/1000} sin {freq} >/dev/null 2>&1") 28 | 29 | else: 30 | def beep(freq, duration): 31 | winsound.Beep(freq, duration) 32 | 33 | 34 | def viable_options(resp, minimum_slots, min_age_booking, fee_type, dose): 35 | options = [] 36 | if len(resp['centers']) >= 0: 37 | for center in resp['centers']: 38 | for session in center['sessions']: 39 | # availability = session['available_capacity'] 40 | availability = session['available_capacity_dose1'] if dose == 1 else session['available_capacity_dose2'] 41 | if (availability >= minimum_slots) \ 42 | and (session['min_age_limit'] <= min_age_booking)\ 43 | and (center['fee_type'] in fee_type): 44 | out = { 45 | 'name': center['name'], 46 | 'district': center['district_name'], 47 | 'pincode': center['pincode'], 48 | 'center_id': center['center_id'], 49 | 'available': availability, 50 | 'date': session['date'], 51 | 'slots': session['slots'], 52 | 'session_id': session['session_id'] 53 | } 54 | options.append(out) 55 | 56 | else: 57 | pass 58 | else: 59 | pass 60 | 61 | return options 62 | 63 | 64 | def display_table(dict_list): 65 | """ 66 | This function 67 | 1. Takes a list of dictionary 68 | 2. Add an Index column, and 69 | 3. Displays the data in tabular format 70 | """ 71 | header = ['idx'] + list(dict_list[0].keys()) 72 | rows = [[idx + 1] + list(x.values()) for idx, x in enumerate(dict_list)] 73 | print(tabulate.tabulate(rows, header, tablefmt='grid')) 74 | 75 | 76 | def display_info_dict(details): 77 | for key, value in details.items(): 78 | if isinstance(value, list): 79 | if all(isinstance(item, dict) for item in value): 80 | print(f"\t{key}:") 81 | display_table(value) 82 | else: 83 | print(f"\t{key}\t: {value}") 84 | else: 85 | print(f"\t{key}\t: {value}") 86 | 87 | 88 | def confirm_and_proceed(collected_details): 89 | print("\n================================= Confirm Info =================================\n") 90 | display_info_dict(collected_details) 91 | 92 | confirm = input("\nProceed with above info (y/n Default y) : ") 93 | confirm = confirm if confirm else 'y' 94 | if confirm != 'y': 95 | print("Details not confirmed. Exiting process.") 96 | os.system("pause") 97 | sys.exit() 98 | 99 | 100 | def save_user_info(filename, details): 101 | print("\n================================= Save Info =================================\n") 102 | save_info = input("Would you like to save this as a JSON file for easy use next time?: (y/n Default y): ") 103 | save_info = save_info if save_info else 'y' 104 | if save_info == 'y': 105 | with open(filename, 'w') as f: 106 | json.dump(details, f) 107 | 108 | print(f"Info saved to {filename} in {os.getcwd()}") 109 | 110 | 111 | def get_saved_user_info(filename): 112 | with open(filename, 'r') as f: 113 | data = json.load(f) 114 | 115 | return data 116 | 117 | 118 | def collect_user_details(request_header): 119 | # Get Beneficiaries 120 | print("Fetching registered beneficiaries.. ") 121 | beneficiary_dtls = get_beneficiaries(request_header) 122 | 123 | if len(beneficiary_dtls) == 0: 124 | print("There should be at least one beneficiary. Exiting.") 125 | os.system("pause") 126 | sys.exit(1) 127 | 128 | # Make sure all beneficiaries have the same type of vaccine 129 | vaccine_types = [beneficiary['vaccine'] for beneficiary in beneficiary_dtls] 130 | statuses = [beneficiary['status'] for beneficiary in beneficiary_dtls] 131 | 132 | if len(set(statuses)) > 1: 133 | print("\n================================= Important =================================\n") 134 | print(f"All beneficiaries in one attempt should be of same vaccination status (same dose). Found {statuses}") 135 | os.system("pause") 136 | sys.exit(1) 137 | 138 | vaccines = set(vaccine_types) 139 | if len(vaccines) > 1 and ('' in vaccines): 140 | vaccines.remove('') 141 | vaccine_types.remove('') 142 | print("\n================================= Important =================================\n") 143 | print(f"Some of the beneficiaries have a set vaccine preference ({vaccines}) and some do not.") 144 | print("Results will be filtered to show only the set vaccine preference.") 145 | os.system("pause") 146 | 147 | if len(vaccines) != 1: 148 | print("\n================================= Important =================================\n") 149 | print(f"All beneficiaries in one attempt should have the same vaccine type. Found {len(vaccines)}") 150 | os.system("pause") 151 | sys.exit(1) 152 | 153 | vaccine_type = vaccine_types[0] 154 | if not vaccine_type: 155 | print("\n================================= Vaccine Info =================================\n") 156 | vaccine_type = get_vaccine_preference() 157 | 158 | print("\n================================= Location Info =================================\n") 159 | # get search method to use 160 | search_option = input( 161 | """Search by Pincode? Or by State/District? \nEnter 1 for Pincode or 2 for State/District. (Default 2) : """) 162 | 163 | if not search_option or int(search_option) not in [1, 2]: 164 | search_option = 2 165 | else: 166 | search_option = int(search_option) 167 | 168 | if search_option == 2: 169 | # Collect vaccination center preferance 170 | location_dtls = get_districts(request_header) 171 | 172 | else: 173 | # Collect vaccination center preferance 174 | location_dtls = get_pincodes() 175 | 176 | print("\n================================= Additional Info =================================\n") 177 | 178 | # Set filter condition 179 | minimum_slots = input(f'Filter out centers with availability less than ? Minimum {len(beneficiary_dtls)} : ') 180 | if minimum_slots: 181 | minimum_slots = int(minimum_slots) if int(minimum_slots) >= len(beneficiary_dtls) else len(beneficiary_dtls) 182 | else: 183 | minimum_slots = len(beneficiary_dtls) 184 | 185 | # Get refresh frequency 186 | refresh_freq = input('How often do you want to refresh the calendar (in seconds)? Default 15. Minimum 5. : ') 187 | refresh_freq = int(refresh_freq) if refresh_freq and int(refresh_freq) >= 5 else 15 188 | 189 | # Get search start date 190 | start_date = input( 191 | '\nSearch for next seven day starting from when?\nUse 1 for today, 2 for tomorrow, or provide a date in the format DD-MM-YYYY. Default 2: ') 192 | if not start_date: 193 | start_date = 2 194 | elif start_date in ['1', '2']: 195 | start_date = int(start_date) 196 | else: 197 | try: 198 | datetime.datetime.strptime(start_date, '%d-%m-%Y') 199 | except ValueError: 200 | print('Invalid Date! Proceeding with tomorrow.') 201 | start_date = 2 202 | 203 | # Get preference of Free/Paid option 204 | fee_type = get_fee_type_preference() 205 | 206 | print("\n=========== CAUTION! =========== CAUTION! CAUTION! =============== CAUTION! =======\n") 207 | print("===== BE CAREFUL WITH THIS OPTION! AUTO-BOOKING WILL BOOK THE FIRST AVAILABLE CENTRE, DATE, AND A RANDOM SLOT! =====") 208 | auto_book = input("Do you want to enable auto-booking? (yes-please or no) Default no: ") 209 | auto_book = 'no' if not auto_book else auto_book 210 | 211 | collected_details = { 212 | 'beneficiary_dtls': beneficiary_dtls, 213 | 'location_dtls': location_dtls, 214 | 'search_option': search_option, 215 | 'minimum_slots': minimum_slots, 216 | 'refresh_freq': refresh_freq, 217 | 'auto_book': auto_book, 218 | 'start_date': start_date, 219 | 'vaccine_type': vaccine_type, 220 | 'fee_type': fee_type 221 | } 222 | 223 | return collected_details 224 | 225 | 226 | def check_calendar_by_district(request_header, vaccine_type, location_dtls, start_date, minimum_slots, min_age_booking, fee_type, dose): 227 | """ 228 | This function 229 | 1. Takes details required to check vaccination calendar 230 | 2. Filters result by minimum number of slots available 231 | 3. Returns False if token is invalid 232 | 4. Returns list of vaccination centers & slots if available 233 | """ 234 | try: 235 | print('===================================================================================') 236 | today = datetime.datetime.today() 237 | base_url = CALENDAR_URL_DISTRICT 238 | 239 | if vaccine_type: 240 | base_url += f"&vaccine={vaccine_type}" 241 | 242 | options = [] 243 | for location in location_dtls: 244 | resp = requests.get(base_url.format(location['district_id'], start_date), headers=request_header) 245 | 246 | if resp.status_code == 401: 247 | print('TOKEN INVALID') 248 | return False 249 | 250 | elif resp.status_code == 200: 251 | resp = resp.json() 252 | if 'centers' in resp: 253 | print(f"Centers available in {location['district_name']} from {start_date} as of {today.strftime('%Y-%m-%d %H:%M:%S')}: {len(resp['centers'])}") 254 | options += viable_options(resp, minimum_slots, min_age_booking, fee_type, dose) 255 | 256 | else: 257 | pass 258 | 259 | for location in location_dtls: 260 | if location['district_name'] in [option['district'] for option in options]: 261 | for _ in range(2): 262 | beep(location['alert_freq'], 150) 263 | return options 264 | 265 | except Exception as e: 266 | print(str(e)) 267 | beep(WARNING_BEEP_DURATION[0], WARNING_BEEP_DURATION[1]) 268 | 269 | 270 | def check_calendar_by_pincode(request_header, vaccine_type, location_dtls, start_date, minimum_slots, min_age_booking, fee_type, dose): 271 | """ 272 | This function 273 | 1. Takes details required to check vaccination calendar 274 | 2. Filters result by minimum number of slots available 275 | 3. Returns False if token is invalid 276 | 4. Returns list of vaccination centers & slots if available 277 | """ 278 | try: 279 | print('===================================================================================') 280 | today = datetime.datetime.today() 281 | base_url = CALENDAR_URL_PINCODE 282 | 283 | if vaccine_type: 284 | base_url += f"&vaccine={vaccine_type}" 285 | 286 | options = [] 287 | for location in location_dtls: 288 | resp = requests.get(base_url.format(location['pincode'], start_date), headers=request_header) 289 | 290 | if resp.status_code == 401: 291 | print('TOKEN INVALID') 292 | return False 293 | 294 | elif resp.status_code == 200: 295 | resp = resp.json() 296 | if 'centers' in resp: 297 | print(f"Centers available in {location['pincode']} from {start_date} as of {today.strftime('%Y-%m-%d %H:%M:%S')}: {len(resp['centers'])}") 298 | options += viable_options(resp, minimum_slots, min_age_booking, fee_type, dose) 299 | 300 | else: 301 | pass 302 | 303 | for location in location_dtls: 304 | if int(location['pincode']) in [option['pincode'] for option in options]: 305 | for _ in range(2): 306 | beep(location['alert_freq'], 150) 307 | 308 | return options 309 | 310 | except Exception as e: 311 | print(str(e)) 312 | beep(WARNING_BEEP_DURATION[0], WARNING_BEEP_DURATION[1]) 313 | 314 | 315 | def generate_captcha(request_header): 316 | print('================================= GETTING CAPTCHA ==================================================') 317 | resp = requests.post(CAPTCHA_URL, headers=request_header) 318 | print(f'Captcha Response Code: {resp.status_code}') 319 | 320 | if resp.status_code == 200: 321 | return captcha_builder(resp.json()) 322 | 323 | 324 | def book_appointment(request_header, details): 325 | """ 326 | This function 327 | 1. Takes details in json format 328 | 2. Attempts to book an appointment using the details 329 | 3. Returns True or False depending on Token Validity 330 | """ 331 | try: 332 | valid_captcha = True 333 | while valid_captcha: 334 | # captcha = generate_captcha(request_header) 335 | # details['captcha'] = captcha 336 | 337 | print('================================= ATTEMPTING BOOKING ==================================================') 338 | 339 | resp = requests.post(BOOKING_URL, headers=request_header, json=details) 340 | print(f'Booking Response Code: {resp.status_code}') 341 | print(f'Booking Response : {resp.text}') 342 | 343 | if resp.status_code == 401: 344 | print('TOKEN INVALID') 345 | return False 346 | 347 | elif resp.status_code == 200: 348 | beep(WARNING_BEEP_DURATION[0], WARNING_BEEP_DURATION[1]) 349 | print('############## BOOKED! ############################ BOOKED! ##############') 350 | print(" Hey, Hey, Hey! It's your lucky day! ") 351 | print('\nPress any key thrice to exit program.') 352 | os.system("pause") 353 | os.system("pause") 354 | os.system("pause") 355 | sys.exit() 356 | 357 | elif resp.status_code == 400: 358 | print(f'Response: {resp.status_code} : {resp.text}') 359 | pass 360 | 361 | else: 362 | print(f'Response: {resp.status_code} : {resp.text}') 363 | return True 364 | 365 | except Exception as e: 366 | print(str(e)) 367 | beep(WARNING_BEEP_DURATION[0], WARNING_BEEP_DURATION[1]) 368 | 369 | 370 | def check_and_book(request_header, beneficiary_dtls, location_dtls, search_option, **kwargs): 371 | """ 372 | This function 373 | 1. Checks the vaccination calendar for available slots, 374 | 2. Lists all viable options, 375 | 3. Takes user's choice of vaccination center and slot, 376 | 4. Calls function to book appointment, and 377 | 5. Returns True or False depending on Token Validity 378 | """ 379 | try: 380 | min_age_booking = get_min_age(beneficiary_dtls) 381 | 382 | minimum_slots = kwargs['min_slots'] 383 | refresh_freq = kwargs['ref_freq'] 384 | auto_book = kwargs['auto_book'] 385 | start_date = kwargs['start_date'] 386 | vaccine_type = kwargs['vaccine_type'] 387 | fee_type = kwargs['fee_type'] 388 | dose = 2 if [beneficiary['status'] for beneficiary in beneficiary_dtls][0] == 'Partially Vaccinated' else 1 389 | 390 | if isinstance(start_date, int) and start_date == 2: 391 | start_date = (datetime.datetime.today() + datetime.timedelta(days=1)).strftime("%d-%m-%Y") 392 | elif isinstance(start_date, int) and start_date == 1: 393 | start_date = datetime.datetime.today().strftime("%d-%m-%Y") 394 | else: 395 | pass 396 | 397 | if search_option == 2: 398 | options = check_calendar_by_district(request_header, vaccine_type, location_dtls, start_date, 399 | minimum_slots, min_age_booking, fee_type, dose) 400 | else: 401 | options = check_calendar_by_pincode(request_header, vaccine_type, location_dtls, start_date, 402 | minimum_slots, min_age_booking, fee_type, dose) 403 | 404 | if isinstance(options, bool): 405 | return False 406 | 407 | options = sorted(options, 408 | key=lambda k: (k['district'].lower(), k['pincode'], 409 | k['name'].lower(), 410 | datetime.datetime.strptime(k['date'], "%d-%m-%Y")) 411 | ) 412 | 413 | tmp_options = copy.deepcopy(options) 414 | if len(tmp_options) > 0: 415 | cleaned_options_for_display = [] 416 | for item in tmp_options: 417 | item.pop('session_id', None) 418 | item.pop('center_id', None) 419 | cleaned_options_for_display.append(item) 420 | 421 | display_table(cleaned_options_for_display) 422 | if auto_book == 'yes-please': 423 | print("AUTO-BOOKING IS ENABLED. PROCEEDING WITH FIRST CENTRE, DATE, and RANDOM SLOT.") 424 | option = options[0] 425 | random_slot = random.randint(1, len(option['slots'])) 426 | choice = f'1.{random_slot}' 427 | else: 428 | choice = inputimeout( 429 | prompt='----------> Wait 20 seconds for updated options OR \n----------> Enter a choice e.g: 1.4 for (1st center 4th slot): ', 430 | timeout=20) 431 | 432 | else: 433 | try: 434 | for i in range(refresh_freq, 0, -1): 435 | msg = f"No viable options. Next update in {i} seconds. OR press 'Ctrl + C' to refresh now." 436 | print(msg, end="\r", flush=True) 437 | sys.stdout.flush() 438 | time.sleep(1) 439 | except KeyboardInterrupt: 440 | pass 441 | choice = '.' 442 | 443 | except TimeoutOccurred: 444 | time.sleep(1) 445 | return True 446 | 447 | else: 448 | if choice == '.': 449 | return True 450 | else: 451 | try: 452 | choice = choice.split('.') 453 | choice = [int(item) for item in choice] 454 | print(f'============> Got Choice: Center #{choice[0]}, Slot #{choice[1]}') 455 | 456 | new_req = { 457 | 'beneficiaries': [beneficiary['bref_id'] for beneficiary in beneficiary_dtls], 458 | 'dose': 2 if [beneficiary['status'] for beneficiary in beneficiary_dtls][0] == 'Partially Vaccinated' else 1, 459 | 'center_id' : options[choice[0] - 1]['center_id'], 460 | 'session_id': options[choice[0] - 1]['session_id'], 461 | 'slot' : options[choice[0] - 1]['slots'][choice[1] - 1] 462 | } 463 | 464 | print(f'Booking with info: {new_req}') 465 | return book_appointment(request_header, new_req) 466 | 467 | except IndexError: 468 | print("============> Invalid Option!") 469 | os.system("pause") 470 | pass 471 | 472 | 473 | def get_vaccine_preference(): 474 | print("It seems you're trying to find a slot for your first dose. Do you have a vaccine preference?") 475 | preference = input("Enter 0 for No Preference, 1 for COVISHIELD, 2 for COVAXIN, or 3 for SPUTNIK V. Default 0 : ") 476 | preference = int(preference) if preference and int(preference) in [0, 1, 2, 3] else 0 477 | 478 | if preference == 1: 479 | return 'COVISHIELD' 480 | elif preference == 2: 481 | return 'COVAXIN' 482 | elif preference == 3: 483 | return 'SPUTNIK V' 484 | else: 485 | return None 486 | 487 | 488 | def get_fee_type_preference(): 489 | print("\nDo you have a fee type preference?") 490 | preference = input("Enter 0 for No Preference, 1 for Free Only, or 2 for Paid Only. Default 0 : ") 491 | preference = int(preference) if preference and int(preference) in [0, 1, 2] else 0 492 | 493 | if preference == 1: 494 | return ['Free'] 495 | elif preference == 2: 496 | return ['Paid'] 497 | else: 498 | return ['Free', 'Paid'] 499 | 500 | 501 | def get_pincodes(): 502 | locations = [] 503 | pincodes = input("Enter comma separated pincodes to monitor: ") 504 | for idx, pincode in enumerate(pincodes.split(',')): 505 | pincode = { 506 | 'pincode': pincode, 507 | 'alert_freq': 440 + ((2 * idx) * 110) 508 | } 509 | locations.append(pincode) 510 | return locations 511 | 512 | 513 | def get_districts(request_header): 514 | """ 515 | This function 516 | 1. Lists all states, prompts to select one, 517 | 2. Lists all districts in that state, prompts to select required ones, and 518 | 3. Returns the list of districts as list(dict) 519 | """ 520 | states = requests.get('https://cdn-api.co-vin.in/api/v2/admin/location/states', headers=request_header) 521 | 522 | if states.status_code == 200: 523 | states = states.json()['states'] 524 | 525 | refined_states = [] 526 | for state in states: 527 | tmp = {'state': state['state_name']} 528 | refined_states.append(tmp) 529 | 530 | display_table(refined_states) 531 | state = int(input('\nEnter State index: ')) 532 | state_id = states[state - 1]['state_id'] 533 | 534 | districts = requests.get(f'https://cdn-api.co-vin.in/api/v2/admin/location/districts/{state_id}', headers=request_header) 535 | 536 | if districts.status_code == 200: 537 | districts = districts.json()['districts'] 538 | 539 | refined_districts = [] 540 | for district in districts: 541 | tmp = {'district': district['district_name']} 542 | refined_districts.append(tmp) 543 | 544 | display_table(refined_districts) 545 | reqd_districts = input('\nEnter comma separated index numbers of districts to monitor : ') 546 | districts_idx = [int(idx) - 1 for idx in reqd_districts.split(',')] 547 | reqd_districts = [{ 548 | 'district_id': item['district_id'], 549 | 'district_name': item['district_name'], 550 | 'alert_freq': 440 + ((2 * idx) * 110) 551 | } for idx, item in enumerate(districts) if idx in districts_idx] 552 | 553 | print(f'Selected districts: ') 554 | display_table(reqd_districts) 555 | return reqd_districts 556 | 557 | else: 558 | print('Unable to fetch districts') 559 | print(districts.status_code) 560 | print(districts.text) 561 | os.system("pause") 562 | sys.exit(1) 563 | 564 | else: 565 | print('Unable to fetch states') 566 | print(states.status_code) 567 | print(states.text) 568 | os.system("pause") 569 | sys.exit(1) 570 | 571 | 572 | def get_beneficiaries(request_header): 573 | """ 574 | This function 575 | 1. Fetches all beneficiaries registered under the mobile number, 576 | 2. Prompts user to select the applicable beneficiaries, and 577 | 3. Returns the list of beneficiaries as list(dict) 578 | """ 579 | beneficiaries = requests.get(BENEFICIARIES_URL, headers=request_header) 580 | 581 | if beneficiaries.status_code == 200: 582 | beneficiaries = beneficiaries.json()['beneficiaries'] 583 | 584 | refined_beneficiaries = [] 585 | for beneficiary in beneficiaries: 586 | beneficiary['age'] = datetime.datetime.today().year - int(beneficiary['birth_year']) 587 | 588 | tmp = { 589 | 'bref_id': beneficiary['beneficiary_reference_id'], 590 | 'name': beneficiary['name'], 591 | 'vaccine': beneficiary['vaccine'], 592 | 'age': beneficiary['age'], 593 | 'status': beneficiary['vaccination_status'] 594 | } 595 | refined_beneficiaries.append(tmp) 596 | 597 | display_table(refined_beneficiaries) 598 | print(""" 599 | ################# IMPORTANT NOTES ################# 600 | # 1. While selecting beneficiaries, make sure that selected beneficiaries are all taking the same dose: either first OR second. 601 | # Please do no try to club together booking for first dose for one beneficiary and second dose for another beneficiary. 602 | # 603 | # 2. While selecting beneficiaries, also make sure that beneficiaries selected for second dose are all taking the same vaccine: COVISHIELD OR COVAXIN. 604 | # Please do no try to club together booking for beneficiary taking COVISHIELD with beneficiary taking COVAXIN. 605 | # 606 | # 3. If you're selecting multiple beneficiaries, make sure all are of the same age group (45+ or 18+) as defined by the govt. 607 | # Please do not try to club together booking for younger and older beneficiaries. 608 | ################################################### 609 | """) 610 | reqd_beneficiaries = input('Enter comma separated index numbers of beneficiaries to book for : ') 611 | beneficiary_idx = [int(idx) - 1 for idx in reqd_beneficiaries.split(',')] 612 | reqd_beneficiaries = [{ 613 | 'bref_id': item['beneficiary_reference_id'], 614 | 'name': item['name'], 615 | 'vaccine': item['vaccine'], 616 | 'age': item['age'], 617 | 'status': item['vaccination_status'] 618 | } for idx, item in enumerate(beneficiaries) if idx in beneficiary_idx] 619 | 620 | print(f'Selected beneficiaries: ') 621 | display_table(reqd_beneficiaries) 622 | return reqd_beneficiaries 623 | 624 | else: 625 | print('Unable to fetch beneficiaries') 626 | print(beneficiaries.status_code) 627 | print(beneficiaries.text) 628 | os.system("pause") 629 | return [] 630 | 631 | 632 | def get_min_age(beneficiary_dtls): 633 | """ 634 | This function returns a min age argument, based on age of all beneficiaries 635 | :param beneficiary_dtls: 636 | :return: min_age:int 637 | """ 638 | age_list = [item['age'] for item in beneficiary_dtls] 639 | min_age = min(age_list) 640 | return min_age 641 | 642 | 643 | def generate_token_OTP(mobile, request_header): 644 | """ 645 | This function generate OTP and returns a new token 646 | """ 647 | 648 | if not mobile: 649 | print("Mobile number cannot be empty") 650 | os.system('pause') 651 | sys.exit() 652 | 653 | valid_token = False 654 | while not valid_token: 655 | try: 656 | data = {"mobile": mobile, 657 | "secret": "U2FsdGVkX1+z/4Nr9nta+2DrVJSv7KS6VoQUSQ1ZXYDx/CJUkWxFYG6P3iM/VW+6jLQ9RDQVzp/RcZ8kbT41xw==" 658 | } 659 | txnId = requests.post(url=OTP_PRO_URL, json=data, headers=request_header) 660 | 661 | if txnId.status_code == 200: 662 | print(f"Successfully requested OTP for mobile number {mobile} at {datetime.datetime.today()}..") 663 | txnId = txnId.json()['txnId'] 664 | 665 | OTP = input("Enter OTP (If this takes more than 2 minutes, press Enter to retry): ") 666 | if OTP: 667 | data = {"otp": sha256(str(OTP).encode('utf-8')).hexdigest(), "txnId": txnId} 668 | print(f"Validating OTP..") 669 | 670 | token = requests.post(url='https://cdn-api.co-vin.in/api/v2/auth/validateMobileOtp', json=data, 671 | headers=request_header) 672 | if token.status_code == 200: 673 | token = token.json()['token'] 674 | print(f'Token Generated: {token}') 675 | valid_token = True 676 | return token 677 | 678 | else: 679 | print('Unable to Validate OTP') 680 | print(f"Response: {token.text}") 681 | 682 | retry = input(f"Retry with {mobile} ? (y/n Default y): ") 683 | retry = retry if retry else 'y' 684 | if retry == 'y': 685 | pass 686 | else: 687 | sys.exit() 688 | 689 | else: 690 | print('Unable to Generate OTP') 691 | print(txnId.status_code, txnId.text) 692 | 693 | retry = input(f"Retry with {mobile} ? (y/n Default y): ") 694 | retry = retry if retry else 'y' 695 | if retry == 'y': 696 | pass 697 | else: 698 | sys.exit() 699 | 700 | except Exception as e: 701 | print(str(e)) 702 | 703 | --------------------------------------------------------------------------------