Spaces:
Paused
Paused
| """ | |
| RentMasseur API Client — bounded, production-ready HTTP client. | |
| Confirmed endpoints only. No guesswork. No spam. | |
| """ | |
| import json | |
| import logging | |
| import os | |
| import re | |
| import time | |
| from typing import Optional, Dict, Any | |
| import requests | |
| log = logging.getLogger("rm_api") | |
| BASE = "https://rentmasseur.com" | |
| API = f"{BASE}/api/v1" | |
| _PROXY_URL = os.environ.get("PROXY_URL", "") | |
| _PROXY_SECRET = os.environ.get("PROXY_SECRET", "") | |
| if _PROXY_URL: | |
| _PROXY_URL = _PROXY_URL.rstrip("/") | |
| API = f"{_PROXY_URL}/api/v1" | |
| BASE = _PROXY_URL | |
| log.info("Using proxy: %s", _PROXY_URL) | |
| class RentMasseurAPI: | |
| """Direct API client for rentmasseur.com using confirmed endpoints.""" | |
| def __init__(self, min_request_interval: float = 2.0): | |
| self.session = requests.Session() | |
| self.session.headers.update({ | |
| "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/131.0.0.0 Safari/537.36", | |
| "Accept": "application/json, text/plain, */*", | |
| "Accept-Language": "en-US,en;q=0.9", | |
| "Referer": f"{BASE}/settings", | |
| "Origin": BASE, | |
| }) | |
| if _PROXY_SECRET: | |
| self.session.headers["X-Proxy-Secret"] = _PROXY_SECRET | |
| self.csrf = None | |
| self.logged_in = False | |
| self.username = None | |
| self.last_request = 0.0 | |
| self.min_request_interval = min_request_interval | |
| self._read_cache = {} | |
| self._cache_ttl = 15.0 | |
| def _wait(self): | |
| """Respectful rate limiting between requests.""" | |
| elapsed = time.time() - self.last_request | |
| if elapsed < self.min_request_interval: | |
| time.sleep(self.min_request_interval - elapsed) | |
| self.last_request = time.time() | |
| def _get(self, path: str, params: Optional[Dict] = None) -> requests.Response: | |
| self._wait() | |
| return self.session.get(f"{API}{path}", params=params, timeout=15) | |
| def _cached_get(self, path: str, cache_key: str = None) -> Dict: | |
| """GET with in-memory TTL cache for read endpoints.""" | |
| key = cache_key or path | |
| now = time.time() | |
| cached = self._read_cache.get(key) | |
| if cached and (now - cached[0]) < self._cache_ttl: | |
| return cached[1] | |
| resp = self._get(path) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| self._read_cache[key] = (now, data) | |
| return data | |
| def invalidate_cache(self, *keys: str): | |
| """Invalidate specific cache keys after a mutation.""" | |
| for k in keys: | |
| self._read_cache.pop(k, None) | |
| def invalidate_all(self): | |
| self._read_cache.clear() | |
| def _post(self, path: str, json_data: Dict) -> requests.Response: | |
| self._wait() | |
| return self.session.post(f"{API}{path}", json=json_data, timeout=15) | |
| def _put(self, path: str, json_data: Dict) -> requests.Response: | |
| self._wait() | |
| return self.session.put(f"{API}{path}", json=json_data, timeout=15) | |
| def _get_csrf(self) -> str: | |
| resp = self.session.get(f"{BASE}/login") | |
| m = re.search(r'csrf["\s:=]+([A-Za-z0-9+/=]{20,})', resp.text) | |
| if m: | |
| self.csrf = m.group(1) | |
| return self.csrf | |
| for cookie in self.session.cookies: | |
| if "csrf" in cookie.name.lower() or "token" in cookie.name.lower(): | |
| self.csrf = cookie.value | |
| return self.csrf | |
| return "" | |
| def login(self, username: str, password: str) -> bool: | |
| """Login via API and store bearer token.""" | |
| self.username = username | |
| csrf = self._get_csrf() | |
| self._wait() | |
| resp = self.session.post(f"{API}/login", json={ | |
| "email": username, | |
| "password": password, | |
| "csrf": csrf, | |
| "remember": True, | |
| }) | |
| if resp.status_code != 200: | |
| log.error("Login failed: %d %s", resp.status_code, resp.text[:200]) | |
| return False | |
| try: | |
| data = resp.json() | |
| except Exception: | |
| log.error("Login response not JSON (captcha/block?): %s", resp.text[:300]) | |
| return False | |
| token = data.get("accessToken") | |
| if token: | |
| self.session.headers["Authorization"] = f"Bearer {token}" | |
| self.logged_in = True | |
| log.info("Login OK as %s", username) | |
| return True | |
| def load_cookies(self, cookies: list): | |
| """Load cookies from a saved session (e.g. from Selenium).""" | |
| for c in cookies: | |
| self.session.cookies.set(c["name"], c["value"], domain=c.get("domain", ""), path=c.get("path", "/")) | |
| self.logged_in = True | |
| # ------------------------------------------------------------------ | |
| # Confirmed read endpoints | |
| # ------------------------------------------------------------------ | |
| def get_dashboard(self) -> Dict: | |
| return self._cached_get("/account/dashboard", "dashboard") | |
| def get_availability(self) -> Dict: | |
| return self._cached_get("/account/dashboard/availability", "availability") | |
| def set_availability(self, option: int = 1, duration: int = 5) -> Dict: | |
| """ | |
| Set availability. | |
| option: 0=Not Set, 1=Available, 2=Not Available | |
| duration: index from timePeriods (0=1h, 1=2h, ..., 5=6h) | |
| """ | |
| resp = self._put("/account/dashboard/availability", {"option": option, "duration": duration}) | |
| resp.raise_for_status() | |
| self.invalidate_cache("availability") | |
| return resp.json() | |
| def get_ad_statistics(self) -> Dict: | |
| return self._cached_get("/account/dashboard/ad-statistics", "ad_statistics") | |
| def get_keeponline(self) -> Dict: | |
| return self._cached_get("/account/keeponline", "keeponline") | |
| def get_about(self) -> Dict: | |
| return self._cached_get("/settings/about", "about") | |
| def get_mailbox(self, page: int = 1, folder: int = 1, sort: int = 1) -> Dict: | |
| resp = self._get("/mailbox", params={"page": page, "folder": folder, "sort": sort}) | |
| resp.raise_for_status() | |
| return resp.json() | |
| def get_blogs(self, page: int = 1) -> Dict: | |
| resp = self._get("/blogs", params={"page": page}) | |
| resp.raise_for_status() | |
| return resp.json() | |
| # ------------------------------------------------------------------ | |
| # Confirmed write endpoints | |
| # ------------------------------------------------------------------ | |
| def set_visibility(self, visible: bool) -> Dict: | |
| resp = self._put("/settings/visibility", {"isAdHidden": not visible}) | |
| resp.raise_for_status() | |
| self.invalidate_cache("keeponline", "dashboard") | |
| return resp.json() | |
| def set_sms_alerts(self, enabled: bool) -> Dict: | |
| resp = self._put("/settings/sms", {"sms": enabled}) | |
| resp.raise_for_status() | |
| self.invalidate_cache("dashboard") | |
| return resp.json() | |
| def set_track_actions(self, enabled: bool) -> Dict: | |
| resp = self._put("/settings/track-actions", {"trackActions": enabled}) | |
| resp.raise_for_status() | |
| self.invalidate_cache("dashboard") | |
| return resp.json() | |
| def set_about(self, headline: str, description: str) -> Dict: | |
| resp = self._put("/settings/about", {"headline": headline, "description": description}) | |
| resp.raise_for_status() | |
| self.invalidate_cache("about") | |
| try: | |
| return resp.json() | |
| except Exception: | |
| return {"status": "ok", "raw": resp.text[:500]} | |
| # ------------------------------------------------------------------ | |
| # Blog endpoints | |
| # ------------------------------------------------------------------ | |
| def get_blog(self, blog_id: str) -> Dict: | |
| resp = self._get(f"/blogs/{blog_id}") | |
| resp.raise_for_status() | |
| return resp.json() | |
| def create_blog(self, title: str, body: str, tags: list = None) -> Dict: | |
| payload = {"title": title, "body": body} | |
| if tags: | |
| payload["tags"] = tags | |
| resp = self._post("/blogs", payload) | |
| resp.raise_for_status() | |
| self.invalidate_cache("blogs") | |
| try: | |
| return resp.json() | |
| except Exception: | |
| return {"status": "ok", "raw": resp.text[:500]} | |
| def update_blog(self, blog_id: str, title: str = None, body: str = None) -> Dict: | |
| payload = {} | |
| if title: | |
| payload["title"] = title | |
| if body: | |
| payload["body"] = body | |
| resp = self._put(f"/blogs/{blog_id}", payload) | |
| resp.raise_for_status() | |
| self.invalidate_cache("blogs") | |
| try: | |
| return resp.json() | |
| except Exception: | |
| return {"status": "ok", "raw": resp.text[:500]} | |
| def delete_blog(self, blog_id: str) -> Dict: | |
| self._wait() | |
| resp = self.session.delete(f"{API}/blogs/{blog_id}", timeout=15) | |
| resp.raise_for_status() | |
| self.invalidate_cache("blogs") | |
| try: | |
| return resp.json() | |
| except Exception: | |
| return {"status": "ok", "raw": resp.text[:500]} | |
| # ------------------------------------------------------------------ | |
| # Search (confirmed working 2026-07-09) | |
| # ------------------------------------------------------------------ | |
| def search_masseurs(self, city: str = "manhattan-ny", page: int = 1) -> Dict: | |
| resp = self._post("/search", {"searchCity": city, "page": page, "skipUsers": "0"}) | |
| resp.raise_for_status() | |
| return resp.json() | |
| # ------------------------------------------------------------------ | |
| # Profile visit (read-only profile fetch) | |
| # ------------------------------------------------------------------ | |
| def visit_profile(self, username: str) -> Dict: | |
| self._wait() | |
| resp = self.session.get(f"{API}/profile/{username}", timeout=15) | |
| try: | |
| return resp.json() | |
| except Exception: | |
| return {"status": "visited", "username": username, "http": resp.status_code} | |
| def get_profile(self, username: str) -> Dict: | |
| return self._cached_get(f"/profile/{username}", f"profile_{username}") | |
| # ------------------------------------------------------------------ | |
| # Audit — verify all endpoints are live | |
| # ------------------------------------------------------------------ | |
| def audit_endpoints(self) -> Dict: | |
| """Test all confirmed endpoints and return status report.""" | |
| results = {} | |
| tests = [ | |
| ("dashboard", lambda: self.get_dashboard()), | |
| ("availability", lambda: self.get_availability()), | |
| ("ad_statistics", lambda: self.get_ad_statistics()), | |
| ("keeponline", lambda: self.get_keeponline()), | |
| ("about", lambda: self.get_about()), | |
| ("mailbox", lambda: self.get_mailbox()), | |
| ("search", lambda: self.search_masseurs()), | |
| ] | |
| for name, fn in tests: | |
| try: | |
| fn() | |
| results[name] = "OK" | |
| except Exception as e: | |
| results[name] = f"FAIL: {e}" | |
| return results | |
| # ------------------------------------------------------------------ | |
| # Messaging | |
| # ------------------------------------------------------------------ | |
| def send_message(self, username: str, message: str) -> Dict: | |
| resp = self._post("/mailbox/send", {"username": username, "message": message}) | |
| resp.raise_for_status() | |
| self.invalidate_cache("mailbox") | |
| try: | |
| return resp.json() | |
| except Exception: | |
| return {"status": "ok", "raw": resp.text[:500]} | |
| def get_conversation(self, username: str, page: int = 1) -> Dict: | |
| resp = self._get(f"/mailbox/conversation/{username}", params={"page": page}) | |
| resp.raise_for_status() | |
| return resp.json() | |
| # ------------------------------------------------------------------ | |
| # Search (read-only) | |
| # ------------------------------------------------------------------ | |
| def search(self, city: str = "manhattan-ny", available_only: bool = False, | |
| page: int = 1, skip: int = 0) -> Dict: | |
| body = {"searchCity": city, "page": page, "skipUsers": str(skip)} | |
| if available_only: | |
| body["available"] = 1 | |
| resp = self._post("/search", body) | |
| resp.raise_for_status() | |
| return resp.json() | |
| # ------------------------------------------------------------------ | |
| # Full status | |
| # ------------------------------------------------------------------ | |
| def full_status(self) -> Dict: | |
| return { | |
| "dashboard": self.get_dashboard(), | |
| "availability": self.get_availability(), | |
| "stats": self.get_ad_statistics(), | |
| "keeponline": self.get_keeponline(), | |
| "about": self.get_about(), | |
| "interview": self.get_interview(), | |
| } | |