├── .env.example ├── .gitignore ├── README.md ├── pm2.sh ├── pm2_update.sh ├── requirements ├── resy.json ├── resy ├── acc_preloader.py ├── accounts.py ├── aesCipher.py ├── database.py ├── discord.py ├── monitor.py ├── network.py ├── proxies.py ├── resy.py ├── utils.py └── worker.py ├── resy_charles.json ├── resy_dev.json ├── resy_monitor.json ├── resy_staging.json └── uas.txt /.env.example: -------------------------------------------------------------------------------- 1 | ENCRYPTION_KEY= 2 | DB_URL= 3 | WEBHOOK_URL= 4 | REDIS_HOST = 5 | REDIS_PORT = 6 | CONFIG_OVERRIDE= 7 | LOGS_WEBHOOK_URL= 8 | MODE= 9 | SENTRY_DSN= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | ### Python Patch ### 167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 168 | poetry.toml 169 | 170 | # ruff 171 | .ruff_cache/ 172 | 173 | dump.rdb 174 | 175 | # LSP config files 176 | pyrightconfig.json 177 | 178 | .DS_Store 179 | *.log 180 | logs/ 181 | proxies.txt 182 | book_proxies.txt 183 | resi_proxies.txt 184 | mobile_proxies.txt 185 | *proxies.txt 186 | # general no git 187 | *.nogit.* 188 | # End of https://www.toptal.com/developers/gitignore/api/python -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RESY BOT 2 | 3 | ## Resy Bot with captcha bypass, proxy support, no rate-limiting 4 | 5 | ### NOTE 6 | 7 | #### THIS DOESN'T INCLUDE OUR TOOLKIT; NEED MONGO, REDIS, AND AN ACCOUNT BANK 8 | 9 | ### What is this? 10 | #### this is the automation engine component of a resy bot I made with my friends a while back. Includes a captcha "solution", optimizations, rate limiting fix and more but *NOTE* -- it is outdated and is missing infrastructure that made this run, and may not work in the current state. An easy way to get this working locally is patching the account management and updating the networking 11 | 12 | ### Plans? 13 | #### theres no plan to develop this out further but it does include alot of good learning material and may be useful for your own projects. this also shouldve been written in GO but python allowed super fast scripting (this took 4 hours to MVP) 14 | 15 | ### LIC 16 | 17 | Open and for educational purposes only 18 | 19 | -------------------------------------------------------------------------------- /pm2.sh: -------------------------------------------------------------------------------- 1 | pm2 start ./resy/resy.py --name "resme_engine" -i 1 --cron-restart="0 9 * * *" --interpreter=.venv/bin/python3 -------------------------------------------------------------------------------- /pm2_update.sh: -------------------------------------------------------------------------------- 1 | pm2 restart resme_engine -------------------------------------------------------------------------------- /requirements: -------------------------------------------------------------------------------- 1 | async-timeout==4.0.3 2 | blinker==1.7.0 3 | certifi==2024.2.2 4 | charset-normalizer==3.3.2 5 | click==8.1.7 6 | colorama==0.4.6 7 | discord-webhook==1.3.0 8 | dnspython==2.5.0 9 | Flask==3.0.2 10 | Flask-Cors==4.0.0 11 | idna==3.6 12 | importlib-metadata==7.0.1 13 | itsdangerous==2.1.2 14 | Jinja2==3.1.3 15 | MarkupSafe==2.1.5 16 | pycryptodome==3.20.0 17 | pymongo==4.6.1 18 | python-dotenv==1.0.1 19 | pytz==2024.1 20 | redis==5.0.1 21 | requests==2.31.0 22 | schedule==1.2.1 23 | sentry-sdk==1.40.5 24 | termcolor==2.4.0 25 | urllib3==2.2.0 26 | waitress==2.1.2 27 | Werkzeug==3.0.1 28 | zipp==3.17.0 29 | -------------------------------------------------------------------------------- /resy.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "COTE Miami", 4 | "enabled": false, 5 | "forceStart": false, 6 | "venueID": 72270, 7 | "partyMin": 4, 8 | "partyMax": 4, 9 | "grabMax": 1, 10 | "baseURL": "https://resy.com/cities/mia/cote-mia", 11 | "offset": 28, 12 | "accountType": "normal", 13 | "passiveMonitoring": false, 14 | "monitor": { 15 | "type": "scheduled", 16 | "start": "09:58", 17 | "drop": "10:00:00", 18 | "end": "10:01:30", 19 | "timer": 4, 20 | "delay": 3, 21 | "exclude": [ 22 | "bar", 23 | "outdoor", 24 | "lunch" 25 | ], 26 | "timeFilter": { 27 | "enabled": true, 28 | "minTime": 17, 29 | "maxTime": 22 30 | } 31 | } 32 | }, 33 | { 34 | "name": "COTE Korean Steakhouse", 35 | "enabled": true, 36 | "venueID": 72271, 37 | "partyMin": 4, 38 | "partyMax": 6, 39 | "grabMax": 25, 40 | "baseURL": "https://resy.com/cities/ny/cote-nyc", 41 | "offset": 29, 42 | "accountType": "normal", 43 | "passiveMonitoring": true, 44 | "monitor": { 45 | "type": "scheduled", 46 | "start": "09:58", 47 | "drop": "10:00:00", 48 | "end": "10:00:45", 49 | "timer": 3, 50 | "delay": 0, 51 | "exclude": [ 52 | "bar", 53 | "lunch", 54 | "outdoor" 55 | ], 56 | "timeFilter": { 57 | "enabled": false 58 | } 59 | } 60 | }, 61 | { 62 | "name": "4 Charles", 63 | "enabled": true, 64 | "venueID": 834, 65 | "partyMin": 4, 66 | "partyMax": 6, 67 | "grabMax": 25, 68 | "baseURL": "https://resy.com/cities/ny/4-charles-prime-rib", 69 | "offset": 20, 70 | "accountType": "elite", 71 | "passiveMonitoring": true, 72 | "monitor": { 73 | "type": "scheduled", 74 | "start": "08:58", 75 | "drop": "09:00:00", 76 | "end": "09:02:00", 77 | "timer": 3, 78 | "delay": 0, 79 | "exclude": [ 80 | "bar" 81 | ], 82 | "timeFilter": { 83 | "enabled": false 84 | } 85 | } 86 | }, 87 | { 88 | "name": "Don Angie", 89 | "enabled": false, 90 | "venueID": 1505, 91 | "partyMin": 2, 92 | "partyMax": 6, 93 | "grabMax": 25, 94 | "baseURL": "https://resy.com/cities/ny/don-angie", 95 | "offset": 6, 96 | "accountType": "normal", 97 | "passiveMonitoring": false, 98 | "monitor": { 99 | "type": "scheduled", 100 | "start": "08:58", 101 | "drop": "09:00:00", 102 | "end": "09:02:00", 103 | "timer": 5, 104 | "delay": 0, 105 | "exclude": [ 106 | "bar" 107 | ], 108 | "timeFilter": { 109 | "enabled": false 110 | } 111 | } 112 | }, 113 | { 114 | "name": "L Artusi", 115 | "enabled": true, 116 | "venueID": 25973, 117 | "partyMin": 2, 118 | "partyMax": 4, 119 | "grabMax": 20, 120 | "baseURL": "https://resy.com/cities/ny/lartusi-ny", 121 | "offset": 14, 122 | "accountType": "normal", 123 | "passiveMonitoring": true, 124 | "monitor": { 125 | "type": "scheduled", 126 | "start": "08:59", 127 | "drop": "09:00:00", 128 | "end": "09:01:00", 129 | "timer": 2, 130 | "delay": 0, 131 | "exclude": [ 132 | "bar", 133 | "outdoor", 134 | "lunch", 135 | "patio", 136 | "counter" 137 | ], 138 | "timeFilter": { 139 | "enabled": true, 140 | "minTime": 16, 141 | "maxTime": 22 142 | } 143 | } 144 | }, 145 | { 146 | "name": "TATIANA By Kwame Onwuachi", 147 | "enabled": true, 148 | "venueID": 65452, 149 | "partyMin": 3, 150 | "partyMax": 6, 151 | "grabMax": 20, 152 | "baseURL": "https://resy.com/cities/ny/tatiana", 153 | "offset": 26, 154 | "accountType": "normal", 155 | "passiveMonitoring": true, 156 | "monitor": { 157 | "type": "scheduled", 158 | "start": "11:58", 159 | "drop": "12:00:00", 160 | "end": "12:00:45", 161 | "timer": 3, 162 | "delay": 0, 163 | "exclude": [ 164 | "bar", 165 | "outdoor", 166 | "lunch" 167 | ], 168 | "timeFilter": { 169 | "enabled": false 170 | } 171 | } 172 | }, 173 | { 174 | "name": "Carbone Miami", 175 | "enabled": true, 176 | "venueID": 43225, 177 | "partyMin": 2, 178 | "partyMax": 4, 179 | "grabMax": 20, 180 | "baseURL": "https://resy.com/cities/mia/carbone-miami", 181 | "offset": 30, 182 | "accountType": "normal", 183 | "passiveMonitoring": true, 184 | "monitor": { 185 | "type": "scheduled", 186 | "start": "09:59", 187 | "drop": "10:00:00", 188 | "end": "10:00:30", 189 | "timer": 2, 190 | "delay": 0.2, 191 | "exclude": [ 192 | "bar", 193 | "outdoor", 194 | "lunch", 195 | "patio" 196 | ], 197 | "timeFilter": { 198 | "enabled": true, 199 | "minTime": 17, 200 | "maxTime": 22 201 | } 202 | } 203 | }, 204 | { 205 | "name": "Funke", 206 | "enabled": true, 207 | "venueID": 60000, 208 | "partyMin": 2, 209 | "partyMax": 4, 210 | "grabMax": 15, 211 | "baseURL": "https://resy.com/cities/la/funke-la", 212 | "offset": 7, 213 | "accountType": "normal", 214 | "passiveMonitoring": true, 215 | "monitor": { 216 | "type": "scheduled", 217 | "start": "11:59", 218 | "drop": "12:00:00", 219 | "end": "12:00:30", 220 | "timer": 3, 221 | "delay": 0, 222 | "exclude": [ 223 | "bar", 224 | "outdoor", 225 | "lunch", 226 | "patio" 227 | ], 228 | "timeFilter": { 229 | "enabled": true, 230 | "minTime": 17, 231 | "maxTime": 22 232 | } 233 | } 234 | }, 235 | { 236 | "name": "I Sodi", 237 | "enabled": true, 238 | "venueID": 443, 239 | "partyMin": 2, 240 | "partyMax": 4, 241 | "grabMax": 20, 242 | "baseURL": "https://resy.com/cities/ny/i-sodi", 243 | "offset": 14, 244 | "accountType": "normal", 245 | "passiveMonitoring": true, 246 | "monitor": { 247 | "type": "scheduled", 248 | "start": "23:58", 249 | "drop": "23:59:59", 250 | "end": "00:01:00", 251 | "timer": 3, 252 | "delay": 0, 253 | "exclude": [ 254 | "bar", 255 | "outdoor", 256 | "lunch" 257 | ], 258 | "timeFilter": { 259 | "enabled": false 260 | } 261 | } 262 | }, 263 | { 264 | "name": "Le Bernardin", 265 | "enabled": true, 266 | "venueID": 1387, 267 | "partyMin": 2, 268 | "partyMax": 4, 269 | "grabMax": 15, 270 | "baseURL": "https://resy.com/cities/ny/le-bernardin", 271 | "offset": 14, 272 | "accountType": "normal", 273 | "passiveMonitoring": true, 274 | "monitor": { 275 | "type": "scheduled", 276 | "start": "06:58", 277 | "drop": "07:00:00", 278 | "end": "07:00:45", 279 | "timer": 3, 280 | "delay": 0, 281 | "exclude": [ 282 | "bar", 283 | "outdoor", 284 | "lunch", 285 | "patio" 286 | ], 287 | "timeFilter": { 288 | "enabled": true, 289 | "minTime": 16, 290 | "maxTime": 22 291 | } 292 | } 293 | }, 294 | { 295 | "name": "Lilia", 296 | "enabled": true, 297 | "venueID": 418, 298 | "partyMin": 2, 299 | "partyMax": 4, 300 | "grabMax": 15, 301 | "baseURL": "https://resy.com/cities/ny/lilia", 302 | "offset": 28, 303 | "accountType": "normal", 304 | "passiveMonitoring": true, 305 | "monitor": { 306 | "type": "scheduled", 307 | "start": "09:59", 308 | "drop": "10:00:00", 309 | "end": "10:00:35", 310 | "timer": 2, 311 | "delay": 0, 312 | "exclude": [ 313 | "bar", 314 | "outdoor", 315 | "lunch", 316 | "patio" 317 | ], 318 | "timeFilter": { 319 | "enabled": true, 320 | "minTime": 16, 321 | "maxTime": 22 322 | } 323 | } 324 | }, 325 | { 326 | "name": "COQODAQ", 327 | "enabled": true, 328 | "venueID": 76033, 329 | "partyMin": 4, 330 | "partyMax": 6, 331 | "grabMax": 20, 332 | "baseURL": "https://resy.com/cities/ny/coqodaq", 333 | "offset": 14, 334 | "accountType": "normal", 335 | "passiveMonitoring": true, 336 | "monitor": { 337 | "type": "scheduled", 338 | "start": "09:58", 339 | "drop": "10:00:00", 340 | "end": "10:01:00", 341 | "timer": 3, 342 | "delay": 0, 343 | "exclude": [ 344 | "bar", 345 | "outdoor", 346 | "lunch" 347 | ], 348 | "timeFilter": { 349 | "enabled": false 350 | } 351 | } 352 | }, 353 | { 354 | "name": "Laser Wolf Brooklyn", 355 | "enabled": true, 356 | "venueID": 58848, 357 | "partyMin": 2, 358 | "partyMax": 6, 359 | "grabMax": 15, 360 | "baseURL": "https://resy.com/cities/ny/laser-wolf-brooklyn", 361 | "offset": 21, 362 | "accountType": "normal", 363 | "passiveMonitoring": true, 364 | "monitor": { 365 | "type": "scheduled", 366 | "start": "09:59", 367 | "drop": "10:00:00", 368 | "end": "10:00:30", 369 | "timer": 2, 370 | "delay": 0.25, 371 | "exclude": [ 372 | "bar", 373 | "outdoor", 374 | "lunch", 375 | "counter" 376 | ], 377 | "timeFilter": { 378 | "enabled": true, 379 | "minTime": 16, 380 | "maxTime": 22 381 | } 382 | } 383 | }, 384 | { 385 | "name": "Raouls", 386 | "enabled": true, 387 | "venueID": 7241, 388 | "partyMin": 2, 389 | "partyMax": 5, 390 | "grabMax": 10, 391 | "baseURL": "https://resy.com/cities/ny/raoulsrestaurant", 392 | "offset": 30, 393 | "accountType": "normal", 394 | "passiveMonitoring": true, 395 | "monitor": { 396 | "type": "scheduled", 397 | "start": "07:59", 398 | "drop": "08:00:00", 399 | "end": "08:00:30", 400 | "timer": 2, 401 | "delay": 0, 402 | "exclude": [ 403 | "bar", 404 | "outdoor", 405 | "lunch", 406 | "counter" 407 | ], 408 | "timeFilter": { 409 | "enabled": true, 410 | "minTime": 17, 411 | "maxTime": 21 412 | } 413 | } 414 | }, 415 | { 416 | "name": "Via Carota", 417 | "enabled": false, 418 | "venueID": 2567, 419 | "partyMin": 2, 420 | "partyMax": 4, 421 | "grabMax": 10, 422 | "baseURL": "https://resy.com/cities/ny/via-carota", 423 | "offset": 30, 424 | "accountType": "normal", 425 | "passiveMonitoring": false, 426 | "monitor": { 427 | "type": "scheduled", 428 | "start": "09:58", 429 | "drop": "10:00:00", 430 | "end": "10:01:00", 431 | "timer": 3, 432 | "delay": 0, 433 | "exclude": [ 434 | "bar", 435 | "outdoor", 436 | "lunch", 437 | "counter" 438 | ], 439 | "timeFilter": { 440 | "enabled": false 441 | } 442 | } 443 | }, 444 | { 445 | "name": "Cipriani Beverly Hills", 446 | "enabled": false, 447 | "venueID": 79635, 448 | "partyMin": 2, 449 | "partyMax": 6, 450 | "grabMax": 5, 451 | "baseURL": "https://resy.com/cities/la/cipriani-beverly-hills", 452 | "offset": 29, 453 | "accountType": "normal", 454 | "passiveMonitoring": false, 455 | "monitor": { 456 | "type": "scheduled", 457 | "start": "08:59", 458 | "drop": "12:00:00", 459 | "end": "12:00:30", 460 | "timer": 2, 461 | "delay": 0, 462 | "exclude": [ 463 | "bar" 464 | ], 465 | "timeFilter": { 466 | "enabled": true, 467 | "minTime": 17, 468 | "maxTime": 21 469 | } 470 | } 471 | } 472 | ] -------------------------------------------------------------------------------- /resy/acc_preloader.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | import time 3 | import random 4 | import json 5 | from utils import Utils 6 | from database import Database 7 | import sys 8 | from network import Network 9 | from proxies import Proxies 10 | from discord import Discord 11 | import threading 12 | import redis 13 | import os 14 | 15 | load_dotenv() 16 | 17 | PRELOAD_NUM_NORM = 1000 18 | PRELOAD_NUM_ELITE = 250 19 | MAX_ACCS = 1750 20 | CHECK_POOL_INTERVAL = 60 21 | 22 | ONE_DAY_EPOCH = 86400 23 | 24 | FLUSH_INTERVAL = ONE_DAY_EPOCH 25 | 26 | if os.getenv("MODE") and (os.getenv("MODE").lower() == "staging"): 27 | PRELOAD_NUM_NORM = 1000 28 | PRELOAD_NUM_ELITE = 250 29 | MAX_ACCS = 1750 30 | CHECK_POOL_INTERVAL = 60 31 | 32 | proxies = Proxies() 33 | database = Database() 34 | utils = Utils() 35 | discord = Discord() 36 | 37 | # give this the queue and it will manage it 38 | class AccPreloader: 39 | def __init__(self): 40 | self.redis = redis.Redis(host=os.getenv("REDIS_HOST"), port=os.getenv("REDIS_PORT"), decode_responses=True, db=0) 41 | 42 | # flush redis if its been over a day 43 | if self.check_need_flush(): 44 | utils.thread_error("Flushing redis db") 45 | discord.logs_wh("Flushing redis db") 46 | self.flush_db() 47 | 48 | while True: 49 | self.check_pool() 50 | 51 | utils.thread_warn(f"Account Preloader has {self.get_preloaded_cnt()} accounts loaded") 52 | time.sleep(CHECK_POOL_INTERVAL) 53 | 54 | def check_pool(self): 55 | if self.check_need_flush(): 56 | utils.thread_error("Flushing redis db") 57 | discord.logs_wh("Flushing redis db") 58 | self.flush_db() 59 | 60 | current_preload_cnt = self.get_preloaded_cnt() 61 | 62 | if (current_preload_cnt <= 0): 63 | utils.thread_error("Preload queue is empty, preloading accounts") 64 | threading.Thread(target=self.preload_accounts, args=(PRELOAD_NUM_NORM, "normal",), name="PreloadThreadNorm").start() 65 | threading.Thread(target=self.preload_accounts, args=(PRELOAD_NUM_ELITE, "elite",), name="PreloadThreadElite").start() 66 | elif (current_preload_cnt < MAX_ACCS): 67 | utils.thread_warn("Preload queue is low, preloading accounts") 68 | threading.Thread(target=self.preload_accounts, args=(PRELOAD_NUM_NORM, "normal",), name="PreloadThreadNorm").start() 69 | threading.Thread(target=self.preload_accounts, args=(PRELOAD_NUM_ELITE, "elite",), name="PreloadThreadElite").start() 70 | 71 | def check_need_flush(self): 72 | if not os.path.isfile("./logs/redis_flush.log"): 73 | # Just so we can have this fresh 74 | return True 75 | 76 | with open("./logs/redis_flush.log", "r") as f: 77 | last_flush = f.read() 78 | if len(last_flush) == 0: 79 | return True 80 | 81 | if (time.time() - float(last_flush)) > (FLUSH_INTERVAL): 82 | return True 83 | else: 84 | return False 85 | 86 | def flush_db(self): 87 | with open("./logs/redis_flush.log", "w+") as f: 88 | f.write(str(time.time())) 89 | 90 | self.redis.flushdb() 91 | 92 | def get_preloaded_cnt(self): 93 | return self.redis.scard("resy-engine:preload-acc-normal") + self.redis.scard("resy-engine:preload-acc-elite") 94 | 95 | def preload_accounts(self, num, acc_type="normal"): 96 | if num <= 0: 97 | return 98 | 99 | retrys_needed = 0 100 | for _ in range(num): 101 | if self.get_preloaded_cnt() > MAX_ACCS: 102 | return 103 | auth_token, pmid, network, chosen_acc = self.login(acc_type) 104 | 105 | if auth_token is None: 106 | retrys_needed += 1 107 | continue 108 | else: 109 | chosen_acc["auth_token"] = auth_token 110 | chosen_acc["pmid"] = pmid 111 | del chosen_acc["_id"] 112 | 113 | self.redis.sadd( 114 | f"resy-engine:preload-acc-{acc_type}", json.dumps(chosen_acc) 115 | ) 116 | 117 | if retrys_needed > 0: 118 | utils.thread_warn(f"Retrying {retrys_needed} accounts to preload") 119 | return self.preload_accounts(retrys_needed, acc_type=acc_type) 120 | 121 | # login and checkAccUsable are stolen from accounts.py, we move diff with these 122 | def login(self, acc_type, retrys=0): 123 | network = Network(proxies.get_book_proxy()) 124 | if retrys > 20: 125 | utils.thread_error("Login failed on max attempts, giving up") 126 | return None, None, None, None 127 | 128 | account = database.get_random_sexy_accounts(acc_type)[0] 129 | 130 | try: 131 | login_res, used_proxy = network.login(account["email"], account["password"]) 132 | except Exception as e: 133 | utils.thread_error(f"Login failed with exception: {e}") 134 | return self.login(acc_type=acc_type, retrys=retrys + 1) 135 | 136 | if not login_res.ok: 137 | utils.thread_warn( 138 | f"Login failed, trying new account [{login_res.status_code}] <{used_proxy}>" 139 | ) 140 | 141 | return self.login(acc_type=acc_type, retrys=retrys + 1) 142 | else: 143 | retrys = 0 144 | 145 | auth_token = login_res.json()["token"] 146 | 147 | network.set_auth_token(auth_token) 148 | 149 | payment_method_id = login_res.json()["payment_method_id"] 150 | 151 | is_acc_usable = self.check_acc_usable(network) 152 | if not is_acc_usable: 153 | 154 | return self.login(acc_type, retrys=retrys + 1) 155 | 156 | return auth_token, payment_method_id, network, account 157 | 158 | def check_acc_usable(self, network): 159 | try: 160 | account_check_res = network.account_reservations() 161 | except Exception as e: 162 | return False 163 | 164 | if not account_check_res.ok: 165 | return False 166 | 167 | if account_check_res.status_code == 200: 168 | if "reservations" in account_check_res.json(): 169 | if len(account_check_res.json()["reservations"]) == 0: 170 | return True 171 | else: 172 | return False 173 | -------------------------------------------------------------------------------- /resy/accounts.py: -------------------------------------------------------------------------------- 1 | import random 2 | from utils import Utils 3 | from database import Database 4 | import sys 5 | import os 6 | import redis 7 | import json 8 | 9 | database = Database() 10 | utils = Utils() 11 | 12 | # TODO: rewrite this using keep track of inactive accounts and redis pool 13 | class Accounts: 14 | def __init__(self, source="worker"): 15 | self.accounts = [] 16 | self.elite_accounts = [] 17 | self.source = source 18 | 19 | self.redis = redis.Redis(host=os.getenv("REDIS_HOST"), port=os.getenv("REDIS_PORT"), decode_responses=True, db=0) 20 | 21 | self.load_accounts() 22 | self.load_elite_accounts() 23 | 24 | if len(self.accounts) <= 0: 25 | utils.thread_error("No normal accounts loaded, killing program") 26 | sys.exit(1) 27 | elif len(self.elite_accounts) <= 0: 28 | utils.thread_error("No elite accounts loaded, killing program") 29 | sys.exit(1) 30 | 31 | utils.thread_success(f"Loaded {len(self.accounts)} resy accounts") 32 | utils.thread_success(f"Loaded {len(self.elite_accounts)} resy elite accounts") 33 | 34 | def load_accounts(self): 35 | normal_accs = database.get_normal_accounts() 36 | for normal_acc in normal_accs: 37 | # patch for faker lib using prefixes as names 38 | if len(normal_acc["first_name"]) > 4: 39 | self.accounts.append(normal_acc) 40 | 41 | random.shuffle(self.accounts) 42 | 43 | def load_elite_accounts(self): 44 | elite_accs = database.get_elite_accounts() 45 | for elite_acc in elite_accs: 46 | if len(elite_acc["first_name"]) > 4: 47 | self.elite_accounts.append(elite_acc) 48 | 49 | random.shuffle(self.elite_accounts) 50 | 51 | def get_account(self, account_type="normal"): 52 | account_type = account_type.lower() 53 | if account_type == "elite": 54 | return self.get_elite_account() 55 | else: 56 | return self.get_normal_account() 57 | 58 | def get_preloaded_cnt(self): 59 | return self.redis.scard("resy-engine:preload-acc-normal") + self.redis.scard("resy-engine:preload-acc-elite") 60 | def get_normal_account(self): 61 | if (self.source != "monitor"): 62 | if (self.get_preloaded_cnt() > 0 ): 63 | random_preload = self.redis.srandmember('resy-engine:preload-acc-normal') 64 | 65 | if random_preload is not None: 66 | self.redis.srem("resy-engine:preload-acc-normal", random_preload) 67 | 68 | utils.thread_log("Using preloaded account") 69 | 70 | unwrapped_item = json.loads(random_preload) 71 | 72 | return unwrapped_item 73 | 74 | if len(self.accounts) <= 0: 75 | utils.thread_error("No Accounts Left In Pool") 76 | sys.exit(1) 77 | 78 | chosen_acc = random.choice(self.accounts) 79 | self.accounts.remove(chosen_acc) 80 | 81 | return chosen_acc 82 | 83 | def get_elite_account(self): 84 | if (self.source != "monitor"): 85 | if (self.get_preloaded_cnt() > 0 ): 86 | random_preload= self.redis.srandmember('resy-engine:preload-acc-elite') 87 | 88 | if random_preload is not None: 89 | self.redis.srem("resy-engine:preload-acc-elite", random_preload) 90 | 91 | utils.thread_log("Using preloaded account") 92 | 93 | unwrapped_item = json.loads(random_preload) 94 | 95 | return unwrapped_item 96 | 97 | if len(self.elite_accounts) <= 0: 98 | utils.thread_error("No Accounts Left In Pool") 99 | sys.exit(1) 100 | 101 | chosen_acc = random.choice(self.elite_accounts) 102 | self.elite_accounts.remove(chosen_acc) 103 | 104 | return chosen_acc 105 | 106 | def get_count(self): 107 | return { 108 | "normal": len(self.accounts), 109 | "elite": len(self.elite_accounts) 110 | } 111 | -------------------------------------------------------------------------------- /resy/aesCipher.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from Crypto import Random 3 | from Crypto.Cipher import AES 4 | from base64 import b64encode, b64decode 5 | 6 | # No idea where I got this code from so lets not lose it 7 | # Lots of custom weird string logic to attach the info 8 | 9 | 10 | class AESCipher(object): 11 | def __init__(self, key): 12 | self.block_size = AES.block_size 13 | self.key = hashlib.sha256(key.encode()).digest() 14 | 15 | def encrypt(self, plain_text): 16 | plain_text = self.__pad(plain_text) 17 | iv = Random.new().read(self.block_size) 18 | cipher = AES.new(self.key, AES.MODE_CBC, iv) 19 | encrypted_text = cipher.encrypt(plain_text.encode()) 20 | return b64encode(iv + encrypted_text).decode("utf-8") 21 | 22 | def decrypt(self, encrypted_text): 23 | encrypted_text = b64decode(encrypted_text) 24 | iv = encrypted_text[: self.block_size] 25 | cipher = AES.new(self.key, AES.MODE_CBC, iv) 26 | plain_text = cipher.decrypt(encrypted_text[self.block_size :]).decode("utf-8") 27 | return self.__unpad(plain_text) 28 | 29 | def __pad(self, plain_text): 30 | number_of_bytes_to_pad = self.block_size - len(plain_text) % self.block_size 31 | ascii_string = chr(number_of_bytes_to_pad) 32 | padding_str = number_of_bytes_to_pad * ascii_string 33 | padded_plain_text = plain_text + padding_str 34 | return padded_plain_text 35 | 36 | @staticmethod 37 | def __unpad(plain_text): 38 | last_character = plain_text[len(plain_text) - 1 :] 39 | return plain_text[: -ord(last_character)] 40 | -------------------------------------------------------------------------------- /resy/database.py: -------------------------------------------------------------------------------- 1 | # Collection of DB calls and helpers 2 | 3 | from pymongo import MongoClient 4 | import os 5 | from bson.json_util import dumps, loads 6 | from aesCipher import AESCipher 7 | from dotenv import load_dotenv 8 | import sys 9 | from utils import Utils 10 | 11 | load_dotenv() 12 | 13 | aesCiper = AESCipher(os.getenv("ENCRYPTION_KEY")) 14 | utils = Utils() 15 | 16 | class Database: 17 | def __init__(self): 18 | if not os.getenv("DB_URL"): 19 | utils.thread_error("DB_URL not set") 20 | return sys.exit(1) 21 | 22 | self.db = MongoClient(host=os.getenv("DB_URL")) 23 | 24 | def get_db(self): 25 | return self.db.resme 26 | 27 | def get_normal_accounts(self): 28 | # TODO: only load accounts marked as usable 29 | accs = self.get_db().resy_accounts.find({"acc_type": "normal", "active": True, "suspended": False}) 30 | accs = loads(dumps(accs)) 31 | 32 | for acc in accs: 33 | acc["password"] = aesCiper.decrypt(acc["password"]) 34 | 35 | return accs 36 | 37 | def get_elite_accounts(self): 38 | accs = self.get_db().resy_accounts.find( 39 | {"acc_type": "elite", "active": True, "suspended": False} 40 | ) 41 | accs = loads(dumps(accs)) 42 | 43 | for acc in accs: 44 | acc["password"] = aesCiper.decrypt(acc["password"]) 45 | 46 | return accs 47 | 48 | def get_random_sexy_accounts(self, acc_type="normal"): 49 | # run an aggregation pipeline to get a random record 50 | pipeline = [ 51 | {"$match": {"acc_type": acc_type, "active": True, "suspended": False}}, 52 | {"$sample": {"size": 1}}, 53 | ] 54 | 55 | accs = self.get_db().resy_accounts.aggregate(pipeline) 56 | accs = loads(dumps(accs)) 57 | 58 | for acc in accs: 59 | acc["password"] = aesCiper.decrypt(acc["password"]) 60 | 61 | return accs 62 | 63 | def update_acc(self, query, exec): 64 | collection = self.get_db().resy_accounts 65 | collection.update_one(query, exec) 66 | 67 | def upload_reservation(self, reservation): 68 | collection = self.get_db().resy_reservations 69 | collection.insert_one(reservation) 70 | 71 | def upload_failed(self, reservation): 72 | collection = self.get_db().resy_failures 73 | collection.insert_one(reservation) 74 | -------------------------------------------------------------------------------- /resy/discord.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime, timedelta 3 | from discord_webhook import DiscordWebhook, DiscordEmbed 4 | from utils import Utils 5 | import os 6 | from random import randint 7 | import sys 8 | 9 | utils = Utils() 10 | 11 | 12 | class Discord: 13 | def __init__(self): 14 | self.webhook_url = "" 15 | self.logs_webhook_url = "" 16 | 17 | if not os.getenv("WEBHOOK_URL"): 18 | utils.thread_warn("Not sending discord webhooks, please set .env") 19 | else: 20 | self.webhook_url = os.getenv("WEBHOOK_URL") 21 | 22 | if not os.getenv("LOGS_WEBHOOK_URL"): 23 | utils.thread_error("No logs webhook found") 24 | else: 25 | self.logs_webhook_url = os.getenv("LOGS_WEBHOOK_URL") 26 | 27 | def successful_book_wh(self, res_config, party_size): 28 | if self.webhook_url is not None: 29 | webhook = DiscordWebhook(url=self.webhook_url) 30 | 31 | embed = DiscordEmbed( 32 | title="ResMe Engine", 33 | description=f"New {res_config['name']} Reservation!", 34 | color="ff38b6", 35 | ) 36 | 37 | embed.set_footer(text=f"Secured @ {datetime.now()}") 38 | 39 | embed.add_embed_field(name="Venue Name", value=res_config["name"]) 40 | embed.add_embed_field(name="Date", value=res_config["date"]) 41 | embed.add_embed_field(name="Party Size", value=party_size) 42 | embed.add_embed_field(name="Time", value=res_config["res_time"]) 43 | 44 | webhook.add_embed(embed) 45 | 46 | try: 47 | webhook.execute() 48 | except: 49 | utils.thread_error("Error sending discord webhook") 50 | 51 | def logs_wh(self, message): 52 | if self.logs_webhook_url is not None: 53 | webhook = DiscordWebhook(url = self.logs_webhook_url) 54 | 55 | embed = DiscordEmbed( 56 | title="ResMe System Robot", 57 | description="New ResMe System Notification", 58 | color="00f2ff", 59 | ) 60 | 61 | embed.set_footer(text=f"Sent @ {datetime.now()}") 62 | 63 | embed.add_embed_field(name="Message", value=message) 64 | 65 | webhook.add_embed(embed) 66 | 67 | webhook.execute() 68 | -------------------------------------------------------------------------------- /resy/monitor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from utils import Utils 3 | import time 4 | from network import Network 5 | # from accounts import Accounts 6 | from proxies import Proxies 7 | from datetime import datetime, timedelta 8 | import copy 9 | from random import randint 10 | from worker import Worker 11 | import threading 12 | from pytz import timezone 13 | 14 | proxies = Proxies() 15 | utils = Utils() 16 | # accounts = Accounts("monitor") 17 | 18 | # match this to resy.py 19 | WORKER_CNT = 2 20 | 21 | class Monitor: 22 | def __init__(self, res_config, party_size): 23 | self.network = Network(proxies.get_proxy()) 24 | self.max_date = (datetime.now() + timedelta(days=365)).strftime("%Y-%m-%d") 25 | self.start_date = (datetime.now() + timedelta(days=4)).strftime("%Y-%m-%d") 26 | self.old_cal = {} 27 | self.initialized_cal = False 28 | self.res_config = res_config 29 | self.party_size = party_size 30 | 31 | def start(self): 32 | # _, _ = self.login() 33 | self.monitor() 34 | 35 | # def login(self, retrys=0): 36 | # if retrys > 20: 37 | # utils.thread_error("Login failed on max attempts, killing worker") 38 | # sys.exit() 39 | 40 | # self.account = accounts.get_account(self.res_config["accountType"]) 41 | 42 | # try: 43 | # login_res = self.network.login(self.account["email"], self.account["password"]) 44 | # except Exception as e: 45 | # utils.thread_error(f"Login failed with exception: {e}") 46 | # time.sleep(self.network.ERROR_DELAY_LOGIN) 47 | # return self.login(retrys=retrys + 1) 48 | 49 | # if not login_res.ok: 50 | # utils.thread_warn("Login failed, trying new account") 51 | 52 | # time.sleep(self.network.ERROR_DELAY_LOGIN) 53 | 54 | # return self.login(retrys=retrys + 1) 55 | # else: 56 | # if retrys != 0: 57 | # utils.thread_log("Successfully logged into anaccount after retry") 58 | 59 | # retrys = 0 60 | 61 | # auth_token = login_res.json()["token"] 62 | 63 | # self.network.set_auth_token(auth_token) 64 | 65 | # payment_method_id = login_res.json()["payment_method_id"] 66 | 67 | # is_acc_usable = self.check_acc_usable() 68 | # if not is_acc_usable: 69 | # utils.thread_error("Account is not usable, retrying login with new account") 70 | 71 | # return self.login(retrys=retrys + 1) 72 | 73 | # return auth_token, payment_method_id 74 | 75 | # def check_acc_usable(self): 76 | # account_check_res = self.network.account_reservations() 77 | 78 | # if not account_check_res.ok: 79 | # return False 80 | 81 | # if account_check_res.status_code == 200: 82 | # if "reservations" in account_check_res.json(): 83 | # if len(account_check_res.json()["reservations"]) == 0: 84 | # return True 85 | # else: 86 | # return False 87 | 88 | def monitor(self): 89 | rate_limited_prev = False 90 | rate_limited_cnt = 0 91 | while True: 92 | try: 93 | new_cal_res = self.network.get_calendar(self.res_config, self.start_date, self.max_date, self.party_size) 94 | except Exception as e: 95 | utils.thread_error(f"Failed to get calendar, trying again: {e}") 96 | continue 97 | 98 | if (not new_cal_res) or (not new_cal_res.ok): 99 | if new_cal_res.status_code == 500: 100 | if rate_limited_cnt > 30: 101 | utils.thread_error("Rate limited over 30 times, something might be wrong") 102 | time.sleep(10) 103 | elif rate_limited_cnt == 10: 104 | # TODO: see if this works 105 | self.network = Network(proxies.get_proxy()) 106 | time.sleep(10) 107 | else: 108 | time.sleep(0.1) 109 | # TODO Change this for non rotating 110 | rate_limited_cnt += 1 111 | rate_limited_prev = True 112 | # time.sleep(0.1) 113 | continue 114 | 115 | utils.thread_error(f"Failed to get calendar for unknown reason, trying again: {new_cal_res.status_code}") 116 | time.sleep(1) 117 | continue 118 | 119 | if (new_cal_res.status_code == 200) or (new_cal_res.status_code == 201) or (new_cal_res.status_code == 204): 120 | if rate_limited_prev and (rate_limited_cnt > 50): 121 | utils.thread_success(f"Successfully fetched calendar after {rate_limited_cnt} attempts") 122 | 123 | new_cal = new_cal_res.json() 124 | 125 | if self.initialized_cal: 126 | new_dates = self.get_calendar_positive_diff(new_cal) 127 | 128 | if (len(new_dates) != 0) and (new_dates != []): 129 | for date in new_dates: 130 | temp_config = copy.deepcopy(self.res_config) 131 | temp_config["date"] = date 132 | temp_config["partyMin"] = self.party_size 133 | temp_config["partyMax"] = self.party_size 134 | temp_config["monitor"]["timer"] = 0.5 135 | 136 | self.master_start_worker(temp_config) 137 | 138 | utils.thread_success( 139 | f"Passive monitor found availability on {', '.join(new_dates)}" 140 | ) 141 | 142 | self.old_cal = new_cal 143 | 144 | if rate_limited_prev: 145 | rate_limited_prev = False 146 | rate_limited_cnt = 0 147 | else: 148 | utils.thread_log("Initialized first calendar") 149 | self.initialized_cal = True 150 | self.old_cal = new_cal 151 | 152 | # wait monitor delay 153 | time.sleep(randint(1,10)) 154 | 155 | def get_calendar_positive_diff(self, latest_cal): 156 | new_dates = [] 157 | if latest_cal != self.old_cal: 158 | # if new date available send right away 159 | if latest_cal["last_calendar_day"] != self.old_cal["last_calendar_day"]: 160 | utils.thread_success("Calendar new last date available!!!!!!!!") 161 | return [latest_cal["last_calendar_day"]] 162 | 163 | # now we iterate for differences s s s s 164 | # probably a better way to do this but this is O(n) so im ok 165 | idx = 0 166 | for date in latest_cal["scheduled"]: 167 | # sanity check comparison, otherwise just pass on giving a diff tbh 168 | old_date = self.old_cal["scheduled"][idx] 169 | if date["date"] != old_date["date"]: 170 | utils.thread_error("Date mismatch in calendar, something is wrong") 171 | return [new_dates] 172 | 173 | if (date["inventory"]["reservation"] == "available") and ( 174 | old_date["inventory"]["reservation"] != "available" 175 | ): 176 | utils.thread_success(f"Found new date available!!!") 177 | new_dates.append(date["date"]) 178 | 179 | idx += 1 180 | 181 | return new_dates 182 | 183 | return [] 184 | 185 | def master_start_worker(self, res_config): 186 | offset_day = (datetime.now() + timedelta(days=res_config["offset"])).strftime("%Y-%m-%d") 187 | 188 | # TODO: add a check to see if its within 5 minutes of drop 189 | if res_config["date"] == offset_day: 190 | if (datetime.now(timezone('EST')).hour <= 13) or ((datetime.now(timezone('EST')).hour == 23) or (datetime.now(timezone("EST")).hour == 0)): 191 | utils.thread_error("Drop day worker trying to launch from monitor inside of drop times, refusing") 192 | return 193 | 194 | minParty = res_config["partyMin"] 195 | maxParty = res_config["partyMax"] 196 | grabmax = res_config["grabMax"] 197 | utils.thread_log( 198 | f"Spinning up {(grabmax * len(range(minParty, maxParty + 1))) * WORKER_CNT} Workers" 199 | ) 200 | 201 | for party_config in range(minParty, maxParty + 1): 202 | for grab_instance in range(grabmax): 203 | for worker_i in range(0, WORKER_CNT): 204 | worker_name = f"{res_config['name'].replace(' ', '-')}:{party_config}:{grab_instance}:{worker_i}:{res_config['date']}" 205 | threading.Thread( 206 | target=self.start_worker, 207 | args=( 208 | res_config, 209 | party_config, 210 | ), 211 | name=worker_name, 212 | ).start() 213 | 214 | # these are copied from the main script, i forgot to abstract and this is easier 215 | def start_worker(self, res_config, party_config): 216 | try: 217 | Worker(res_config, party_config, parent="monitor").start_bot() 218 | except (KeyboardInterrupt, SystemExit): 219 | sys.exit() 220 | -------------------------------------------------------------------------------- /resy/network.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import urllib3 3 | from utils import Utils 4 | import os 5 | import time 6 | from random import choice 7 | from proxies import Proxies 8 | import sys 9 | 10 | 11 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 12 | 13 | utils = Utils() 14 | proxies = Proxies() 15 | 16 | 17 | class Network: 18 | def __init__(self, proxy): 19 | # Config keys 20 | self.USER_AGENT = ( 21 | "Resy/2.81 (com.resy.ResyApp; build:5433; iOS 17.4.1) Alamofire/5.8.0" 22 | ) 23 | 24 | self.RESY_KEY = 'ResyAPI api_key="AIcdK2rLXG6TYwJseSbmrBAy3RP81ocd"' 25 | self.RESY_KEY_NORM = 'ResyAPI api_key="VbWk7s3L4KiK5fzlO7JD3Q5EYolJI7n5"' 26 | 27 | self.normal_user_agents = [] 28 | 29 | if not os.path.isfile("./uas.txt"): 30 | utils.thread_error("No user agent file found") 31 | sys.exit(1) 32 | 33 | with open("./uas.txt", "r") as f: 34 | for line in f: 35 | self.normal_user_agents.append(line.strip()) 36 | 37 | # Static Values 38 | self.MAX_ACC_RETRYS = 5 39 | self.MAX_INIT_BOOK_RETRYS = 20 40 | self.ERROR_DELAY = 2 41 | self.MAX_BOOK_RETRYS = 20 42 | self.ERROR_DELAY_CAL = 3 43 | self.ERROR_DELAY_LOGIN = 1 44 | 45 | self.session = requests.Session() 46 | self.proxies = proxy 47 | 48 | def get_session(self): 49 | return self.session 50 | 51 | def get_random_ua(self): 52 | return choice(self.normal_user_agents) 53 | 54 | def update_proxy(self, proxy): 55 | self.proxies = proxy 56 | 57 | def set_auth_token(self, auth_token): 58 | self.auth_token = auth_token 59 | 60 | def login(self, email, password): 61 | # Cant find the password endpoint on mobile 62 | 63 | url = "https://api.resy.com/3/auth/password" 64 | 65 | payload = {"email": email, "password": password} 66 | headers = { 67 | "Accept": "application/json, text/plain, */*", 68 | "Accept-Encoding": "gzip, deflate, br, zstd", 69 | "Accept-Language": "en-US,en;q=0.9", 70 | "Authorization": self.RESY_KEY_NORM, 71 | "Cache-Control": "no-cache", 72 | "Content-Type": "application/x-www-form-urlencoded", 73 | "Dnt": "1", 74 | "Origin": "https://resy.com", 75 | "Priority": "u=1, i", 76 | "Referer": "https://resy.com/", 77 | "User-Agent": self.get_random_ua(), 78 | "X-Origin": "https://resy.com", 79 | } 80 | 81 | use_proxies = proxies.get_mobile_proxy() 82 | response = self.session.post( 83 | url, data=payload, headers=headers, proxies=use_proxies, verify=False, timeout=10 84 | ) 85 | 86 | return response, use_proxies 87 | 88 | def account_reservations(self): 89 | url = f"https://api.resy.com/3/user/reservations?limit=1&offset=1&type=upcoming&book_on_behalf_of=false" 90 | 91 | headers = { 92 | "Accept": "*/*", 93 | "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", 94 | "Accept-Language": "en-US;q=1.0, fr-US;q=0.9", 95 | "Authorization": self.RESY_KEY_NORM, 96 | "Connection": "keep-alive", 97 | "Host": "api.resy.com", 98 | "User-Agent": self.get_random_ua(), 99 | "X-Resy-Auth-Token": self.auth_token, 100 | "X-Resy-Universal-Auth": self.auth_token, 101 | "cache-control": "no-cache", 102 | } 103 | response = self.session.get(url, headers=headers, proxies=proxies.get_proxy(), verify=False, timeout=10) 104 | 105 | return response 106 | 107 | def find_availability(self, res_config, party_size): 108 | url = "https://api.resy.com/4/find" 109 | 110 | querystring = { 111 | "lat": "0", 112 | "long": "0", 113 | "day": res_config["date"], 114 | "party_size": party_size, 115 | "venue_id": res_config["venueID"], 116 | "sort_by": "available", 117 | } 118 | 119 | headers = { 120 | "Accept": "application/json, text/plain, */*", 121 | "Accept-Encoding": "gzip, deflate, br, zstd", 122 | "Accept-Language": "en-US,en;q=0.9", 123 | "Authorization": self.RESY_KEY_NORM, 124 | "Cache-Control": "no-cache", 125 | "Connection": "keep-alive", 126 | "Origin": "https://resy.com", 127 | "Referer": "https://resy.com/", 128 | "User-Agent": self.get_random_ua(), 129 | "X-Origin": "https://resy.com" 130 | } 131 | 132 | use_proxy = choice([proxies.get_resi_proxy(), proxies.get_proxy(), proxies.get_proxy(), proxies.get_proxy()]) 133 | 134 | # manually overwrite proxy to avoid RL 135 | response = self.session.get( 136 | url, headers=headers, params=querystring, proxies=use_proxy, verify=False, timeout=4 137 | ) 138 | 139 | return response 140 | 141 | def init_book(self, config_id, date, party_size): 142 | url = "https://api.resy.com/3/details" 143 | 144 | payload = { 145 | "commit": 1, 146 | "config_id": config_id, 147 | "day": date, 148 | "party_size": party_size 149 | } 150 | 151 | headers = { 152 | "Accept": "application/json, text/plain, */*", 153 | "Accept-Encoding": "gzip, deflate, br", 154 | "Accept-Language": "en-US,en;q=0.9", 155 | "Authorization": self.RESY_KEY, 156 | "Cache-Control": "no-cache", 157 | "Content-Type": "application/json", 158 | "Origin": "https://widgets.resy.com", 159 | "Referer": "https://widgets.resy.com/", 160 | "User-Agent": self.get_random_ua(), 161 | "X-Resy-Auth-Token": self.auth_token, 162 | "X-Resy-Universal-Auth": self.auth_token, 163 | "X-Origin": "https://widgets.resy.com" 164 | } 165 | 166 | response = self.session.get(url, params=payload, headers=headers, proxies=proxies.get_mobile_proxy(), verify=False, timeout=4) 167 | 168 | return response 169 | 170 | def book(self, book_token, payment_method_id): 171 | url = "https://api.resy.com/3/book" 172 | 173 | payload = { 174 | "book_token": book_token, 175 | "struct_payment_method": f'{{"id": {payment_method_id}}}', 176 | "source_id": "resy.com-venue-details", 177 | } 178 | 179 | headers = { 180 | "Accept": "application/json, text/plain, */*", 181 | "Accept-Encoding": "gzip, deflate, br", 182 | "Accept-Language": "en-US,en;q=0.9", 183 | "Authorization": self.RESY_KEY_NORM, 184 | "Cache-Control": "no-cache", 185 | "Connection": "keep-alive", 186 | "Content-Type": "application/x-www-form-urlencoded", 187 | "Origin": "https://widgets.resy.com", 188 | "Referer": "https://widgets.resy.com/", 189 | "User-Agent": self.get_random_ua(), 190 | "X-Resy-Auth-Token": self.auth_token, 191 | "X-Resy-Universal-Auth": self.auth_token 192 | } 193 | 194 | response = self.session.post(url, data=payload, headers=headers, proxies=proxies.get_mobile_proxy(), verify=False, timeout=60) 195 | 196 | return response 197 | 198 | def get_calendar(self, res_config, start_date, end_date, party_size): 199 | url = f"https://api.resy.com/4/venue/calendar?venue_id={res_config['venueID']}&num_seats={party_size}&start_date={start_date}&end_date={end_date}" 200 | 201 | headers = { 202 | 'authority': 'api.resy.com', 203 | 'accept': 'application/json, text/plain, */*', 204 | 'accept-language': 'en-US,en;q=0.9', 205 | 'authorization': self.RESY_KEY_NORM, 206 | 'cache-control': 'no-cache', 207 | 'connection': 'keep-alive', 208 | 'origin': 'https://resy.com', 209 | 'referer': 'https://resy.com/', 210 | 'user-agent': self.get_random_ua(), 211 | 'x-origin': 'https://resy.com', 212 | } 213 | 214 | response = self.session.get(url, headers=headers, proxies=proxies.get_proxy(), verify=False, timeout=10) 215 | 216 | return response 217 | -------------------------------------------------------------------------------- /resy/proxies.py: -------------------------------------------------------------------------------- 1 | import random 2 | import os 3 | import sys 4 | from dotenv import load_dotenv 5 | from utils import Utils 6 | 7 | load_dotenv() 8 | utils = Utils() 9 | # TODO: add support for non DC proxies 10 | 11 | class Proxies: 12 | def __init__(self): 13 | self.proxies = [] 14 | 15 | # Proxies that are better for speed n shit 16 | self.book_proxies = [] 17 | 18 | # residential proxies 19 | self.resi_proxies = [] 20 | 21 | # mobile proxies 22 | self.mobile_proxies = [] 23 | 24 | if (not os.path.isfile("./proxies.txt")) or (not os.path.isfile("./book_proxies.txt") or (not os.path.isfile("./resi_proxies.txt"))): 25 | utils.thread_error("No proxies.txt or book_proxies.txt or resi_proxies.txt file found, please make sure you have both") 26 | sys.exit(1) 27 | 28 | with open("./proxies.txt", "r") as file: 29 | raw_proxies = file.read().splitlines() 30 | 31 | for raw_proxy in raw_proxies: 32 | proxy_parts = raw_proxy.split(":") 33 | 34 | if len(proxy_parts) == 2: 35 | valid_proxy = f"{proxy_parts[0]}:{proxy_parts[1]}" 36 | else: 37 | valid_proxy = f"{proxy_parts[2]}:{proxy_parts[3]}@{proxy_parts[0]}:{proxy_parts[1]}" 38 | 39 | formatted_proxy = { 40 | "http": "http://" + valid_proxy + "/", 41 | "https": "http://" + valid_proxy + "/", 42 | } 43 | 44 | self.proxies.append(formatted_proxy) 45 | 46 | with open("./book_proxies.txt", "r") as file: 47 | raw_proxies = file.read().splitlines() 48 | 49 | for raw_proxy in raw_proxies: 50 | proxy_parts = raw_proxy.split(":") 51 | 52 | if len(proxy_parts) == 2: 53 | valid_proxy = f"{proxy_parts[0]}:{proxy_parts[1]}" 54 | else: 55 | valid_proxy = f"{proxy_parts[2]}:{proxy_parts[3]}@{proxy_parts[0]}:{proxy_parts[1]}" 56 | 57 | formatted_proxy = { 58 | "http": "http://" + valid_proxy + "/", 59 | "https": "http://" + valid_proxy + "/", 60 | } 61 | 62 | self.book_proxies.append(formatted_proxy) 63 | 64 | with open("./resi_proxies.txt", "r") as file: 65 | raw_proxies = file.read().splitlines() 66 | 67 | for raw_proxy in raw_proxies: 68 | proxy_parts = raw_proxy.split(":") 69 | 70 | if len(proxy_parts) == 2: 71 | valid_proxy = f"{proxy_parts[0]}:{proxy_parts[1]}" 72 | else: 73 | valid_proxy = f"{proxy_parts[2]}:{proxy_parts[3]}@{proxy_parts[0]}:{proxy_parts[1]}" 74 | 75 | formatted_proxy = { 76 | "http": "http://" + valid_proxy + "/", 77 | "https": "http://" + valid_proxy + "/", 78 | } 79 | 80 | self.resi_proxies.append(formatted_proxy) 81 | 82 | with open("./mobile_proxies.txt", "r") as file: 83 | raw_proxies = file.read().splitlines() 84 | 85 | for raw_proxy in raw_proxies: 86 | proxy_parts = raw_proxy.split(":") 87 | 88 | if len(proxy_parts) == 2: 89 | valid_proxy = f"{proxy_parts[0]}:{proxy_parts[1]}" 90 | else: 91 | valid_proxy = f"{proxy_parts[2]}:{proxy_parts[3]}@{proxy_parts[0]}:{proxy_parts[1]}" 92 | 93 | formatted_proxy = { 94 | "http": "http://" + valid_proxy + "/", 95 | "https": "http://" + valid_proxy + "/", 96 | } 97 | 98 | self.mobile_proxies.append(formatted_proxy) 99 | 100 | self.print_proxy_output() 101 | 102 | def get_proxy(self): 103 | return random.choice(self.proxies) 104 | 105 | def get_book_proxy(self): 106 | return random.choice(self.book_proxies) 107 | 108 | def get_resi_proxy(self): 109 | return random.choice(self.resi_proxies) 110 | 111 | def get_proxy_list(self): 112 | return self.proxies 113 | 114 | def get_book_proxy_list(self): 115 | return self.book_proxies 116 | 117 | def get_mobile_proxy(self): 118 | return random.choice(self.mobile_proxies) 119 | 120 | def print_proxy_output(self): 121 | utils.thread_log(f"Loaded {len(self.proxies)} proxie(s)") 122 | utils.thread_log(f"Loaded {len(self.book_proxies)} book proxie(s)") 123 | utils.thread_log(f"Loaded {len(self.resi_proxies)} residental proxie(s))") 124 | utils.thread_log(f"Loaded {len(self.mobile_proxies)} mobile proxie(s))") 125 | -------------------------------------------------------------------------------- /resy/resy.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from utils import Utils 5 | import threading 6 | import time 7 | from dotenv import load_dotenv 8 | from worker import Worker 9 | from monitor import Monitor 10 | from datetime import datetime, timedelta 11 | from acc_preloader import AccPreloader 12 | from pytz import timezone 13 | from discord import Discord 14 | import schedule 15 | import sentry_sdk 16 | 17 | # this is to adjust the monitoring recursive limit, its not heavy python can chill 18 | sys.setrecursionlimit(99999) 19 | 20 | # load the environment from .env 21 | load_dotenv() 22 | 23 | if os.getenv("SENTRY_DSN"): 24 | sentry_sdk.init( 25 | dsn=os.getenv("SENTRY_DSN") 26 | ) 27 | 28 | # Initialize classes 29 | utils = Utils() 30 | discord = Discord() 31 | 32 | # Initialize constants 33 | 34 | # Constants that are dynamically assigned 35 | # sites we are going to run bots on 36 | resy_bot_lst = [] 37 | # sites we are going to run the monitor on 38 | resy_monitor_lst = [] 39 | # signals that we are in dev mode 40 | DEV_MODE = os.getenv("DEV_MODE") 41 | # signals that we are in debug mode 42 | DEBUG = os.getenv("DEBUG") 43 | 44 | MONITOR_WORKERS = 2 45 | WORKER_CNT = 3 46 | WORKER_LAUNCH_TIME = 10 47 | 48 | 49 | def file_init(): 50 | # initialize the required files and folders 51 | if not os.path.isdir("./logs"): 52 | os.mkdir("./logs") 53 | utils.thread_success("Created logs folder") 54 | 55 | # create error log file 56 | if not os.path.isfile("./logs/error.log"): 57 | open("./logs/error.log", "w+") 58 | 59 | # create general log file 60 | if not os.path.isfile("./logs/log.log"): 61 | open("./logs/log.log", "w+") 62 | 63 | # create success log file 64 | if not os.path.isfile("./logs/success.log"): 65 | with open("./logs/success.log", "a+") as f: 66 | f.write("\n") 67 | f.write("-" * 50) 68 | f.write("\n") 69 | 70 | if not os.path.isfile("./logs/failed.log"): 71 | with open("./logs/failed.log", "a+") as f: 72 | f.write("\n") 73 | f.write("-" * 50) 74 | f.write("\n") 75 | 76 | utils.thread_success("Validated file integrity") 77 | 78 | def config_init(): 79 | config_path = "./resy.json" 80 | 81 | config_override_path = os.getenv("CONFIG_OVERRIDE") 82 | if config_override_path and (os.path.isfile(config_override_path)): 83 | utils.thread_log(f"USING CONFIG OVERRIDE PATH {config_override_path}") 84 | config_path = config_override_path 85 | 86 | # TODO: move this to the database 87 | if not os.path.isfile(config_path): 88 | utils.thread_error("No valid config file found") 89 | sys.exit(1) 90 | 91 | with open(config_path, "r") as f: 92 | resy_config = json.load(f) 93 | 94 | for resy_restaurant in resy_config: 95 | if resy_restaurant["enabled"]: 96 | 97 | resy_bot_lst.append(resy_restaurant) 98 | 99 | if ("forceStart" in resy_restaurant) and (resy_restaurant["forceStart"]): 100 | init_bot(resy_restaurant) 101 | 102 | if resy_restaurant["passiveMonitoring"]: 103 | resy_monitor_lst.append(resy_restaurant) 104 | 105 | utils.thread_success(f"Loaded {len(resy_bot_lst)} resy restaurants to bot") 106 | utils.thread_success(f"Loaded {len(resy_monitor_lst)} resy restaurants to monitor") 107 | 108 | 109 | def config_bots(): 110 | for restaurant in resy_bot_lst: 111 | drop_start_raw = restaurant["monitor"]["drop"] 112 | drop_start_t = datetime.strptime(drop_start_raw, "%H:%M:%S").time() 113 | drop_start_dt = datetime.combine(datetime.now(), drop_start_t, tzinfo=timezone("EST")) 114 | drop_worker_start_dt = drop_start_dt - timedelta(minutes=WORKER_LAUNCH_TIME) 115 | 116 | # if drop time within 10 minutes we want to start it asap no rocky 117 | if (drop_worker_start_dt < datetime.now(timezone("EST"))) and (datetime.now(timezone("EST")) < drop_start_dt): 118 | utils.thread_warn( 119 | f"Drop time for {restaurant['name']} is within {WORKER_LAUNCH_TIME} minutes, starting bot now" 120 | ) 121 | init_bot_sch(restaurant) 122 | 123 | drop_worker_start = drop_worker_start_dt.strftime("%H:%M:%S") 124 | 125 | schedule.every().day.at(drop_worker_start, "America/New_York").do( 126 | init_bot_sch, res_config=restaurant 127 | ) 128 | 129 | utils.thread_log( 130 | f"Scheduled {restaurant['name']} to start at {drop_worker_start} daily" 131 | ) 132 | 133 | while True: 134 | schedule.run_pending() 135 | time.sleep(1) 136 | 137 | def init_bot_sch(res_config): 138 | # initialize the botting flow 139 | utils.thread_success("Starting bot from scheduler!") 140 | thread_name = f"Master-{res_config['name']}" 141 | try: 142 | discord.logs_wh(f"Starting bot from scheduler: {thread_name}") 143 | except: 144 | pass 145 | res_config["parent"] = "scheduled" 146 | t = threading.Thread( 147 | target=master_start_worker, 148 | args=(res_config,), 149 | name=thread_name, 150 | ) 151 | 152 | t.start() 153 | 154 | def init_bot(res_config): 155 | # initialize the botting flow 156 | utils.thread_success("Generic starting bot") 157 | res_config["parent"] = "unknown" 158 | thread_name = f"Master-{res_config['name']}" 159 | t = threading.Thread( 160 | target=master_start_worker, 161 | args=(res_config,), 162 | name=thread_name, 163 | ) 164 | 165 | t.start() 166 | 167 | def init_preloader(): 168 | thread_name = "AccountPreloader" 169 | t = threading.Thread( 170 | target=start_acc_preloader, 171 | args=(), 172 | name=thread_name, 173 | ).start() 174 | 175 | def init_monitors(): 176 | # initialize the monitoring flow 177 | for restaurant in resy_monitor_lst: 178 | thread_name = f"MonitorMaster-{restaurant['name']}" 179 | t = threading.Thread( 180 | target=master_start_monitor, 181 | args=(restaurant,), 182 | name=thread_name 183 | ) 184 | 185 | t.start() 186 | 187 | def master_start_worker(res_config): 188 | if "date" not in res_config: 189 | res_config["date"] = ( 190 | datetime.now() + timedelta(days=res_config["offset"]) 191 | ).strftime("%Y-%m-%d") 192 | 193 | if "parent" not in res_config: 194 | res_config["parent"] = "N/A" 195 | 196 | 197 | minParty = res_config["partyMin"] 198 | maxParty = res_config["partyMax"] 199 | grabmax = res_config["grabMax"] 200 | utils.thread_log(f"Spinning up {(grabmax * len(range(minParty, maxParty + 1))) * WORKER_CNT} Workers") 201 | 202 | for party_config in range(minParty, maxParty + 1): 203 | for grab_instance in range(grabmax): 204 | for worker_i in range(0, WORKER_CNT): 205 | worker_name = f"{res_config['name'].replace(' ', '-')}:{party_config}:{grab_instance}:{worker_i}:{res_config['date']}" 206 | t = threading.Thread( 207 | target=start_worker, 208 | args=(res_config, party_config,), 209 | name=worker_name, 210 | ) 211 | 212 | t.start() 213 | time.sleep(0.1) 214 | 215 | def master_start_monitor(res_config): 216 | minParty = res_config["partyMin"] 217 | maxParty = res_config["partyMax"] 218 | 219 | for party_config in range(minParty, maxParty + 1): 220 | for monitor_instance in range(0, MONITOR_WORKERS): 221 | monitor_name = f"{res_config['name'].replace(' ', '-')}:{party_config}:{monitor_instance}:MONITOR" 222 | t = threading.Thread( 223 | target=start_montior, 224 | args=(res_config, party_config,), 225 | name=monitor_name, 226 | ) 227 | 228 | t.start() 229 | time.sleep(0.2) 230 | 231 | 232 | def start_worker(res_config, party_config): 233 | try: 234 | Worker(res_config, party_config, parent=res_config['parent']).start_bot() 235 | except (KeyboardInterrupt, SystemExit): 236 | sys.exit() 237 | 238 | def start_montior(res_config, party_config): 239 | try: 240 | Monitor(res_config, party_config).start() 241 | except (KeyboardInterrupt, SystemExit): 242 | sys.exit() 243 | 244 | def start_acc_preloader(): 245 | try: 246 | AccPreloader() 247 | except (KeyboardInterrupt, SystemExit): 248 | sys.exit() 249 | 250 | 251 | if __name__ == "__main__": 252 | utils.thread_log("Resy Botting Engine, Booting Up") 253 | discord.logs_wh("Starting up an instance of the Resy engine") 254 | 255 | # run file init 256 | file_init() 257 | 258 | # run resy config init 259 | config_init() 260 | 261 | # run monitors 262 | init_monitors() 263 | 264 | init_preloader() 265 | 266 | # config bots 267 | config_bots() 268 | -------------------------------------------------------------------------------- /resy/utils.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import colorama 3 | from termcolor import colored 4 | from datetime import datetime 5 | 6 | colorama.init() 7 | 8 | class Utils: 9 | def __init__(self): 10 | return 11 | 12 | def thread_log(self, message): 13 | msg = f"[{threading.current_thread().name}] <{datetime.utcnow()}> {message}" 14 | 15 | print(colored(msg, "cyan")) 16 | 17 | 18 | def thread_error(self, message): 19 | msg = f"[{threading.current_thread().name}] <{datetime.utcnow()}> {message}" 20 | 21 | print(colored(msg, "red")) 22 | 23 | 24 | def thread_warn(self, message): 25 | msg = f"[{threading.current_thread().name}] <{datetime.utcnow()}> {message}" 26 | 27 | print(colored(msg, "yellow")) 28 | 29 | 30 | def thread_success(self, message): 31 | msg = f"[{threading.current_thread().name}] <{datetime.utcnow()}> {message}" 32 | 33 | print(colored(msg, "green")) 34 | 35 | 36 | def thread_print(self, message): 37 | print(f"[{threading.current_thread().name}] <{datetime.utcnow()}> {message}") 38 | -------------------------------------------------------------------------------- /resy/worker.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from utils import Utils 3 | from datetime import datetime, timedelta 4 | import os 5 | from network import Network 6 | from proxies import Proxies 7 | import time 8 | from accounts import Accounts 9 | from random import choice, randint 10 | from database import Database 11 | from aesCipher import AESCipher 12 | from discord import Discord 13 | from pytz import timezone 14 | import threading 15 | import json 16 | 17 | utils = Utils() 18 | proxies = Proxies() 19 | accounts = Accounts() 20 | database = Database() 21 | discord = Discord() 22 | 23 | aesCiper = AESCipher(os.getenv("ENCRYPTION_KEY")) 24 | 25 | START_DELAY = 30 26 | 27 | class Worker: 28 | def __init__(self, res_config, party_size, parent="N/A"): 29 | self.parent = parent 30 | self.res_config = res_config 31 | self.party_size = party_size 32 | self.network = Network(proxies.get_book_proxy()) 33 | self.end_time = datetime.now(timezone("EST")) + timedelta( 34 | minutes=res_config["monitor"]["timer"] 35 | ) 36 | self.drop_time = datetime.combine( 37 | datetime.now(timezone("EST")), 38 | datetime.strptime(res_config["monitor"]["drop"], "%H:%M:%S").time(), 39 | tzinfo=timezone("EST"), 40 | ) 41 | self.start_time = datetime.now(timezone("EST")) 42 | if self.is_scheduled(): 43 | end_time = datetime.strptime(res_config["monitor"]["end"], "%H:%M:%S").time() 44 | if end_time.hour == 0: 45 | self.end_time = datetime.combine( 46 | datetime.now(timezone("EST")) + timedelta(days=1), 47 | datetime.strptime(res_config["monitor"]["end"], "%H:%M:%S").time(), 48 | tzinfo=timezone("EST"), 49 | ) 50 | else: 51 | self.end_time = datetime.combine(datetime.now(timezone("EST")), datetime.strptime(res_config["monitor"]["end"], "%H:%M:%S").time(), tzinfo=timezone("EST")) 52 | self.start_time = self.drop_time - timedelta(seconds=START_DELAY + randint(1, 30)) 53 | 54 | utils.thread_log(f"Start time: {self.start_time}, End time: {self.end_time}") 55 | 56 | def start_bot(self): 57 | if self.is_scheduled() and (datetime.now(tz=timezone("EST")) >= self.end_time): 58 | utils.thread_error("Drop worker trying to launch post drop, rejecting...") 59 | sys.exit() 60 | 61 | _, payment_method_id = self.login() 62 | config_id = self.get_availability() 63 | book_token = self.init_book(config_id) 64 | booked, _ = self.book(book_token, payment_method_id) 65 | 66 | if booked: 67 | return self.successful_worker(config_id) 68 | else: 69 | return self.failed_worker(config_id) 70 | 71 | def successful_worker(self, configID): 72 | with open("./logs/success.log", "a") as f: 73 | content = f"{self.res_config['date']}|{self.party_size}|{self.res_config['venueID']}|{self.res_config['name']}|{self.account['email']}|{self.account['password']}|{self.account['first_name']}|{self.account['last_name']}|{self.res_config['res_time']}\n" 74 | f.write(content) 75 | 76 | res_obj = { 77 | "venue_name": self.res_config["name"], 78 | "date": self.res_config["date"], 79 | "party_size": self.party_size, 80 | "venue_id": self.res_config["venueID"], 81 | "email": self.account["email"], 82 | "password": aesCiper.encrypt(self.account["password"]), 83 | "first_name": self.account["first_name"], 84 | "last_name": self.account["last_name"], 85 | "phone_num": self.account["phone_num"], 86 | "reviewed": False, 87 | "res_time": self.res_config["res_time"], 88 | "cancelled": False, 89 | "createdAt": str(datetime.now(timezone("EST"))), 90 | "configID": configID, 91 | "selly": False, 92 | "parent_process": self.parent 93 | } 94 | 95 | database.upload_reservation(res_obj) 96 | database.update_acc( 97 | {"email": self.account["email"]}, {"$set": {"active": False}} 98 | ) 99 | 100 | utils.thread_success("Booked successfully!") 101 | 102 | try: 103 | discord.successful_book_wh(self.res_config, self.party_size) 104 | except: 105 | utils.thread_error("Error sending discord webhook") 106 | sys.exit() 107 | sys.exit() 108 | 109 | def failed_worker(self, config_id): 110 | # failed_obj = { 111 | # "venue_name": self.res_config["name"], 112 | # "date": self.res_config["date"], 113 | # "party_size": self.party_size, 114 | # "venue_id": self.res_config["venueID"], 115 | # "res_time": self.res_config["res_time"], 116 | # "createdAt": str(datetime.now(timezone("EST"))), 117 | # "configID": config_id, 118 | # } 119 | 120 | # database.upload_failed(failed_obj) 121 | 122 | end_msg = f"Failed to book slot, {threading.active_count()} threads running total, [{config_id}]" 123 | utils.thread_error(end_msg) 124 | with open("./logs/failed.log", "a+") as f: 125 | f.write(f"[{threading.current_thread().name}] {end_msg}\n") 126 | 127 | sys.exit() 128 | 129 | def is_scheduled(self): 130 | return self.parent.lower() == "scheduled" 131 | 132 | def login(self, retrys=0): 133 | if retrys > 20: 134 | utils.thread_error("Login failed on max attempts, killing worker") 135 | sys.exit() 136 | 137 | self.account = accounts.get_account(self.res_config["accountType"]) 138 | 139 | if "auth_token" in self.account: 140 | # self.network.session.cookies.update(self.account["cookies"]) 141 | self.network.set_auth_token(self.account["auth_token"]) 142 | 143 | return self.account["auth_token"], self.account["pmid"] 144 | 145 | try: 146 | login_res, used_proxy = self.network.login(self.account["email"], self.account["password"]) 147 | except Exception as e: 148 | utils.thread_error(f"Login failed with exception: {e}") 149 | return self.login(retrys=retrys + 1) 150 | 151 | if not login_res.ok: 152 | utils.thread_warn("Login failed, trying new account") 153 | 154 | return self.login(retrys=retrys + 1) 155 | else: 156 | if retrys != 0: 157 | utils.thread_log("Successfully logged into anaccount after retry") 158 | 159 | retrys = 0 160 | 161 | auth_token = login_res.json()["token"] 162 | 163 | self.network.set_auth_token(auth_token) 164 | 165 | payment_method_id = login_res.json()["payment_method_id"] 166 | 167 | is_acc_usable = self.check_acc_usable() 168 | if not is_acc_usable: 169 | utils.thread_error("Account is not usable, retrying login with new account") 170 | 171 | return self.login(retrys=retrys + 1) 172 | 173 | return auth_token, payment_method_id 174 | 175 | def check_acc_usable(self): 176 | try: 177 | account_check_res = self.network.account_reservations() 178 | except Exception as e: 179 | return False 180 | 181 | if not account_check_res.ok: 182 | return False 183 | 184 | if account_check_res.status_code == 200: 185 | if "reservations" in account_check_res.json(): 186 | if len(account_check_res.json()["reservations"]) == 0: 187 | return True 188 | else: 189 | return False 190 | 191 | # ! TODO: change this from recursion to a while loop 192 | def get_availability(self, drop_wait=False): 193 | monitor_delay = self.res_config["monitor"]["delay"] 194 | 195 | # wait until drop 196 | if self.parent.lower() == "scheduled" and (not drop_wait): 197 | if datetime.now(tz=timezone("EST")) < self.start_time: 198 | sleep_time = self.start_time - datetime.now(tz=timezone("EST")) 199 | if sleep_time.seconds > 1: 200 | utils.thread_log(f"Waiting for drop start time {self.start_time}, sleeping for {sleep_time.seconds} second(s)") 201 | time.sleep((self.start_time - datetime.now(tz=timezone("EST"))).seconds) 202 | drop_wait = True 203 | 204 | if datetime.now(timezone("EST")) > self.end_time: 205 | utils.thread_error( 206 | f"Time limit reached, killing thread. {threading.active_count()} threads running total" 207 | ) 208 | sys.exit() 209 | 210 | try: 211 | avail_res = self.network.find_availability(self.res_config, self.party_size) 212 | except Exception as e: 213 | utils.thread_error(f"Failed to fetch availability, retrying: {e}") 214 | 215 | return self.get_availability(drop_wait=drop_wait) 216 | 217 | if not avail_res.ok: 218 | utils.thread_warn(f"Failed to fetch availability, retrying [{avail_res.status_code}]") 219 | 220 | if avail_res.status_code == 500: 221 | time.sleep(0.25) 222 | 223 | return self.get_availability(drop_wait=drop_wait) 224 | 225 | avail_res_json = avail_res.json() 226 | 227 | # bug fix for empty venues 228 | if len(avail_res_json["results"]["venues"]) == 0: 229 | time.sleep(monitor_delay) 230 | 231 | return self.get_availability(drop_wait=drop_wait) 232 | 233 | slots = avail_res_json["results"]["venues"][0]["slots"] 234 | 235 | if (len(slots) == 0) or (slots == []) or (slots == "[]"): 236 | if os.getenv("MODE") and (os.getenv("MODE").lower() == "staging"): 237 | utils.thread_warn("No slots found, retrying") 238 | time.sleep(monitor_delay) 239 | return self.get_availability(drop_wait=drop_wait) 240 | 241 | config_id = self.choose_slot(slots) 242 | 243 | if config_id is None: 244 | utils.thread_error(slots) 245 | utils.thread_error(f"Slots found but none matching, killing thread") 246 | sys.exit() 247 | 248 | res_time = config_id.split("/")[8] 249 | 250 | self.res_config["res_time"] = res_time 251 | self.res_config["bot_start"] = time.time() 252 | 253 | # Disable this for now for speed 254 | # utils.thread_log(f"Found slot {config_id}") 255 | 256 | return config_id 257 | 258 | def choose_slot(self, slots): 259 | if len(slots) == 0: 260 | return None 261 | 262 | possible_slot = choice(slots) 263 | slot_config_id = possible_slot["config"]["token"] 264 | 265 | if any(exc in slot_config_id.lower() for exc in self.res_config["monitor"]["exclude"]): 266 | slots.remove(possible_slot) 267 | return self.choose_slot(slots) 268 | 269 | if self.res_config["monitor"]["timeFilter"]["enabled"]: 270 | res_time = int(slot_config_id.split(":")[1].split("/")[-1]) 271 | if res_time <= self.res_config["monitor"]["timeFilter"]["minTime"]: 272 | slots.remove(possible_slot) 273 | return self.choose_slot(slots) 274 | 275 | if res_time >= self.res_config["monitor"]["timeFilter"]["maxTime"]: 276 | slots.remove(possible_slot) 277 | return self.choose_slot(slots) 278 | 279 | return slot_config_id 280 | 281 | def init_book(self, config_id, retrys=0): 282 | if retrys > self.network.MAX_INIT_BOOK_RETRYS: 283 | utils.thread_error("Failed to init book after max attempts, killing thread") 284 | sys.exit() 285 | 286 | try: 287 | init_book_res = self.network.init_book(config_id, self.res_config["date"], self.party_size) 288 | except Exception as e: 289 | utils.thread_error(f"Failed to init book with exception: {e}") 290 | return self.init_book(config_id, retrys=retrys + 1) 291 | 292 | if not init_book_res.ok: 293 | utils.thread_warn(f"Failed to init book, retrying [{init_book_res.status_code}] [{init_book_res.text}]") 294 | 295 | if retrys == 10: 296 | time.sleep(0.3) 297 | return self.init_book(config_id, retrys=retrys + 1) 298 | 299 | book_token = init_book_res.json()["book_token"]["value"] 300 | 301 | return book_token 302 | 303 | def book(self, book_token, payment_method_id, retrys=0): 304 | if retrys > self.network.MAX_BOOK_RETRYS: 305 | return False, None 306 | 307 | try: 308 | book_res = self.network.book(book_token, payment_method_id) 309 | except Exception as e: 310 | utils.thread_error(f"Failed to book with exception: {e}") 311 | return self.book(book_token, payment_method_id, retrys=retrys + 1) 312 | 313 | if book_res.status_code == 404: 314 | utils.thread_error("Failed to book, slot already booked up") 315 | return False, None 316 | 317 | if not book_res.ok: 318 | utils.thread_error(f"Failed to book, retrying [{book_res.status_code}]") 319 | 320 | return self.book(book_token, payment_method_id, retrys=retrys + 1) 321 | 322 | return True, book_res.text 323 | -------------------------------------------------------------------------------- /resy_charles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "4 Charles", 4 | "enabled": true, 5 | "venueID": 834, 6 | "partyMin": 4, 7 | "partyMax": 6, 8 | "grabMax": 80, 9 | "baseURL": "https://resy.com/cities/ny/4-charles-prime-rib", 10 | "offset": 20, 11 | "accountType": "elite", 12 | "passiveMonitoring": true, 13 | "monitor": { 14 | "type": "scheduled", 15 | "start": "08:58", 16 | "drop": "09:00:00", 17 | "end": "09:01:00", 18 | "timer": 3, 19 | "delay": 0, 20 | "exclude": [ 21 | "bar" 22 | ], 23 | "timeFilter": { 24 | "enabled": false 25 | } 26 | } 27 | } 28 | ] -------------------------------------------------------------------------------- /resy_dev.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "COTE Miami", 4 | "enabled": true, 5 | "forceStart": true, 6 | "venueID": 72270, 7 | "partyMin": 4, 8 | "partyMax": 5, 9 | "grabMax": 2, 10 | "baseURL": "https://resy.com/cities/mia/cote-mia", 11 | "offset": 28, 12 | "accountType": "normal", 13 | "passiveMonitoring": true, 14 | "monitor": { 15 | "type": "scheduled", 16 | "start": "09:58", 17 | "drop": "06:30:00", 18 | "end": "06:30:30", 19 | "timer": 4, 20 | "delay": 3, 21 | "exclude": [ 22 | "bar", 23 | "outdoor", 24 | "lunch" 25 | ], 26 | "timeFilter": { 27 | "enabled": false, 28 | "minTime": 17, 29 | "maxTime": 22 30 | } 31 | } 32 | } 33 | ] -------------------------------------------------------------------------------- /resy_monitor.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "COTE Miami", 4 | "enabled": false, 5 | "forceStart": false, 6 | "venueID": 72270, 7 | "partyMin": 4, 8 | "partyMax": 4, 9 | "grabMax": 1, 10 | "baseURL": "https://resy.com/cities/mia/cote-mia", 11 | "offset": 28, 12 | "accountType": "normal", 13 | "passiveMonitoring": false, 14 | "monitor": { 15 | "type": "scheduled", 16 | "start": "09:58", 17 | "drop": "10:00:00", 18 | "end": "10:01:30", 19 | "timer": 4, 20 | "delay": 3, 21 | "exclude": [ 22 | "bar", 23 | "outdoor", 24 | "lunch" 25 | ], 26 | "timeFilter": { 27 | "enabled": true, 28 | "minTime": 17, 29 | "maxTime": 22 30 | } 31 | } 32 | }, 33 | { 34 | "name": "COTE Korean Steakhouse", 35 | "enabled": false, 36 | "venueID": 72271, 37 | "partyMin": 4, 38 | "partyMax": 6, 39 | "grabMax": 35, 40 | "baseURL": "https://resy.com/cities/ny/cote-nyc", 41 | "offset": 30, 42 | "accountType": "normal", 43 | "passiveMonitoring": true, 44 | "monitor": { 45 | "type": "scheduled", 46 | "start": "09:58", 47 | "drop": "10:00:00", 48 | "end": "10:01:00", 49 | "timer": 3, 50 | "delay": 0, 51 | "exclude": [ 52 | "bar", 53 | "outdoor", 54 | "lunch" 55 | ], 56 | "timeFilter": { 57 | "enabled": true, 58 | "minTime": 17, 59 | "maxTime": 22 60 | } 61 | } 62 | }, 63 | { 64 | "name": "4 Charles", 65 | "enabled": false, 66 | "venueID": 834, 67 | "partyMin": 4, 68 | "partyMax": 6, 69 | "grabMax": 25, 70 | "baseURL": "https://resy.com/cities/ny/4-charles-prime-rib", 71 | "offset": 20, 72 | "accountType": "elite", 73 | "passiveMonitoring": true, 74 | "monitor": { 75 | "type": "scheduled", 76 | "start": "08:58", 77 | "drop": "09:00:00", 78 | "end": "09:01:45", 79 | "timer": 3, 80 | "delay": 0, 81 | "exclude": [ 82 | "bar" 83 | ], 84 | "timeFilter": { 85 | "enabled": false 86 | } 87 | } 88 | }, 89 | { 90 | "name": "Don Angie", 91 | "enabled": false, 92 | "venueID": 1505, 93 | "partyMin": 2, 94 | "partyMax": 6, 95 | "grabMax": 25, 96 | "baseURL": "https://resy.com/cities/ny/don-angie", 97 | "offset": 6, 98 | "accountType": "normal", 99 | "passiveMonitoring": false, 100 | "monitor": { 101 | "type": "scheduled", 102 | "start": "08:58", 103 | "drop": "09:00:00", 104 | "end": "09:02:00", 105 | "timer": 5, 106 | "delay": 0, 107 | "exclude": [ 108 | "bar" 109 | ], 110 | "timeFilter": { 111 | "enabled": false 112 | } 113 | } 114 | }, 115 | { 116 | "name": "L Artusi", 117 | "enabled": false, 118 | "forceStart": false, 119 | "venueID": 25973, 120 | "partyMin": 2, 121 | "partyMax": 4, 122 | "grabMax": 16, 123 | "baseURL": "https://resy.com/cities/ny/lartusi-ny", 124 | "offset": 14, 125 | "accountType": "normal", 126 | "passiveMonitoring": true, 127 | "monitor": { 128 | "type": "scheduled", 129 | "start": "08:59", 130 | "drop": "09:00:00", 131 | "end": "09:01:00", 132 | "timer": 2, 133 | "delay": 0, 134 | "exclude": [ 135 | "bar", 136 | "outdoor", 137 | "lunch", 138 | "patio", 139 | "counter" 140 | ], 141 | "timeFilter": { 142 | "enabled": true, 143 | "minTime": 16, 144 | "maxTime": 22 145 | } 146 | } 147 | }, 148 | { 149 | "name": "TATIANA By Kwame Onwuachi", 150 | "enabled": false, 151 | "venueID": 65452, 152 | "partyMin": 3, 153 | "partyMax": 6, 154 | "grabMax": 30, 155 | "baseURL": "https://resy.com/cities/ny/tatiana", 156 | "offset": 27, 157 | "accountType": "normal", 158 | "passiveMonitoring": true, 159 | "monitor": { 160 | "type": "scheduled", 161 | "start": "11:58", 162 | "drop": "12:00:00", 163 | "end": "12:01:00", 164 | "timer": 3, 165 | "delay": 0, 166 | "exclude": [ 167 | "bar", 168 | "outdoor", 169 | "lunch" 170 | ], 171 | "timeFilter": { 172 | "enabled": false 173 | } 174 | } 175 | }, 176 | { 177 | "name": "Carbone Miami", 178 | "enabled": false, 179 | "venueID": 43225, 180 | "partyMin": 2, 181 | "partyMax": 4, 182 | "grabMax": 3, 183 | "baseURL": "https://resy.com/cities/mia/carbone-miami", 184 | "offset": 30, 185 | "accountType": "normal", 186 | "passiveMonitoring": true, 187 | "monitor": { 188 | "type": "scheduled", 189 | "start": "09:59", 190 | "drop": "10:00:00", 191 | "end": "10:00:45", 192 | "timer": 2, 193 | "delay": 1, 194 | "exclude": [ 195 | "bar", 196 | "outdoor", 197 | "lunch", 198 | "patio" 199 | ], 200 | "timeFilter": { 201 | "enabled": true, 202 | "minTime": 17, 203 | "maxTime": 22 204 | } 205 | } 206 | }, 207 | { 208 | "name": "Funke", 209 | "enabled": false, 210 | "venueID": 60000, 211 | "partyMin": 2, 212 | "partyMax": 4, 213 | "grabMax": 3, 214 | "baseURL": "https://resy.com/cities/la/funke-la", 215 | "offset": 7, 216 | "accountType": "normal", 217 | "passiveMonitoring": false, 218 | "monitor": { 219 | "type": "scheduled", 220 | "start": "11:59", 221 | "drop": "12:00:00", 222 | "end": "12:00:30", 223 | "timer": 3, 224 | "delay": 3, 225 | "exclude": [ 226 | "bar", 227 | "outdoor", 228 | "lunch", 229 | "patio" 230 | ], 231 | "timeFilter": { 232 | "enabled": true, 233 | "minTime": 17, 234 | "maxTime": 21 235 | } 236 | } 237 | }, 238 | { 239 | "name": "I Sodi", 240 | "enabled": false, 241 | "venueID": 443, 242 | "partyMin": 2, 243 | "partyMax": 4, 244 | "grabMax": 40, 245 | "baseURL": "https://resy.com/cities/ny/i-sodi", 246 | "offset": 14, 247 | "accountType": "normal", 248 | "passiveMonitoring": true, 249 | "monitor": { 250 | "type": "scheduled", 251 | "start": "23:58", 252 | "drop": "00:00:00", 253 | "end": "00:01:00", 254 | "timer": 3, 255 | "delay": 0, 256 | "exclude": [ 257 | "bar", 258 | "outdoor", 259 | "lunch", 260 | "patio" 261 | ], 262 | "timeFilter": { 263 | "enabled": false 264 | } 265 | } 266 | }, 267 | { 268 | "name": "Le Bernardin", 269 | "enabled": false, 270 | "venueID": 1387, 271 | "partyMin": 2, 272 | "partyMax": 4, 273 | "grabMax": 10, 274 | "baseURL": "https://resy.com/cities/ny/le-bernardin", 275 | "offset": 14, 276 | "accountType": "normal", 277 | "passiveMonitoring": true, 278 | "monitor": { 279 | "type": "scheduled", 280 | "start": "06:58", 281 | "drop": "07:00:00", 282 | "end": "07:01:00", 283 | "timer": 3, 284 | "delay": 0, 285 | "exclude": [ 286 | "bar", 287 | "outdoor", 288 | "lunch", 289 | "patio" 290 | ], 291 | "timeFilter": { 292 | "enabled": true, 293 | "minTime": 17, 294 | "maxTime": 22 295 | } 296 | } 297 | }, 298 | { 299 | "name": "Lilia", 300 | "enabled": false, 301 | "venueID": 418, 302 | "partyMin": 2, 303 | "partyMax": 4, 304 | "grabMax": 12, 305 | "baseURL": "https://resy.com/cities/ny/lilia", 306 | "offset": 28, 307 | "accountType": "normal", 308 | "passiveMonitoring": true, 309 | "monitor": { 310 | "type": "scheduled", 311 | "start": "09:59", 312 | "drop": "10:00:00", 313 | "end": "10:01:00", 314 | "timer": 2, 315 | "delay": 0.5, 316 | "exclude": [ 317 | "bar", 318 | "outdoor", 319 | "lunch", 320 | "patio" 321 | ], 322 | "timeFilter": { 323 | "enabled": true, 324 | "minTime": 17, 325 | "maxTime": 21 326 | } 327 | } 328 | }, 329 | { 330 | "name": "COQODAQ", 331 | "enabled": false, 332 | "venueID": 76033, 333 | "partyMin": 4, 334 | "partyMax": 6, 335 | "grabMax": 25, 336 | "baseURL": "https://resy.com/cities/ny/coqodaq", 337 | "offset": 14, 338 | "accountType": "normal", 339 | "passiveMonitoring": true, 340 | "monitor": { 341 | "type": "scheduled", 342 | "start": "09:58", 343 | "drop": "10:00:00", 344 | "end": "10:01:15", 345 | "timer": 3, 346 | "delay": 0, 347 | "exclude": [ 348 | "bar", 349 | "outdoor", 350 | "lunch" 351 | ], 352 | "timeFilter": { 353 | "enabled": true 354 | } 355 | } 356 | }, 357 | { 358 | "name": "Laser Wolf Brooklyn", 359 | "enabled": false, 360 | "venueID": 58848, 361 | "partyMin": 2, 362 | "partyMax": 6, 363 | "grabMax": 3, 364 | "baseURL": "https://resy.com/cities/ny/laser-wolf-brooklyn", 365 | "offset": 21, 366 | "accountType": "normal", 367 | "passiveMonitoring": true, 368 | "monitor": { 369 | "type": "scheduled", 370 | "start": "09:59", 371 | "drop": "10:00:00", 372 | "end": "10:00:30", 373 | "timer": 2, 374 | "delay": 1, 375 | "exclude": [ 376 | "bar", 377 | "outdoor", 378 | "lunch", 379 | "counter" 380 | ], 381 | "timeFilter": { 382 | "enabled": true, 383 | "minTime": 18, 384 | "maxTime": 22 385 | } 386 | } 387 | }, 388 | { 389 | "name": "Raouls", 390 | "enabled": false, 391 | "venueID": 7241, 392 | "partyMin": 2, 393 | "partyMax": 5, 394 | "grabMax": 3, 395 | "baseURL": "https://resy.com/cities/ny/raoulsrestaurant", 396 | "offset": 30, 397 | "accountType": "normal", 398 | "passiveMonitoring": true, 399 | "monitor": { 400 | "type": "scheduled", 401 | "start": "07:59", 402 | "drop": "08:00:00", 403 | "end": "08:00:30", 404 | "timer": 2, 405 | "delay": 1, 406 | "exclude": [ 407 | "bar", 408 | "outdoor", 409 | "lunch", 410 | "counter" 411 | ], 412 | "timeFilter": { 413 | "enabled": true, 414 | "minTime": 18, 415 | "maxTime": 22 416 | } 417 | } 418 | }, 419 | { 420 | "name": "Via Carota", 421 | "enabled": false, 422 | "venueID": 2567, 423 | "partyMin": 2, 424 | "partyMax": 4, 425 | "grabMax": 15, 426 | "baseURL": "https://resy.com/cities/ny/via-carota", 427 | "offset": 30, 428 | "accountType": "normal", 429 | "passiveMonitoring": true, 430 | "monitor": { 431 | "type": "scheduled", 432 | "start": "09:58", 433 | "drop": "10:00:00", 434 | "end": "10:01:30", 435 | "timer": 3, 436 | "delay": 0, 437 | "exclude": [ 438 | "bar", 439 | "outdoor", 440 | "lunch", 441 | "counter" 442 | ], 443 | "timeFilter": { 444 | "enabled": false 445 | } 446 | } 447 | } 448 | ] -------------------------------------------------------------------------------- /resy_staging.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "COTE Miami", 4 | "enabled": false, 5 | "forceStart": false, 6 | "venueID": 72270, 7 | "partyMin": 4, 8 | "partyMax": 4, 9 | "grabMax": 1, 10 | "baseURL": "https://resy.com/cities/mia/cote-mia", 11 | "offset": 28, 12 | "accountType": "normal", 13 | "passiveMonitoring": false, 14 | "monitor": { 15 | "type": "scheduled", 16 | "start": "09:58", 17 | "drop": "10:00:00", 18 | "end": "10:01:30", 19 | "timer": 4, 20 | "delay": 3, 21 | "exclude": [ 22 | "bar", 23 | "outdoor", 24 | "lunch" 25 | ], 26 | "timeFilter": { 27 | "enabled": true, 28 | "minTime": 17, 29 | "maxTime": 22 30 | } 31 | } 32 | }, 33 | { 34 | "name": "COTE Korean Steakhouse", 35 | "enabled": true, 36 | "venueID": 72271, 37 | "partyMin": 4, 38 | "partyMax": 6, 39 | "grabMax": 20, 40 | "baseURL": "https://resy.com/cities/ny/cote-nyc", 41 | "offset": 30, 42 | "accountType": "normal", 43 | "passiveMonitoring": true, 44 | "monitor": { 45 | "type": "scheduled", 46 | "start": "09:58", 47 | "drop": "10:00:00", 48 | "end": "10:01:30", 49 | "timer": 3, 50 | "delay": 0, 51 | "exclude": [ 52 | "bar", 53 | "outdoor", 54 | "lunch" 55 | ], 56 | "timeFilter": { 57 | "enabled": true, 58 | "minTime": 17, 59 | "maxTime": 22 60 | } 61 | } 62 | }, 63 | { 64 | "name": "4 Charles", 65 | "enabled": true, 66 | "venueID": 834, 67 | "partyMin": 4, 68 | "partyMax": 6, 69 | "grabMax": 25, 70 | "baseURL": "https://resy.com/cities/ny/4-charles-prime-rib", 71 | "offset": 21, 72 | "accountType": "elite", 73 | "passiveMonitoring": true, 74 | "monitor": { 75 | "type": "scheduled", 76 | "start": "08:58", 77 | "drop": "09:00:00", 78 | "end": "09:02:00", 79 | "timer": 3, 80 | "delay": 0, 81 | "exclude": [ 82 | "bar" 83 | ], 84 | "timeFilter": { 85 | "enabled": false 86 | } 87 | } 88 | }, 89 | { 90 | "name": "Don Angie", 91 | "enabled": false, 92 | "venueID": 1505, 93 | "partyMin": 2, 94 | "partyMax": 6, 95 | "grabMax": 25, 96 | "baseURL": "https://resy.com/cities/ny/don-angie", 97 | "offset": 6, 98 | "accountType": "normal", 99 | "passiveMonitoring": false, 100 | "monitor": { 101 | "type": "scheduled", 102 | "start": "08:58", 103 | "drop": "09:00:00", 104 | "end": "09:02:00", 105 | "timer": 5, 106 | "delay": 0, 107 | "exclude": [ 108 | "bar" 109 | ], 110 | "timeFilter": { 111 | "enabled": false 112 | } 113 | } 114 | }, 115 | { 116 | "name": "L Artusi", 117 | "enabled": true, 118 | "forceStart": false, 119 | "venueID": 25973, 120 | "partyMin": 2, 121 | "partyMax": 4, 122 | "grabMax": 15, 123 | "baseURL": "https://resy.com/cities/ny/lartusi-ny", 124 | "offset": 14, 125 | "accountType": "normal", 126 | "passiveMonitoring": true, 127 | "monitor": { 128 | "type": "scheduled", 129 | "start": "08:59", 130 | "drop": "09:00:00", 131 | "end": "09:01:00", 132 | "timer": 2, 133 | "delay": 0, 134 | "exclude": [ 135 | "bar", 136 | "outdoor", 137 | "lunch", 138 | "patio", 139 | "counter" 140 | ], 141 | "timeFilter": { 142 | "enabled": true, 143 | "minTime": 16, 144 | "maxTime": 22 145 | } 146 | } 147 | }, 148 | { 149 | "name": "TATIANA By Kwame Onwuachi", 150 | "enabled": true, 151 | "venueID": 65452, 152 | "partyMin": 3, 153 | "partyMax": 6, 154 | "grabMax": 20, 155 | "baseURL": "https://resy.com/cities/ny/tatiana", 156 | "offset": 27, 157 | "accountType": "normal", 158 | "passiveMonitoring": true, 159 | "monitor": { 160 | "type": "scheduled", 161 | "start": "11:58", 162 | "drop": "12:00:00", 163 | "end": "12:01:30", 164 | "timer": 3, 165 | "delay": 0, 166 | "exclude": [ 167 | "bar", 168 | "outdoor", 169 | "lunch" 170 | ], 171 | "timeFilter": { 172 | "enabled": false 173 | } 174 | } 175 | }, 176 | { 177 | "name": "Carbone Miami", 178 | "enabled": false, 179 | "venueID": 43225, 180 | "partyMin": 2, 181 | "partyMax": 4, 182 | "grabMax": 10, 183 | "baseURL": "https://resy.com/cities/mia/carbone-miami", 184 | "offset": 30, 185 | "accountType": "normal", 186 | "passiveMonitoring": false, 187 | "monitor": { 188 | "type": "scheduled", 189 | "start": "09:59", 190 | "drop": "10:00:00", 191 | "end": "10:01:30", 192 | "timer": 2, 193 | "delay": 0.5, 194 | "exclude": [ 195 | "bar", 196 | "outdoor", 197 | "lunch", 198 | "patio" 199 | ], 200 | "timeFilter": { 201 | "enabled": true, 202 | "minTime": 17, 203 | "maxTime": 22 204 | } 205 | } 206 | }, 207 | { 208 | "name": "Funke", 209 | "enabled": false, 210 | "venueID": 60000, 211 | "partyMin": 2, 212 | "partyMax": 4, 213 | "grabMax": 3, 214 | "baseURL": "https://resy.com/cities/la/funke-la", 215 | "offset": 7, 216 | "accountType": "normal", 217 | "passiveMonitoring": false, 218 | "monitor": { 219 | "type": "scheduled", 220 | "start": "11:59", 221 | "drop": "12:00:00", 222 | "end": "12:00:30", 223 | "timer": 3, 224 | "delay": 3, 225 | "exclude": [ 226 | "bar", 227 | "outdoor", 228 | "lunch", 229 | "patio" 230 | ], 231 | "timeFilter": { 232 | "enabled": true, 233 | "minTime": 17, 234 | "maxTime": 21 235 | } 236 | } 237 | }, 238 | { 239 | "name": "I Sodi", 240 | "enabled": true, 241 | "venueID": 443, 242 | "partyMin": 2, 243 | "partyMax": 4, 244 | "grabMax": 20, 245 | "baseURL": "https://resy.com/cities/ny/i-sodi", 246 | "offset": 14, 247 | "accountType": "normal", 248 | "passiveMonitoring": true, 249 | "monitor": { 250 | "type": "scheduled", 251 | "start": "23:58", 252 | "drop": "00:00:00", 253 | "end": "00:01:30", 254 | "timer": 3, 255 | "delay": 0, 256 | "exclude": [ 257 | "bar", 258 | "outdoor", 259 | "lunch", 260 | "patio" 261 | ], 262 | "timeFilter": { 263 | "enabled": false 264 | } 265 | } 266 | }, 267 | { 268 | "name": "Le Bernardin", 269 | "enabled": true, 270 | "venueID": 1387, 271 | "partyMin": 2, 272 | "partyMax": 4, 273 | "grabMax": 10, 274 | "baseURL": "https://resy.com/cities/ny/le-bernardin", 275 | "offset": 14, 276 | "accountType": "normal", 277 | "passiveMonitoring": true, 278 | "monitor": { 279 | "type": "scheduled", 280 | "start": "06:58", 281 | "drop": "07:00:00", 282 | "end": "07:01:30", 283 | "timer": 3, 284 | "delay": 0, 285 | "exclude": [ 286 | "bar", 287 | "outdoor", 288 | "lunch", 289 | "patio" 290 | ], 291 | "timeFilter": { 292 | "enabled": true, 293 | "minTime": 17, 294 | "maxTime": 22 295 | } 296 | } 297 | }, 298 | { 299 | "name": "Lilia", 300 | "enabled": false, 301 | "venueID": 418, 302 | "partyMin": 2, 303 | "partyMax": 4, 304 | "grabMax": 10, 305 | "baseURL": "https://resy.com/cities/ny/lilia", 306 | "offset": 28, 307 | "accountType": "normal", 308 | "passiveMonitoring": false, 309 | "monitor": { 310 | "type": "scheduled", 311 | "start": "09:59", 312 | "drop": "10:00:00", 313 | "end": "10:01:00", 314 | "timer": 2, 315 | "delay": 0.5, 316 | "exclude": [ 317 | "bar", 318 | "outdoor", 319 | "lunch", 320 | "patio" 321 | ], 322 | "timeFilter": { 323 | "enabled": true, 324 | "minTime": 17, 325 | "maxTime": 21 326 | } 327 | } 328 | }, 329 | { 330 | "name": "COQODAQ", 331 | "enabled": true, 332 | "venueID": 76033, 333 | "partyMin": 4, 334 | "partyMax": 6, 335 | "grabMax": 20, 336 | "baseURL": "https://resy.com/cities/ny/coqodaq", 337 | "offset": 14, 338 | "accountType": "normal", 339 | "passiveMonitoring": true, 340 | "monitor": { 341 | "type": "scheduled", 342 | "start": "09:58", 343 | "drop": "10:00:00", 344 | "end": "10:01:30", 345 | "timer": 3, 346 | "delay": 0, 347 | "exclude": [ 348 | "bar", 349 | "outdoor", 350 | "lunch" 351 | ], 352 | "timeFilter": { 353 | "enabled": true 354 | } 355 | } 356 | }, 357 | { 358 | "name": "Laser Wolf Brooklyn", 359 | "enabled": false, 360 | "venueID": 58848, 361 | "partyMin": 2, 362 | "partyMax": 6, 363 | "grabMax": 3, 364 | "baseURL": "https://resy.com/cities/ny/laser-wolf-brooklyn", 365 | "offset": 21, 366 | "accountType": "normal", 367 | "passiveMonitoring": false, 368 | "monitor": { 369 | "type": "scheduled", 370 | "start": "09:59", 371 | "drop": "10:00:00", 372 | "end": "10:01:00", 373 | "timer": 2, 374 | "delay": 1, 375 | "exclude": [ 376 | "bar", 377 | "outdoor", 378 | "lunch", 379 | "counter" 380 | ], 381 | "timeFilter": { 382 | "enabled": true, 383 | "minTime": 18, 384 | "maxTime": 22 385 | } 386 | } 387 | }, 388 | { 389 | "name": "Raouls", 390 | "enabled": false, 391 | "venueID": 7241, 392 | "partyMin": 2, 393 | "partyMax": 5, 394 | "grabMax": 3, 395 | "baseURL": "https://resy.com/cities/ny/raoulsrestaurant", 396 | "offset": 30, 397 | "accountType": "normal", 398 | "passiveMonitoring": false, 399 | "monitor": { 400 | "type": "scheduled", 401 | "start": "07:59", 402 | "drop": "08:00:00", 403 | "end": "08:00:30", 404 | "timer": 2, 405 | "delay": 1, 406 | "exclude": [ 407 | "bar", 408 | "outdoor", 409 | "lunch", 410 | "counter" 411 | ], 412 | "timeFilter": { 413 | "enabled": true, 414 | "minTime": 18, 415 | "maxTime": 22 416 | } 417 | } 418 | }, 419 | { 420 | "name": "Via Carota", 421 | "enabled": false, 422 | "venueID": 2567, 423 | "partyMin": 2, 424 | "partyMax": 4, 425 | "grabMax": 15, 426 | "baseURL": "https://resy.com/cities/ny/via-carota", 427 | "offset": 30, 428 | "accountType": "normal", 429 | "passiveMonitoring": false, 430 | "monitor": { 431 | "type": "scheduled", 432 | "start": "09:58", 433 | "drop": "10:00:00", 434 | "end": "10:01:30", 435 | "timer": 3, 436 | "delay": 0, 437 | "exclude": [ 438 | "bar", 439 | "outdoor", 440 | "lunch", 441 | "counter" 442 | ], 443 | "timeFilter": { 444 | "enabled": false 445 | } 446 | } 447 | } 448 | ] -------------------------------------------------------------------------------- /uas.txt: -------------------------------------------------------------------------------- 1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 2 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 3 | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0 4 | Mozilla/5.0 (Macintosh; Intel Mac OS X 14.4; rv:124.0) Gecko/20100101 Firefox/124.0 5 | Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Safari/605.1.15 --------------------------------------------------------------------------------