_scripts.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import logging
  2. import os
  3. import subprocess
  4. import sys
  5. import threading
  6. from pathlib import Path
  7. from room_cal._utils import get_config
  8. _log = logging.getLogger(__name__)
  9. def _data_home() -> Path:
  10. override = os.environ.get('ROOM_CAL_DATA_HOME')
  11. return Path(override) if override else Path.home() / '.local' / 'room_cal'
  12. def _ts_dir() -> Path:
  13. return Path(__file__).parent / 'typescript'
  14. def _npm_env() -> dict:
  15. dh = _data_home()
  16. env = os.environ.copy()
  17. env.setdefault('ROOM_CAL_DATA_HOME', str(dh))
  18. env.setdefault('PLAYWRIGHT_BROWSERS_PATH', str(dh / '.playwright-browsers'))
  19. api_url = get_config().api_url
  20. if api_url:
  21. env.setdefault('ROOM_CAL_API_URL', api_url)
  22. return env
  23. def _ensure_installed(ts_dir: Path) -> None:
  24. if not (ts_dir / 'node_modules').exists():
  25. print('=== npm install (service-api) ===')
  26. subprocess.run(['npm', 'install'], cwd=ts_dir, check=True)
  27. print()
  28. # One SMS code is valid at a time and Booking.com rate-limits sends regardless of how often we ask,
  29. # so there is no benefit to concurrent booking browser operations — serialize them all.
  30. _booking_lock = threading.Lock()
  31. def refresh_booking_cookies() -> None:
  32. ts_dir = _ts_dir()
  33. _ensure_installed(ts_dir)
  34. _log.info('booking: starting credential refresh')
  35. with _booking_lock:
  36. try:
  37. subprocess.run(['npm', 'run', 'refresh-booking'], cwd=ts_dir, env=_npm_env(), check=True)
  38. except subprocess.CalledProcessError as e:
  39. _log.error('booking: credential refresh failed (exit %d)', e.returncode)
  40. from .types import ServiceAuthError
  41. raise ServiceAuthError(f'Booking.com credential refresh failed (exit {e.returncode})') from None
  42. _log.info('booking: credential refresh complete')
  43. def refresh_airbnb_cookies() -> None:
  44. ts_dir = _ts_dir()
  45. _ensure_installed(ts_dir)
  46. _log.info('airbnb: starting credential refresh')
  47. env = _npm_env()
  48. env['HEADLESS'] = '1'
  49. try:
  50. subprocess.run(['npm', 'run', 'refresh-airbnb'], cwd=ts_dir, env=env, check=True)
  51. except subprocess.CalledProcessError as e:
  52. _log.error('airbnb: credential refresh failed (exit %d)', e.returncode)
  53. from .types import ServiceAuthError
  54. raise ServiceAuthError(f'Airbnb session expired — re-run without HEADLESS to re-authenticate (exit {e.returncode})') from None
  55. _log.info('airbnb: credential refresh complete')
  56. def refresh_homestay_cookies() -> None:
  57. ts_dir = _ts_dir()
  58. _ensure_installed(ts_dir)
  59. _log.info('homestay: starting credential refresh')
  60. env = _npm_env()
  61. try:
  62. subprocess.run(['npm', 'run', 'refresh-homestay'], cwd=ts_dir, env=env, check=True)
  63. except subprocess.CalledProcessError as e:
  64. _log.error('homestay: credential refresh failed (exit %d)', e.returncode)
  65. from .types import ServiceAuthError
  66. raise ServiceAuthError(f'Homestay session expired — re-run without HEADLESS to re-authenticate (exit {e.returncode})') from None
  67. _log.info('homestay: credential refresh complete')
  68. def trigger_booking_chat_auth() -> None:
  69. """Run booking-2fa; raises ServiceAuthError on failure. Safe to call from within the server."""
  70. ts_dir = _ts_dir()
  71. _ensure_installed(ts_dir)
  72. _log.info('booking: starting standalone messaging 2FA prime')
  73. with _booking_lock:
  74. result = subprocess.run(['npm', 'run', 'booking-2fa'], cwd=ts_dir, env=_npm_env())
  75. if result.returncode != 0:
  76. _log.error('booking: booking-2fa failed (exit %d)', result.returncode)
  77. from .types import ServiceAuthError
  78. raise ServiceAuthError(f'booking-2fa failed (exit {result.returncode}) — SMS gateway may not be configured')
  79. _log.info('booking: messaging 2FA prime complete')
  80. def _trigger_booking_chat_auth_cli() -> None:
  81. """CLI entry point for `uv run booking-2fa`."""
  82. ts_dir = _ts_dir()
  83. _ensure_installed(ts_dir)
  84. result = subprocess.run(['npm', 'run', 'booking-2fa', '--'] + sys.argv[1:], cwd=ts_dir, env=_npm_env())
  85. sys.exit(result.returncode)
  86. def refresh_service(key: str) -> None:
  87. """Trigger the automatic credential refresh for the named service. Raises ServiceAuthError on failure."""
  88. handlers = {
  89. 'booking': refresh_booking_cookies,
  90. 'airbnb': refresh_airbnb_cookies,
  91. 'homestay': refresh_homestay_cookies,
  92. }
  93. fn = handlers.get(key)
  94. if fn is None:
  95. from .types import ServiceAuthError
  96. raise ServiceAuthError(f'No automatic refresh handler for service: {key}')
  97. fn()
  98. def refresh_cookies() -> None:
  99. ts_dir = _ts_dir()
  100. _ensure_installed(ts_dir)
  101. result = subprocess.run(['npm', 'run', 'refresh-all', '--'] + sys.argv[1:], cwd=ts_dir, env=_npm_env())
  102. sys.exit(result.returncode)