@gonzih/of-scraper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLAN.md +50 -0
- package/README.md +113 -0
- package/TODO.md +27 -0
- package/dist/browser.d.ts +8 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +141 -0
- package/dist/browser.js.map +1 -0
- package/dist/financials.d.ts +12 -0
- package/dist/financials.d.ts.map +1 -0
- package/dist/financials.js +88 -0
- package/dist/financials.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +108 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +11 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +134 -0
- package/dist/messages.js.map +1 -0
- package/dist/profiles.d.ts +11 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +90 -0
- package/dist/profiles.js.map +1 -0
- package/dist/redis.d.ts +7 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +38 -0
- package/dist/redis.js.map +1 -0
- package/package.json +27 -0
- package/src/browser.ts +134 -0
- package/src/financials.ts +104 -0
- package/src/index.ts +136 -0
- package/src/messages.ts +163 -0
- package/src/profiles.ts +105 -0
- package/src/redis.ts +34 -0
- package/tsconfig.json +19 -0
package/PLAN.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# PLAN.md — of-scraper
|
|
2
|
+
|
|
3
|
+
## Task Understanding
|
|
4
|
+
Build a Node.js + TypeScript service that:
|
|
5
|
+
1. Maintains a persistent Puppeteer browser session logged into OnlyFans
|
|
6
|
+
2. Polls the chats page every 30s for new messages
|
|
7
|
+
3. For each new message, scrapes sender profile + financial data
|
|
8
|
+
4. Publishes all data to Redis Streams (of:messages, of:profiles, of:financials)
|
|
9
|
+
5. Avoids duplicates via Redis SET of:seen_messages
|
|
10
|
+
6. Handles session save/restore across restarts
|
|
11
|
+
|
|
12
|
+
## Approaches
|
|
13
|
+
|
|
14
|
+
### Approach A: Monolithic single-file
|
|
15
|
+
- Everything in index.ts
|
|
16
|
+
- Simple, but hard to test and maintain
|
|
17
|
+
- Not chosen — doesn't match the required modular structure
|
|
18
|
+
|
|
19
|
+
### Approach B: Modular TypeScript (chosen)
|
|
20
|
+
- Separate modules: browser.ts, messages.ts, profiles.ts, financials.ts, redis.ts, index.ts
|
|
21
|
+
- Each module has a clear single responsibility
|
|
22
|
+
- Easy to test each part independently
|
|
23
|
+
- Matches the spec exactly
|
|
24
|
+
|
|
25
|
+
### Approach C: Worker threads / separate processes
|
|
26
|
+
- Each scraping task runs in a worker thread
|
|
27
|
+
- Better concurrency, but massively over-engineered for a single-user scraper
|
|
28
|
+
- Not chosen — unnecessary complexity
|
|
29
|
+
|
|
30
|
+
## Chosen Approach: B (Modular TypeScript)
|
|
31
|
+
|
|
32
|
+
## Files to Create
|
|
33
|
+
- `package.json` — dependencies, scripts, bin entry
|
|
34
|
+
- `tsconfig.json` — TypeScript config
|
|
35
|
+
- `.npmrc` — npm auth token (gitignored)
|
|
36
|
+
- `src/browser.ts` — Puppeteer launch, session save/restore
|
|
37
|
+
- `src/messages.ts` — Scrape chat list
|
|
38
|
+
- `src/profiles.ts` — Scrape profile page
|
|
39
|
+
- `src/financials.ts` — Scrape financial data from chat thread
|
|
40
|
+
- `src/redis.ts` — Redis client + XADD helpers
|
|
41
|
+
- `src/index.ts` — Main loop + orchestration
|
|
42
|
+
- `README.md` — Setup docs
|
|
43
|
+
|
|
44
|
+
## Risks & Unknowns
|
|
45
|
+
- OnlyFans DOM structure changes frequently — selectors may need adjustment
|
|
46
|
+
- puppeteer-extra-plugin-stealth availability/compatibility with latest puppeteer
|
|
47
|
+
- Session serialization completeness (cookies alone may not be sufficient)
|
|
48
|
+
- Rate limiting by OnlyFans if scraping too aggressively
|
|
49
|
+
- npm package name `@gonzih/of-scraper` must be available on npm registry
|
|
50
|
+
- .npmrc must be gitignored to avoid GitHub push protection blocking the token
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# @gonzih/of-scraper
|
|
2
|
+
|
|
3
|
+
A Node.js + TypeScript service that maintains a live Puppeteer browser session logged into OnlyFans, scrapes incoming messages, and feeds them into Redis Streams.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Persistent browser sessions via `session.json` (cookies + localStorage)
|
|
8
|
+
- Polls `/my/chats` every 30s for new messages
|
|
9
|
+
- For each new message, scrapes:
|
|
10
|
+
- Sender profile (displayName, bio, profilePic, subscription status)
|
|
11
|
+
- Financial data (tips, purchases, lifetime value)
|
|
12
|
+
- All data published to Redis Streams
|
|
13
|
+
- Duplicate prevention via Redis SET `of:seen_messages`
|
|
14
|
+
- Stealth mode via `puppeteer-extra-plugin-stealth`
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- Node.js 18+
|
|
19
|
+
- Redis 6+ (running locally or via `REDIS_URL`)
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @gonzih/of-scraper
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install globally:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g @gonzih/of-scraper
|
|
31
|
+
of-scraper
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Initial Login
|
|
35
|
+
|
|
36
|
+
On first run (or when the session has expired), the service opens a browser window and waits for you to log in manually:
|
|
37
|
+
|
|
38
|
+
1. Set `HEADLESS=false` to see the browser
|
|
39
|
+
2. Log in to OnlyFans in the browser window
|
|
40
|
+
3. The service detects the successful login, saves `session.json`, and starts polling
|
|
41
|
+
|
|
42
|
+
Subsequent runs will restore the saved session automatically.
|
|
43
|
+
|
|
44
|
+
## Environment Variables
|
|
45
|
+
|
|
46
|
+
| Variable | Default | Description |
|
|
47
|
+
|----------|---------|-------------|
|
|
48
|
+
| `REDIS_URL` | `redis://localhost:6379` | Redis connection URL |
|
|
49
|
+
| `POLL_INTERVAL_MS` | `30000` | Polling interval in milliseconds |
|
|
50
|
+
| `SESSION_FILE` | `./session.json` | Path to session persistence file |
|
|
51
|
+
| `HEADLESS` | `true` | Run browser headlessly (`false` for initial login) |
|
|
52
|
+
|
|
53
|
+
## Redis Stream Schemas
|
|
54
|
+
|
|
55
|
+
### `of:messages`
|
|
56
|
+
|
|
57
|
+
| Field | Type | Description |
|
|
58
|
+
|-------|------|-------------|
|
|
59
|
+
| `messageId` | string | Unique message identifier |
|
|
60
|
+
| `fromUsername` | string | Sender's OnlyFans username |
|
|
61
|
+
| `text` | string | Message text content |
|
|
62
|
+
| `timestamp` | string | ISO 8601 timestamp |
|
|
63
|
+
| `threadId` | string | Chat thread ID |
|
|
64
|
+
|
|
65
|
+
### `of:profiles`
|
|
66
|
+
|
|
67
|
+
| Field | Type | Description |
|
|
68
|
+
|-------|------|-------------|
|
|
69
|
+
| `username` | string | OnlyFans username |
|
|
70
|
+
| `displayName` | string | Display name |
|
|
71
|
+
| `isSubscribed` | string | `"true"` or `"false"` |
|
|
72
|
+
| `profilePic` | string | URL of profile picture |
|
|
73
|
+
| `bio` | string | Profile bio text |
|
|
74
|
+
| `fetchedAt` | string | ISO 8601 fetch timestamp |
|
|
75
|
+
|
|
76
|
+
### `of:financials`
|
|
77
|
+
|
|
78
|
+
| Field | Type | Description |
|
|
79
|
+
|-------|------|-------------|
|
|
80
|
+
| `username` | string | OnlyFans username |
|
|
81
|
+
| `totalTips` | string | Total tips received from this fan |
|
|
82
|
+
| `totalPurchases` | string | Total PPV/content purchases |
|
|
83
|
+
| `subscriptionStatus` | string | Current subscription status |
|
|
84
|
+
| `lastPaymentDate` | string | Date of last payment |
|
|
85
|
+
| `lifetimeValue` | string | Total lifetime spend |
|
|
86
|
+
| `updatedAt` | string | ISO 8601 update timestamp |
|
|
87
|
+
|
|
88
|
+
## Consuming Streams
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Read all messages from the beginning
|
|
92
|
+
redis-cli XREAD COUNT 10 STREAMS of:messages 0-0
|
|
93
|
+
|
|
94
|
+
# Read new messages since last read (consumer group pattern)
|
|
95
|
+
redis-cli XGROUP CREATE of:messages myapp $ MKSTREAM
|
|
96
|
+
redis-cli XREADGROUP GROUP myapp consumer1 COUNT 10 STREAMS of:messages >
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Error Handling
|
|
100
|
+
|
|
101
|
+
If the service cannot verify the login state, it saves a screenshot to `/tmp/of_login_required.png` for debugging. Delete `session.json` and restart with `HEADLESS=false` to re-authenticate.
|
|
102
|
+
|
|
103
|
+
## Architecture
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
src/
|
|
107
|
+
browser.ts — Puppeteer session management (launch, save, restore)
|
|
108
|
+
messages.ts — Scrape chat list, extract new messages
|
|
109
|
+
profiles.ts — Scrape profile details for a username
|
|
110
|
+
financials.ts — Scrape transaction/tip history for a username
|
|
111
|
+
redis.ts — Redis client + XADD stream helpers
|
|
112
|
+
index.ts — Main loop + orchestration
|
|
113
|
+
```
|
package/TODO.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# TODO.md — of-scraper
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
- [x] Create package.json with all dependencies
|
|
5
|
+
- [x] Create tsconfig.json
|
|
6
|
+
- [x] Create .npmrc with auth token (gitignored)
|
|
7
|
+
|
|
8
|
+
## Source Files
|
|
9
|
+
- [x] src/redis.ts — Redis client + XADD stream helpers
|
|
10
|
+
- [x] src/browser.ts — Puppeteer session management
|
|
11
|
+
- [x] src/messages.ts — Scrape chat list for new messages
|
|
12
|
+
- [x] src/profiles.ts — Scrape profile page for a username
|
|
13
|
+
- [x] src/financials.ts — Scrape financial data from chat thread
|
|
14
|
+
- [x] src/index.ts — Main loop + orchestration
|
|
15
|
+
|
|
16
|
+
## Documentation
|
|
17
|
+
- [x] README.md
|
|
18
|
+
|
|
19
|
+
## Build & Publish
|
|
20
|
+
- [x] Run npm install
|
|
21
|
+
- [x] Run npm run build (tsc)
|
|
22
|
+
- [ ] git checkout -b feat/mvp
|
|
23
|
+
- [ ] git add + commit
|
|
24
|
+
- [ ] git push
|
|
25
|
+
- [ ] gh pr create
|
|
26
|
+
- [ ] gh pr merge
|
|
27
|
+
- [ ] npm version 1.0.0 && npm publish
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Browser, Page } from 'puppeteer';
|
|
2
|
+
export declare function launchBrowser(): Promise<Browser>;
|
|
3
|
+
export declare function saveSession(page: Page): Promise<void>;
|
|
4
|
+
export declare function restoreSession(page: Page): Promise<void>;
|
|
5
|
+
export declare function sessionFileExists(): Promise<boolean>;
|
|
6
|
+
export declare function ensureLoggedIn(browser: Browser): Promise<Page>;
|
|
7
|
+
export declare function takeErrorScreenshot(page: Page): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAe/C,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAWtD;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB3D;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB9D;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAE1D;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCpE;AAmBD,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAOnE"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.takeErrorScreenshot = exports.ensureLoggedIn = exports.sessionFileExists = exports.restoreSession = exports.saveSession = exports.launchBrowser = void 0;
|
|
30
|
+
const puppeteer_extra_1 = __importDefault(require("puppeteer-extra"));
|
|
31
|
+
const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extra-plugin-stealth"));
|
|
32
|
+
const fs = __importStar(require("fs"));
|
|
33
|
+
const path = __importStar(require("path"));
|
|
34
|
+
puppeteer_extra_1.default.use((0, puppeteer_extra_plugin_stealth_1.default)());
|
|
35
|
+
const SESSION_FILE = process.env.SESSION_FILE ?? './session.json';
|
|
36
|
+
const HEADLESS = process.env.HEADLESS !== 'false';
|
|
37
|
+
async function launchBrowser() {
|
|
38
|
+
const browser = await puppeteer_extra_1.default.launch({
|
|
39
|
+
headless: HEADLESS,
|
|
40
|
+
args: [
|
|
41
|
+
'--no-sandbox',
|
|
42
|
+
'--disable-setuid-sandbox',
|
|
43
|
+
'--disable-blink-features=AutomationControlled',
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
console.log(`[${new Date().toISOString()}] Browser launched (headless=${HEADLESS})`);
|
|
47
|
+
return browser;
|
|
48
|
+
}
|
|
49
|
+
exports.launchBrowser = launchBrowser;
|
|
50
|
+
async function saveSession(page) {
|
|
51
|
+
const cookies = await page.cookies();
|
|
52
|
+
const localStorage = await page.evaluate(() => {
|
|
53
|
+
const data = {};
|
|
54
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
55
|
+
const key = window.localStorage.key(i);
|
|
56
|
+
if (key !== null) {
|
|
57
|
+
data[key] = window.localStorage.getItem(key) ?? '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return data;
|
|
61
|
+
});
|
|
62
|
+
const session = { cookies, localStorage };
|
|
63
|
+
const sessionPath = path.resolve(SESSION_FILE);
|
|
64
|
+
fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2));
|
|
65
|
+
console.log(`[${new Date().toISOString()}] Session saved to ${sessionPath}`);
|
|
66
|
+
}
|
|
67
|
+
exports.saveSession = saveSession;
|
|
68
|
+
async function restoreSession(page) {
|
|
69
|
+
const sessionPath = path.resolve(SESSION_FILE);
|
|
70
|
+
if (!fs.existsSync(sessionPath)) {
|
|
71
|
+
console.log(`[${new Date().toISOString()}] No session file found at ${sessionPath}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const raw = fs.readFileSync(sessionPath, 'utf-8');
|
|
75
|
+
const session = JSON.parse(raw);
|
|
76
|
+
await page.goto('https://onlyfans.com', { waitUntil: 'domcontentloaded' });
|
|
77
|
+
await page.setCookie(...session.cookies);
|
|
78
|
+
await page.evaluate((lsData) => {
|
|
79
|
+
for (const [key, value] of Object.entries(lsData)) {
|
|
80
|
+
window.localStorage.setItem(key, value);
|
|
81
|
+
}
|
|
82
|
+
}, session.localStorage);
|
|
83
|
+
console.log(`[${new Date().toISOString()}] Session restored from ${sessionPath}`);
|
|
84
|
+
}
|
|
85
|
+
exports.restoreSession = restoreSession;
|
|
86
|
+
async function sessionFileExists() {
|
|
87
|
+
return fs.existsSync(path.resolve(SESSION_FILE));
|
|
88
|
+
}
|
|
89
|
+
exports.sessionFileExists = sessionFileExists;
|
|
90
|
+
async function ensureLoggedIn(browser) {
|
|
91
|
+
const page = await browser.newPage();
|
|
92
|
+
await page.setViewport({ width: 1280, height: 900 });
|
|
93
|
+
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
|
94
|
+
if (await sessionFileExists()) {
|
|
95
|
+
console.log(`[${new Date().toISOString()}] Restoring existing session...`);
|
|
96
|
+
await restoreSession(page);
|
|
97
|
+
await page.goto('https://onlyfans.com/my/chats', { waitUntil: 'networkidle2', timeout: 30000 });
|
|
98
|
+
const isLoggedIn = await checkLoggedIn(page);
|
|
99
|
+
if (isLoggedIn) {
|
|
100
|
+
console.log(`[${new Date().toISOString()}] Session valid — logged in`);
|
|
101
|
+
return page;
|
|
102
|
+
}
|
|
103
|
+
console.log(`[${new Date().toISOString()}] Session invalid or expired`);
|
|
104
|
+
}
|
|
105
|
+
// Need to log in manually
|
|
106
|
+
await page.goto('https://onlyfans.com/login', { waitUntil: 'networkidle2', timeout: 30000 });
|
|
107
|
+
console.log(`[${new Date().toISOString()}] Waiting for manual login at https://onlyfans.com/login ...`);
|
|
108
|
+
// Wait for navigation away from /login (indicates successful login)
|
|
109
|
+
await page.waitForFunction(() => !window.location.href.includes('/login'), { timeout: 300000 } // 5 minute timeout for human to log in
|
|
110
|
+
);
|
|
111
|
+
console.log(`[${new Date().toISOString()}] Login detected — saving session`);
|
|
112
|
+
await saveSession(page);
|
|
113
|
+
return page;
|
|
114
|
+
}
|
|
115
|
+
exports.ensureLoggedIn = ensureLoggedIn;
|
|
116
|
+
async function checkLoggedIn(page) {
|
|
117
|
+
try {
|
|
118
|
+
const result = await page.evaluate(() => {
|
|
119
|
+
return (document.querySelector('.b-header__user') !== null ||
|
|
120
|
+
document.querySelector('[data-type="messages"]') !== null ||
|
|
121
|
+
document.querySelector('.g-header__left .b-ddmenu') !== null ||
|
|
122
|
+
document.querySelector('.b-chats') !== null ||
|
|
123
|
+
document.querySelector('.l-sidebar__user') !== null);
|
|
124
|
+
});
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function takeErrorScreenshot(page) {
|
|
132
|
+
try {
|
|
133
|
+
await page.screenshot({ path: '/tmp/of_login_required.png', fullPage: true });
|
|
134
|
+
console.log(`[${new Date().toISOString()}] Screenshot saved to /tmp/of_login_required.png`);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.error(`[${new Date().toISOString()}] Failed to take screenshot:`, err);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.takeErrorScreenshot = takeErrorScreenshot;
|
|
141
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Page } from 'puppeteer';
|
|
2
|
+
export interface Financials {
|
|
3
|
+
username: string;
|
|
4
|
+
totalTips: string;
|
|
5
|
+
totalPurchases: string;
|
|
6
|
+
subscriptionStatus: string;
|
|
7
|
+
lastPaymentDate: string;
|
|
8
|
+
lifetimeValue: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function scrapeFinancials(page: Page, username: string, threadId: string): Promise<Financials | null>;
|
|
12
|
+
//# sourceMappingURL=financials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"financials.d.ts","sourceRoot":"","sources":["../src/financials.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA2FjH"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scrapeFinancials = void 0;
|
|
4
|
+
async function scrapeFinancials(page, username, threadId) {
|
|
5
|
+
console.log(`[${new Date().toISOString()}] Fetching financials for @${username} (thread ${threadId})`);
|
|
6
|
+
const chatUrl = `https://onlyfans.com/my/chats/chat/${threadId}`;
|
|
7
|
+
try {
|
|
8
|
+
await page.goto(chatUrl, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
console.error(`[${new Date().toISOString()}] Failed to navigate to thread for financials:`, err);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
// Try to open the statistics/info panel
|
|
15
|
+
try {
|
|
16
|
+
const statsBtnSelectors = [
|
|
17
|
+
'button[class*="statistic"]',
|
|
18
|
+
'button[class*="stat"]',
|
|
19
|
+
'[data-type="stats"]',
|
|
20
|
+
'button[title*="Statistic"]',
|
|
21
|
+
'button[aria-label*="statistic"]',
|
|
22
|
+
'.b-chat__info-btn',
|
|
23
|
+
'[class*="infoBtn"]',
|
|
24
|
+
];
|
|
25
|
+
for (const sel of statsBtnSelectors) {
|
|
26
|
+
const btn = await page.$(sel);
|
|
27
|
+
if (btn) {
|
|
28
|
+
await btn.click();
|
|
29
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Stats panel may already be visible or not available
|
|
36
|
+
}
|
|
37
|
+
const financials = await page.evaluate((uname) => {
|
|
38
|
+
const getText = (selectors) => {
|
|
39
|
+
for (const sel of selectors) {
|
|
40
|
+
const el = document.querySelector(sel);
|
|
41
|
+
if (el?.textContent?.trim())
|
|
42
|
+
return el.textContent.trim();
|
|
43
|
+
}
|
|
44
|
+
return '0';
|
|
45
|
+
};
|
|
46
|
+
const totalTips = getText([
|
|
47
|
+
'[class*="tips"] [class*="amount"]',
|
|
48
|
+
'[class*="tip"] [class*="total"]',
|
|
49
|
+
'[data-type="tips"]',
|
|
50
|
+
'.b-stat__tips',
|
|
51
|
+
]);
|
|
52
|
+
const totalPurchases = getText([
|
|
53
|
+
'[class*="purchase"] [class*="amount"]',
|
|
54
|
+
'[class*="purchase"] [class*="total"]',
|
|
55
|
+
'[data-type="purchase"]',
|
|
56
|
+
'.b-stat__purchases',
|
|
57
|
+
]);
|
|
58
|
+
const subscriptionStatus = getText([
|
|
59
|
+
'[class*="subscription"] [class*="status"]',
|
|
60
|
+
'[class*="subscriptionStatus"]',
|
|
61
|
+
'[data-type="subscriptionStatus"]',
|
|
62
|
+
]) || 'unknown';
|
|
63
|
+
const lastPaymentDate = getText([
|
|
64
|
+
'[class*="lastPayment"]',
|
|
65
|
+
'[class*="last-payment"]',
|
|
66
|
+
'[data-type="lastPayment"]',
|
|
67
|
+
]);
|
|
68
|
+
const lifetimeValue = getText([
|
|
69
|
+
'[class*="lifetimeValue"]',
|
|
70
|
+
'[class*="lifetime"]',
|
|
71
|
+
'[class*="totalSpent"]',
|
|
72
|
+
'[data-type="lifetimeValue"]',
|
|
73
|
+
]);
|
|
74
|
+
return {
|
|
75
|
+
username: uname,
|
|
76
|
+
totalTips,
|
|
77
|
+
totalPurchases,
|
|
78
|
+
subscriptionStatus,
|
|
79
|
+
lastPaymentDate,
|
|
80
|
+
lifetimeValue,
|
|
81
|
+
updatedAt: new Date().toISOString(),
|
|
82
|
+
};
|
|
83
|
+
}, username);
|
|
84
|
+
console.log(`[${new Date().toISOString()}] Financials for @${username}: tips=${financials.totalTips}, purchases=${financials.totalPurchases}`);
|
|
85
|
+
return financials;
|
|
86
|
+
}
|
|
87
|
+
exports.scrapeFinancials = scrapeFinancials;
|
|
88
|
+
//# sourceMappingURL=financials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"financials.js","sourceRoot":"","sources":["../src/financials.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const redis_1 = require("./redis");
|
|
5
|
+
const browser_1 = require("./browser");
|
|
6
|
+
const messages_1 = require("./messages");
|
|
7
|
+
const profiles_1 = require("./profiles");
|
|
8
|
+
const financials_1 = require("./financials");
|
|
9
|
+
const POLL_INTERVAL_MS = parseInt(process.env.POLL_INTERVAL_MS ?? '30000', 10);
|
|
10
|
+
const RATE_LIMIT_MS = 2000;
|
|
11
|
+
async function sleep(ms) {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
async function processMessage(page, messageId, fromUsername, text, timestamp, threadId) {
|
|
15
|
+
const ts = new Date().toISOString();
|
|
16
|
+
console.log(`[${ts}] Processing message ${messageId} from @${fromUsername}`);
|
|
17
|
+
await (0, redis_1.xadd)('of:messages', {
|
|
18
|
+
messageId,
|
|
19
|
+
fromUsername,
|
|
20
|
+
text,
|
|
21
|
+
timestamp,
|
|
22
|
+
threadId,
|
|
23
|
+
});
|
|
24
|
+
await sleep(RATE_LIMIT_MS);
|
|
25
|
+
const profile = await (0, profiles_1.scrapeProfile)(page, fromUsername);
|
|
26
|
+
if (profile) {
|
|
27
|
+
await (0, redis_1.xadd)('of:profiles', {
|
|
28
|
+
username: profile.username,
|
|
29
|
+
displayName: profile.displayName,
|
|
30
|
+
isSubscribed: String(profile.isSubscribed),
|
|
31
|
+
profilePic: profile.profilePic,
|
|
32
|
+
bio: profile.bio,
|
|
33
|
+
fetchedAt: profile.fetchedAt,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
await sleep(RATE_LIMIT_MS);
|
|
37
|
+
const financials = await (0, financials_1.scrapeFinancials)(page, fromUsername, threadId);
|
|
38
|
+
if (financials) {
|
|
39
|
+
await (0, redis_1.xadd)('of:financials', {
|
|
40
|
+
username: financials.username,
|
|
41
|
+
totalTips: financials.totalTips,
|
|
42
|
+
totalPurchases: financials.totalPurchases,
|
|
43
|
+
subscriptionStatus: financials.subscriptionStatus,
|
|
44
|
+
lastPaymentDate: financials.lastPaymentDate,
|
|
45
|
+
lifetimeValue: financials.lifetimeValue,
|
|
46
|
+
updatedAt: financials.updatedAt,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function pollLoop(browser, page) {
|
|
51
|
+
while (true) {
|
|
52
|
+
const loopStart = new Date().toISOString();
|
|
53
|
+
console.log(`[${loopStart}] Starting poll cycle`);
|
|
54
|
+
try {
|
|
55
|
+
const messages = await (0, messages_1.scrapeNewMessages)(page);
|
|
56
|
+
for (const msg of messages) {
|
|
57
|
+
const seen = await (0, redis_1.isSeen)(msg.messageId);
|
|
58
|
+
if (seen) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
await (0, redis_1.markSeen)(msg.messageId);
|
|
62
|
+
await processMessage(page, msg.messageId, msg.fromUsername, msg.text, msg.timestamp, msg.threadId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.error(`[${new Date().toISOString()}] Poll cycle error:`, err);
|
|
67
|
+
try {
|
|
68
|
+
await (0, browser_1.takeErrorScreenshot)(page);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// ignore screenshot errors
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
console.log(`[${new Date().toISOString()}] Poll cycle complete — sleeping ${POLL_INTERVAL_MS}ms`);
|
|
75
|
+
await sleep(POLL_INTERVAL_MS);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function main() {
|
|
79
|
+
console.log(`[${new Date().toISOString()}] of-scraper starting up`);
|
|
80
|
+
console.log(`[${new Date().toISOString()}] POLL_INTERVAL_MS=${POLL_INTERVAL_MS}`);
|
|
81
|
+
await (0, redis_1.connectRedis)();
|
|
82
|
+
let browser = null;
|
|
83
|
+
let page = null;
|
|
84
|
+
try {
|
|
85
|
+
browser = await (0, browser_1.launchBrowser)();
|
|
86
|
+
page = await (0, browser_1.ensureLoggedIn)(browser);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
console.error(`[${new Date().toISOString()}] Failed to launch browser or log in:`, err);
|
|
90
|
+
if (browser)
|
|
91
|
+
await browser.close().catch(() => { });
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const shutdown = async () => {
|
|
95
|
+
console.log(`[${new Date().toISOString()}] Shutting down...`);
|
|
96
|
+
if (browser)
|
|
97
|
+
await browser.close().catch(() => { });
|
|
98
|
+
process.exit(0);
|
|
99
|
+
};
|
|
100
|
+
process.on('SIGINT', () => { shutdown().catch(console.error); });
|
|
101
|
+
process.on('SIGTERM', () => { shutdown().catch(console.error); });
|
|
102
|
+
await pollLoop(browser, page);
|
|
103
|
+
}
|
|
104
|
+
main().catch((err) => {
|
|
105
|
+
console.error(`[${new Date().toISOString()}] Fatal error:`, err);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Page } from 'puppeteer';
|
|
2
|
+
export interface Message {
|
|
3
|
+
messageId: string;
|
|
4
|
+
fromUsername: string;
|
|
5
|
+
text: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
threadId: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function scrapeNewMessages(page: Page): Promise<Message[]>;
|
|
10
|
+
export declare function scrapeThreadMessages(page: Page, threadId: string): Promise<Message[]>;
|
|
11
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAmFtE;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAmE3F"}
|