├── .gitignore ├── LICENSE ├── README.md ├── client ├── entry.py ├── hook-readchar.py ├── requirements.txt ├── resygrabber.py └── task_executor.py ├── server ├── Dockerfile ├── bin │ ├── Activate.ps1 │ ├── activate │ ├── activate.csh │ ├── activate.fish │ ├── dotenv │ ├── email_validator │ ├── fastapi │ ├── flask │ ├── gunicorn │ ├── httpx │ ├── markdown-it │ ├── normalizer │ ├── pip │ ├── pip3 │ ├── pip3.10 │ ├── pip3.9 │ ├── pygmentize │ ├── python │ ├── python3 │ ├── python3.9 │ ├── typer │ ├── uvicorn │ └── watchfiles ├── pyvenv.cfg ├── railway.json ├── requirements.txt ├── runtime.txt ├── server.py └── start.py └── start.py /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific configuration files 2 | proxies.json 3 | tasks.json 4 | info.json 5 | resrevations.json 6 | accounts.json 7 | license_key.json 8 | **/suspicious_commits.txt 9 | 10 | # Python cache files 11 | __pycache__/ 12 | **/__pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | 34 | # Unit test / coverage reports 35 | htmlcov/ 36 | .tox/ 37 | .coverage 38 | .coverage.* 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | *.cover 43 | .hypothesis/ 44 | 45 | # Virtual environments 46 | venv/ 47 | env/ 48 | ENV/ 49 | .env 50 | .venv 51 | *-venv/ 52 | 53 | # IDE specific files 54 | .idea/ 55 | .vscode/ 56 | *.swp 57 | *.swo 58 | .DS_Store 59 | 60 | # Log files 61 | *.log 62 | logs/ 63 | 64 | # Local development 65 | .env.local 66 | .env.development.local 67 | 68 | # Build artifacts 69 | *.spec -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ResyGrabber Contributors 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ResyGrabber - Restaurant Reservation Bot 2 | 3 | \*THIS USED TO BE CLOSED SOURCE, BUT I QUICKLY VIBECODED AN OPEN SOURCE VERSION WITHOUT LICENSING LOGIC. I ALSO QUICKLY MERGED TWO REPOS INTO THIS ONE. PLEASE LET ME KNOW IF ANY ISSUES ARE ENCOUNTERED, BUT YOU CAN AT LEAST EXAMINE THE CODE. FEEL FREE TO REACH OUT TO LEARN MORE ABOUT HOW I CREATED THIS! 4 | 5 | This is an open-source tool to help manage restaurant reservations on Resy.com. It was previously a SaaS product but has been converted to a locally runnable application with no authentication required. This is the first SAAS product that I've released and it was very fun to work on! I am open sourcing this now though because New York passed laws making it illegal to sell dinner reservations. 6 | 7 | ## Features 8 | 9 | - Create and manage reservation tasks 10 | - Use proxies to avoid IP blocks 11 | - Schedule tasks to run at specific times 12 | - Automatically book reservations when they become available 13 | - Bypass captchas by using an undocumented/deprecated API endpoint 14 | 15 | ## Setup and Installation 16 | 17 | ### Requirements 18 | 19 | - Python 3.7 or higher 20 | - pip (Python package manager) 21 | 22 | ### Installation 23 | 24 | 1. Clone this repository: 25 | 26 | ``` 27 | git clone https://github.com/yourusername/resybot-open.git 28 | cd resybot-open 29 | ``` 30 | 31 | 2. Install the dependencies for both client and server: 32 | 33 | ``` 34 | cd client 35 | pip install -r requirements.txt 36 | cd ../server 37 | pip install -r requirements.txt 38 | ``` 39 | 40 | ## Running the Application 41 | 42 | ### Simple Start (Recommended) 43 | 44 | To start both the server and client with a single command: 45 | 46 | ``` 47 | python start.py 48 | ``` 49 | 50 | This will launch both the server and client components automatically. 51 | 52 | ### Manual Start 53 | 54 | Alternatively, you can start each component separately: 55 | 56 | 1. Start the Server: 57 | 58 | ``` 59 | cd server 60 | python server.py 61 | ``` 62 | 63 | 2. Start the Client (in a separate terminal): 64 | 65 | ``` 66 | cd client 67 | python entry.py 68 | ``` 69 | 70 | The client will launch a menu-driven interface where you can: 71 | 72 | - Add and manage reservation tasks 73 | - Configure proxies 74 | - Manage Resy.com accounts 75 | - View and cancel existing reservations 76 | - Schedule and run tasks 77 | 78 | ## Configuration 79 | 80 | ### Adding Resy.com Accounts 81 | 82 | 1. From the main menu, select "4) Manage Accounts" 83 | 2. Follow the prompts to add your Resy.com account information 84 | - You'll need your Auth Token and Payment ID from Resy.com 85 | 86 | ### Adding Proxies (Optional but Recommended) 87 | 88 | 1. From the main menu, select "2) Proxies" 89 | 2. Add your proxy information to avoid IP rate limits 90 | 91 | ### Creating Tasks 92 | 93 | 1. From the main menu, select "1) Show tasks" 94 | 2. Select "a) Add task" 95 | 3. Follow the prompts to create a new reservation task 96 | 97 | ### Running Tasks 98 | 99 | 1. From the main menu, select "7) Start Tasks" 100 | 101 | ## Contributing 102 | 103 | Contributions are welcome! Please feel free to submit a Pull Request. 104 | 105 | ## Open Source Changes 106 | 107 | This project was previously a SaaS product with the following licensing/authentication requirements: 108 | 109 | - Required a valid license key through Whop 110 | - Client connected to a remote server hosted on Railway 111 | - HWID validation to prevent license key sharing 112 | 113 | The following changes were made to open source the project: 114 | 115 | - Removed all licensing and authentication requirements 116 | - Configured the client to connect to a local server instead of the remote server 117 | - Added a simple startup script to run both client and server with one command 118 | - Updated documentation with clear instructions for local usage 119 | 120 | ## License 121 | 122 | This project is licensed under the MIT License - see the LICENSE file for details. 123 | -------------------------------------------------------------------------------- /client/entry.py: -------------------------------------------------------------------------------- 1 | import click 2 | import os 3 | 4 | @click.command() 5 | def start(): 6 | # Directly import and run the main application 7 | import resygrabber 8 | resygrabber.menu() 9 | 10 | if __name__ == "__main__": 11 | start() 12 | -------------------------------------------------------------------------------- /client/hook-readchar.py: -------------------------------------------------------------------------------- 1 | # hook-readchar.py 2 | from PyInstaller.utils.hooks import copy_metadata 3 | 4 | datas = copy_metadata('readchar') 5 | -------------------------------------------------------------------------------- /client/requirements.txt: -------------------------------------------------------------------------------- 1 | altgraph==0.17.4 2 | APScheduler==3.10.4 3 | blessed==1.20.0 4 | capsolver==1.0.7 5 | certifi==2024.6.2 6 | charset-normalizer==3.3.2 7 | click==8.1.7 8 | editor==1.6.6 9 | idna==3.7 10 | importlib_metadata==7.1.0 11 | inquirer==3.2.5 12 | macholib==1.16.3 13 | markdown-it-py==3.0.0 14 | mdurl==0.1.2 15 | microvenv==2023.5.post1 16 | packaged==0.5.3 17 | packaging==24.1 18 | Pygments==2.18.0 19 | pyinstaller==6.8.0 20 | pyinstaller-hooks-contrib==2024.7 21 | pytz==2024.2 22 | readchar==4.1.0 23 | requests==2.32.3 24 | rich==13.7.1 25 | runs==1.2.2 26 | six==1.16.0 27 | termcolor==2.3.0 28 | tomli==2.0.1 29 | tzlocal==5.2 30 | urllib3==2.2.1 31 | uuid==1.30 32 | wcwidth==0.2.13 33 | xmod==1.8.1 34 | yaspin==3.0.2 35 | yen==0.4.4 36 | zipp==3.19.2 37 | -------------------------------------------------------------------------------- /client/resygrabber.py: -------------------------------------------------------------------------------- 1 | import click 2 | import inquirer 3 | import json 4 | import random 5 | import os 6 | import time 7 | import capsolver 8 | import re 9 | import uuid 10 | import urllib.parse 11 | import requests 12 | from datetime import datetime 13 | import datetime 14 | from apscheduler.schedulers.background import BackgroundScheduler 15 | from apscheduler.triggers.cron import CronTrigger 16 | from apscheduler.jobstores.memory import MemoryJobStore 17 | import threading 18 | import time 19 | from task_executor import run_tasks_concurrently 20 | 21 | scheduler = BackgroundScheduler() 22 | scheduler.add_jobstore(MemoryJobStore(), 'default') 23 | scheduler.start() 24 | 25 | running_tasks = {} 26 | 27 | # File paths for storing data 28 | TASKS_FILE = 'tasks.json' 29 | PROXIES_FILE = 'proxies.json' 30 | INFO_FILE = 'info.json' 31 | ACCESS_KEY_FILE = 'access_key.json' 32 | ACCOUNTS_FILE = 'accounts.json' 33 | RESERVATIONS_FILE = 'resrevations.json' 34 | 35 | # Load existing data 36 | def load_data(file, default): 37 | if os.path.exists(file): 38 | with open(file, 'r') as f: 39 | return json.load(f) 40 | return default 41 | 42 | # Save data to file 43 | def save_data(file, data): 44 | with open(file, 'w') as f: 45 | json.dump(data, f, indent=4) 46 | 47 | # Main CLI 48 | @click.group() 49 | def cli(): 50 | pass 51 | 52 | # Main Menu 53 | @cli.command() 54 | def menu(): 55 | while True: 56 | click.clear() 57 | click.echo(click.style('ResyGrabber', bold=True, fg='cyan', bg='black')) 58 | questions = [ 59 | inquirer.List('choice', 60 | message="Choose an option", 61 | choices=['1) Show tasks', '2) Proxies', '3) Info', '4) Manage Accounts', '5) Generate Accounts', '6) View Reservations', '7) Start Tasks', '8) Schedule Tasks', '9) Manage Scheduled tasks', 'Exit'], 62 | carousel=True) 63 | ] 64 | answers = inquirer.prompt(questions) 65 | 66 | if answers['choice'].startswith('1'): 67 | show_tasks() 68 | elif answers['choice'].startswith('2'): 69 | manage_proxies() 70 | elif answers['choice'].startswith('3'): 71 | manage_info() 72 | elif answers['choice'].startswith('4'): 73 | manage_accounts() 74 | elif answers['choice'].startswith('5'): 75 | generate_accounts() 76 | elif answers['choice'].startswith('6'): 77 | list_reservations() 78 | elif answers['choice'].startswith('7'): 79 | print("Tasks are running...") 80 | try: 81 | start_tasks() 82 | except Exception as e: 83 | print(f"Error starting tasks: {e}") 84 | elif answers['choice'].startswith('8'): 85 | schedule_tasks() 86 | elif answers['choice'].startswith('9'): 87 | view_scheduled_tasks() 88 | elif answers['choice'] == 'Exit': 89 | break 90 | 91 | import atexit 92 | atexit.register(lambda: scheduler.shutdown()) 93 | 94 | # Show Tasks 95 | def show_tasks(): 96 | while True: 97 | tasks = load_data(TASKS_FILE, []) 98 | click.clear() 99 | click.echo(click.style('Tasks', bold=True, fg='cyan')) 100 | for idx, task in enumerate(tasks): 101 | click.echo(f'{idx + 1}) {task}') 102 | questions = [ 103 | inquirer.List('task_choice', 104 | message="Choose an option", 105 | choices=['a) Add task', 'd) Delete task', 'Back'], 106 | carousel=True) 107 | ] 108 | answers = inquirer.prompt(questions) 109 | 110 | if answers['task_choice'] == 'a) Add task': 111 | add_task() 112 | elif answers['task_choice'] == 'd) Delete task': 113 | delete_task(tasks) 114 | elif answers['task_choice'] == 'Back': 115 | break 116 | 117 | """ def add_task(): 118 | info = load_data(INFO_FILE, {}) 119 | accounts = load_data(ACCOUNTS_FILE, []) 120 | captcha_services = [] 121 | if 'capsolver_key' in info: 122 | captcha_services.append('CAPSolver') 123 | if 'capmonster_key' in info: 124 | captcha_services.append('CapMonster') 125 | 126 | if not accounts: 127 | click.echo('No accounts found. Please add accounts before adding tasks.') 128 | return 129 | 130 | account_choices = [ 131 | (f'{idx + 1}) Account Name: {account["account_name"]}', idx) 132 | for idx, account in enumerate(accounts) 133 | ] 134 | 135 | questions = [ 136 | inquirer.List('account_choice', message="Select an account for this task:", choices=account_choices), 137 | inquirer.Text('restaurant_id', message="Please enter the restaurant ID:"), 138 | inquirer.Text('party_sz', message="Please enter the party sizes (comma-separated, e.g., 2,3,4):"), 139 | inquirer.Text('start_date', message="Please enter the start date (YYYY-MM-DD):"), 140 | inquirer.Text('end_date', message="Please enter the end date (YYYY-MM-DD):"), 141 | inquirer.Text('start_time', message="Please enter the start time (Hour only, 0-23):"), 142 | inquirer.Text('end_time', message="Please enter the end time (Hour only, 0-23):"), 143 | ] 144 | if captcha_services: 145 | questions.append(inquirer.List('captcha_service', 146 | message="Select the CAPTCHA solving service:", 147 | choices=captcha_services)) 148 | questions.append(inquirer.Text('delay', message="Enter the delay in milliseconds:")) 149 | questions.append(inquirer.Confirm('save_task', message="Do you want to save these tasks?", default=True)) 150 | 151 | answers = inquirer.prompt(questions) 152 | 153 | if answers['save_task']: 154 | selected_account_index = answers['account_choice'] 155 | selected_account = accounts[selected_account_index] 156 | 157 | # Parse multiple party sizes 158 | party_sizes = [int(size.strip()) for size in answers['party_sz'].split(',') if size.strip().isdigit()] 159 | 160 | tasks = load_data(TASKS_FILE, []) 161 | 162 | for party_size in party_sizes: 163 | task = { 164 | 'account_name': selected_account['account_name'], 165 | 'auth_token': selected_account['auth_token'], 166 | 'payment_id': selected_account['payment_id'], 167 | 'restaurant_id': answers['restaurant_id'], 168 | 'party_sz': party_size, 169 | 'start_date': answers['start_date'], 170 | 'end_date': answers['end_date'], 171 | 'start_time': int(answers['start_time']), 172 | 'end_time': int(answers['end_time']), 173 | 'captcha_service': answers.get('captcha_service'), 174 | 'delay': int(answers['delay']) 175 | } 176 | tasks.append(task) 177 | 178 | save_data(TASKS_FILE, tasks) 179 | else: 180 | click.echo('Tasks not saved.') 181 | """ 182 | 183 | def add_task(): 184 | info = load_data(INFO_FILE, {}) 185 | accounts = load_data(ACCOUNTS_FILE, []) 186 | captcha_services = [] 187 | if 'capsolver_key' in info: 188 | captcha_services.append('CAPSolver') 189 | if 'capmonster_key' in info: 190 | captcha_services.append('CapMonster') 191 | 192 | if not accounts: 193 | click.echo('No accounts found. Please add accounts before adding tasks.') 194 | return 195 | 196 | account_choices = [ 197 | inquirer.Checkbox('selected_accounts', 198 | message="Select accounts for this task (use spacebar to select, enter to confirm):", 199 | choices=[(account['account_name'], idx) for idx, account in enumerate(accounts)]) 200 | ] 201 | 202 | # First, prompt for account selection 203 | account_answers = inquirer.prompt(account_choices) 204 | 205 | if not account_answers['selected_accounts']: 206 | click.echo('No accounts selected. Task creation cancelled.') 207 | return 208 | 209 | # Then, prompt for other task details 210 | task_questions = [ 211 | inquirer.Text('restaurant_id', message="Please enter the restaurant ID:"), 212 | inquirer.Text('party_sz', message="Please enter the party sizes (comma-separated, e.g., 2,3,4):"), 213 | inquirer.Text('start_date', message="Please enter the start date (YYYY-MM-DD):"), 214 | inquirer.Text('end_date', message="Please enter the end date (YYYY-MM-DD):"), 215 | inquirer.Text('start_time', message="Please enter the start time (Hour only, 0-23):"), 216 | inquirer.Text('end_time', message="Please enter the end time (Hour only, 0-23):"), 217 | ] 218 | if captcha_services: 219 | task_questions.append(inquirer.List('captcha_service', 220 | message="Select the CAPTCHA solving service:", 221 | choices=captcha_services)) 222 | task_questions.append(inquirer.Text('delay', message="Enter the delay in milliseconds:")) 223 | task_questions.append(inquirer.Confirm('save_task', message="Do you want to save these tasks?", default=True)) 224 | 225 | task_answers = inquirer.prompt(task_questions) 226 | 227 | if task_answers['save_task']: 228 | selected_account_indices = account_answers['selected_accounts'] 229 | 230 | # Parse multiple party sizes 231 | party_sizes = [int(size.strip()) for size in task_answers['party_sz'].split(',') if size.strip().isdigit()] 232 | 233 | tasks = load_data(TASKS_FILE, []) 234 | 235 | for selected_account_index in selected_account_indices: 236 | selected_account = accounts[selected_account_index] 237 | for party_size in party_sizes: 238 | task = { 239 | 'account_name': selected_account['account_name'], 240 | 'auth_token': selected_account['auth_token'], 241 | 'payment_id': selected_account['payment_id'], 242 | 'restaurant_id': task_answers['restaurant_id'], 243 | 'party_sz': party_size, 244 | 'start_date': task_answers['start_date'], 245 | 'end_date': task_answers['end_date'], 246 | 'start_time': int(task_answers['start_time']), 247 | 'end_time': int(task_answers['end_time']), 248 | 'captcha_service': task_answers.get('captcha_service'), 249 | 'delay': int(task_answers['delay']) 250 | } 251 | tasks.append(task) 252 | 253 | save_data(TASKS_FILE, tasks) 254 | click.echo(f'Tasks saved for {len(selected_account_indices)} accounts.') 255 | else: 256 | click.echo('Tasks not saved.') 257 | 258 | 259 | def delete_task(tasks): 260 | # Filter out non-dictionary tasks 261 | valid_tasks = [task for task in tasks if isinstance(task, dict)] 262 | 263 | task_choices = [f'{idx + 1}) {task["restaurant_id"]}, {task["start_date"]}-{task["end_date"]}, {task["start_time"]}-{task["end_time"]}, Delay: {task["delay"]}ms' for idx, task in enumerate(valid_tasks)] 264 | questions = [ 265 | inquirer.List('task_to_delete', 266 | message="Select a task to delete", 267 | choices=task_choices + ['Cancel']) 268 | ] 269 | answers = inquirer.prompt(questions) 270 | if answers['task_to_delete'] != 'Cancel': 271 | task_index = int(answers['task_to_delete'].split(')')[0]) - 1 272 | valid_tasks.pop(task_index) 273 | save_data(TASKS_FILE, valid_tasks) 274 | click.echo('Task deleted!') 275 | 276 | # Manage Proxies 277 | def manage_proxies(): 278 | while True: 279 | proxies = load_data(PROXIES_FILE, []) 280 | click.clear() 281 | click.echo(click.style('Proxies', bold=True, fg='cyan')) 282 | for idx, proxy in enumerate(proxies): 283 | click.echo(f'{idx + 1}) {proxy}') 284 | questions = [ 285 | inquirer.List('proxy_choice', 286 | message="Choose an option", 287 | choices=['a) Add proxy', 'b) Delete proxy', 'c) Delete all proxies', 'Back'], 288 | carousel=True) 289 | ] 290 | answers = inquirer.prompt(questions) 291 | 292 | if answers['proxy_choice'] == 'a) Add proxy': 293 | add_proxy() 294 | elif answers['proxy_choice'] == 'b) Delete proxy': 295 | delete_proxy() 296 | elif answers['proxy_choice'] == 'c) Delete all proxies': 297 | delete_all_proxies() 298 | elif answers['proxy_choice'] == 'Back': 299 | break 300 | 301 | def add_proxy(): 302 | questions = [ 303 | inquirer.Text('proxies', message="Enter the proxies (separated by commas):") 304 | ] 305 | answers = inquirer.prompt(questions) 306 | proxies = load_data(PROXIES_FILE, []) # Reload proxies to get the latest list 307 | new_proxies = [proxy.strip() for proxy in answers['proxies'].split(',')] 308 | proxies.extend(new_proxies) 309 | save_data(PROXIES_FILE, proxies) 310 | click.echo('Proxies added!') 311 | 312 | def delete_proxy(): 313 | while True: 314 | proxies = load_data(PROXIES_FILE, []) # Reload proxies to get the latest list 315 | proxy_choices = [f'{idx + 1}) {proxy}' for idx, proxy in enumerate(proxies)] 316 | questions = [ 317 | inquirer.List('proxy_to_delete', 318 | message="Select a proxy to delete", 319 | choices=proxy_choices + ['Cancel']) 320 | ] 321 | answers = inquirer.prompt(questions) 322 | if answers['proxy_to_delete'] != 'Cancel': 323 | proxy_index = int(answers['proxy_to_delete'].split(')')[0]) - 1 324 | proxies.pop(proxy_index) 325 | save_data(PROXIES_FILE, proxies) 326 | click.echo('Proxy deleted!') 327 | if answers['proxy_to_delete'] == 'Cancel': 328 | break 329 | 330 | def delete_all_proxies(): 331 | questions = [ 332 | inquirer.Confirm('confirm_delete_all', message="Are you sure you want to delete all proxies?", default=False) 333 | ] 334 | answers = inquirer.prompt(questions) 335 | if answers['confirm_delete_all']: 336 | save_data(PROXIES_FILE, []) 337 | click.echo('All proxies deleted!') 338 | else: 339 | click.echo('No proxies were deleted.') 340 | 341 | # Manage User Info 342 | def manage_info(): 343 | info = load_data(INFO_FILE, {}) 344 | while True: 345 | click.clear() 346 | click.echo(click.style('User Info', bold=True, fg='cyan')) 347 | # click.echo(f'Payment ID: {info.get("payment_id", "Not set")}') 348 | # click.echo(f'Auth Token: {info.get("auth_token", "Not set")}') 349 | click.echo(f'CAPSolver Key: {info.get("capsolver_key", "Not set")}') 350 | click.echo(f'CapMonster Key: {info.get("capmonster_key", "Not set")}') 351 | click.echo(f'Discord Webhook: {info.get("discord_webhook", "Not set")}') 352 | questions = [ 353 | inquirer.List('info_choice', 354 | message="Choose an option", 355 | choices=['Set CAPSolver Key', 'Set CapMonster Key', 'Set Discord Webhook', 'Back'], 356 | carousel=True) 357 | ] 358 | answers = inquirer.prompt(questions) 359 | 360 | # if answers['info_choice'] == 'Set Payment ID': 361 | # set_payment_id(info) 362 | # elif answers['info_choice'] == 'Set Auth Token': 363 | # set_auth_token(info) 364 | if answers['info_choice'] == 'Set CAPSolver Key': 365 | set_capsolver_key(info) 366 | elif answers['info_choice'] == 'Set CapMonster Key': 367 | set_capmonster_key(info) 368 | elif answers['info_choice'] == 'Set Discord Webhook': 369 | set_discord_webhook(info) 370 | elif answers['info_choice'] == 'Back': 371 | break 372 | 373 | def set_payment_id(info): 374 | questions = [ 375 | inquirer.Text('payment_id', message="Enter your Resy Payment ID") 376 | ] 377 | answers = inquirer.prompt(questions) 378 | info['payment_id'] = answers['payment_id'] 379 | save_data(INFO_FILE, info) 380 | click.echo('Payment ID set!') 381 | 382 | def set_auth_token(info): 383 | auth_token = input("Enter your Resy Auth Token: ").strip() 384 | sanitized_auth_token = re.sub(r'\s+', '', auth_token) 385 | info['auth_token'] = sanitized_auth_token 386 | save_data(INFO_FILE, info) 387 | click.echo('Auth Token set!') 388 | 389 | def set_capsolver_key(info): 390 | questions = [ 391 | inquirer.Text('capsolver_key', message="Enter your CAPSolver Key") 392 | ] 393 | answers = inquirer.prompt(questions) 394 | info['capsolver_key'] = answers['capsolver_key'] 395 | save_data(INFO_FILE, info) 396 | click.echo('CAPSolver Key set!') 397 | 398 | def set_capmonster_key(info): 399 | questions = [ 400 | inquirer.Text('capmonster_key', message="Enter your CapMonster Key") 401 | ] 402 | answers = inquirer.prompt(questions) 403 | info['capmonster_key'] = answers['capmonster_key'] 404 | save_data(INFO_FILE, info) 405 | click.echo('CapMonster Key set!') 406 | 407 | def set_discord_webhook(info): 408 | discord_webhook = input("Enter your Discord Webhook URL: ").strip() 409 | sanitized_webhook = re.sub(r'\s+', '', discord_webhook) 410 | info['discord_webhook'] = sanitized_webhook 411 | save_data(INFO_FILE, info) 412 | click.echo('Discord Webhook URL set!') 413 | 414 | def manage_accounts(): 415 | while True: 416 | accounts = load_data(ACCOUNTS_FILE, []) 417 | click.clear() 418 | click.echo(click.style('Accounts', bold=True, fg='cyan')) 419 | for idx, account in enumerate(accounts): 420 | click.echo(f'{idx + 1}) {account}') 421 | questions = [ 422 | inquirer.List('account_choice', 423 | message="Choose an option", 424 | choices=['a) Add account', 'b) Delete account', 'Back'], 425 | carousel=True) 426 | ] 427 | answers = inquirer.prompt(questions) 428 | 429 | if answers['account_choice'] == 'a) Add account': 430 | add_account() 431 | elif answers['account_choice'] == 'b) Delete account': 432 | delete_account() 433 | elif answers['account_choice'] == 'Back': 434 | break 435 | 436 | def add_account(): 437 | accounts = load_data(ACCOUNTS_FILE, []) 438 | 439 | auth_token = input("Enter your Resy Auth Token: ").strip() 440 | payment_id = input("Enter your Resy Payment ID: ").strip() 441 | account_name = input("Enter a name for this account: ").strip() 442 | 443 | account = { 444 | 'auth_token': auth_token, 445 | 'payment_id': payment_id, 446 | 'account_name': account_name 447 | } 448 | accounts.append(account) 449 | save_data(ACCOUNTS_FILE, accounts) 450 | click.echo('Account added!') 451 | 452 | def delete_account(): 453 | accounts = load_data(ACCOUNTS_FILE, []) 454 | if not accounts: 455 | click.echo('No accounts found.') 456 | return 457 | account_choices = [f'{idx + 1}) Account Name: {account["account_name"]}' for idx, account in enumerate(accounts)] 458 | questions = [ 459 | inquirer.List('account_to_delete', 460 | message="Select an account to delete", 461 | choices=account_choices + ['Cancel']) 462 | ] 463 | answers = inquirer.prompt(questions) 464 | if answers['account_to_delete'] != 'Cancel': 465 | account_index = int(answers['account_to_delete'].split(')')[0]) - 1 466 | accounts.pop(account_index) 467 | save_data(ACCOUNTS_FILE, accounts) 468 | click.echo('Account deleted!') 469 | 470 | def get_random_proxy(): 471 | proxies = load_data(PROXIES_FILE, []) 472 | proxy = random.choice(proxies) 473 | ip_port, user_pass = proxy.rsplit(':', 2)[0], proxy.rsplit(':', 2)[1:] 474 | proxiesObj = { 475 | 'http': f'http://{user_pass[0]}:{user_pass[1]}@{ip_port}', 476 | 'https': f'http://{user_pass[0]}:{user_pass[1]}@{ip_port}' 477 | } 478 | return proxiesObj 479 | 480 | def generate_accounts(): 481 | accounts = load_data(ACCOUNTS_FILE, []) 482 | info = load_data(INFO_FILE, {}) 483 | proxies = load_data(PROXIES_FILE, []) 484 | 485 | if(info['capsolver_key'] == ''): 486 | click.echo('Please set your CAPSolver Key before generating accounts.') 487 | return 488 | 489 | if not proxies: 490 | click.echo('No proxies found. Please add proxies before generating accounts.') 491 | return 492 | 493 | questions = [ 494 | inquirer.Text('first_name', message="Enter the first name"), 495 | inquirer.Text('last_name', message="Enter the last name"), 496 | inquirer.Text('mobile_number', message="Enter the phone number (ex: 2145557505)"), 497 | inquirer.Text('em_address', message="Enter the email"), 498 | inquirer.Text('password', message="Enter the password"), 499 | #inquirer.Text('card_number', message="Enter the card number"), 500 | #inquirer.Text('exp_month', message="Enter the expiration month (2 digits, ex: 02)"), 501 | #inquirer.Text('exp_year', message="Enter the expiration year (2 digits, ex: 24)"), 502 | #inquirer.Text('cvv', message="Enter the CVV (3 digits)"), 503 | inquirer.Text('zip_code', message="Enter the zip code"), 504 | inquirer.Text('acc_name', message="Enter the account name"), 505 | ] 506 | 507 | answers = inquirer.prompt(questions) 508 | proxy = random.choice(proxies) 509 | capToken = get_captcha_token(info['capsolver_key'], '6Lfw-dIZAAAAAESRBH4JwdgfTXj5LlS1ewlvvCYe', 'https://resy.com', proxy) 510 | new_device_token = str(uuid.uuid4()) 511 | print(f'CAPTCHA Token: {capToken}\n') 512 | data = { 513 | 'first_name': answers['first_name'], 514 | 'last_name': answers['last_name'], 515 | 'mobile_number': f'+1{answers["mobile_number"]}', 516 | 'em_address': answers['em_address'], 517 | 'policies_accept': 1, 518 | 'complete': 1, 519 | 'device_type_id': 3, 520 | 'device_token': new_device_token, 521 | 'marketing_opt_in': 0, 522 | 'isNonUS': 0, 523 | 'password': answers['password'], 524 | 'captcha_token': capToken, 525 | } 526 | 527 | # paymentdata = { 528 | # 'card_number': answers['card_number'], 529 | # 'exp_month': answers['exp_month'], 530 | # 'exp_year': answers['exp_year'], 531 | # 'cvv': answers['cvv'], 532 | # 'zip_code': answers['zip_code'], 533 | # } 534 | 535 | headers = { 536 | 'Host': 'api.resy.com', 537 | 'X-Origin': 'https://resy.com', 538 | 'Authorization': 'ResyAPI api_key="VbWk7s3L4KiK5fzlO7JD3Q5EYolJI7n5"', 539 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', 540 | 'Accept': 'application/json, text/plain, */*', 541 | 'Cache-Control': 'no-cache', 542 | 'Sec-Fetch-Dest': 'empty', 543 | 'Referer': 'https://resy.com/', 544 | 'Content-Type': 'application/x-www-form-urlencoded', 545 | } 546 | ip_port, user_pass = proxy.rsplit(':', 2)[0], proxy.rsplit(':', 2)[1:] 547 | proxiesObj = { 548 | 'http': f'http://{user_pass[0]}:{user_pass[1]}@{ip_port}', 549 | 'https': f'http://{user_pass[0]}:{user_pass[1]}@{ip_port}' 550 | } 551 | 552 | res = requests.post('https://api.resy.com/2/user/registration', headers=headers, data=data, proxies=proxiesObj) 553 | response = res.json() 554 | accToken = response['user']['token'] 555 | print(f'Account Token: {accToken}\n') 556 | #Sleep for 2 seconds 557 | time.sleep(2) 558 | 559 | # Create new account entry 560 | new_account = { 561 | 'account_name': answers['acc_name'], 562 | 'auth_token': accToken, 563 | 'payment_id': '0' # Placeholder payment ID 564 | } 565 | 566 | # Add the new account to the accounts list 567 | accounts.append(new_account) 568 | 569 | # Save updated accounts list to file 570 | save_data(ACCOUNTS_FILE, accounts) 571 | 572 | print(f'Account "{answers["acc_name"]}" has been saved successfully!') 573 | 574 | #client_secret = setup_intent(accToken, proxiesObj) 575 | #pm = getPm(paymentdata, client_secret, proxiesObj) 576 | #setPm(accToken, pm, proxiesObj) 577 | 578 | #time.sleep(15) 579 | 580 | def get_captcha_token(captcha_key, site_key, url, proxy): 581 | #put http:// in front of the proxy 582 | proxy = 'http://' + proxy 583 | capsolver.api_key = captcha_key 584 | PAGE_URL = url 585 | PAGE_KEY = site_key 586 | print(f'Proxy: {proxy}') 587 | print('Solving CAPTCHA...') 588 | return solve_recaptcha_v2(PAGE_URL, PAGE_KEY, proxy) 589 | 590 | def solve_recaptcha_v2(url, key, proxy): 591 | while True: 592 | try: 593 | solution = capsolver.solve({ 594 | "type": "RecaptchaV2Task", 595 | "websiteURL": url, 596 | "websiteKey": key, 597 | "proxy": proxy 598 | }) 599 | 600 | if 'gRecaptchaResponse' in solution: 601 | return solution['gRecaptchaResponse'] 602 | else: 603 | # Handle cases where the solution does not include the expected key 604 | print("CAPTCHA solving is still in progress or there was an error.") 605 | time.sleep(3) # Wait before trying again 606 | 607 | except Exception as e: 608 | print(f"An error occurred: {e}") 609 | time.sleep(5) # Wait before trying again 610 | 611 | def setup_intent(accToken, proxiesObj): 612 | headers = { 613 | 'Host': 'api.resy.com', 614 | 'Accept': 'application/json, text/plain, */*', 615 | 'Authorization': 'ResyAPI api_key="VbWk7s3L4KiK5fzlO7JD3Q5EYolJI7n5"', 616 | 'Sec-Fetch-Site': 'same-site', 617 | 'X-Origin': 'https://resy.com', 618 | 'Accept-Language': 'en-US,en;q=0.9', 619 | 'Cache-Control': 'no-cache', 620 | 'X-Resy-Auth-Token': accToken, 621 | 'Sec-Fetch-Mode': 'cors', 622 | 'Origin': 'https://resy.com', 623 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15', 624 | 'Referer': 'https://resy.com/', 625 | 'Connection': 'keep-alive', 626 | 'Sec-Fetch-Dest': 'empty', 627 | 'X-Resy-Universal-Auth': accToken, 628 | } 629 | 630 | response = requests.post('https://api.resy.com/3/stripe/setup_intent', headers=headers, proxies=proxiesObj) 631 | res = response.json() 632 | return res['client_secret'] 633 | 634 | def setPm(accToken, pm, proxiesObj): 635 | headers = { 636 | 'Host': 'api.resy.com', 637 | 'Accept': 'application/json, text/plain, */*', 638 | 'Authorization': 'ResyAPI api_key="VbWk7s3L4KiK5fzlO7JD3Q5EYolJI7n5"', 639 | 'Sec-Fetch-Site': 'same-site', 640 | 'X-Origin': 'https://resy.com', 641 | 'Accept-Language': 'en-US,en;q=0.9', 642 | 'Cache-Control': 'no-cache', 643 | 'Sec-Fetch-Mode': 'cors', 644 | 'X-Resy-Auth-Token': accToken, 645 | 'Origin': 'https://resy.com', 646 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15', 647 | 'Referer': 'https://resy.com/', 648 | 'Connection': 'keep-alive', 649 | 'Sec-Fetch-Dest': 'empty', 650 | 'X-Resy-Universal-Auth': accToken, 651 | 'Content-Type': 'application/x-www-form-urlencoded', 652 | } 653 | 654 | data = { 655 | 'stripe_payment_method_id': pm, 656 | } 657 | response = requests.post('https://api.resy.com/3/stripe/payment_method', headers=headers, data=data, proxies=proxiesObj) 658 | print(f'Final Response: {response.text}') 659 | 660 | """ def get_captcha_token(captcha_key, site_key, url, proxies): 661 | proxy = random.choice(proxies) 662 | payload = { 663 | "clientKey": captcha_key, 664 | "task": { 665 | "type": "RecaptchaV2Task", 666 | "websiteKey": site_key, 667 | "websiteURL": url, 668 | "proxy": f'{proxy}' 669 | } 670 | } 671 | 672 | res = requests.post('https://api.capsolver.com/createtask', json=payload) 673 | print(f'res: {res}') 674 | print(f'res text: {res.text}') 675 | resp = res.json() 676 | print(f'resp: {resp}') 677 | print 678 | task_id = resp.get('taskId') 679 | if not task_id: 680 | print(f'Failed to get CAPTCHA token: {resp}') 681 | return 682 | print(f"Got taskId: {task_id} / Getting result...") 683 | 684 | while True: 685 | time.sleep(3) # delay 686 | payload = {"clientKey": captcha_key, "taskId": task_id} 687 | res = requests.post("https://api.capsolver.com/getTaskResult", json=payload) 688 | resp = res.json() 689 | status = resp.get("status") 690 | if status == "ready": 691 | print(f'Solved, response: {resp.get("solution")}') 692 | return resp.get("solution", {}).get('gRecaptchaResponse') 693 | if status == "failed" or resp.get("errorId"): 694 | print("Solve failed! response:", res.text) 695 | return """ 696 | 697 | def list_reservations(): 698 | accounts = load_data(ACCOUNTS_FILE, []) 699 | if not accounts: 700 | click.echo('No accounts found. Please add accounts before listing reservations.') 701 | return 702 | all_reservations = [] 703 | # Get all reservations for each account 704 | for account in accounts: 705 | auth_token = account['auth_token'] 706 | account_name = account['account_name'] 707 | reservations = get_account_reservations(auth_token, account_name) 708 | all_reservations.extend(reservations) 709 | 710 | save_data(RESERVATIONS_FILE, all_reservations) 711 | 712 | if all_reservations: 713 | show_reservations() 714 | 715 | def show_reservations(): 716 | reservations = load_data(RESERVATIONS_FILE, []) 717 | click.clear() 718 | if not reservations: 719 | click.echo('No reservations found.') 720 | return 721 | 722 | while True: 723 | click.echo(click.style('Reservations', bold=True, fg='cyan')) 724 | 725 | # Create a list of reservation choices 726 | res_choices = [] 727 | for idx, res in enumerate(reservations): 728 | res_info = f'{idx + 1}) Email: {res["email"]}, Venue: {res["venue"]}, Day: {res["day"]}, Time Slot: {res["time_slot"]}, Seats: {res["num_seats"]}, Link: {res["link"]}' 729 | if 'cancel_by' in res: 730 | res_info += f', Cancel By: {res["cancel_by"]}' 731 | res_choices.append(res_info) 732 | 733 | # Add a 'Back' option 734 | res_choices.append('Back') 735 | 736 | # Prompt user to select a reservation or go back 737 | questions = [ 738 | inquirer.List('res_choice', 739 | message='Select a reservation or choose Back', 740 | choices=res_choices) 741 | ] 742 | answers = inquirer.prompt(questions) 743 | 744 | if answers['res_choice'] == 'Back': 745 | return 746 | 747 | # Extract reservation index and details 748 | res_index = int(answers['res_choice'].split(')')[0]) - 1 749 | res = reservations[res_index] 750 | 751 | # Show reservation details and options 752 | action = show_reservation_details(res) 753 | 754 | if action == 'cancel': 755 | # Remove the cancelled reservation from the list 756 | reservations.pop(res_index) 757 | # Save updated reservations 758 | save_data(RESERVATIONS_FILE, reservations) 759 | # If all reservations are cancelled, exit the function 760 | if not reservations: 761 | click.echo('No more reservations.') 762 | return 763 | # If action is not 'cancel', it's 'back', so we continue the loop 764 | 765 | 766 | 767 | def show_reservation_details(res): 768 | while True: 769 | click.clear() 770 | click.echo(click.style('Reservation Details', bold=True, fg='cyan')) 771 | click.echo(f'Email: {res["email"]}') 772 | click.echo(f'Venue: {res["venue"]}') 773 | click.echo(f'Day: {res["day"]}') 774 | click.echo(f'Time Slot: {res["time_slot"]}') 775 | click.echo(f'Seats: {res["num_seats"]}') 776 | click.echo(f'Link: {res["link"]}') 777 | if 'cancel_by' in res: 778 | click.echo(f'Cancel By: {res["cancel_by"]}') 779 | 780 | action_questions = [ 781 | inquirer.List('res_action', 782 | message='Choose an option', 783 | choices=['Cancel reservation', 'Back to list']) 784 | ] 785 | action_answers = inquirer.prompt(action_questions) 786 | if action_answers['res_action'] == 'Cancel reservation': 787 | cancel_reservation(res['auth_token'], res['resy_token']) 788 | return 'cancel' # Indicate that the reservation was cancelled 789 | elif action_answers['res_action'] == 'Back to list': 790 | return 'back' # Go back to the reservation list 791 | 792 | 793 | def cancel_reservation(auth_token, resy_token): 794 | click.echo('Cancelling reservation...') 795 | proxy = get_random_proxy() 796 | headers = { 797 | 'Host': 'api.resy.com', 798 | 'Accept': 'application/json, text/plain, */*', 799 | 'Authorization': 'ResyAPI api_key="VbWk7s3L4KiK5fzlO7JD3Q5EYolJI7n5"', 800 | 'Sec-Fetch-Site': 'same-site', 801 | 'X-Origin': 'https://resy.com', 802 | 'Accept-Language': 'en-US,en;q=0.9', 803 | 'Cache-Control': 'no-cache', 804 | 'Sec-Fetch-Mode': 'cors', 805 | 'X-Resy-Auth-Token': auth_token, 806 | 'Origin': 'https://resy.com', 807 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15', 808 | 'Referer': 'https://resy.com/', 809 | 'Connection': 'keep-alive', 810 | 'Sec-Fetch-Dest': 'empty', 811 | 'X-Resy-Universal-Auth': auth_token, 812 | 'Content-Type': 'application/x-www-form-urlencoded', 813 | } 814 | data = { 815 | 'resy_token': resy_token, 816 | } 817 | try: 818 | response = requests.post('https://api.resy.com/3/cancel', headers=headers, data=data, proxies=proxy) 819 | if response.status_code == 200: 820 | click.echo(click.style('Reservation cancelled successfully!', fg='green')) 821 | reservations = load_data(RESERVATIONS_FILE, []) 822 | reservations = [res for res in reservations if res['resy_token'] != resy_token] 823 | save_data(RESERVATIONS_FILE, reservations) 824 | else: 825 | click.echo(click.style('Failed to cancel reservation.', fg='red')) 826 | except Exception as e: 827 | click.echo(click.style(f'Error cancelling reservation: {e}', fg='red')) 828 | input('Press Enter to continue...') 829 | 830 | def get_account_reservations(auth_token, account_name): 831 | proxy = get_random_proxy() 832 | headers = { 833 | 'Host': 'api.resy.com', 834 | 'Authorization': 'ResyAPI api_key="VbWk7s3L4KiK5fzlO7JD3Q5EYolJI7n5"', 835 | 'Sec-Fetch-Site': 'same-site', 836 | 'X-Origin': 'https://resy.com', 837 | 'Accept-Language': 'en-US,en;q=0.9', 838 | 'Cache-Control': 'no-cache', 839 | 'X-Resy-Auth-Token': auth_token, 840 | 'Sec-Fetch-Mode': 'cors', 841 | 'Origin': 'https://resy.com', 842 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15', 843 | 'Referer': 'https://resy.com/', 844 | 'Connection': 'keep-alive', 845 | 'Accept': 'application/json, text/plain, */*', 846 | 'Sec-Fetch-Dest': 'empty', 847 | 'X-Resy-Universal-Auth': auth_token, 848 | } 849 | 850 | params = { 851 | 'type': 'upcoming', 852 | } 853 | 854 | response = requests.get('https://api.resy.com/3/user/reservations', params=params, headers=headers, proxies=proxy) 855 | res = response.json() 856 | 857 | account_reservations = [] 858 | for reservation in res['reservations']: 859 | venue_id = str(reservation['venue']['id']) 860 | res = { 861 | 'resy_token': reservation['resy_token'], 862 | 'auth_token': auth_token, 863 | #'venue': reservation['venue']['id'], 864 | 'venue': res['venues'][venue_id]['name'], 865 | 'first_name': reservation['party'][0]['first_name'], 866 | 'last_name': reservation['party'][0]['last_name'], 867 | 'email': reservation['party'][0]['user']['em_address'], 868 | 'day': reservation['day'], 869 | 'time_slot': reservation['time_slot'], 870 | 'num_seats': reservation['num_seats'], 871 | 'link': reservation['share']['link'], 872 | } 873 | 874 | if 'cancellation' in reservation and reservation['cancellation'] and 'date_refund_cut_off' in reservation['cancellation']: 875 | res['cancel_by'] = reservation['cancellation']['date_refund_cut_off'] 876 | account_reservations.append(res) 877 | 878 | return account_reservations 879 | 880 | def schedule_tasks(): 881 | tasks = load_data(TASKS_FILE, []) 882 | if not tasks: 883 | click.echo('No tasks found. Please add tasks before scheduling.') 884 | return 885 | 886 | questions = [ 887 | inquirer.List('task_index', 888 | message="Select a task to schedule:", 889 | choices=[(f"Task {i+1}: {task['restaurant_id']}", i) for i, task in enumerate(tasks)]), 890 | inquirer.Text('schedule_time', message="Enter the time to schedule (HH:MM):"), 891 | inquirer.List('repeat', 892 | message="Repeat schedule?", 893 | choices=['Daily', 'Weekly', 'Once']), 894 | inquirer.Text('duration', message="Enter task duration in seconds (5-10 recommended):", default="10") 895 | ] 896 | answers = inquirer.prompt(questions) 897 | 898 | task_index = answers['task_index'] 899 | schedule_time = datetime.datetime.strptime(answers['schedule_time'], "%H:%M").time() 900 | duration = int(answers['duration']) 901 | 902 | job_id = f"task_{task_index}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}" 903 | 904 | if answers['repeat'] == 'Daily': 905 | scheduler.add_job(start_and_stop_task, 'cron', args=[task_index, duration, job_id], 906 | hour=schedule_time.hour, minute=schedule_time.minute, id=job_id) 907 | elif answers['repeat'] == 'Weekly': 908 | scheduler.add_job(start_and_stop_task, 'cron', args=[task_index, duration, job_id], 909 | day_of_week='mon-sun', hour=schedule_time.hour, minute=schedule_time.minute, id=job_id) 910 | else: # Once 911 | next_run = datetime.datetime.combine(datetime.date.today(), schedule_time) 912 | if next_run <= datetime.datetime.now(): 913 | next_run += datetime.timedelta(days=1) 914 | scheduler.add_job(start_and_stop_task, 'date', args=[task_index, duration, job_id], 915 | run_date=next_run, id=job_id) 916 | 917 | click.echo(f"Task scheduled to run at {schedule_time.strftime('%H:%M')} {answers['repeat']} for {duration} seconds") 918 | 919 | def start_and_stop_task(task_index, duration, job_id): 920 | task_thread = threading.Thread(target=run_task_with_timeout, args=(task_index, duration, job_id)) 921 | task_thread.start() 922 | 923 | def run_task_with_timeout(task_index, duration, job_id): 924 | tasks = load_data(TASKS_FILE, []) 925 | proxies = load_data(PROXIES_FILE, []) 926 | info = load_data(INFO_FILE, {}) 927 | 928 | if task_index >= len(tasks): 929 | print(f"Error: Task index {task_index} out of range") 930 | return 931 | 932 | task = tasks[task_index] 933 | running_tasks[job_id] = { 934 | 'thread': threading.current_thread(), 935 | 'start_time': time.time(), 936 | 'duration': duration, 937 | 'task': task 938 | } 939 | 940 | try: 941 | run_tasks_concurrently([task], info['capsolver_key'], info['capmonster_key'], proxies, info['discord_webhook']) 942 | except Exception as e: 943 | print(f"Error starting scheduled task: {e}") 944 | finally: 945 | time.sleep(duration) 946 | print(f"Task {job_id} completed after {duration} seconds") 947 | if job_id in running_tasks: 948 | del running_tasks[job_id] 949 | 950 | def view_scheduled_tasks(): 951 | while True: 952 | click.clear() 953 | click.echo(click.style('Scheduled Tasks', bold=True, fg='cyan')) 954 | jobs = scheduler.get_jobs() 955 | if not jobs: 956 | click.echo("No scheduled tasks.") 957 | else: 958 | for i, job in enumerate(jobs): 959 | next_run = job.next_run_time.strftime("%Y-%m-%d %H:%M:%S") if job.next_run_time else "N/A" 960 | click.echo(f"{i+1}) Task ID: {job.id}, Next run: {next_run}") 961 | 962 | click.echo("\nRunning Tasks:") 963 | for i, (job_id, task_info) in enumerate(running_tasks.items()): 964 | elapsed = time.time() - task_info['start_time'] 965 | click.echo(f"{i+1}) Task ID: {job_id}, Running for: {elapsed:.2f}s, Max duration: {task_info['duration']}s") 966 | 967 | questions = [ 968 | inquirer.List('action', 969 | message="Choose an action", 970 | choices=['Remove scheduled task', 'Stop running task', 'Back'], 971 | carousel=True) 972 | ] 973 | answers = inquirer.prompt(questions) 974 | 975 | if answers['action'] == 'Remove scheduled task': 976 | remove_scheduled_task(jobs) 977 | elif answers['action'] == 'Stop running task': 978 | stop_running_task() 979 | elif answers['action'] == 'Back': 980 | break 981 | 982 | def remove_scheduled_task(jobs): 983 | if not jobs: 984 | click.echo("No scheduled tasks to remove.") 985 | time.sleep(2) 986 | return 987 | 988 | choices = [(f"Task {i+1}: {job.id}", job.id) for i, job in enumerate(jobs)] 989 | questions = [ 990 | inquirer.List('job_id', 991 | message="Select a task to remove", 992 | choices=choices) 993 | ] 994 | answers = inquirer.prompt(questions) 995 | 996 | scheduler.remove_job(answers['job_id']) 997 | click.echo(f"Removed scheduled task: {answers['job_id']}") 998 | time.sleep(2) 999 | 1000 | def stop_running_task(): 1001 | if not running_tasks: 1002 | click.echo("No running tasks to stop.") 1003 | time.sleep(2) 1004 | return 1005 | 1006 | choices = [(f"Task {i+1}: {job_id}", job_id) for i, (job_id, _) in enumerate(running_tasks.items())] 1007 | questions = [ 1008 | inquirer.List('job_id', 1009 | message="Select a task to stop", 1010 | choices=choices) 1011 | ] 1012 | answers = inquirer.prompt(questions) 1013 | 1014 | job_id = answers['job_id'] 1015 | if job_id in running_tasks: 1016 | running_tasks[job_id]['thread'].join(0.1) # Give the thread a chance to finish 1017 | if job_id in running_tasks: 1018 | del running_tasks[job_id] 1019 | click.echo(f"Stopped running task: {job_id}") 1020 | else: 1021 | click.echo(f"Task {job_id} is no longer running.") 1022 | time.sleep(2) 1023 | 1024 | def start_tasks(): 1025 | tasks = load_data(TASKS_FILE, []) 1026 | proxies = load_data(PROXIES_FILE, []) 1027 | info = load_data(INFO_FILE, {}) 1028 | 1029 | if not tasks: 1030 | click.echo('No tasks found. Please add tasks before starting.') 1031 | return 1032 | if not proxies: 1033 | click.echo('No proxies found. Please add proxies before starting.') 1034 | return 1035 | if not info: 1036 | click.echo('No user info found. Please set user info before starting.') 1037 | return 1038 | 1039 | try: 1040 | run_tasks_concurrently(tasks, info['capsolver_key'], info['capmonster_key'], proxies, info['discord_webhook']) 1041 | except Exception as e: 1042 | print(f"Error starting tasks: {e}") 1043 | 1044 | input('Press Enter to continue...') 1045 | 1046 | 1047 | 1048 | 1049 | if __name__ == '__main__': 1050 | cli() 1051 | -------------------------------------------------------------------------------- /client/task_executor.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | import requests 4 | from concurrent.futures import ThreadPoolExecutor, as_completed 5 | import capsolver 6 | from urllib.parse import quote 7 | 8 | 9 | def format_proxy(proxy_str): 10 | ip, port, user, password = proxy_str.split(':') 11 | return { 12 | 'http': f'http://{user}:{password}@{ip}:{port}', 13 | 'https': f'http://{user}:{password}@{ip}:{port}', 14 | } 15 | 16 | def execute_task(task, capsolver_key, capmonster_key, proxies, webhook_url): 17 | auth_token = task['auth_token'] 18 | payment_id = task['payment_id'] 19 | restaurant_id = task['restaurant_id'] 20 | party_sz = task['party_sz'] 21 | start_date = task['start_date'] 22 | end_date = task['end_date'] 23 | start_time = task['start_time'] 24 | end_time = task['end_time'] 25 | delay = task['delay'] 26 | #captcha_service = task['captcha_service'] 27 | 28 | headers = { 29 | 'X-Resy-Auth-Token': auth_token, 30 | 'Authorization': 'ResyAPI api_key="VbWk7s3L4KiK5fzlO7JD3Q5EYolJI7n5"', 31 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 32 | 'X-Resy-Universal-Auth': auth_token, 33 | 'Accept-Encoding': 'gzip, deflate, br', 34 | 'Host': 'api.resy.com', 35 | 'Accept': 'application/json, text/plain, */*', 36 | 'Referer': 'https://resy.com/', 37 | } 38 | 39 | #captcha_key = capsolver_key if captcha_service == 'CAPSolver' else capmonster_key 40 | #capsolver.api_key = captcha_key 41 | 42 | while True: 43 | try: 44 | select_proxy = format_proxy(random.choice(proxies)) 45 | 46 | url = f"https://api.resy.com/4/venue/calendar?venue_id={restaurant_id}&num_seats={party_sz}&start_date={start_date}&end_date={end_date}" 47 | response = requests.get(url, headers=headers, proxies=select_proxy) 48 | 49 | if response.status_code != 200: 50 | send_discord_notification(webhook_url, f'(1) Failed to get availability for restaurant {restaurant_id} - {response.text} - {response.status_code}') 51 | return 52 | 53 | data = response.json() 54 | if 'scheduled' not in data: 55 | send_discord_notification(webhook_url, f'Unexpected response format for API1 for restaurant {restaurant_id} - {data}') 56 | return 57 | for entry in data['scheduled']: 58 | if entry['inventory']['reservation'] == 'available': 59 | 60 | url2 = f"https://api.resy.com/4/find?lat=0&long=0&day={entry['date']}&party_size={party_sz}&venue_id={restaurant_id}" 61 | response2 = requests.get(url2, headers=headers, proxies=select_proxy) 62 | 63 | if response2.status_code != 200: 64 | send_discord_notification(webhook_url, f'(2) Failed to get availability for restaurant {restaurant_id}') 65 | return 66 | 67 | data2 = response2.json() 68 | 69 | if 'results' not in data2 : 70 | send_discord_notification(webhook_url, f'Unexpected response format for API2 for restaurant {restaurant_id} - {data2}') 71 | return 72 | 73 | if 'results' in data2 and 'venues' in data2['results'] and data2['results']['venues']: 74 | for slot in data2['results']['venues'][0]['slots']: 75 | config_token = slot['config']['token'] 76 | parts = config_token.split('/') 77 | time_part = parts[8].split(':')[0] 78 | if int(time_part) >= int(start_time) and int(time_part) <= int(end_time): 79 | book_token = get_details(entry['date'], party_sz, config_token, restaurant_id, headers, select_proxy) 80 | print('\nBook_token is :', book_token) 81 | reservationVal = book_reservation(book_token, auth_token, payment_id, entry['date'], party_sz, restaurant_id, config_token, headers, select_proxy) 82 | 83 | if 'reservation_id' in reservationVal or ('specs' in reservationVal and 'reservation_id' in reservationVal['specs']): 84 | send_discord_notification(webhook_url, f'Reservation booked for restaurant {restaurant_id} - {reservationVal}') 85 | return 86 | else: 87 | send_discord_notification(webhook_url, f'Failed to book reservation for restaurant {restaurant_id} - {reservationVal}') 88 | return 89 | 90 | else: 91 | send_discord_notification(webhook_url, f'Unexpected response format for API2 for restaurant {restaurant_id} - {data2}') 92 | return 93 | else: 94 | continue 95 | except Exception as e: 96 | import traceback 97 | print('failed to execute task') 98 | traceback.print_exc() 99 | break 100 | time.sleep(delay/1000) 101 | 102 | 103 | def get_captcha_token(captcha_key, site_key, url, proxy): 104 | solution = capsolver.solve({ 105 | "type": "RecaptchaV2Task", 106 | "websiteKey": site_key, 107 | "websiteURL": url, 108 | "proxy": proxy['http'] 109 | }) 110 | gRecaptchaResponse = solution['gRecaptchaResponse'] 111 | return gRecaptchaResponse 112 | 113 | def get_details(day, party_size, config_token, restaurant_id, headers, select_proxy): 114 | url = 'http://127.0.0.1:8000/api/get-details' 115 | payload = { 116 | 'day': day, 117 | 'party_size': party_size, 118 | 'config_token': config_token, 119 | 'restaurant_id': restaurant_id, 120 | 'headers': headers, 121 | 'select_proxy': select_proxy 122 | } 123 | 124 | response = requests.post(url, json=payload) 125 | 126 | if response.status_code != 200: 127 | print(f'Failed to get details for restaurant {restaurant_id} - {response.text} - {response.status_code}') 128 | return 129 | 130 | data = response.json() 131 | return data['response_value'] 132 | 133 | def book_reservation(book_token, auth_token, payment_id, day, party_size, restaurant_id, config_token, headers, select_proxy): 134 | url = 'http://127.0.0.1:8000/api/book-reservation' 135 | payload = { 136 | 'book_token': book_token, 137 | 'auth_token': auth_token, 138 | 'payment_id': payment_id, 139 | 'day': day, 140 | 'party_size': party_size, 141 | 'restaurant_id': restaurant_id, 142 | 'config_token': config_token, 143 | 'headers': headers, 144 | 'select_proxy': select_proxy 145 | } 146 | 147 | response = requests.post(url, json=payload) 148 | 149 | return response.json() 150 | 151 | def send_discord_notification(webhook_url, message): 152 | data = {"content": message} 153 | requests.post(webhook_url, json=data) 154 | 155 | def run_tasks_concurrently(tasks, capsolver_key, capmonster_key, proxies, webhook_url): 156 | with ThreadPoolExecutor(max_workers=len(tasks)) as executor: 157 | futures = [executor.submit(execute_task, task, capsolver_key, capmonster_key, proxies, webhook_url) for task in tasks] 158 | for future in as_completed(futures): 159 | try: 160 | future.result() 161 | except Exception as e: 162 | print('Failed to execute task') 163 | print(e) -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim-buster as final 2 | WORKDIR /python-docker 3 | COPY requirements.txt requirements.txt 4 | RUN pip3 install -r requirements.txt 5 | COPY . . 6 | CMD ["python", "start.py"] -------------------------------------------------------------------------------- /server/bin/Activate.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Activate a Python virtual environment for the current PowerShell session. 4 | 5 | .Description 6 | Pushes the python executable for a virtual environment to the front of the 7 | $Env:PATH environment variable and sets the prompt to signify that you are 8 | in a Python virtual environment. Makes use of the command line switches as 9 | well as the `pyvenv.cfg` file values present in the virtual environment. 10 | 11 | .Parameter VenvDir 12 | Path to the directory that contains the virtual environment to activate. The 13 | default value for this is the parent of the directory that the Activate.ps1 14 | script is located within. 15 | 16 | .Parameter Prompt 17 | The prompt prefix to display when this virtual environment is activated. By 18 | default, this prompt is the name of the virtual environment folder (VenvDir) 19 | surrounded by parentheses and followed by a single space (ie. '(.venv) '). 20 | 21 | .Example 22 | Activate.ps1 23 | Activates the Python virtual environment that contains the Activate.ps1 script. 24 | 25 | .Example 26 | Activate.ps1 -Verbose 27 | Activates the Python virtual environment that contains the Activate.ps1 script, 28 | and shows extra information about the activation as it executes. 29 | 30 | .Example 31 | Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv 32 | Activates the Python virtual environment located in the specified location. 33 | 34 | .Example 35 | Activate.ps1 -Prompt "MyPython" 36 | Activates the Python virtual environment that contains the Activate.ps1 script, 37 | and prefixes the current prompt with the specified string (surrounded in 38 | parentheses) while the virtual environment is active. 39 | 40 | .Notes 41 | On Windows, it may be required to enable this Activate.ps1 script by setting the 42 | execution policy for the user. You can do this by issuing the following PowerShell 43 | command: 44 | 45 | PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser 46 | 47 | For more information on Execution Policies: 48 | https://go.microsoft.com/fwlink/?LinkID=135170 49 | 50 | #> 51 | Param( 52 | [Parameter(Mandatory = $false)] 53 | [String] 54 | $VenvDir, 55 | [Parameter(Mandatory = $false)] 56 | [String] 57 | $Prompt 58 | ) 59 | 60 | <# Function declarations --------------------------------------------------- #> 61 | 62 | <# 63 | .Synopsis 64 | Remove all shell session elements added by the Activate script, including the 65 | addition of the virtual environment's Python executable from the beginning of 66 | the PATH variable. 67 | 68 | .Parameter NonDestructive 69 | If present, do not remove this function from the global namespace for the 70 | session. 71 | 72 | #> 73 | function global:deactivate ([switch]$NonDestructive) { 74 | # Revert to original values 75 | 76 | # The prior prompt: 77 | if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { 78 | Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt 79 | Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT 80 | } 81 | 82 | # The prior PYTHONHOME: 83 | if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { 84 | Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME 85 | Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME 86 | } 87 | 88 | # The prior PATH: 89 | if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { 90 | Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH 91 | Remove-Item -Path Env:_OLD_VIRTUAL_PATH 92 | } 93 | 94 | # Just remove the VIRTUAL_ENV altogether: 95 | if (Test-Path -Path Env:VIRTUAL_ENV) { 96 | Remove-Item -Path env:VIRTUAL_ENV 97 | } 98 | 99 | # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: 100 | if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { 101 | Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force 102 | } 103 | 104 | # Leave deactivate function in the global namespace if requested: 105 | if (-not $NonDestructive) { 106 | Remove-Item -Path function:deactivate 107 | } 108 | } 109 | 110 | <# 111 | .Description 112 | Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the 113 | given folder, and returns them in a map. 114 | 115 | For each line in the pyvenv.cfg file, if that line can be parsed into exactly 116 | two strings separated by `=` (with any amount of whitespace surrounding the =) 117 | then it is considered a `key = value` line. The left hand string is the key, 118 | the right hand is the value. 119 | 120 | If the value starts with a `'` or a `"` then the first and last character is 121 | stripped from the value before being captured. 122 | 123 | .Parameter ConfigDir 124 | Path to the directory that contains the `pyvenv.cfg` file. 125 | #> 126 | function Get-PyVenvConfig( 127 | [String] 128 | $ConfigDir 129 | ) { 130 | Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" 131 | 132 | # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). 133 | $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue 134 | 135 | # An empty map will be returned if no config file is found. 136 | $pyvenvConfig = @{ } 137 | 138 | if ($pyvenvConfigPath) { 139 | 140 | Write-Verbose "File exists, parse `key = value` lines" 141 | $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath 142 | 143 | $pyvenvConfigContent | ForEach-Object { 144 | $keyval = $PSItem -split "\s*=\s*", 2 145 | if ($keyval[0] -and $keyval[1]) { 146 | $val = $keyval[1] 147 | 148 | # Remove extraneous quotations around a string value. 149 | if ("'""".Contains($val.Substring(0, 1))) { 150 | $val = $val.Substring(1, $val.Length - 2) 151 | } 152 | 153 | $pyvenvConfig[$keyval[0]] = $val 154 | Write-Verbose "Adding Key: '$($keyval[0])'='$val'" 155 | } 156 | } 157 | } 158 | return $pyvenvConfig 159 | } 160 | 161 | 162 | <# Begin Activate script --------------------------------------------------- #> 163 | 164 | # Determine the containing directory of this script 165 | $VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition 166 | $VenvExecDir = Get-Item -Path $VenvExecPath 167 | 168 | Write-Verbose "Activation script is located in path: '$VenvExecPath'" 169 | Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" 170 | Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" 171 | 172 | # Set values required in priority: CmdLine, ConfigFile, Default 173 | # First, get the location of the virtual environment, it might not be 174 | # VenvExecDir if specified on the command line. 175 | if ($VenvDir) { 176 | Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" 177 | } 178 | else { 179 | Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." 180 | $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") 181 | Write-Verbose "VenvDir=$VenvDir" 182 | } 183 | 184 | # Next, read the `pyvenv.cfg` file to determine any required value such 185 | # as `prompt`. 186 | $pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir 187 | 188 | # Next, set the prompt from the command line, or the config file, or 189 | # just use the name of the virtual environment folder. 190 | if ($Prompt) { 191 | Write-Verbose "Prompt specified as argument, using '$Prompt'" 192 | } 193 | else { 194 | Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" 195 | if ($pyvenvCfg -and $pyvenvCfg['prompt']) { 196 | Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" 197 | $Prompt = $pyvenvCfg['prompt']; 198 | } 199 | else { 200 | Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" 201 | Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" 202 | $Prompt = Split-Path -Path $venvDir -Leaf 203 | } 204 | } 205 | 206 | Write-Verbose "Prompt = '$Prompt'" 207 | Write-Verbose "VenvDir='$VenvDir'" 208 | 209 | # Deactivate any currently active virtual environment, but leave the 210 | # deactivate function in place. 211 | deactivate -nondestructive 212 | 213 | # Now set the environment variable VIRTUAL_ENV, used by many tools to determine 214 | # that there is an activated venv. 215 | $env:VIRTUAL_ENV = $VenvDir 216 | 217 | if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { 218 | 219 | Write-Verbose "Setting prompt to '$Prompt'" 220 | 221 | # Set the prompt to include the env name 222 | # Make sure _OLD_VIRTUAL_PROMPT is global 223 | function global:_OLD_VIRTUAL_PROMPT { "" } 224 | Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT 225 | New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt 226 | 227 | function global:prompt { 228 | Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " 229 | _OLD_VIRTUAL_PROMPT 230 | } 231 | } 232 | 233 | # Clear PYTHONHOME 234 | if (Test-Path -Path Env:PYTHONHOME) { 235 | Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME 236 | Remove-Item -Path Env:PYTHONHOME 237 | } 238 | 239 | # Add the venv to the PATH 240 | Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH 241 | $Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" 242 | -------------------------------------------------------------------------------- /server/bin/activate: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate" *from bash* 2 | # you cannot run it directly 3 | 4 | deactivate () { 5 | # reset old environment variables 6 | if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then 7 | PATH="${_OLD_VIRTUAL_PATH:-}" 8 | export PATH 9 | unset _OLD_VIRTUAL_PATH 10 | fi 11 | if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then 12 | PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" 13 | export PYTHONHOME 14 | unset _OLD_VIRTUAL_PYTHONHOME 15 | fi 16 | 17 | # This should detect bash and zsh, which have a hash command that must 18 | # be called to get it to forget past commands. Without forgetting 19 | # past commands the $PATH changes we made may not be respected 20 | if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then 21 | hash -r 2> /dev/null 22 | fi 23 | 24 | if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then 25 | PS1="${_OLD_VIRTUAL_PS1:-}" 26 | export PS1 27 | unset _OLD_VIRTUAL_PS1 28 | fi 29 | 30 | unset VIRTUAL_ENV 31 | if [ ! "${1:-}" = "nondestructive" ] ; then 32 | # Self destruct! 33 | unset -f deactivate 34 | fi 35 | } 36 | 37 | # unset irrelevant variables 38 | deactivate nondestructive 39 | 40 | VIRTUAL_ENV="/Users/korbinschulz/Desktop/projects/resybot-server" 41 | export VIRTUAL_ENV 42 | 43 | _OLD_VIRTUAL_PATH="$PATH" 44 | PATH="$VIRTUAL_ENV/bin:$PATH" 45 | export PATH 46 | 47 | # unset PYTHONHOME if set 48 | # this will fail if PYTHONHOME is set to the empty string (which is bad anyway) 49 | # could use `if (set -u; : $PYTHONHOME) ;` in bash 50 | if [ -n "${PYTHONHOME:-}" ] ; then 51 | _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" 52 | unset PYTHONHOME 53 | fi 54 | 55 | if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then 56 | _OLD_VIRTUAL_PS1="${PS1:-}" 57 | PS1="(resybot-server) ${PS1:-}" 58 | export PS1 59 | fi 60 | 61 | # This should detect bash and zsh, which have a hash command that must 62 | # be called to get it to forget past commands. Without forgetting 63 | # past commands the $PATH changes we made may not be respected 64 | if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then 65 | hash -r 2> /dev/null 66 | fi 67 | -------------------------------------------------------------------------------- /server/bin/activate.csh: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate.csh" *from csh*. 2 | # You cannot run it directly. 3 | # Created by Davide Di Blasi . 4 | # Ported to Python 3.3 venv by Andrew Svetlov 5 | 6 | alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate' 7 | 8 | # Unset irrelevant variables. 9 | deactivate nondestructive 10 | 11 | setenv VIRTUAL_ENV "/Users/korbinschulz/Desktop/projects/resybot-server" 12 | 13 | set _OLD_VIRTUAL_PATH="$PATH" 14 | setenv PATH "$VIRTUAL_ENV/bin:$PATH" 15 | 16 | 17 | set _OLD_VIRTUAL_PROMPT="$prompt" 18 | 19 | if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then 20 | set prompt = "(resybot-server) $prompt" 21 | endif 22 | 23 | alias pydoc python -m pydoc 24 | 25 | rehash 26 | -------------------------------------------------------------------------------- /server/bin/activate.fish: -------------------------------------------------------------------------------- 1 | # This file must be used with "source /bin/activate.fish" *from fish* 2 | # (https://fishshell.com/); you cannot run it directly. 3 | 4 | function deactivate -d "Exit virtual environment and return to normal shell environment" 5 | # reset old environment variables 6 | if test -n "$_OLD_VIRTUAL_PATH" 7 | set -gx PATH $_OLD_VIRTUAL_PATH 8 | set -e _OLD_VIRTUAL_PATH 9 | end 10 | if test -n "$_OLD_VIRTUAL_PYTHONHOME" 11 | set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME 12 | set -e _OLD_VIRTUAL_PYTHONHOME 13 | end 14 | 15 | if test -n "$_OLD_FISH_PROMPT_OVERRIDE" 16 | functions -e fish_prompt 17 | set -e _OLD_FISH_PROMPT_OVERRIDE 18 | functions -c _old_fish_prompt fish_prompt 19 | functions -e _old_fish_prompt 20 | end 21 | 22 | set -e VIRTUAL_ENV 23 | if test "$argv[1]" != "nondestructive" 24 | # Self-destruct! 25 | functions -e deactivate 26 | end 27 | end 28 | 29 | # Unset irrelevant variables. 30 | deactivate nondestructive 31 | 32 | set -gx VIRTUAL_ENV "/Users/korbinschulz/Desktop/projects/resybot-server" 33 | 34 | set -gx _OLD_VIRTUAL_PATH $PATH 35 | set -gx PATH "$VIRTUAL_ENV/bin" $PATH 36 | 37 | # Unset PYTHONHOME if set. 38 | if set -q PYTHONHOME 39 | set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME 40 | set -e PYTHONHOME 41 | end 42 | 43 | if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" 44 | # fish uses a function instead of an env var to generate the prompt. 45 | 46 | # Save the current fish_prompt function as the function _old_fish_prompt. 47 | functions -c fish_prompt _old_fish_prompt 48 | 49 | # With the original prompt function renamed, we can override with our own. 50 | function fish_prompt 51 | # Save the return status of the last command. 52 | set -l old_status $status 53 | 54 | # Output the venv prompt; color taken from the blue of the Python logo. 55 | printf "%s%s%s" (set_color 4B8BBE) "(resybot-server) " (set_color normal) 56 | 57 | # Restore the return status of the previous command. 58 | echo "exit $old_status" | . 59 | # Output the original/"old" prompt. 60 | _old_fish_prompt 61 | end 62 | 63 | set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" 64 | end 65 | -------------------------------------------------------------------------------- /server/bin/dotenv: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from dotenv.__main__ import cli 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(cli()) 9 | -------------------------------------------------------------------------------- /server/bin/email_validator: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from email_validator.__main__ import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/fastapi: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from fastapi_cli.cli import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/flask: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from flask.cli import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/gunicorn: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from gunicorn.app.wsgiapp import run 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(run()) 9 | -------------------------------------------------------------------------------- /server/bin/httpx: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from httpx import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/markdown-it: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from markdown_it.cli.parse import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/normalizer: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from charset_normalizer.cli import cli_detect 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(cli_detect()) 9 | -------------------------------------------------------------------------------- /server/bin/pip: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/pip3: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/pip3.10: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/pip3.9: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/pygmentize: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pygments.cmdline import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/python: -------------------------------------------------------------------------------- 1 | python3 -------------------------------------------------------------------------------- /server/bin/python3: -------------------------------------------------------------------------------- 1 | /Library/Frameworks/Python.framework/Versions/3.9/bin/python3 -------------------------------------------------------------------------------- /server/bin/python3.9: -------------------------------------------------------------------------------- 1 | python3 -------------------------------------------------------------------------------- /server/bin/typer: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from typer.cli import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/uvicorn: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from uvicorn.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /server/bin/watchfiles: -------------------------------------------------------------------------------- 1 | #!/Users/korbinschulz/Desktop/projects/resybot-server/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from watchfiles.cli import cli 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(cli()) 9 | -------------------------------------------------------------------------------- /server/pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = /Library/Frameworks/Python.framework/Versions/3.9/bin 2 | include-system-site-packages = false 3 | version = 3.9.13 4 | -------------------------------------------------------------------------------- /server/railway.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://railway.app/railway.schema.json", 3 | "build": { 4 | "builder": "DOCKERFILE", 5 | "dockerfilePath": "Dockerfile" 6 | }, 7 | "deploy": { 8 | "runtime": "V2", 9 | "numReplicas": 1, 10 | "startCommand": "python start.py", 11 | "sleepApplication": false, 12 | "restartPolicyType": "ON_FAILURE", 13 | "restartPolicyMaxRetries": 10 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.4.0 3 | blinker==1.8.2 4 | certifi==2024.6.2 5 | charset-normalizer==3.3.2 6 | click==8.1.7 7 | dnspython==2.6.1 8 | email_validator==2.2.0 9 | exceptiongroup==1.2.1 10 | fastapi==0.111.0 11 | fastapi-cli==0.0.4 12 | Flask==3.0.3 13 | gunicorn==22.0.0 14 | h11==0.14.0 15 | httpcore==1.0.5 16 | httptools==0.6.1 17 | httpx==0.27.0 18 | idna==3.7 19 | importlib_metadata==7.1.0 20 | itsdangerous==2.2.0 21 | Jinja2==3.1.4 22 | markdown-it-py==3.0.0 23 | MarkupSafe==2.1.5 24 | mdurl==0.1.2 25 | orjson==3.10.6 26 | packaging==24.1 27 | pydantic==2.8.2 28 | pydantic_core==2.20.1 29 | Pygments==2.18.0 30 | python-dotenv==1.0.1 31 | python-multipart==0.0.9 32 | PyYAML==6.0.1 33 | requests==2.32.3 34 | rich==13.7.1 35 | shellingham==1.5.4 36 | sniffio==1.3.1 37 | starlette==0.37.2 38 | typer==0.12.3 39 | typing_extensions==4.12.2 40 | ujson==5.10.0 41 | urllib3==2.2.1 42 | uvicorn==0.30.1 43 | uvloop==0.19.0 44 | watchfiles==0.22.0 45 | websockets==12.0 46 | Werkzeug==3.0.3 47 | zipp==3.19.2 48 | -------------------------------------------------------------------------------- /server/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.6 2 | -------------------------------------------------------------------------------- /server/server.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException, Header 2 | from fastapi.responses import JSONResponse 3 | from pydantic import BaseModel 4 | from urllib.parse import urlparse 5 | import httpx 6 | import json 7 | import logging 8 | from fastapi.middleware.cors import CORSMiddleware 9 | 10 | app = FastAPI() 11 | 12 | # Add CORS middleware to allow client to connect locally 13 | app.add_middleware( 14 | CORSMiddleware, 15 | allow_origins=["*"], # Allow all origins 16 | allow_credentials=True, 17 | allow_methods=["*"], # Allow all methods 18 | allow_headers=["*"], # Allow all headers 19 | ) 20 | 21 | # Configure logging 22 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 23 | logger = logging.getLogger(__name__) 24 | 25 | # Pydantic models for request validation 26 | class DetailsRequest(BaseModel): 27 | day: str 28 | party_size: int 29 | config_token: str 30 | restaurant_id: str 31 | headers: dict 32 | select_proxy: dict 33 | 34 | class ReservationRequest(BaseModel): 35 | book_token: str 36 | payment_id: int 37 | headers: dict 38 | select_proxy: dict 39 | 40 | def format_proxy_url(proxy_url: str) -> str: 41 | if not urlparse(proxy_url).scheme: 42 | return f"http://{proxy_url}" 43 | return proxy_url 44 | 45 | @app.get("/") 46 | async def index(): 47 | logger.info("Index route accessed") 48 | return {"message": "Server is live!"} 49 | 50 | @app.post("/api/get-details") 51 | async def get_details(data: DetailsRequest): 52 | logger.info("Get details endpoint accessed") 53 | logger.debug(f"Request data: {data}") 54 | 55 | # Format proxy URLs 56 | formatted_proxies = {} 57 | if data.select_proxy: 58 | for scheme, proxy in data.select_proxy.items(): 59 | formatted_proxies[f"{scheme}://"] = format_proxy_url(proxy) 60 | formatted_proxies['https://'] = formatted_proxies.get('http://', formatted_proxies.get('https://', '')) 61 | 62 | url = f'https://api.resy.com/3/details?day={data.day}&party_size={data.party_size}&x-resy-auth-token={data.headers["X-Resy-Auth-Token"]}&venue_id={data.restaurant_id}&config_id={data.config_token}' 63 | headers = { 64 | 'Accept': '*/*', 65 | 'Accept-Encoding': 'gzip, deflate, br', 66 | 'Authorization': data.headers["Authorization"], 67 | 'Host': 'api.resy.com', 68 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 69 | } 70 | 71 | async with httpx.AsyncClient(proxies=formatted_proxies) as client: 72 | try: 73 | response = await client.get(url, headers=headers) 74 | logger.info(f"Get Details API request made to {url} using proxy {formatted_proxies}") 75 | logger.debug(f"Response status code: {response.status_code}") 76 | response.raise_for_status() 77 | except httpx.ProxyError as e: 78 | logger.error(f"Proxy error: {e}") 79 | raise HTTPException(status_code=500, detail="Proxy connection failed") 80 | except httpx.RequestError as e: 81 | logger.error(f"Request failed: {e}") 82 | raise HTTPException(status_code=500, detail="Request failed") 83 | 84 | if response.status_code != 200: 85 | logger.warning(f"Failed to get details for restaurant {data.restaurant_id}. Status code: {response.status_code}") 86 | raise HTTPException(status_code=response.status_code, detail=f"Failed to get details for restaurant {data.restaurant_id}") 87 | 88 | response_data = response.json() 89 | logger.info("Details retrieved successfully") 90 | return {"response_value": response_data['book_token']['value']} 91 | 92 | @app.post("/api/book-reservation") 93 | async def book_reservation(data: ReservationRequest): 94 | logger.info("Book reservation endpoint accessed") 95 | logger.debug(f"Request data: {data}") 96 | 97 | # Format proxy URLs 98 | formatted_proxies = {} 99 | if data.select_proxy: 100 | for scheme, proxy in data.select_proxy.items(): 101 | formatted_proxies[f"{scheme}://"] = format_proxy_url(proxy) 102 | formatted_proxies['https://'] = formatted_proxies.get('http://', formatted_proxies.get('https://', '')) 103 | 104 | url = 'https://api.resy.com/3/book' 105 | payload = { 106 | 'book_token': data.book_token, 107 | 'struct_payment_method': json.dumps({"id": data.payment_id}), 108 | 'source_id': 'resy.com-venue-details', 109 | } 110 | 111 | headers = { 112 | 'Host': 'api.resy.com', 113 | 'X-Origin': 'https://widgets.resy.com', 114 | 'X-Resy-Auth-Token': data.headers['X-Resy-Auth-Token'], 115 | 'Authorization': data.headers['Authorization'], 116 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/', 117 | 'X-Resy-Universal-Auth': data.headers['X-Resy-Auth-Token'], 118 | 'Accept': 'application/json, text/plain, */*', 119 | 'Cache-Control': 'no-cache', 120 | 'Sec-Fetch-Dest': 'empty', 121 | 'Referer': 'https://widgets.resy.com/', 122 | 'Content-Type': 'application/x-www-form-urlencoded', 123 | } 124 | 125 | async with httpx.AsyncClient(proxies=formatted_proxies) as client: 126 | response = await client.post(url, data=payload, headers=headers) 127 | 128 | logger.info(f"Reservation request made. Status code: {response.status_code} using proxy {formatted_proxies}") 129 | return JSONResponse(content=response.json(), status_code=response.status_code) 130 | 131 | if __name__ == '__main__': 132 | import uvicorn 133 | logger.info("Starting FastAPI application") 134 | uvicorn.run(app, host="0.0.0.0", port=8000) 135 | -------------------------------------------------------------------------------- /server/start.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uvicorn 3 | 4 | if __name__ == "__main__": 5 | port = int(os.environ.get("PORT", 8000)) 6 | uvicorn.run("server:app", host="0.0.0.0", port=port, workers=8) -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | import time 5 | import signal 6 | 7 | def run_server(): 8 | os.chdir("server") 9 | server_process = subprocess.Popen([sys.executable, "server.py"]) 10 | os.chdir("..") 11 | return server_process 12 | 13 | def run_client(): 14 | os.chdir("client") 15 | client_process = subprocess.Popen([sys.executable, "entry.py"]) 16 | os.chdir("..") 17 | return client_process 18 | 19 | def cleanup(server_process, client_process): 20 | print("\nShutting down...") 21 | server_process.terminate() 22 | client_process.terminate() 23 | server_process.wait() 24 | client_process.wait() 25 | print("Successfully shut down all processes.") 26 | 27 | if __name__ == "__main__": 28 | print("Starting ResyGrabber...") 29 | print("Starting server...") 30 | server_process = run_server() 31 | 32 | # Wait for server to start 33 | print("Waiting for server to start (3 seconds)...") 34 | time.sleep(3) 35 | 36 | print("Starting client...") 37 | client_process = run_client() 38 | 39 | try: 40 | # Wait for the client to exit 41 | client_process.wait() 42 | except KeyboardInterrupt: 43 | pass 44 | finally: 45 | cleanup(server_process, client_process) --------------------------------------------------------------------------------