Initial commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Todd
2026-03-29 22:42:55 -04:00
commit 0d7b2b1aab
389 changed files with 280296 additions and 0 deletions

View 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"],
}