109
modules/paid_content/onlyfans_signing.py
Normal file
109
modules/paid_content/onlyfans_signing.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
OnlyFans Request Signing Module
|
||||
|
||||
Handles the dynamic request signing required by the OnlyFans API.
|
||||
Fetches signing rules from the DATAHOARDERS/dynamic-rules GitHub repo
|
||||
and computes SHA-1 based signatures for each API request.
|
||||
|
||||
Isolated module so it's easy to update when OF changes their signing scheme.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import time
|
||||
from typing import Dict, Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
RULES_URL = "https://raw.githubusercontent.com/DATAHOARDERS/dynamic-rules/main/onlyfans.json"
|
||||
|
||||
|
||||
class OnlyFansSigner:
|
||||
"""
|
||||
Computes request signatures for the OnlyFans API.
|
||||
|
||||
Uses dynamic rules fetched from a public GitHub repo (same source as OF-Scraper).
|
||||
Rules are cached locally and refreshed every 6 hours.
|
||||
"""
|
||||
|
||||
RULES_TTL = 6 * 3600 # 6 hours
|
||||
|
||||
def __init__(self, rules_url: Optional[str] = None):
|
||||
self.rules_url = rules_url or RULES_URL
|
||||
self._rules: Optional[Dict] = None
|
||||
self._rules_fetched_at: float = 0
|
||||
|
||||
@property
|
||||
def rules_stale(self) -> bool:
|
||||
"""Check if cached rules need refreshing"""
|
||||
if self._rules is None:
|
||||
return True
|
||||
return (time.time() - self._rules_fetched_at) > self.RULES_TTL
|
||||
|
||||
async def get_rules(self) -> Dict:
|
||||
"""
|
||||
Fetch signing rules, using cache if fresh.
|
||||
|
||||
Returns:
|
||||
Dict with keys: static_param, format, checksum_indexes,
|
||||
checksum_constants, checksum_constant, app_token
|
||||
"""
|
||||
if not self.rules_stale:
|
||||
return self._rules
|
||||
|
||||
timeout = aiohttp.ClientTimeout(total=15)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(self.rules_url) as resp:
|
||||
if resp.status != 200:
|
||||
if self._rules is not None:
|
||||
# Use stale cache rather than failing
|
||||
return self._rules
|
||||
raise RuntimeError(
|
||||
f"Failed to fetch OF signing rules: HTTP {resp.status}"
|
||||
)
|
||||
self._rules = await resp.json(content_type=None)
|
||||
self._rules_fetched_at = time.time()
|
||||
|
||||
return self._rules
|
||||
|
||||
async def sign(self, endpoint_path: str, user_id: str = "0") -> Dict[str, str]:
|
||||
"""
|
||||
Compute signing headers for an OnlyFans API request.
|
||||
|
||||
Args:
|
||||
endpoint_path: The full URL path (e.g. "/api2/v2/users/me")
|
||||
user_id: The authenticated user's ID (from auth_id cookie)
|
||||
|
||||
Returns:
|
||||
Dict with 'sign', 'time', 'app-token' headers
|
||||
"""
|
||||
rules = await self.get_rules()
|
||||
# Timestamp in milliseconds (matching OF-Scraper's implementation)
|
||||
timestamp = str(round(time.time() * 1000))
|
||||
|
||||
# 1. Build the message to hash
|
||||
msg = "\n".join([
|
||||
rules["static_param"],
|
||||
timestamp,
|
||||
endpoint_path,
|
||||
str(user_id),
|
||||
])
|
||||
|
||||
# 2. SHA-1 hash
|
||||
sha1_hash = hashlib.sha1(msg.encode("utf-8")).hexdigest()
|
||||
sha1_bytes = sha1_hash.encode("ascii")
|
||||
|
||||
# 3. Checksum from indexed byte positions + single constant
|
||||
# (matching OF-Scraper's implementation)
|
||||
checksum_indexes = rules["checksum_indexes"]
|
||||
checksum_constant = rules.get("checksum_constant", 0)
|
||||
checksum = sum(sha1_bytes[i] for i in checksum_indexes) + checksum_constant
|
||||
|
||||
# 4. Build the sign header using the format template
|
||||
# Typical format: "53760:{}:{:x}:69723085"
|
||||
sign_value = rules["format"].format(sha1_hash, abs(checksum))
|
||||
|
||||
return {
|
||||
"sign": sign_value,
|
||||
"time": timestamp,
|
||||
"app-token": rules["app_token"],
|
||||
}
|
||||
Reference in New Issue
Block a user