|
@@ -191,6 +191,8 @@ class _AirbnbClient:
|
|
|
return await client.get(url, params=params, headers=self._headers(referer))
|
|
return await client.get(url, params=params, headers=self._headers(referer))
|
|
|
try:
|
|
try:
|
|
|
r = await _attempt()
|
|
r = await _attempt()
|
|
|
|
|
+ except httpx.TimeoutException as e:
|
|
|
|
|
+ raise httpx.ReadTimeout(f'Airbnb request timed out ({op})') from e
|
|
|
except (httpx.TransportError, OSError, ExceptionGroup) as e:
|
|
except (httpx.TransportError, OSError, ExceptionGroup) as e:
|
|
|
raise AirbnbAuthError(f'Airbnb connection failed — session may be expired, run: {_AUTH_HINT}') from e
|
|
raise AirbnbAuthError(f'Airbnb connection failed — session may be expired, run: {_AUTH_HINT}') from e
|
|
|
if r.status_code in (400, 401, 403):
|
|
if r.status_code in (400, 401, 403):
|
|
@@ -323,49 +325,56 @@ class _AirbnbClient:
|
|
|
dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
|
|
dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
|
|
|
return dt.astimezone(ZoneInfo(self._tz)).date().isoformat()
|
|
return dt.astimezone(ZoneInfo(self._tz)).date().isoformat()
|
|
|
|
|
|
|
|
|
|
+ _sem = asyncio.Semaphore(3)
|
|
|
|
|
+
|
|
|
async def fetch_details(c: str) -> tuple[str, str | None, str | None, str | None, str | None, str | None]:
|
|
async def fetch_details(c: str) -> tuple[str, str | None, str | None, str | None, str | None, str | None]:
|
|
|
- resp = await self._gql(
|
|
|
|
|
- 'StayHostingDetailsQuery', _HASHES['StayHostingDetailsQuery'],
|
|
|
|
|
- {'viewerTimeZoneOffset': 0, 'requestSource': 'RESERVATION_TAB', 'confirmationCode': c},
|
|
|
|
|
- f'https://www.airbnb.com/hosting/reservations/details/{c}',
|
|
|
|
|
- )
|
|
|
|
|
- sections = (
|
|
|
|
|
- resp.get('data', {}).get('presentation', {})
|
|
|
|
|
- .get('hostingDetails', {}).get('stayHostingDetails', {})
|
|
|
|
|
- .get('rootPlacement', [])
|
|
|
|
|
- )
|
|
|
|
|
- whoscoming = next((s for s in sections if s.get('sectionId') == 'WHOSCOMING_SECTION'), None)
|
|
|
|
|
- wc_groups = (whoscoming or {}).get('sectionData', {}).get('reservationGroups', [])
|
|
|
|
|
- wc_guests = wc_groups[0].get('guests', []) if wc_groups else []
|
|
|
|
|
- facepile = next((s for s in sections if s.get('sectionId') == 'FACEPILE_SECTION'), None)
|
|
|
|
|
- fp_guests = (facepile or {}).get('sectionData', {}).get('guests', [])
|
|
|
|
|
- name = (wc_guests[0].get('name') if wc_guests else None) or (fp_guests[0].get('name') if fp_guests else None)
|
|
|
|
|
-
|
|
|
|
|
- checkinout = next((s for s in sections if s.get('sectionId') == 'CHECK_IN_CHECK_OUT_SECTION'), None)
|
|
|
|
|
- ci_out_data = (checkinout or {}).get('sectionData') or {}
|
|
|
|
|
- action = ci_out_data.get('checkInAction', {})
|
|
|
|
|
- ci_ts = action.get('checkInTime')
|
|
|
|
|
- co_ts = action.get('checkOutTime')
|
|
|
|
|
- ci = _utc_to_local_date(ci_ts) if ci_ts else None
|
|
|
|
|
- co = _utc_to_local_date(co_ts) if co_ts else None
|
|
|
|
|
- door_code = (ci_out_data.get('airkeyAction') or {}).get('guestPhoneNumberLastFourDigits')
|
|
|
|
|
-
|
|
|
|
|
- # thread_id is in footerPlacement FLOATINGFOOTER_SECTION, not rootPlacement
|
|
|
|
|
- details = resp.get('data', {}).get('presentation', {}).get('hostingDetails', {}).get('stayHostingDetails', {})
|
|
|
|
|
- footer_sections = details.get('footerPlacement', [])
|
|
|
|
|
- footer = next((s for s in footer_sections if s.get('sectionId') == 'FLOATINGFOOTER_SECTION'), None)
|
|
|
|
|
- buttons = ((footer or {}).get('sectionData') or {}).get('buttons', [])
|
|
|
|
|
- thread_id = None
|
|
|
|
|
- for btn in buttons:
|
|
|
|
|
- action = btn.get('buttonAction') or {}
|
|
|
|
|
- if action.get('__typename') == 'NavigateToMessageGuest':
|
|
|
|
|
- url = action.get('url', '')
|
|
|
|
|
- m = __import__('re').search(r'/thread/(\d+)', url)
|
|
|
|
|
- if m:
|
|
|
|
|
- thread_id = m.group(1)
|
|
|
|
|
- break
|
|
|
|
|
-
|
|
|
|
|
- return c, name, ci, co, door_code, thread_id
|
|
|
|
|
|
|
+ async with _sem:
|
|
|
|
|
+ try:
|
|
|
|
|
+ resp = await self._gql(
|
|
|
|
|
+ 'StayHostingDetailsQuery', _HASHES['StayHostingDetailsQuery'],
|
|
|
|
|
+ {'viewerTimeZoneOffset': 0, 'requestSource': 'RESERVATION_TAB', 'confirmationCode': c},
|
|
|
|
|
+ f'https://www.airbnb.com/hosting/reservations/details/{c}',
|
|
|
|
|
+ )
|
|
|
|
|
+ except httpx.TimeoutException:
|
|
|
|
|
+ _log.warning('airbnb: StayHostingDetailsQuery timed out for %s — skipping details', c)
|
|
|
|
|
+ return c, None, None, None, None, None
|
|
|
|
|
+ sections = (
|
|
|
|
|
+ resp.get('data', {}).get('presentation', {})
|
|
|
|
|
+ .get('hostingDetails', {}).get('stayHostingDetails', {})
|
|
|
|
|
+ .get('rootPlacement', [])
|
|
|
|
|
+ )
|
|
|
|
|
+ whoscoming = next((s for s in sections if s.get('sectionId') == 'WHOSCOMING_SECTION'), None)
|
|
|
|
|
+ wc_groups = (whoscoming or {}).get('sectionData', {}).get('reservationGroups', [])
|
|
|
|
|
+ wc_guests = wc_groups[0].get('guests', []) if wc_groups else []
|
|
|
|
|
+ facepile = next((s for s in sections if s.get('sectionId') == 'FACEPILE_SECTION'), None)
|
|
|
|
|
+ fp_guests = (facepile or {}).get('sectionData', {}).get('guests', [])
|
|
|
|
|
+ name = (wc_guests[0].get('name') if wc_guests else None) or (fp_guests[0].get('name') if fp_guests else None)
|
|
|
|
|
+
|
|
|
|
|
+ checkinout = next((s for s in sections if s.get('sectionId') == 'CHECK_IN_CHECK_OUT_SECTION'), None)
|
|
|
|
|
+ ci_out_data = (checkinout or {}).get('sectionData') or {}
|
|
|
|
|
+ action = ci_out_data.get('checkInAction', {})
|
|
|
|
|
+ ci_ts = action.get('checkInTime')
|
|
|
|
|
+ co_ts = action.get('checkOutTime')
|
|
|
|
|
+ ci = _utc_to_local_date(ci_ts) if ci_ts else None
|
|
|
|
|
+ co = _utc_to_local_date(co_ts) if co_ts else None
|
|
|
|
|
+ door_code = (ci_out_data.get('airkeyAction') or {}).get('guestPhoneNumberLastFourDigits')
|
|
|
|
|
+
|
|
|
|
|
+ # thread_id is in footerPlacement FLOATINGFOOTER_SECTION, not rootPlacement
|
|
|
|
|
+ det = resp.get('data', {}).get('presentation', {}).get('hostingDetails', {}).get('stayHostingDetails', {})
|
|
|
|
|
+ footer_sections = det.get('footerPlacement', [])
|
|
|
|
|
+ footer = next((s for s in footer_sections if s.get('sectionId') == 'FLOATINGFOOTER_SECTION'), None)
|
|
|
|
|
+ buttons = ((footer or {}).get('sectionData') or {}).get('buttons', [])
|
|
|
|
|
+ thread_id = None
|
|
|
|
|
+ for btn in buttons:
|
|
|
|
|
+ action = btn.get('buttonAction') or {}
|
|
|
|
|
+ if action.get('__typename') == 'NavigateToMessageGuest':
|
|
|
|
|
+ url = action.get('url', '')
|
|
|
|
|
+ m = __import__('re').search(r'/thread/(\d+)', url)
|
|
|
|
|
+ if m:
|
|
|
|
|
+ thread_id = m.group(1)
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ return c, name, ci, co, door_code, thread_id
|
|
|
|
|
|
|
|
details_list = await asyncio.gather(*[fetch_details(c) for c in unique_codes])
|
|
details_list = await asyncio.gather(*[fetch_details(c) for c in unique_codes])
|
|
|
details = {c: (name, ci, co, door_code, thread_id) for c, name, ci, co, door_code, thread_id in details_list}
|
|
details = {c: (name, ci, co, door_code, thread_id) for c, name, ci, co, door_code, thread_id in details_list}
|